본문 바로가기
Spring 7기 프로젝트/모임 플렛폼 프로젝트

Spring의 @TransactionalEventListener와 새로운 트랜잭션 전파

by JuNo_12 2025. 8. 5.

문제 상황

회원탈퇴 기능을 구현하면서 아웃박스 패턴을 사용하던 중, 다음과 같은 오류가 발생했습니다.

TransactionRequiredException: Executing an update/delete query

오류 발생 원인

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)를 사용하여 이벤트를 처리할 때, 기존 트랜잭션이 이미 커밋되어 종료된 상태에서 새로운 데이터베이스 업데이트 작업을 시도했기 때문입니다.

이벤트 처리 플로우

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleUserWithdrawn(UserEvents.Withdrawn event) {
    // 1. RabbitMQ로 메시지 발행
    userEventPublisher.publishUserWithdrawn(event.userId(), event.email(), event.nickname());
    
    // 2. 아웃박스 상태 업데이트 시도 - 이 시점에서 트랜잭션이 없음!
    userOutboxService.markEventAsPublished(event.userId(), "USER_WITHDRAWN");
}

해결 방법: REQUIRES_NEW 전파 속성 사용

markEventAsPublished 메서드에 @Transactional(propagation = Propagation.REQUIRES_NEW)를 적용하여 새로운 독립적인 트랜잭션을 시작하도록 했습니다.


트랜잭션 전파 속성 선택 기준

각 메서드별 적절한 트랜잭션 설정

  1. markEventAsPublished: REQUIRES_NEW
    • 이유: @TransactionalEventListener(AFTER_COMMIT) 후 호출되므로 기존 트랜잭션이 이미 종료됨
    • 새로운 독립적인 트랜잭션이 필요
  2. saveUserWithdrawnEvent: 일반 @Transactional
    • 이유: 회원탈퇴 비즈니스 로직과 같은 트랜잭션 내에서 실행
    • 회원 삭제와 아웃박스 저장이 하나의 단위로 처리되어야 함
  3. retryEvent: 일반 @Transactional
    • 이유: 스케줄러에서 독립적으로 호출
    • 재시도와 상태 업데이트가 하나의 트랜잭션으로 처리
  4. cleanupOldPublishedEvents: 일반 @Transactional
    • 이유: 스케줄러에서 독립적으로 호출되는 배치 작업
  5. getRetryableEvents: readOnly = true (기본값)
    • 이유: 조회만 하는 메서드

핵심 포인트

@TransactionalEventListener의 AFTER_COMMIT 단계에서 호출되는 메서드가 데이터베이스 변경 작업을 수행해야 한다면, 반드시 REQUIRES_NEW 전파 속성을 사용하여 새로운 트랜잭션을 시작해야 합니다.

이는 이벤트 리스너가 실행되는 시점에서 기존 트랜잭션이 이미 커밋되어 종료된 상태이기 때문입니다.


결과

이 수정을 통해 아웃박스 패턴이 정상적으로 작동하게 되었고, 메시지 발행과 상태 업데이트가 올바른 순서로 처리되었습니다.