본문 바로가기

⛳️ 공동구매 서비스 총대마켓

👀 @TransactionalEventListener: 학습테스트로 동작 방식 확인해보기

@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에 의해 트랜잭션이 롤백되었을 때 이벤트가 처리되지 않는다는 것은, 이벤트 발행 자체를 막는 것일까 이벤트 발행은 되지만 이벤트 리스너의 처리를 막는 것일까?' 하는 궁금증에서 시작되었다.

 

이벤트 발행부터 이벤트 리스너 처리까지의 과정은 아래와 같다.

  1. ApplicationEventpublisher의 publishEvent()를 통해 이벤트 발행
  2. ApplicationContext에 이벤트 게시됨
  3. 해당되는 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://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/event/TransactionalEventListener.html

 

- 이벤트 발행과 처리 방식

https://tecoble.techcourse.co.kr/post/2022-11-14-spring-event/

https://ksh-coding.tistory.com/111