TIL
흐름 1. @Version 필드가 없거나, 필드는 있는데 Primitive타입이라서 null체크가 안되는지 확인
@Version이 사용된 필드가 없거나, 있는데 필드의 값이 Primitive타입이라서null체크가 불가능한 경우:- 흐름2로 넘어간다. =>
AbstractEntityInformation의isNew()를 호출한다.
- 흐름2로 넘어간다. =>
- 그렇지 않은 경우: 클래스 구성에
@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이 하는 일versionAttribute가 있으면 필드 이름을 알아냅니다 (예: “version”).wrapper를 이용해 실제 엔티티 객체의 그 필드 값을 가져옵니다.- 그 값이
null인지 확인한 결과(true/false)를 반환합니다. - 만약
versionAttribute가 없다면? 그냥true(새로운 것)를 반환합니다.
- if문과 return문 두 곳에서 모두 null인지 체크하는 이유:
- if문에서는
@Version이 사용된 필드가 있는지 먼저 확인한다. 이는 추후 getName() 등 해당 필드에 접근할 때없는 객체입니다라는 에러를 마주하지 않기 위해서이다.
- if문에서는
흐름 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()를 호출하게 되면 이미 있는 객체로 인지하고- DB에 가서 진짜 있는지 먼저 확인하려 한다. →
SELECT발생 - 실제로 있는 객체면 →
UPDATE, 새로운 객체면INSERT쿼리 발생
→ 결국 새 객체임은 확실한데, DB에 가서 확인하는 과정에서 불필요한 리소스 낭비가 발생하게 된다.
- 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() 메서드의 동작 흐름:
persist(등록): “이건 처음 보는 새 객체네? 바로 DB에 저장해야지.” (INSERT)merge(병합): “어? 이거 본 적 있는 객체 같은데? 일단 서가에 있는지 찾아보고(SELECT), 있으면 내용을 수정하고 없으면 새로 꽂아야지.” (SELECT 후 INSERT/UPDATE)
<정리: 왜 새로운 객체인지 판단하는게 중요할까?>
- SimpleJpaRepository의
save()메서드에서isNew()를 사용하여 persist를 수행할지 merge를 수행할지 결정- 만약 ID 직접 지정 → 신규 ENTITY로 판단 X → MERGE 수행하게 됨 ⇒ 문제 지점: 신규임에도 불구하고 DB를 조회하는 것
- 따라서 불필요한 리소스 낭비를 막기 위해 중요한 것.