AbstractAggregateRoot와 JPARepository save() 안티 패턴에 대해

Solomon Maeng
5 min readAug 11, 2022

--

편의상 평어체로 쓰는 점 이해 부탁드립니다 :)

최근 개발 관련 오픈 채팅 방에서 도메인 엔터티의 이벤트 발행 방식에 대해 의논하다가, Spring Data 프로젝트의 AbstractAggregateRoot를 사용하여 이벤트를 발행하고 있다는 다른 분의 얘기를 듣고 글을 쓰게 되었다.

전에 썼던 JPA Persistence Context 글에서도 언급 했지만, JPARepository를 사용할 때, 다음과 같은 코드를 주의해야 한다.

@Transactional
public void savePostTitle(Long postId, String title) {
Post post = postRepository.findOne(postId);
post.setTitle(title);
postRepository.save(post);
}

이미 로드한 엔터티를 변경하고, save()를 호출하게 되면 JPARepository.save()의 구현체인 SimpleJpaRepository에서 EntityManager.merge()를 호출하여 이미 persist 상태인 엔터티에 대해 다시 mergeEvent를 발생하게 한다.

아래와 같이 Hibernate의 DefaultMergeEventListener 클래스에 있는entityIsPersistent 메소드 내부의 copyValues()를 재 호출하게 되는 것이다.

protected void entityIsPersistent(MergeEvent event, Map copyCache) {
cascadeOnMerge( source, persister, entity, copyCache );
copyValues( persister, entity, entity, source, copyCache );

event.setResult( entity );
}

대다수의 JPA를 쓰는 사람들이 변경 감지라는 메커니즘을 알기 때문에 굳이 위와 같은 상황에서 save()를 호출하는 일을 만들지는 않겠지만, Spring Data 프로젝트의 AbstractAggregateRoot라는걸 사용하게 되면 상황이 달라진다.

1:N 관계의 Order와 OrderLine 엔터티를 구성하고 테스트를 해보았다.

실제로 MergeEvent가 발생하는지 확인할 OrderService 코드이다.

테스트 코드는 다음과 같이 작성하였다.

먼저 save()를 주석처리하고 테스트를 실행하면, 변경 감지 메커니즘에 의해서 Order 엔터티의 상태가 변경되고 코드가 잘 실행된다.

그런데 문제는 Order의 엔터티의 상태 변경을 전파하고자 OrderCompletedEvent라는 것을 사용하여 시스템 내부에서 이벤트를 발행하고 싶은 경우이다.

Order 엔터티에는 AbstractAggregateRoot를 상속 받았기 때문에, registerEvent() 메소드를 통해 이벤트를 등록해주고, Repository.save()를 호출하면 EventPublishingRepositoryProxyPostProcessor 라는 처리기에서 이벤트를 발행해주게 된다.

여기까지만 보면 아무 문제가 없는 것 같지만, AbstractAggregateRoot라는 Spring data의 클래스는 Repository.save()를 명시적으로 호출해주어야만 이벤트를 발행하도록 구현되어 있다.

따라서 위에서 언급했던 MergeEvent가 발생되는 것이다. MergeEvent는 detached 상태인 엔터티를 persist 상태로 만들기 위함이기 때문에, save()를 호출하면 총 4번(Order 1개, OrderLine 3개)의 MergeEvent를 다시 발생시키게 한다.

만약 하나의 엔터티가 다수의 엔터티와 연관관계를 맺고 있으며, 컬렉션으로 관리하고 있다면 때에 따라 문제가 될 소지가 있어보인다.

반드시 AbstractAggregateRoot 클래스를 상속해서 이벤트를 등록해야만 한다면, 어쩔 수 없지만 차선책으로 ApplicationEventPublisher를 사용해도 될 것 같다.

위 문제에 대하여 VladMihalcea라는 분이 다음 글에서https://vladmihalcea.com/best-spring-data-jparepository/ Hibernate의 Session.update()를 호출하는 것이 바람직하다고 언급하고 있다.

글을 읽고 난 후 우연찮게 댓글을 살펴보았는데 Spring Data Project의 일원인 Oliver Drotbohm 이 추상화와 세부사항에 대한 좋은 의견을 남겨놓았었다.

영어 실력이 부족하여 내용을 언급할 수 없지만, 궁금하면 한번 살펴보길 바란다.

예제 github : https://github.com/Rebwon/save-anti-patterns

--

--