Spring Data JPA를 사용하다 보면 save() 메서드를 많이 사용하게 됩니다. 그런데 이 save() 내부에서는 도대체 어떤 기준으로 insert 할지, 아니면 update 할지를 결정할까요? 그 판단의 핵심이 바로 isNew() 메서드입니다.
이번 글에서는 isNew()가 어떻게 동작하는지, 실제 원본 코드와 함께 천천히 풀어보며 이해해봅시다.
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);
return entity;
} else {
return entityManager.merge(entity);
}
}
우선 Spring Data JPA의 save()는 내부적으로 위의 코드와 같이 작동하게 됩니다.
조건문 분기를 보면 알 수 있듯이, true 값이 나오게 되면 persist()를 아니면 merge()를 수행하게 됩니다. 여기서 각각의 차이점은 persist() → 새 엔티티로 간주하여 insert를 하는 것이며 merge() → 기존 엔티티로 간주하여 select + update를 하는 것입니다.
이렇게 isNew()가 언제 true인지, 언제 false인지 아는 것이 성능과 동작을 이해하는 데 매우 중요합니다.
어떤 기준으로 판단이 되는가?
Spring Data JPA는 내부적으로 JpaEntityInformation이라는 인터페이스를 통해 엔티티 정보를 추상화합니다. 각 구현체가 어느 상황에서 동작하는 지 살펴봅시다.
일반적인 경우 (ID 기반 판단)
기본적으로는 AbstractEntityInformation의 isNew() 메서드가 실행됩니다:
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));
}
- ID가 primitive 타입이 아니고, null이면 → 새 엔티티로 간주 (persist)
- ID가 숫자형(예: Long) 이고, 값이 0이면 → 새 엔티티로 간주 (persist)
- 나머지는 → 기존 엔티티로 간주 (merge)
@Version이 있는 경우
@Override
public boolean isNew(T entity) {
if (versionAttribute.isEmpty()
|| versionAttribute.map(Attribute::getJavaType).map(Class::isPrimitive).orElse(false)) {
return super.isNew(entity); // 위에 본 ID 기반 판단 방식
}
BeanWrapper wrapper = new DirectFieldAccessFallbackBeanWrapper(entity);
return versionAttribute.map(it -> wrapper.getPropertyValue(it.getName()) == null).orElse(true);
}
만약 엔티티에 @Version 필드가 있다면, JpaMetamodelEntityInformation 쪽 코드에서 다음처럼 동작합니다. 즉, @Version이 존재하고 primitive 타입이 아니면, 버전 필드가 null인지 여부로 새 엔티티 여부를 판단합니다
예제 코드
@Entity
public class User {
@Id @GeneratedValue
private Long id;
private String name;
}
User user = new User();
user.setName("홍길동");
userRepository.save(user); // id == null → isNew() == true → persist 실행
User user = new User();
user.setId(1L);
user.setName("임꺽정");
userRepository.save(user); // id != null → isNew() == false → merge 실행
중요: 개발하다가 맞닥뜨릴 수 있는 문제점 -> 직접 ID를 할당하는 경우
@GeneratedValue 없이 ID를 수동으로 지정할 경우, id != null 이기 때문에 Spring은 기존 엔티티로 간주합니다. 하지만 실제로 DB에는 없는 객체라면, merge()로 인해 불필요한 select 쿼리가 발생하게 됩니다.
이러한 경우를 해결하려면 다음과 같이 Persistable<T> 인터페이스를 구현해야 합니다.
@Entity
public class User implements Persistable<Long> {
@Id
private Long id;
private String name;
@Transient
private boolean isNew = true;
@Override
public Long getId() {
return id;
}
@Override
public boolean isNew() {
return this.isNew;
}
public void setNotNew() {
this.isNew = false;
}
}
이렇게 하면 JpaPersistableEntityInformation 클래스가 적용되어 isNew()의 판단 기준을 직접 정할 수 있습니다.
마무리
Spring JPA는 햄버거 가게로 생각하시면 편합니다. 주문서에 주문 번호가 없을 경우, 새 주문으로 간주해서 persist()를 호출해 새 엔티티로 저장합니다.
주문 번호가 있을 경우는 기존 주문으로 간주하여 select + update를 하는 merge()를 하게 됩니다. 하지만 어떤 손님은 주문 번호를 직접 적어서 제출하기도 하는데 Spring은 "이거 예전에 만든 거 아냐?"라고 생각해서 DB에 찾아보러 갑니다. 그런데 DB에 없으면? 쿼리 낭비가 되는 거죠 ㅎㅎ
그래서 새주문임을 알려줄 필요가 있는데 그게 바로 Persistable 입니다.
'CS' 카테고리의 다른 글
| 데이터베이스 인덱스에 대해서 설명해주세요 (0) | 2025.04.12 |
|---|---|
| 일급 컬렉션이 무엇인가요? (0) | 2025.04.09 |
| 스프링 Ioc와 DIP란? (1) | 2025.02.10 |
| 트랜잭션 전파 제어 (0) | 2025.01.27 |
| Thread Pool이란? (1) | 2025.01.20 |