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 필드가 없다면 -> 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이면 새 Entitylong, int 같은 기본형 -> 0이면 새 Entity@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은 이 중 낙관적 락을 구현하는 방법이다.
Member member = new Member();
member.setId(100L); // ID 직접 지정
member.setName("홍길동");
memberRepository.save(member);
이렇게 직접 ID를 세팅하는 경우엔,
JPA는 id가 null이 아니기 때문에 이미 존재하는 엔티티라고 착각해서 merge()를 실행한다.
이를 방지하기 위해 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()가 true면 persist()를 호출한다.
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 쿼리를 먼저 날림