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()를 호출JpaPersistableEntityInformationPersistable 인터페이스를 구현하면 사용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 간격에 따른 메모리·실행시간 비교