본문 바로가기
Back-End

Spring Data JPA에서 새로운 Entity인지 판단하는 방법

by 코젼 2025. 2. 28.
728x90
반응형
@Override
public boolean isNew(T entity) {

    if(versionAttribute.isEmpty()
          || versionAttribute.map(Attribute::getJavaType).map(Class::isPrimitive).orElse(false)) {
        return super.isNew(entity);
    }

    BeanWrapper wrapper = new DirectFieldAccessFallbackBeanWrapper(entity);

    return versionAttribute.map(it -> wrapper.getPropertyValue(it.getName()) == null).orElse(true);
}

새로운 Entity인지 여부는 JpaEntityInformation 의 isNew(T entity)에 의해 판단됩니다. 다른 설정이 없으면 JpaEntityInformation의 구현체 중 JpaMetamodelEntityInformation 클래스가 동작합니다.

@Version이 사용된 필드가 없거나 @Version이 사용된 필드가 primitive 타입이면 AbstractEntityInformation의 isNew()를 호출합니다. @Version이 사용된 필드가 wrapper class이면 null여부를 확인합니다.

  • @Version 어노테이션: 엔티티의 버전을 관리할 수 있다. (낙관적 락)
public boolean isNew(T entity) {

    Id id = getId(entity);
    Class<ID> idType = getIdType();

    if (!idType.isPrimitive()) {
        return id == null;
    }

    if (id instanceof Number) {
        return ((Number) id).longValue() == 0L;
    }

    throw new IllegalArgumentException(String.format("Unsupported primitive id type %s", idType));
}

@Version이 사용된 필드가 없어서 AbstractEntityInformation 클래스가 동작하면 @Id 어노테이션을 사용한 필드를 확인해서 primitive 타입이 아니라면 null 여부, Number하위 타입이면 0인지 여부를 확인합니다.

  • Number의 하위 타입: Byte, Short, Int, Long, Float, Double
  • Number 클래스는 추상 클래스로, 다양한 숫자 타입의 상위 타입 역할을 한다.
  • Java 에서는 Integer, BigInteger, BigDecimal 도 상속한다.

@GeneratedValue 어노테이션으로 키 생성 전략을 사용하면 데이터베이스에 저장될 때 id가 할당됩니다. 따라서 데이터베이스에 저장되기 전에 메모리에서 생성된 객체id가 비어있기 때문에 isNew()는 true가 되어 새로운 entity로 판단합니다.

직접 ID를 할당하는 경우에는 어떻게 동작하나요? 🤔

키 생성 전략을 사용하지 않고 직접 ID를 할당하는 경우 새로운 entity로 간주되지 않습니다. 이 때는 엔티티에서 Persistable<T> 인터페이스를 구현해서 JpaMetamodelEntityInformation 클래스가 아닌 JpaPersistableEntityInformation의 isNew()가 동작하도록 해야 합니다.

public class JpaPersistableEntityInformation<T extends Persistable<ID, ID>
        extends JpaMetamodelEntityInformation<T, ID> {

    public JpaPersistableEntityInformation(Class<T> domainClass, Metamodel metamodel,
            PersistenceUnitUtil persistenceUnitUtil) {
        super(domainClass, metamodel, persistenceUnitUtil);
    }

    @Override
    public boolean isNew(T entity) {
        return entity.isNew();
    }

    @Nullable
    @Override
    public ID getId(T entity) {
        return entity.getId();
    }
}

새로운 Entity인지 판단하는게 왜 중요할까요? 🤓

@Override
@Transactional
public <S extends T> S save(S entity) {

    Assert.notNull(entity, "Entity must not be null");

	if (entityInformation.isNew(entity)) {
		entityManager.persist(entity);
		return entity;
	} else {
		return entityManager.merge(entity);
	}
}

SimpleJpaRepository의 save() 메서드에서 isNew()를 사용하여 persist를 수행할지 merge를 수행할지 결정합니다. 만약 ID를 직접 지정해주는 경우에는 신규 entity라고 판단하지 않기 때문merge를 수행합니다. 이때 해당 entity는 신규임에도 불구하고 DB를 조회하기 때문에 비효율적입니다. 따라서, 새로운 entity인지 판단하는 것은 중요한 부분입니다.


개인 정리

JpaEntityInformation 의 isNew 함수를 통해 알 수 있다. 별다른 설정이 없을 경우, JpaMetamodelEntityInformation 구현체가 동작한다.

@Version 이 사용된 부분이 없거나 @Version 을 사용한 필드가 primitive 타입인 경우, AbstractEntityInformationisNew 함수를 호출한다. primitive 타입이 아닌 경우 null 여부를 체크하고, Number 하위 타입이면 0인지를 체크한다.

@Version 이 사용된 필드가 wrapper class 인 경우는 null 인지 확인한다.

@GeneratedValue 를 통해 키 생성 전략을 사용하는 경우, 데이터베이스에 저장될 때 id 가 할당되기 때문에 현재 메모리에서 생성된 객체는 id 가 없으므로 isNew 가 true 여서 새로운 엔티티로 간주한다.

키 생성 전략을 사용하지 않고 직접 id 를 할당하는 경우 새로운 엔티티로 간주되지 않는다. JpaPersistableEntityInformation 구현체의 isNew 함수가 호출되도록 해야 한다.

SimpleJpaRepository의 save() 메서드에서 isNew() 를 사용하여 persist 와 merge 중에서 어떤 걸 수행할지 결정하는데, id 를 직접 지정하는 경우는 신규 엔티티라고 판단하지 않으므로 merge 를 수행한다. 이는 신규임에도 불구하고 db 를 조회하므로 비효율적이기 때문에 신규 엔티티를 판단하는 것은 중요한 부분이다.

728x90
반응형

댓글