전부터 궁금했던 스프링 이벤트에 대해 써볼 기회가 생겨서 정리하는 글을 작성해봅니다.
Spring에는 이벤트 발행과 구독 기능(Event Publishing and Listening)이 있습니다.
간단한 동작구조는 다음과 같습니다.

- 특정 로직이 실행되면 이벤트를 발행해서,
- 이벤트리스너가 이를 처리하는 구조이다.
스프링 이벤트를 사용하면 이벤트 발행 도메인과 처리 도메인간의 강한 의존성을 줄일 수 있어 유용하다네요.
메인 트랜잭션 스레드와 이벤트 리스너를 처리하는 스레드는 동일합니다.
쉽게 말해서 동기 처리입니다.
동기 처리의 단점은 작업을 순차적으로 처리하기 때문에 처리 소요 시간이 누적형, 오래 걸린다는 것이다.
스프링은 @Async 어노테이션으로 비동기 이벤트 처리도 지원합니다. 이를 통해 메인 트랜잭션과 이벤트 처리를 별도의 스레드에서 실행해 성능을 최적화할 수 있습니다. 그러나 비동기 처리는 순서와 트랜잭션 일관성을 보장하지 않습니다. 기존 트랜잭션 컨텍스트와는 분리된 상태로 작업이 수행되기 때문입니다. 스레드가 여러 개가 되므로 동시성 문제도 고려해야 될 것입니다.
만약 비동기 리스너를 사용한다 하면 새로운 스레드에서 실행되므로 트랜잭션 컨텍스트를 공유하지 않아 메인 트랜잭션이 종료되기 전에 리스너가 비동기적으로 실행될 수 있고, 이로 인해 트랜잭션 일관성이 무너질 수 있습니다.
예를 들면, 좋아요를 다는 도중에 예외가 발생해서 롤백이 됐는데, 좋아요가 달렸다는 알림은 가버리는 상황이 발생할 수 있다는 것이죠.
즉, 메인 트랜잭션이 커밋되면 리스너가 실행됐으면 좋겠다, 이런 상황을 해결할 수 있는 것이 @TransactionalEventListener 입니다.
TransactionalEventListener는 EventListener의 실행 시점을 지정할 수 있습니다.
실행 시점 옵션은 다음과 같습니다.
- AFTER_COMMIT (기본값) - 트랜잭션이 성공적으로 마무리(commit)됬을 때 이벤트 실행
- AFTER_ROLLBACK – 트랜잭션이 rollback 됬을 때 이벤트 실행
- AFTER_COMPLETION – 트랜잭션이 마무리 됐을 때(commit or rollback) 이벤트 실행
- BEFORE_COMMIT - 트랜잭션의 커밋 전에 이벤트 실행
TransactionalEventListener 내부에선 TransactionSynchronizationManager의 콜백 메서드들이 동작합니다.
TransactionSynchronizationManager는 스레드 로컬에 저장된 트랜잭션 컨텍스트를 추적하고 동기화를 위한 콜백 메서드를 제공합니다.
만약 트랜잭션이 커밋된다면, TransactionalEventListener는 내부적으로 다음과 같이 동작합니다.
- TransactionSynchronizationManager는 항상 List< TransactionSynchronization>를 관리한다. (TransactionSynchronization은 스프링 트랜잭션이 커밋 또는 롤백될 때 실행할 콜백을 등록할 수 있게 해주는 인터페이스)
- @TransactionalEventListener는 하나의 이벤트당 하나의 TransactionalApplicationListenerSynchronization을 생성하여 이 리스트에 추가한다. (TransactionSynchronization은 TransactionalApplicationListenerSynchronization의 상위 인터페이스)
- 트랜잭션이 커밋되면, TransactionSynchronizationManager는 이 리스트를 순회하며 각 TransactionSynchronization 객체의 afterCommit() 메소드를 호출한다.
- TransactionalApplicationListenerSynchronization 객체는 자신이 캡슐화하고 있던 이벤트를 해당 TransactionalEventListener에게 전달하여 비즈니스 로직을 실행한다.
결국 쉽게 말해 각 이벤트 리스너들이 메세지 발행을 리스트에 넣어서 예약 해놓고, 커밋 되면 afterCommit()을 호출해 예약을 실행하는 구조입니다.
여기서 주의해야할 점이 있는데, TransactionalEventListener의 경우 이벤트 리스너 내에 새로운 트랜잭션이 존재한다면, 이를 명시적으로 시작해줘야 트랜잭션이 실행됩니다.
왜냐면 TransactionalEventListener(phase = AFTER_COMMIT)이 실행되는 시점은 트랜잭션 커밋 후라 합류할 트랜잭션 컨텍스트가 존재하지 않기 때문입니다. 그냥 @Transactional만 붙이는 것도 안됩니다. 기본 옵션이 REQUIRED인데, 합류할 트랜잭션 컨텍스트가 존재하지 않기 때문에 트랜잭션이 시작되지 않습니다.
따라서 @Transactional(propagation = Propagation.REQUIRES_NEW) 를 추가 설정해줘야 하는데, 리스너 메서드가 이전 트랜잭션과 별도로 새로운 트랜잭션을 시작하겠다는 뜻입니다. 메인 트랜잭션의 커밋을 보장하며 리스너에서도 새로운 트랜잭션을 시작할 수 있게 합니다.
여기까지 스프링 이벤트의 동작 구조와 TransactionalEventListener, 주의점까지 알아봤습니다. 그럼 결론적으로 어떻게 이벤트를 처리해야 할까요?