JpaMetamodelEntityInformation의 isNew()
public class JpaMetamodelEntityInformation<T, ID> extends AbstractEntityInformation<T, ID>
implements JpaEntityInformation<T, ID> {
private final Optional<Attribute<?, ?>> versionAttribute;
// 생성자와 다른 필드 및 메서드들...
@Override
public boolean isNew(T entity) {
if(versionAttribute.isEmpty() // @Version 어노테이션이 적용된 필드가 없는지 확인
|| versionAttribute.map(Attribute::getJavaType).map(Class::isPrimitive).orElse(false)
// @Version 필드가 기본(primitive)타입인지 확인
) {
return super.isNew(entity);
}
// @Version이 Wrapper 클래스일 경우
BeanWrapper wrapper = new DirectFieldAccessFallbackBeanWrapper(entity);
// entity 필드에 접근 -> @Version 필드의 값이 null인지 확인하여 null이면 새 엔티티로 판단
return versionAttribute.map(it -> wrapper.getPropertyValue(it.getName()) == null).orElse(true);
}
// 기타 메서드들...
}
새로운 Entity인지 여부는 `JpaEntityInformation`의 `isNew(T entity)` 함수에서 다음과 같은 과정을 거쳐 판단한다.
- 엔티티에 `@Version` 어노테이션이 적용된 필드가 없는지 확인 +
`@Version`이 적용된 필드가 있다면 해당 필드가 기본(primitive) 타입인지 확인 - 1번 조건을 만족하면 `AbstractEntityInformation`의 `isNew()`를 호출
1번 조건을 만족하지 않으면 `@Version`은 Wrapper 클래스임 - `DirectFieldAccessFallbackBeanWrapper`를 사용해 엔티티의 필드에 접근
- `@Version` 필드의 값이 `null`인지 확인하여 `null`이면 새 엔티티로 판단
`@Version` 조건의 의미
`@Version` 필드가 없다면 ➡️ `@Version` 기반 판단이 불가능
`@Version` 필드가 기본 타입이라면 ➡️ 기본 타입의 초기값인 0 실제 version에 저장된 0과 구분이 불가능
`@Version` 필드가 참조 타입(래퍼 클래스)이라면 ➡️ `null` 체크로 정확한 판별 가능
`AbstractEntityInformation`의 `isNew()`
public boolean isNew(T entity) {
Id id = getId(entity);
Class<ID> idType = getIdType();
if (!idType.isPrimitive()) {
// id가 null이면 새 엔티티
return id == null;
}
if (id instanceof Number) {
// id가 0이면 이면 새 엔티티
return ((Number) id).longValue() == 0L;
}
// 지원하지 않는 타입이라면 예외 발생
throw new IllegalArgumentException(String.format("Unsupported primitive id type %s", idType));
}
`AbstractEntityInformation`의 `isNew`()는 `@Id` 필드를 통해 새 엔티티인지 판단한다.
`@Id`의 필드가 기본 타입이 아니라면 `null`의 여부, `Number`의 하위 타입이라면 0인지의 여부를 통해 새 엔티티인지 판단한다.
💡 "왜 Id가 0이면 새 엔티티일까? 컴퓨터는 첫 요소를 0으로 표현하지 않나?"라는 의문을 가질 수 있다.
이는 `@GeneratedValue` 어노테이션으로 키 생성 전략을 사용하면 데이터베이스에 저장될 때 id가 할당되는데, 대부분 0보다 큰 값을 할당하기 때문이다.
키 생성 전략을 사용하지 않고 직접 Id를 할당한다면?
키 생성 전략을 사용하지 않고 직접 Id를 할당하는 경우 새로운 엔티티로 간주되지 않는다. 이 때는 엔티티에서 `Persistable<T>` 인터페이스를 구현해 `JpaMetamodelEntityInformation` 클래스가 아닌 `JpaPersistableEntityInformation`의 `isNew()`가 동작하도록 해야한다.
public class JpaPersistableEntityInformation<T extends Persistable<ID, ID>
extends JpaMetamodelEntityInformation<T, ID> {
public JpaPersistableEntityInformation(Class<T> domainClass, Metamodel metamodel,
PersistenceUnitUtil persistenceUnitUtil) {
super(domainClass, metamodel, persistenceUnitUtil);
}
// 엔티티 자체의 isNew()를 호출해 새 엔티티인지 판단
@Override
public boolean isNew(T entity) {
return entity.isNew();
}
// 엔티티의 ID를 가져오고 새 엔티티라면 null일 수도 있음
@Nullable
@Override
public ID getId(T entity) {
return entity.getId();
}
}
----------------------------------------------------------------------------------------
public interface Persistable<ID> {
ID getId();
boolean isNew();
}
⚠️ 직접 Id를 할당할 때 문제점
Id를 직접 할당하는 엔티티의 경우, `isNew()` 메서드는 기본적으로 Id의 존재 여부로 판단한다.
신규 엔티티이지만 이미 존재하는 Id를 할당할 수도 있고, 그렇다면 `merge()`를 호출해 새 엔티티임에도 불구하고 데이스베이스에 두 번이나 접근 (select + insert) 하기 때문이다.
➡️ `Persistable` 인터페이스를 구현해서 생성 시간을 기준으로 새 엔티티를 판별해 해결할 수 있다.
새 Entity인지 판단하는게 왜 중요할까?
@Repository
public class SimpleJpaRepository<T, ID> implements JpaRepository<T, ID> {
private final JpaEntityInformation<T, ?> entityInformation;
private final EntityManager entityManager;
// 생성자 및 기타 필드 생략...
@Override
@Transactional
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null");
if (entityInformation.isNew(entity)) {
// 새 엔티티라면 persist 수행
entityManager.persist(entity);
return entity;
} else {
// 새 엔티티가 아니라면 merge 수행
return entityManager.merge(entity);
}
}
// 기타 메서드들 생략...
}
`SimpleJpaRepository`의 `save()`메서드에서 `isNew()`를 사용하여 `persist`를 수행할지 `merge`를 수행할지 결정한다. 만약 ID를 직접 지정해주는 경우 신규 엔티티라고 판단하지 않기 때문에 `merge`를 수행한다. 이 때 해당 엔티티가 새 엔티티임에도 불구하고 DB를 조회하기 때문에 비효율적이다. 따라서, 새로운 엔티티인지 판단하는 것은 중요한 부분이다!!
💡 persist vs merge
persist
새로운 엔티티를 데이터베이스에 삽입한다.
데이터베이스 조회 없이 바로 insert를 수행한다.
merge
분리(detached) 상태의 엔티티를 영속 상태로 변경한다.
데이터베이스를 조회해
조회 결과가 없으면 새 엔티티로 간주하고 영속화 후 insert,
조회 결과가 있으면 변경 사항을 적용하고 update
각 과정을 수행한다.
`@Version` 어노테이션이 뭐길래 해당 어노테이션을 기준으로 새 엔티티인지 판단할까?
`@Version`
`@Version` 어노테이션은 JPA를 사용할 때 낙관적 락(Optimistic Locking)을 구현하기 위해 사용되는 어노테이션이다.
`@Version`이 적용된 필드는 엔티티 업데이트마다 자동으로 증가하며, 동시성 제어를 위한 확인 수단으로 사용된다.
✅ 특징
- 자동 증가
- JPA가 자동으로 관리하고 `@Version` 필드 값을 직접 변경해도 이를 무시하고 JPA가 자체적으로 증가 - 하나의 필드만 허용
- 한 엔티티 클래스는 하나의 `@Version` 필드만 가짐 - null값 처리
- `Ingeter`, `Long`같은 참조 타입에서 `@Version` 필드가 `null`이면 새 엔티티로 간주 - 트랜잭션 경계
- `@Version` 필드 증가는 트랜잭션이 커밋될 때 발생
- 같은 트랜잭션 내에서 여러번 수정해도 `@Version` 필드는 한 번만 증가
💡 낙관적 락(Optimistic Lock)과 비관적 락(Pessimistic Lock)
가볍게 보고 넘어가자. 뭔지는 궁금하니까. 그렇다고 딥하게 찾아보면 오늘 이 글 못 쓴다......
두 가지 락은 데이터베이스에서 동시성 제어를 위한 방식이다.
낙관적 락
'충돌이 드물게 발생한다'라고 가정해,
데이터 접근 시 실제로 락을 걸지 않고 데이터 변경 시점에 충돌을 검사하는 방식
장점:
- 락으로 인한 대기시간이 없어 성능이 좋다.
- 데드락이 발생하지 않는다.
단점:
- 충돌 발생시 해결하는 로직을 구현해야 한다.
- 충돌이 자주 발생하는 환경에선 재시도 처리로 인한 오버헤드가 발생할 수 있다.
비관적 락
'충돌이 자주 발생한다'라고 가정해,
데이터에 접근하기 전에 실제로 데이터베이스 수준의 락을 획득하는 방식
장점:
- 충돌 가능성이 높은 환경에서 데이터 일관성을 보장한다.
- 충돌 해결을 위한 추가 로직이 필요 없다.
단점:
- 락 획득 및 관리로 인한 성능 저하가 발생할 수 있다.
- 데드락이 발생할 가능성이 있다.
- 확장성이 제한될 수 있다.
'Spring > Spring Data JPA' 카테고리의 다른 글
| [Spring Data JPA] ID에 관하여 (2) | 2025.03.13 |
|---|---|
| [Spring Data JPA] JPA의 N + 1 문제 (0) | 2025.03.07 |
| [Spring Data JPA] 엔티티 매니저(Entity Manager) (0) | 2025.03.05 |
| [Spring Data JPA] 영속성 컨텍스트(Persistence Context) (0) | 2025.03.04 |
| [Spring Data JPA] JPA의 ddl-auto 옵션 (0) | 2025.02.28 |