[JPA] 새로운 Entity인지를 판별하는 방법

짱챌·2025년 4월 16일

JPA

목록 보기
1/5
post-thumbnail

JPA에서 Entity가 새로운 것인지 판단하는 기준은
해당 엔티티의 식별자(ID)가 설정되어 있는지 여부이다.

핵심 로직

JpaEntityInformation<T, ID>.isNew(T entity)

JPA에서 엔티티가 새로운지 여부는 내부적으로 isNew(T entity)에서 메서드를 호출하여 판단한다.
이 메서드는 JpaEntityInformation 인터페이스의 구현체에서 제공하는데, 보통 JpaMetamodelEntityInformation 클래스가 실제로 동작한다.

@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);
}

✅ 기본 판단 방식

@Version이 없는 경우

엔티티에 @Version 필드가 없다면 -> AbstractEntityInformation.isNew() 메서드를 사용한다.
이 경우 판단 기준은 ID 필드 (@ID)의 값이다.

public boolean isNew(T entity) {
    Id id = getId(entity); // ID 필드의 값
    Class<ID> idType = getIdType(); // ID 타입

    if (!idType.isPrimitive()) {
        return id == null; // 객체 타입이면 null이면 새 Entity
    }

    if (id instanceof Number) {
        return ((Number) id).longValue() == 0L; // 숫자 타입이면 0이면 새 Entity
    }

    throw new IllegalArgumentException("지원하지 않는 primitive 타입");
}

✔ 즉,

  • Long, Integer 등의 래퍼 클래스 -> null이면 새 Entity
  • long, int 같은 기본형 -> 0이면 새 Entity

@Version이 있는 경우

@Version 필드는 낙관적 락을 위해 사용되는 필드이다.
이 필드가 있다면, 이걸 통해 새 엔티티인지 확인할 수 있다.
다만 primitive 타입이면 체크가 어려워서 @Id 기준으로 판단한다.

if(version 필드가 없거나 primitive 타입이면) {
    return id가 null이거나, 숫자인 경우 0이면 새 Entity
} else {
    return version 필드의 값이 null이면 새 Entity
}

✅ 정리

JPA는 @Version 필드가 있으면 그 값이 null인지,
@Version 필드가 없으면 @Id 값이 null 또는 0인지 확인해서 새 엔티티인지 판단한다.


➕ @Version?

@Version은 엔티티의 버정 정보를 나타내는 필드에 붙인다.
엔티티가 수정될 때마다 이 버전 값이 자동으로 증가한다.
이걸 이용해서 다른 트랜잭션이 같은 데이터를 먼저 수정헀는지 확인할 수 있다.


동시에 여러 사용자가 같은 데이터를 수정하려고 하면 문제가 생긴다. 이를 막기 위한 두 가지 방식이 있는데
락 방식설명
비관적 락데이터를 미리 잠가버림 (트랜잭션 동안 다른 사용자는 접근 불가)
낙관적 락일단 모두 접근 허용, 변경 시점에 충돌 검사

@Version은 이 중 낙관적 락을 구현하는 방법이다.


✅ 직접 ID를 할당하는 경우

Member member = new Member();
member.setId(100L); // ID 직접 지정
member.setName("홍길동");

memberRepository.save(member);

이렇게 직접 ID를 세팅하는 경우엔,
JPA는 idnull이 아니기 때문에 이미 존재하는 엔티티라고 착각해서 merge()를 실행한다.

이를 방지하기 위해 Persistable<T> 인터페이스를 사용한다.

Persistable<T> 인터페이스

JPA는 Persistable<T> 인터페이스를 구현한 엔티티의 경우
isNew() 메서드를 직접 호출해서 신규 여부를 판단해준다.

public class Member implements Persistable<Long> {

    @Id
    private Long id;
    
    private String name;

    @Transient
    private boolean isNew = true; // 생성 시점에 true로 설정

    @Override
    public Long getId() {
        return this.id;
    }

    @Override
    public boolean isNew() {
        return isNew;
    }

    // 저장 이후에는 false로 바꿔주는 코드도 필요할 수 있음
    @PostPersist
    public void markNotNew() {
        this.isNew = false;
    }
}

이렇게 하면 ID가 있어도, isNew()truepersist()를 호출한다.


내부 동작

public class JpaPersistableEntityInformation<T extends Persistable<ID>, ID>
       extends JpaMetamodelEntityInformation<T, ID> {

    @Override
    public boolean isNew(T entity) {
        return entity.isNew(); // 직접 구현한 isNew() 호출!
    }
}

✔ 즉, 일반적인 JpaMetamodelEntityInformation 대신
JpaPersistableEntityInformation이 동작하면서 커스텀 판단을 해준다.


✅ 신규 여부 판단이 중요한 이유

save() 메서드에서 핵심 로직은 다음과 같다

if (entityInformation.isNew(entity)) {
    entityManager.persist(entity); // 새 엔티티 → INSERT
} else {
    entityManager.merge(entity); // 기존 엔티티 → SELECT + UPDATE
}

🚨 만약 새 엔티티인데 잘못해서 merge()를 하면

  • merge()는 대부적으로 DB에 SELECT 쿼리를 먼저 날림
  • 따라서 진짜 존재하지 않는 ID라면 -> 불필요한 DB 조회 발생
  • insert는 되긴 하지만, 비효율적

profile
애옹: Magic Cat Academy

0개의 댓글