통상적으로 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 |
댓글