TIL

흐름 1. @Version 필드가 없거나, 필드는 있는데 Primitive타입이라서 null체크가 안되는지 확인

  • @Version 이 사용된 필드가 없거나, 있는데 필드의 값이 Primitive타입이라서 null체크가 불가능한 경우:
    • 흐름2로 넘어간다. => AbstractEntityInformationisNew() 를 호출한다.
  • 그렇지 않은 경우: 클래스 구성에 @Version이 있고, Long같은 객체 타입, 그러니까 Wrapper 타입이라서 null체크가 가능한경우
    • 실제 객체의 그 필드 값을 열어보고
    • 진짜 null이면 → 새로운 객체 → true반환
    • 아니면 → 기존에 있던 객체 → false반환
@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);
}
  • return문의 map이 하는 일
    1. versionAttribute가 있으면 필드 이름을 알아냅니다 (예: “version”).
    2. wrapper를 이용해 실제 엔티티 객체의 그 필드 값을 가져옵니다.
    3. 그 값이 null인지 확인한 결과(true/false)를 반환합니다.
    4. 만약 versionAttribute가 없다면? 그냥 true(새로운 것)를 반환합니다.
  • if문과 return문 두 곳에서 모두 null인지 체크하는 이유:
    • if문에서는 @Version이 사용된 필드가 있는지 먼저 확인한다. 이는 추후 getName() 등 해당 필드에 접근할 때 없는 객체입니다라는 에러를 마주하지 않기 위해서이다.

흐름 2. @Id 필드를 체크한다.

  • 객체 타입 Long, String 인 경우:
    • id == null 인지 확인한다
  • 기본 타입 long, int 인 경우:
    • id == 0 인지 확인한다.
public boolean isNew(T entity) {

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

    if (!idType.isPrimitive()) {
        return id == null;  // 객체 타입(Long)이면 null인지 확인
    }

    if (id instanceof Number) {
        return ((Number) id).longValue() == 0L; // 숫자 기본형(long)이면 0인지 확인
    }

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

  • 구체적인 설명

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

    @GeneratedValue 어노테이션으로 키 생성 전략을 사용하면 데이터베이스에 저장될 때 id가 할당됩니다.

    따라서 데이터베이스에 저장되기 전에 메모리에서 생성된 객체는 id가 비어있기 때문에 isNew()는 true가 되어 새로운 entity로 판단합니다.

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

키 생성 전략을 사용하지 않고 직접 ID를 할당하는 경우 새로운 entity로 간주되지 않습니다.

  • 이유: 직접 id를 부여해서 save() 를 호출하게 되면 이미 있는 객체로 인지하고
    1. DB에 가서 진짜 있는지 먼저 확인하려 한다. → SELECT 발생
    2. 실제로 있는 객체면 → UPDATE, 새로운 객체면 INSERT쿼리 발생

    → 결국 새 객체임은 확실한데, DB에 가서 확인하는 과정에서 불필요한 리소스 낭비가 발생하게 된다.

이 때는 엔티티에서 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
public class Item implements Persistable<String> {
    @Id
    private String id;

    @CreatedDate // 생성일자 활용
    private LocalDateTime createdDate;

    @Override
    public boolean isNew() {
        // 생성일자가 없으면 진짜 새 데이터라고 명시적으로 알려줌!
        return createdDate == null;
    }
}

새로운 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);
	}
}

Spring Data JPA의 save() 메서드의 동작 흐름:

  1. persist (등록): “이건 처음 보는 새 객체네? 바로 DB에 저장해야지.” (INSERT)
  2. merge (병합): “어? 이거 본 적 있는 객체 같은데? 일단 서가에 있는지 찾아보고(SELECT), 있으면 내용을 수정하고 없으면 새로 꽂아야지.” (SELECT 후 INSERT/UPDATE)

<정리: 왜 새로운 객체인지 판단하는게 중요할까?>

  1. SimpleJpaRepository의 save() 메서드에서 isNew()를 사용하여 persist를 수행할지 merge를 수행할지 결정
    1. 만약 ID 직접 지정 → 신규 ENTITY로 판단 X → MERGE 수행하게 됨 ⇒ 문제 지점: 신규임에도 불구하고 DB를 조회하는 것
    2. 따라서 불필요한 리소스 낭비를 막기 위해 중요한 것.

추가 학습 자료를 공유합니다.

results matching ""

    No results matching ""