JpaRepository.save() 안티패턴

이상민·2023년 1월 12일
1

1. save()가 안티패턴이라고?

이 글은 Hibernate Types의 개발자 Vlad Mihalcea블로그 글의 내용을 담고있다. 해당 블로그 글은 아래와 같은 이유로 save() 메소드를 호출하는 것이 안티패턴인 경우에 대해 설명한다.

미리 요약하자면 성능 오버헤드가 있다는 이야기이다. Mihalcea가 High-Performance Java Persistence라는 책의 저자인 만큼 성능에 중점을 두는 것 같다. 하지만 클라우드 환경에서는 백엔드 어플리케이션을 한없이 스케일 아웃할 수 있는데 이렇게까지 해야하나하는 생각이 든다. 그리고 JPA의 스펙에 맞추기 위해 너무 벤더 특정적인 코드를 만들게 되지 않을까하는 걱정도 된다.

나만 이렇게 생각한 것이 아닌지 블로그 글의 댓글창에 가면 반발하는 목소리가 많다 ㅋㅋㅋㅋ. Mihalcea도 이를 인지하고 있고 "성능이 더 필요한 경우에 사용하는 익스텐션 개념으로 생각하라"라고 말하고 있다.

아래 글에서 설명하겠지만 배치에 경우에는 확실히 save메소드를 사용하는데 주의해야할거 같다.


2. JPA의 구조

  • JPA에 save란 메소드는 없다. 왜냐하면 JPA는 액티브 레코드 패턴이 아닌 ORM 패러다임을 구현하기 때문이다

액티브 레코드 패턴이란?
관계형 데이터베이스에 인메모리 객체를 저장할때 사용되는 아키텍쳐 배턴이다. 모델에 쿼리 메소드를 정의하고, 해당 메소드를 사용하여 모델을 저장, 제거, 수정한다. 마틴 파울러의 P of EAA에서 등장했다

  • JPA는 엔티티 상태 머신이라고 볼 수 있다.
    • 새로운 엔티티를 만든다면 persist를 호출하여 엔티티가 관리되도록하고, flush를 통해 데이터베이스에 삽입한다
    • 엔티티가 detached인 상태에서 변경을 하면, 변경이 데이터베이스로 전파되도록 해야한다. 이를 위해 mergeupdate를 한다. merge는 엔티티의 영속성 컨텍스트에 의해 로딩된 새로운 객체에 상태를 복사하고 flush 시점에 업데이트가 필요한지 확인한다. update는 현재 엔티티 상태로 flush를 트리거 하도록 강제한다
    • remove 메소드는 삭제를 예약하고, flush가 삭제 쿼리를 트리거한다

  • 하지만 JpaRepository는 CrudRepository로부터 save메소드를 상속한다
  • JpaRepository의 save 메소드는 아래처럼 구현되어 있다
@Transactional
public <S extends T> S save(S entity) {
    if (this.entityInformation.isNew(entity)) {
        this.em.persist(entity);
        return entity;
    } else {
        return this.em.merge(entity);
    }
}

3. JpaRepository.save()가 안티패턴인 경우

  • JpaRepository가 save메소드를 제공하기 때문에, 대부분의 개발자들이 자기도 모르게 아래와 같은 안티패턴에 빠지게 된다
@Transactional
public void saveAntiPattern(Long postId, String postTitle) {        
    Post post = postRepository.findById(postId).orElseThrow();
    post.setTitle(postTitle);
    postRepository.save(post);
}
  • 위 코드에서 문제인 부분은 불필요할 뿐만 아니라 비용이 드는 save 메소드의 호출이다
  • 관리되고 있는 엔티티를 merge하면 MergeEvent가 트리거 되면서 CPU 사이클을 사용하게 된다
  • 이외에도 save 메소드는 엔티티가 새로운 객체인지 판단하지 못할때도 있다. 만약 식별자가 할당되어 있는 엔티티라면, Spring Data JPA는 persist대신 merge를 호출하게 된다. 이로인해 불필요한 조회 쿼리가 실행 될 수 있다.
  • 이는 배치 프로세싱 작업에서 더욱 문제가 될 수 있다.

4. JpaRepostory.save()의 대안

  • 대안은 커스텀 레포지토리 인터페이스를 만드는 것이다. 이 인터페이스는 JpaRepository의 save를 deprecate 시킨다
  • 해당 인터페이스는 JPA의 스펙에 맞게 merge, update과 같은 메소드를 사용하도록 강제한다
public interface HibernateRepository<T> {
 
    //The findAll method will trigger an UnsupportedOperationException
 
    @Deprecated
    List<T> findAll();
 
    //Save methods will trigger an UnsupportedOperationException
     
    @Deprecated
    <S extends T> S save(S entity);
 
    @Deprecated
    <S extends T> List<S> saveAll(Iterable<S> entities);
 
    @Deprecated
    <S extends T> S saveAndFlush(S entity);
 
    @Deprecated
    <S extends T> List<S> saveAllAndFlush(Iterable<S> entities);
 
    //Persist methods are meant to save newly created entities
 
    <S extends T> S persist(S entity);
 
    <S extends T> S persistAndFlush(S entity);
 
    <S extends T> List<S> persistAll(Iterable<S> entities);
 
    <S extends T> List<S> peristAllAndFlush(Iterable<S> entities);
 
    //Merge methods are meant to propagate detached entity state changes
    //if they are really needed
     
    <S extends T> S merge(S entity);
 
    <S extends T> S mergeAndFlush(S entity);
 
    <S extends T> List<S> mergeAll(Iterable<S> entities);
 
    <S extends T> List<S> mergeAllAndFlush(Iterable<S> entities);
 
    //Update methods are meant to force the detached entity state changes
 
    <S extends T> S update(S entity);
 
    <S extends T> S updateAndFlush(S entity);
 
    <S extends T> List<S> updateAll(Iterable<S> entities);
 
    <S extends T> List<S> updateAllAndFlush(Iterable<S> entities);
 
}
  • 위 인터페이스를 구현하고 아래처럼 사용한다. 구현체에 대한 내용은 Vlad의 블로그 글에서 확인할 수 있다.
@Repository
public interface PostRepository extends HibernateRepository<Post>, JpaRepository<Post, Long> {
 	...
}

참고

https://vladmihalcea.com/best-spring-data-jparepository/

profile
편하게 읽기 좋은 단위의 포스트를 추구하는 개발자입니다

0개의 댓글