본문 바로가기

CS

Spring Data JPA에서 새로운 Entity인지 판단하는 방법은 무엇일까요?

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