@TransactionalEventListener로 문제를 해결한 경험이 있다. 하지만 문제 해결에 그치고 싶지 않았고 이 어노테이션의 동작 방식을 더 깊이 알아보고 싶었다. 그 학습 여정을 공유하는 글이다.
@TransactionalEventListener 이벤트 처리 시점
아래와 같이 @TransactionalEventListener 어노테이션의 phase 옵션을 통해 이벤트가 처리되는 시점을 지정할 수 있다.
BEFORE_COMMIT | 트랜잭션이 커밋되기 이전에 이벤트를 처리한다. |
AFTER_COMMIT | 트랜잭션이 성공적으로 커밋된 후 이벤트를 처리한다. (default) |
AFTER_ROLLBACK | 트랜잭션이 롤백될 때 이벤트를 처리한다. |
AFTER_COMPLETION | 트랜잭션이 완료(커밋 혹은 롤백)된 후 이벤트를 처리한다. |
@EventListener vs @TransactionalEventListener(BEFORE_COMMIT)
@EventListener가 동작하는 방식과 @TransactionalEventListener의 BEFORE_COMMIT 옵션이 동작하는 방식이 유사해 보여 갖게 된 궁금증이다. 두 방식 모두 이벤트 발행 즉시 처리되는 것처럼 보였다. 그러나 아래와 같은 차이점이 존재했다.
- @EventListener: 트랜잭션의 상태와 무관하게 이벤트 발행 즉시 처리됨
- @TransactionalEventListener(BEFORE_COMMIT): 트랜잭션 내부에서 커밋 이전에 실행되며 트랜잭션이 커밋될 경우에만 실행됨 (롤백되면 실행되지 않음)
이벤트 발행 - 이벤트 처리
'@TransactionalEventListener에 의해 트랜잭션이 롤백되었을 때 이벤트가 처리되지 않는다는 것은, 이벤트 발행 자체를 막는 것일까 이벤트 발행은 되지만 이벤트 리스너의 처리를 막는 것일까?' 하는 궁금증에서 시작되었다.
이벤트 발행부터 이벤트 리스너 처리까지의 과정은 아래와 같다.
- ApplicationEventpublisher의 publishEvent()를 통해 이벤트 발행
- ApplicationContext에 이벤트 게시됨
- 해당되는 EventListener가 이벤트를 받아 트랜잭션 상태에 따라 동기 혹은 비동기로 처리
결과적으로, 트랜잭션이 롤백되면 이벤트는 발행되지만 이벤트 리스너가 처리하지 않음을 알게 되었다.
👀 이벤트 처리 방식, 눈으로 직접 확인해보자!
아래 어노테이션에 따른 Listener의 이벤트 처리 방식을 테스트 코드를 통해 직접 확인해보았다.
- @EventListener ↔ @TransactionalEventListener
- @Async ↔ Sync
1. 테스트용 이벤트 선언
public class TestApplicationEvent extends ApplicationEvent {
public TestApplicationEvent(Object source) {
super(source);
}
}
2. 트랜잭션 및 예외 반환 유무에 따른 EventPublisher 선언
@Component
public class TestEventPublisher {
@Autowired
ApplicationEventPublisher eventPublisher;
public void publishWithoutTransaction(ApplicationEvent event) {
// 트랜잭션 없이 이벤트 발행
eventPublisher.publishEvent(event);
}
@Transactional
public void publishWithTransaction(ApplicationEvent event) {
// 트랜잭션 내부에서 이벤트 발행
eventPublisher.publishEvent(event);
}
@Transactional
public void publishWithTransactionThenThrowException(ApplicationEvent event) {
// 트랜잭션 내부에서 이벤트 발행 후 예외 발생
eventPublisher.publishEvent(event);
throw new RuntimeException();
}
}
3. 테스트해보기
3-A. 트랜잭션 없이 이벤트를 발행했을 때
@Test
void 트랜잭션_없이_이벤트_발행시_이벤트_처리과정() {
TestApplicationEvent event = mock(TestApplicationEvent.class);
testEventPublisher.publishWithoutTransaction(event);
}
호출 메서드의 트랜잭션이 열리지 않은 상황이라면 @TransactionalEventListener에 정의된 로직은 기본적으로 실행되지 않는다. 그러나, @TransactionalEventListener의 fallbackExecution 옵션을 정의해 실행시킬 수 있다.
3-B. 트랜잭션 내부에서 이벤트를 발행했을 때
@Test
void 트랜잭션_내부에서_이벤트_발행시_이벤트_처리과정() {
TestApplicationEvent event = mock(TestApplicationEvent.class);
testEventPublisher.publishWithTransaction(event);
}
1. @EventListener
@EventListener
public void handleApplicationEvent(ApplicationEvent event){
log.info("이벤트 실행");
}
- 실행 시점: 이벤트 발행 즉시
- 실행 위치: 동일 스레드
2. @EventListener + @Async
@EventListener
@Async
public void handleApplicationEvent(TestApplicationEvent event) {
log.info("이벤트 실행");
}
- 실행 시점: 이벤트 발행 즉시
- 실행 위치: 새로운 스레드
3. @TransactionalEventListener(AFTER_COMMIT)
@TransactionalEventListener(phase = AFTER_COMMIT)
public void handleApplicationEvent(TestApplicationEvent event){
log.info("이벤트 실행");
}
- 실행 시점: 트랜잭션 커밋 후
- 실행 위치: 동일 스레드
4. @TransactionalEventListener(AFTER_COMMIT) + @Async
@TransactionalEventListener(phase = AFTER_COMMIT)
@Async
public void handleApplicationEvent(TestApplicationEvent event) {
log.info("이벤트 실행");
}
- 실행 시점: 트랜잭션 커밋 후
- 실행 위치: 새로운 스레드
! 알게된 점 !
트랜잭션 내부에서 이벤트 발행 후 각 Listener의 동작 방식은 아래와 같다.
- @EventListener: 이벤트를 발행 즉시 실행시킴
- @TransactionalEventListener: 트랜잭션의 상태에 따라 이벤트를 실행시킴
- @Async: 호출 스레드가 아닌 새로운 스레드에서 비동기적으로 이벤트를 실행시킴
3-C. 트랜잭션 내부에서 이벤트를 발행한 후 예외가 발생했을 때
@Async로 인해 새로운 스레드에서 로직이 실행됨은 3-B 테스트를 통해 확인하였으므로 생략한다.
@Test
void 트랜잭션_내부에서_이벤트_발행후_예외발생시_이벤트_처리과정() {
TestApplicationEvent event = mock(TestApplicationEvent.class);
try {
testEventPublisher.publishWithTransactionThenThrowException(event);
} catch (Exception ignored) {
}
}
1. @EventListener
@EventListener
public void handleApplicationEvent(ApplicationEvent event){
log.info("이벤트 실행");
}
- 실행 여부: O
2. @TransactionalEventListener(AFTER_COMMIT)
@TransactionalEventListener(phase = AFTER_COMMIT)
public void handleApplicationEvent(TestApplicationEvent event){
log.info("이벤트 실행");
}
- 실행 여부: X
- 이유: 트랜잭션 커밋 전 예외가 발생했기 때문에 실행되지 않음
3. @TransactionalEventListener(AFTER_ROLLBACK)
@TransactionalEventListener(phase = AFTER_ROLLBACK)
public void handleApplicationEvent(TestApplicationEvent event){
log.info("이벤트 실행");
}
- 실행 여부: O
- 이유: 트랜잭션이 롤백되었기 때문에 실행됨
4. @TransactionalEventListener(AFTER_COMPLETION)
@TransactionalEventListener(phase = AFTER_COMPLETION)
public void handleApplicationEvent(TestApplicationEvent event){
log.info("이벤트 실행");
}
- 실행 여부: O
- 이유: 트랜잭션이 완료되었기 때문에 실행됨 (완료는 커밋 혹은 롤백 여부와 무관함)
5. @TransactionalEventListener(BEFORE_COMMIT)
@TransactionalEventListener(phase = BEFORE_COMMIT)
public void handleApplicationEvent(TestApplicationEvent event){
log.info("이벤트 실행");
}
- 실행 여부: X
- 이유: 트랜잭션이 커밋되지 않았기 때문에 실행되지 않음
! 알게된 점 !
트랜잭션 내부에서 이벤트 발행 후 예외가 발생했을 때 각 Listener의 동작 방식은 아래와 같다.
- @EventListener: 실행시킴
- @TransactionalEventListener
- AFTER_ROLLBACK & AFTER_COMPLETION: 실행시킴
- BEFORE_COMMIT & AFTER_COMMIT: 실행시키지 않음
참고
- @TransactionalEventListener 사용 방법
- 이벤트 발행과 처리 방식
https://tecoble.techcourse.co.kr/post/2022-11-14-spring-event/
'⛳️ 공동구매 서비스 총대마켓' 카테고리의 다른 글
⏪ 트랜잭션 내부에서 예외가 발생했을 때 이미 발행된 이벤트를 어떻게 처리할까: @TransactionalEventListener (0) | 2024.12.23 |
---|---|
동시성 이슈 해결 과정: 트랜잭션 격리 수준, 낙관적 잠금, 비관적 잠금 (2) | 2024.12.18 |
FCM 푸시알림 트러블슈팅: 비동기 처리로 동시성 이슈 방지하기 (0) | 2024.11.25 |
FCM 푸시알림 도입기 (with 안드로이드 코틀린 & 백엔드 자바) (0) | 2024.11.20 |
쿼리 최적화와 인덱스로 API Latency 30배 개선하기 (3) | 2024.11.15 |