통상적으로 Spring Data Jpa를 사용하면 Hibernate를 사용하게 되고, 그로인해 dirty checking도 사용하게 된다.
그렇기 때문에 아래와 같이 코드에 따로 update를 명시하지 않아도, entity의 변경을 감지하여 update 문이 실행된다.
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @Transactional public class UpdatePostService { private final PostRepository postRepository; public UpdatePostService(PostRepository postRepository) { this.postRepository = postRepository; } public void update(Long postId, String title, String content) { var post = this.postRepository.findById(postId).orElseThrow(); post.update(title, content); } } interface PostRepository extends JpaRepository<Post, Long> { }
하지만 위 코드의 문제는 Spring Data JPA 의존적인 코드라는 점이다.
만약에 위 PostRepository의 구현체를 Spring Data JDBC와 같은 다른 ORM framework나 MyBatis와 같은 SQL mapping framework와 같이 dirty checking을 지원하지 않는 구현체로 갈아끼운다면 위 코드에서 update 문이 실행될 수 있을까?
분명 그렇지 않을 것이다. 이는 OCP를 명백하게 위반하는 부분이다.
그래서 이러한 Spring Data JPA 의존적인 코드를 지양하고자 아래와 같이 entity update 후 직접적으로 repository의 save 메서드를 호출하는 방법이 있을 것이다.
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @Transactional public class UpdatePostService { private final PostRepository postRepository; public UpdatePostService(PostRepository postRepository) { this.postRepository = postRepository; } public void update(Long postId, String title, String content) { var post = this.postRepository.findById(postId).orElseThrow(); post.update(title, content); this.postRepository.save(post); // <-- explicit save } } interface PostRepository extends JpaRepository<Post, Long> { }
이렇게하면 repository를 다른 구현체로 갈아끼워도 호환성에 있어서 문제가 없을 것이다.
까지는 여러 사람들이 생각을 해봤겠지만,
실제로 Spring Data JPA를 사용하면서 위와 같이 save를 명시적으로 호출하면 내부적으로 뭔가가 더 일어나게 된다.
JpaRepository의 구현체인 SimpleJpaRepository의 save 메서드를 살펴보면 아래와 같이 entity 상태에 따라 분기를 타게되는데,
현재 살펴보는 예제의 entity는 이미 DB로부터 조회된 entity이기 때문에 em.merge가 실행될 것이다.
package org.springframework.data.jpa.repository.support; @Repository @Transactional(readOnly = true) public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> { // ... @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); } } // ... }
merge가 실행되고 나서는 내부적으로 MergeEvent가 생성되고 MergeEventListener가 이 이벤트를 처리하게된다.
package org.hibernate.event.internal; public class DefaultMergeEventListener extends AbstractSaveEventListener<MergeContext> implements MergeEventListener { // ... public void onMerge(MergeEvent event, MergeContext copiedAlready) throws HibernateException { // 여러 과정을 거쳐 아래 entityIsPersistent 메서드 호출 } protected void entityIsPersistent(MergeEvent event, MergeContext copyCache) { LOG.trace( "Ignoring persistent instance" ); //TODO: check that entry.getIdentifier().equals(requestedId) final Object entity = event.getEntity(); final EventSource source = event.getSession(); final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity ); copyCache.put( entity, entity, true ); //before cascade! cascadeOnMerge( source, persister, entity, copyCache ); copyValues( persister, entity, entity, source, copyCache ); event.setResult( entity ); } // ... }
이는 불필요한 오버헤드를 일으키게 되며 오히려 save를 직접 호출하지 않는 것이 더 좋을 수 있다.
그럼 이를 어떻게 처리하면 좋을까?
Hypersistence Optimizer의 개발자 Vlad Mihalcea의 블로그 글에서는 이를 save method anti-pattern이라고 하며
JPA에는 save가 없기 때문에 이를 사용하지 말고, hypersistence-utils-hibernate의 HibernateRepository를 JpaRepository와 같이 사용하라고 얘기한다.
import io.hypersistence.utils.spring.repository.HibernateRepository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @Transactional public class UpdatePostService { private final PostRepository postRepository; public UpdatePostService(PostRepository postRepository) { this.postRepository = postRepository; } public void update(Long postId, String title, String content) { var post = this.postRepository.findById(postId).orElseThrow(); post.update(title, content); this.postRepository.update(post); // HibernateRepository.update } } @Repository interface PostRepository extends HibernateRepository<Post>, JpaRepository<Post, Long> { }
(예제 코드는 여기를 참고)
이렇게 함으로써 JPA에 존재하지 않는 save를 사용하는 것을 피하게 하고, 성능에 있어서 장점을 제공한다고 한다.
앞서 소개한 방법은 이런 방식도 있구나해서 정리를 한 것이지만, 너무 프레임워크 관련 지식이 필요 이상으로 많아지는 것 같다.
요청 처리에 있어서 너무 많은 리소스를 잡아먹지 않는 이상 그대로 JpaRepository.save를 쓰되,
트래픽이 어마무시하게 많아지거나 이로인한 문제가 발견되었을 때 갈아끼워도 되지 않을까 싶다. 🤔
'Java & Kotlin > Spring' 카테고리의 다른 글
[Spring] Swagger UI 대신 Scalar API Reference를 사용하여 API 문서 사용하기 (1) | 2024.10.29 |
---|---|
[Spring Data JPA] @OneToMany Entity 연관 관계에 대하여 (7) | 2023.01.20 |
[springdoc-openapi] 고정 header 설정하기 (0) | 2022.12.31 |
Request Rate Limiting with Spring Cloud Gateway - 부록. custom Filter 만들기 (0) | 2022.02.08 |
Request Rate Limiting with Spring Cloud Gateway - 3. RateLimiterFilter 파헤치기 (2) | 2022.02.08 |
댓글