문제
Spring Boot 2.7 에서 3.2.3 로 마이그레이션을 하던 중 아래와 같은 에러가 발생했다.
원인
에러 내용을 읽어보면 @TransactionalEventListener 가 쓰인 곳에서 @Transactional 을 사용하려면 반드시 REQUIRES_NEW 또는 NOT_SUPPORTED 전파속성을 명시해야 한다고 한다.
에러가 발생한 코드는 스프링 이벤트 리스너를 사용하면서 커밋된 후 이벤트를 발생시키기 위해서
(관심사와 비관심사 로직을 분리하기 위해)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) 을 사용했고 이 트랜잭션과 분리하여 새 트랜잭션에서 커밋을 발생시키기 위해 @Transactional 을 같이 사용하였다.
@Transactional
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void event(EventModel event) {
/**
* 비즈니스 로직
*/
}
@Transactional 어노테이션을 사용할 때 기본 전파 속성은 REQUIRED 이다.
그리고 실제로 의도한대로
관심사 로직에서 커밋이 된 후에야 이벤트 리스너가 호출되고 비관심사 로직에서는 예외가 발생해도 관심사 로직은 이미 커밋되었으므로 롤백이 된다거나 하는 일이 일어나지 않았다. 이 코드가 spring boot 3 버전 이전까지는 가능했고 아무런 에러도 발생하지 않았다.
그렇다면 Spring Boot 3.2.3 에서 무엇이 바뀌었길래 에러가 발생한걸까?
발생한 에러 로그를 보면 RestrictedTransactionalEventListenerFactory 클래스에서 예외가 발생했음을 알 수 있다.
at org.springframework.transaction.annotation.RestrictedTransactionalEventListenerFactory.createApplicationListener(RestrictedTransactionalEventListenerFactory.java:44)
실제로 클래스에 들어가보면 해당 조건문이 있는 것을 볼 수 있다.
추가로 @TransactionalEventListener 의 @interface 를 들어가서 TransactionSynchronization 을 들어가보자.
java docs 를 읽어보면 afterCommit 일 때 모든 트랜잭션의 전파속성을 REQUIREDS_NEW 로 명시하라고 되어있다.
그리고 NOTE 부분을 조금 더 읽어보면
트랜잭션은 이미 커밋되었지만 트랜잭션 리소스는 여전히 활성화되어 있고 액세스할 수 있습니다. 따라서 이 시점에서 트리거된 모든 데이터 액세스 코드는 별도의 트랜잭션에서 실행해야 한다고 명시적으로 선언하지 않는 한 여전히 원래 트랜잭션에 '참여'하여 일부 정리를 수행할 수 있습니다(더 이상 커밋을 따르지 않아도 됩니다!).
라고 쓰여있다. 이말인 즉슨 REQUIREDS_NEW 전파속성은 새 트랜잭션을 생성하고 현재 트랜잭션이 있다면 이를 일시중지 하기 때문에 명시적으로 새 트랜잭션을 실행할 수 있도록 선언해야 한다는 뜻이다.
그렇다면 좀 더 안전(?) 하게 하기 위해서 Spring Boot 3.2 부터 이런 제약이 생긴걸까?
2.7 로 돌아가서 java docs 를 보았는데 똑같이 권장하고 있었다. 그렇다면 RestrictedTransactionalEventListenerFactory 이곳에서 변화가 일어난 것 같아서 조금 더 알아보았다.
RestrictedTransactionalEventListenerFactory 클래스의 createApplicationListener 메서드가 호출되는 곳은 EventListenerMethodProcessor 의 processBean 메서드이다.
EventListenerMethodProcessor 클래스를 살펴보면 List 를 멤버변수로 가지고 있다.
그리고 아래 코드를 보면 EventListenerFactory 리스트를 토대로 반복문을 돌려 applicationListener 를 생성하고 있는 것을 볼 수 있다.
그러면 EventListenerFactory 를 상속하고 있는 구현체들은 무엇들이 있을까?
인텔리제이의 show diagram 기능을 통해서 보면 아래와 같은 관계를 가지고 있는 것을 볼 수 있다.
그렇다면 이런 문제가 발생하지 않았던 Spring Boot 2.7.3 버전에서는 왜 발생하지 않았는가 하니 RestrictedTransactionalEventListenerFactory 클래스가 존재하지 않았다. 사실 위에 첨부한RestrictedTransactionalEventListenerFactory 클래스 이미지의 @since 를 보면 미리 알 수 있었을 것이다.
실제로 빨간점을 찍고 디버깅해보면 아무런 제약조건 없이 applicationListener 를 생성하는 것을 볼 수 있다.
직접 라이브러리를 들어가서 보면 클래스다이어그램을 만들어보고 클래스들을 볼 수 있다.
(Spring Boot 2.7.3 은 spring-tx:5.3.22 이다)
해결
원인의 대한 설명이 길었지만 결국 @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) 일 때 트랜잭션을 완전히 분리하려면 트랜잭션 전파 속성은 반드시 REQUIRES_NEW 로 명시해주어야 한다.
스프링 부트 버전 마이그레이션을 진행하면서 이전 버전의 내부 클래스 코드와 마이그레이션할 버전의 내부 클래스 코드를 번갈아 가면서 디버깅해보다 보니 생각보다 재미있고 리스너를 생성하는 구조를 알게되어 좋았던 것 같다.
또한… 진작에 전파속성을 명시하는 습관이 있었다면 이런 에러는 만나지 않았을텐데! 습관을 잘 들여야겠다.