JPA Persistable으로 성능최적화 해보기
개요
JPA에서 @GenerateValue를 사용하지 않고, ID를 직접 생성하는 경우 save = insert를 수행할 때 select 쿼리가 한번 나가는 경험을 하게 되어 왜 그런지? 어떻게 개선할 수 있는지 알아보고자 합니다.
JPA에서 Save는 어떻게 동작할까?
- 저장하려는 엔티티가 새로운 엔티티라면 persist
- 저장하려는 엔티티가 새로운 엔티티가 아니라면 merge
@Transactional
@Override
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null");
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
SimpleJpaRepository의 코드를 살펴보면 isNew인경우에 persist, 아닐 경우에는 merge를 수행하는 것을 볼 수 있습니다.
새로운 엔티티인지는 어떻게 판별할까?
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));
}
Entity의 신규 생성 여부는 JpaMetamodelEntityInformation을 거쳐 AbstractEntityInformation 클래스를 통해 primitive 타입이 아닌 경우에는 id가 null인 경우에만 새로운 엔티티로 판단합니다.
primitive 타입이라면 값이 0인 경우에만 새로운 엔티티라 판단합니다.
Merge는 어떻게 동작할까?
id가 null이 아니였기 때문에 merge가 동작했습니다.
merge는 엔티티의 식별자값을 1차 캐시 혹은 데이터베이스에서 조회하기 때문에 여기서 select query가 발생하게 됩니다.
이때 빈값은 null으로 넣어주기 때문에 주의해야 합니다.
SELECT 쿼리를 어떻게 최적화할 수 있을까?
Persistable 인터페이스를 활용하여 해결할 수 있습니다.
Persistable Interface를 상속받고 getId와 isNew함수를 구현해 주면 됩니다.
@Entity
class Foo(
name: String
) : Persistable<UUID> {
@Id
private val id: UUID = UUID.randomUUID()
@Column
var name: String = name
protected set
override fun getId(): UUID = id
override fun isNew(): Boolean = true
}
Delete시에 문제 발생
@Override
@Transactional
@SuppressWarnings("unchecked")
public void delete(T entity) {
Assert.notNull(entity, "Entity must not be null!");
if (entityInformation.isNew(entity)) {
return;
}
Class<?> type = ProxyUtils.getUserClass(entity);
T existing = (T) em.find(type, entityInformation.getId(entity));
// if the entity to be deleted doesn't exist, delete is a NOOP
if (existing == null) {
return;
}
em.remove(em.contains(entity) ? entity : em.merge(entity));
}
이렇게 최적화를 수행했을때 SimpleJpaRepository의 delete함수에서 문제가 발생할 수 있습니다.
entityInformation.isNew를 호출하고 새롭게 생성된 엔티티라면 remove를 호출하지 않고 return 해버립니다.
이를 해결하기 위해서는 영속화를 한 이후와 Entity를 조회했을 때는 isNew가 false여야 합니다.
JPA의 @PostPersist, @PostLoad를 활용하여 해결할 수 있습니다.
JPA 생명주기 이벤트에 대해 콜백을 받아 영속화 한 이후, 조회한 이후에 함수가 실행됩니다.
@PostPersist, @PostLaod 활용
@Entity
class Foo(
name: String
) : Persistable<UUID> {
@Id
private val id: UUID = UUID.randomUUID()
@Column
var name: String = name
protected set
override fun getId(): UUID = id
@Transient
private var _isNew = true
override fun isNew(): Boolean = _isNew
@PostPersist
@PostLoad
protected fun load() {
_isNew = false
}
}
isNew의 상태를 관리해주어야 하므로 프로퍼티를 추가하고 영속화는 하지 않도록 @Transient를 선언해 주었습니다.
이제 모든 문제가 해결되었습니다.
최적화를 수행해 보면서 JPA와 조금 더 친해진 것 같습니다.
참고자료
https://spoqa.github.io/2022/08/16/kotlin-jpa-entity.html