@TransactionalEventListener를 사용했을 때 발생한 문제에 대해 정리해 보려고 한다.
배경
개발하고 있는 서비스에 상점을 생성할 때 알림 정보도 생성되어야 하는 요구사항이 발생하였다.
- 상점이 정상적으로 생성되었을 때 알림 정보를 생성해야 한다.
- 알림 정보 생성 시 예외가 발생했을 경우 상점 생성으로 예외가 전파되면 안 된다.
즉 상점 생성 후에 알림이 생성되어야 하는 순서가 존재하지만 둘은 다른 트랜잭션으로 구분돼야 한다.
이 문제를 해결하기 위해 @TransactionalEventListener + @Async를 사용하여 트랜잭션을 분리하는 방법을 선택했다.
@TransactionalEventListener
- 상점과 알림 서비스간의 의존성을 줄일 수 있다.
- 트랜잭션이 완료될 때 리스너가 동작하게 된다.
-> AFTER_COMMIT 설정으로 상점 생성의 트랜잭션이 정상적으로 끝났을 때 알림을 생성할 수 있다.
@Async
- 알림 생성을 비동기로 실행한다.
그런데 실수로 @EnableAsync를 선언하지 않으면서 @Async가 동작하지 않게 되자 문제가 발생했다.
어떠한 문제였는지 확인해 보자.
문제 상황
예외가 발생하지 않고 있었다...
위와 같이 @Async를 주석 처리했을 경우 storeNotificationRepository.saveAll()의 insert 쿼리가 발생하지 않는 것을 보고 처음에는 트랜잭션이 동작하지 않는 줄 알았다. 하지만 디버깅을 해본 결과 insert 쿼리가 발생하지 않는 것이 아니라 예외가 발생하고 있었다는 사실을 알았다.
다만 이 예외가 전파되지 않고 DEBUG로 찍히고 있어서 log level이 INFO인 내 환경에서는 예외를 확인할 수 없었다.(예외가 발생하지 않게 처리를 한 후에는 정상적으로 기능이 동작하는 것을 확인했다.)
그렇다면 왜 예외가 전파되지 않고 있을까? 문제를 확인해 보자.
문제 확인
일단 stack trace를 확인해 보자.
TransactionSynchronizationUtils에서 DEBUG로 로그를 출력하는 것을 확인할 수 있다
2023-03-26 21:32:22.783 DEBUG 8113 --- [nio-8080-exec-2] o.s.t.s.TransactionSynchronizationUtils : TransactionSynchronization.afterCompletion threw exception
그럼 로그를 찍고 있는 코드도 한번 확인해 보자.
TransactionSynchronizationUtils.invokeAfterCompletion()에서 다음과 같이 afeterCompletion에서 발생한 예외를 잡아 로그로 출력하고 있다.
예외를 잡아서 전파하지 않고 DEBUG로 찍는 것을 실제로 확인했고, 여기서 두 가지 의문이 생겼다.
의문 1) @TransactionalEventListener의 phase 기본값은 AFTER_COMMIT인데 왜 afterCompletion 이 호출되는가?
의문 2) 왜 예외를 전파하지 않고 DEBUG로 로그를 찍는 것인가?
이 두 가지 의문에 대해 확인을 해보자.
의문 1) @TransactionalEventListener의 phase가 AFTER_COMMIT인데 왜 AfterCompletion 이 호출되는가?
일단 TransactionPhase의 AFTER_COMMIT의 설명을 확인해 보자.
This is a specialization of AFTER_COMPLETION and therefore executes in the same sequence of events as AFTER_COMPLETION (and not in TransactionSynchronization.afterCommit()).
AFTER_COMMIT은 AFTER_COMPLETION의 특수화로 AFTER_COMPLETION과 동일한 이벤트 시퀀스에서 실행이 된다.
즉 pahse가 AFTER_COMMIT일 경우 TransactionSynchronization.afterCommit()이 아니라 AFTER_COMPLETION과 같은 TransactionSynchronization.afterCompletion()이 실행된다는 것이다.
AFTER_COMMIT이 AFTER_COMPLETION과 동일한 이벤트 시퀀스에서 실행이 된다면, 둘의 차이점은 무엇일까?
AFTER_COMMIT과 AFTER_COMPLETION의 주요 차이점은 AFTER_COMMIT은 성공적인 커밋 후에만 실행되는 반면 AFTER_COMPLETION은 결과에 관계없이 트랜잭션이 완료된 후에 실행된다는 것이다.
의문 2) 왜 예외를 전파하지 않고 로그를 찍는 것인가?
@throws RuntimeException in case of errors; will be logged but not propagated
TransactionSynchronization.afterCompletion()의 JavaDocs를 확인해 보면 RuntimeException의 경우 기록되지만 전파되지는 않는다는 사실을 알 수 있다.
AFTER_COMMIT과 AFTER_COMPLETION은 트랜잭션이 완료된 이후에 이벤트를 실행해야 한다. 그런데 만약 이벤트 로직이 실행 과정에서 예외가 발생하게 되면 이전 트랜잭션의 부분까지 함께 롤백이 되게 때문에 aferCompletion에서 예외를 발생시키지 않고 예외를 잡아 log를 찍고 있는 것으로 예상된다.