Spring Data JPA가 save() 호출 한 번으로 INSERT(persist)와 UPDATE(merge)를 자동으로 구분해 주는 비밀은 JpaEntityInformation#isNew()에 숨어 있습니다.
코드를 뜯어보며 동작 방식을 이해하고, 직접 ID를 할당할 때 주의해야 할 점까지 정리했습니다.
isNew()
가 호출되는 곳@Transactional
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null");
if (entityInformation.isNew(entity)) {
entityManager.persist(entity); // INSERT
return entity;
} else {
return entityManager.merge(entity); // UPDATE
}
}
JpaMetamodelEntityInformation
@Version
필드가 없거나 primitive 타입일 때 AbstractEntityInformation#isNew()
를 호출JpaPersistableEntityInformation
Persistable
인터페이스를 구현하면 사용entity.isNew()
값을 그대로 따름@Override
public boolean isNew(T entity) {
if (versionAttribute.isEmpty()
|| versionAttribute.map(Attribute::getJavaType)
.map(Class::isPrimitive).orElse(false)) {
return super.isNew(entity); // (1)
}
// (2) Wrapper 타입(@Version Long 등) → null 여부
BeanWrapper wrapper = new DirectFieldAccessFallbackBeanWrapper(entity);
return versionAttribute
.map(it -> wrapper.getPropertyValue(it.getName()) == null)
.orElse(true);
}
@Version
이 없다 혹은 primitive(long
, int
) ➜ AbstractEntityInformation#isNew()
호출@Version
이 랩퍼 타입(Long
, Integer
) ➜ 값이 null
이면 신규로 간주public boolean isNew(T entity) {
ID id = getId(entity);
Class<ID> idType = getIdType();
if (!idType.isPrimitive()) {
return id == null; // (A)
}
if (id instanceof Number) {
return ((Number) id).longValue() == 0L; // (B)
}
throw new IllegalArgumentException(
String.format("Unsupported primitive id type %s", idType));
}
null
이면 신규 (A)0
이면 신규 (B)@GeneratedValue
전략이면 메모리상 ID가 null
→ 언제나 신규id != null
→ isNew()
가 false
→ merge 호출Persistable
인터페이스 구현@Entity
public class Order implements Persistable<Long> {
@Id
private Long id;
@Override public Long getId() { return id; }
@Override public boolean isNew() { return id == null; } // or createdAt == null
}
ID / @Version / Persistable 세 조합으로 “신규 vs 기존”을 올바르게 판단해야 save() 가 원하는 대로 persist 혹은 merge 를 수행한다.
질문 | 답변 요약 |
---|---|
Q1. saveAll() 에서도 같은 로직이 적용될까? | 내부에서 각 엔티티마다 save() 호출 → isNew() 비용이 배수로 증가. 대용량이면 persist() + chunk flush(예: 1000건) 권장 |
Q2. 영속성 컨텍스트에 이미 존재하는 엔티티를 persist() 하면? | 같은 ID 있으면 EntityExistsException . Spring Data는 isNew() 로 걸러주지만 순수 JPA 코드에서는 직접 확인 필요 |
Q3. 복합 키(@EmbeddedId )를 쓰면 isNew() 는? | @EmbeddedId 객체 자체의 null 여부로 판단. 내부 필드가 비어 있어도 isNew() 가 false 될 수 있으니 조립 과정 주의 또는 Persistable 사용 |
isNew()
로그 확인persist()
vs merge()
각각 호출 → SQL 차이 분석saveAll()
대용량 배치에서 flush 간격에 따른 메모리·실행시간 비교