@TransactionalEventListener는 어떻게 이벤트를 받을까?

2025. 8. 29. 14:14·스프링

이번에 이벤트 테스트를 하면서 다음과 같은 상황이 있었다.

@Test
@DisplayName("UserActivityEvent 이벤트가 발행 됐을 때, 이를 처리한다.")
void handle_userActivityEvent() {
  eventPublisher.publishEvent(new LikeEvent.Liked(1L, 1L));

  await()
      .atMost(3, TimeUnit.SECONDS)
      .untilAsserted(() ->
          verify(userActivityEventListener, times(1))
              .handle(any()));
}

class Listener {
    @Async
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handle(UserActivityEvent event) {
    }
}

위 테스트를 수행하면 실패한다. 그 이유는 트랜잭션 내에서 이벤트를 발행하지 않았기 때문이다.

결과를 알고보면 당연하기도 하고, 이름부터가 TransactionalEventListener 이므로 트랜잭션이 필수적으로 있어야 한다.

그래서 테스트를 다음과 같이 수정하였다.

class Test {
    @Test
    @DisplayName("UserActivityEvent 이벤트가 발행 됐을 때, 이를 처리한다.")
    void handle_userActivityEvent() {
        TransactionStatus transaction = transactionManager.getTransaction(new DefaultTransactionDefinition());
        eventPublisher.publishEvent(new LikeEvent.Liked(1L, 1L));
        transactionManager.commit(transaction);

        await()
          .atMost(3, TimeUnit.SECONDS)
          .untilAsserted(() ->
              verify(userActivityEventListener, times(1))
                  .handle(any()));
    }
}

그런데 어떻게 트랜잭션이 있으면 처리하고, 없으면 패스하는걸까?

@TransactionalEventListener

@TransactionalEventListener는 다양한 phase 옵션이 있다.

  • BEFORE_COMMIT
    • 트랜잭션 커밋 전 실행
  • AFTER_COMMIT
    • 트랜잭션 커밋 후 실행
  • AFTER_ROLLBACK
    • 트랜잭션 롤백 후 실행
  • AFTER_COMPLETION
    • 트랜잭션 끝나면 실행 (커멋/롤백 상관 X)

우리는 이러한 옵션들을 활용하여 코드를 작성한다.

public class OrderService {
    private final ApplicationEventPublisher eventPublisher;
    
    public void order() {
        // ...
        eventPublisher.publishEvent(new OrderEvent.Created(~));
    }
}

public class EventListener {
    
    @Async
    @TrasactionalEventListener(phase = AFTER_COMMIT)
    public void handle(OrderEvent.Created event) {
        // ..
    }
}

그런데 스프링은 이를 어떻게 추상화하여 우리에게 기능을 제공하는걸까?

스프링 내부 동작 확인하기

우리는 @Transactional 이라는 추상화된 트랜잭션 기능을 사용한다.

@Transactional을 사용하면, 스프링 내부적으로 PlatformTransactionalManager가 트랜잭션 관련 작업을 수행한다.

PlatformTransactionalManager의 구현체인 AbstractPlatformTransactionalManager는 트랜잭션 커밋 시점에 대략 다음 작업을 수행한다.

AbstractPlatformTransactionalManager의 커밋 과정

private void processCommit(DefaultTransactionStatus status) {
    try {
		prepareForCommit(status);
		triggerBeforeCommit(status);
		triggerBeforeCompletion(status);
	
		doCommit(status);
		
		triggerAfterCommit(status);
	catch (~) {
	}
	finally {
  	triggerAfterCompletion(status);
	}
}

위 코드를 보면, trigger 작업이 여럿 존재한다.

triggerAfterCommit 메서드를 살펴보면 다음과 같다.

// AbstractPlatformTransactionalManager
private void triggerAfterCommit(DefaultTransactionStatus status) {
	if (status.isNewSynchronization()) {
		TransactionSynchronizationUtils.triggerAfterCommit();
	}
}

// TrasactionalSynchronizationUtils
public static void triggerAfterCommit() {
	invokeAfterCommit(TransactionSynchronizationManager.getSynchronizations());
}

public static void invokeAfterCommit(@Nullable List<TransactionSynchronization> synchronizations) {
    if (synchronizations != null) {
      // synchronizations를 순회하며 afterCommit 작업을 수행한다.
        for (TransactionSynchronization synchronization : synchronizations) {
            synchronization.afterCommit();
        }
    }
}

TrasactionalSynchronizationUtils의 triggerAfterCommit()을 호출하면 invokeAfterCommit 작업을 수행한다.

이때 각 TransactionSynchronization 별로 afterCommit()을 수행하는데 이게 뭘까?

TransactionSynchronization는 인터페이스이므로, 각 구현체가 afterCommit과 같은 행동을 정의하게 된다.

public interface TransactionSynchronization {
    default void beforeCommit(boolean readOnly) {
	}
	
	default void beforeCompletion() {
	}
	
	default void afterCommit() {
	}
	
	default void afterCompletion(int status) {
	}
}

TransactionSynchronization의 이름에서부터 느껴지듯이, 트랜잭션 동기화 작업을 수행하는 인터페이스이다. 이를 통해, 커밋되었을 때 수행하고자 하는 작업들을 모두 수행하게 된다. 이를 통해 우리가 @TransactionalEventListener(phase = AFTER_COMMIT) 어노테이션을 단 작업이 TransactionSynchronization로 등록되어야 함을 알 수 있다.

@TransactionalEventListener가 TransactionSynchronization로 등록되는 과정

우리가 @TransactionalEventListener(phase = AFTER_COMMIT) 어노테이션을 등록한 메서드는 스프링이 뜰 때, TransactionalApplicationListenerMethodAdapter로 감싸져서 등록된다.

ApplicationEventPublisher가 publishEvent를 수행하게 되면, TransactionalApplicationListenerMethodAdapter가 TransactionSynchronization를 만들어 등록시킨다. 등록된 TransactionSynchronization는 triggerAfterCommit() 시점에 우리가 원하는 작업을 수행하게 된다.

위 과정을 간략하게 정리하면 다음과 같다.

  1. @TransactionalEventListener 가 달린 메서드는 초기에 TransactionalApplicationListenerMethodAdapter로 감싸져 등록됨
  2. 트랜잭션 내에서 이벤트 발행
    1. TransactionalApplicationListenerMethodAdapter.onApplicationEvent() 호출 → 이벤트 리스너 작업을 TransactionSynchronization로 등록
  3. 트랜잭션 커밋
    1. AbstractPlatformTransactionManager가 트랜잭션 훅 수행
    2. 등록된 TransactionSynchronization를 순회하면서 각각의 작업 수행
    3. 이때 우리가 등록한 TransactionalApplicationListenerMethodAdapter가 수행된다.

마무리

이번에 이벤트로 구조를 분리하면서, 스프링이 어떻게 이벤트 리스너를 처리하는지 궁금했다.

또한 트랜잭션 내에서 이벤트를 발행하지 않으면 @TransactionalEventListener가 동작하지 않는데, 이에 대해서도 알아보고자 하였다.

이전에는 단순히 @TransactionalEventListener는, 트랜잭션 내에서 이벤트 발행하지 않으면 이벤트를 처리하지 못한다고만 이해하고 있었다. 이번 기회를 통해서 “왜” 처리하지 못하는지 정확히 알 수 있었다.

편리하게 추상화된 기능을 사용하는 것도 좋지만, 이러한 내부 구현을 확인해보며 이해함으로써, 발생할 수 있는 사이드 이펙트를 줄일 수 있다고 생각한다.

'스프링' 카테고리의 다른 글

알림을 비동기 방식으로 보내기  (0) 2025.02.25
@Modifying의 동작 알아보기  (0) 2025.01.23
[JPA] 연관관계의 주인이란?  (0) 2025.01.13
Mybatis vs JPA  (0) 2025.01.08
SELECT 작업에 트랜잭션은 필요할까?  (0) 2025.01.08
'스프링' 카테고리의 다른 글
  • 알림을 비동기 방식으로 보내기
  • @Modifying의 동작 알아보기
  • [JPA] 연관관계의 주인이란?
  • Mybatis vs JPA
g-hwang
g-hwang
g-hwang 님의 블로그 입니다.
  • g-hwang
    g-hwang 님의 블로그
    g-hwang
  • 전체
    오늘
    어제
    • 분류 전체보기 (57)
      • 데브코스 (7)
      • 스프링 (8)
      • 자바 (3)
      • 아키텍처 (3)
      • 트러블 슈팅 (4)
      • 알고리즘 (0)
      • 개발서적 (7)
      • 인프런 워밍업 스터디 (7)
      • 오픈소스 기여 (2)
      • WIL (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    이벤트 스토밍
    jpa 순환참조
    워밍업 스터디
    다중 컬럼 인덱스
    도메인 모델링
    ZSet
    virtual thread
    트랜잭션
    JPA
    스터디3기
    스프링
    자바
    인프런 워밍업 스터디
    인덱스
    코드래빗
    레이어 아키텍처
    래퍼 클래스
    카프카
    real mysql 8.0
    도메인
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
g-hwang
@TransactionalEventListener는 어떻게 이벤트를 받을까?
상단으로

티스토리툴바