Spring Boot에서 AOP와 트랜잭션 전파, 이벤트 발행을 겪으면서 문제를 겪었고 이에 대하여 정리해보겠다!
이론적인 내용을 정리하는 것은 별도의 포스트에 작성하고
당시의 상황에 대한 트러블 슈팅 기록을 해본다.
당시의 상황
당시의 상황을 우선적으로 정리해보자
Lock + Transaction + PublishEvent
위의 상황이었다.
좀더 구체적으로 적어본다.

일기를 작성하는 서비스였고, 일기는 하루에 한 번씩만 작성이 가능하다.
하지만 클라이언트 측에서 동시에 요청을 보내버리면 하루에 일기가 두 번 작성되고, 이에 대하여 데이터 정합성이 어긋나는 문제가 발생하여 나는 네임드락(Named Lock)을 통하여 해결하려고 하였다.
그리고 일기를 특정 개수를 만족한다면,
작성 후에 아이템 보상을 받는 Event를 Publish하였다.
따라서 비즈니스 요구사항은 다음과 같았다.
- 같은 날짜에 두 번 일기를 못 쓰도록 동시성을 제어하기 위한 락(lock) 걸기
- 일기 작성과 멤버의 보상 일수 관련 수정을 하나의 트랜잭션으로 처리
- reward가 특정 값이면 아이템 업데이트하는 이벤트를 발행
처음엔 아래처럼 하나의 서비스 코드에서 모두 처리했다.
@Transactional
public DiaryResponseDto.CreateDto createDiary(DiaryRequestDto.CreateDto requestDto) {
// 락 획득 후 @Transactional 메서드 실행
Diary diary = diaryLockTemplate.executeWithLock(lockName,
() -> createDiaryTransactional(member, requestDto)
);
// 이벤트 발행
if (member.getReward() == 7) {
eventPublisher.publishEvent(new ItemUpdateEvent(this, member.getId()));
}
emotionSummaryService.updateMonthlyEmotionSummary(requestDto.getRecordDate());
...
}
위의 executeWithLock은 네임드락을 설정해 둔 것이다.
그리고 하나의 createDiary 라는 메서드 안에서 락을 잡고,
작성까지 완료하도록 @Transactional 어노테이션을 작성하여 DB에 반영하도록 하였다.
@TransactionalEventListener(phase = AFTER_COMMIT)
public void handelEvent(ItemUpdateEvent event) {
// 생략...
}
그런데 문제는 여기서 발생했다!
문제상황. AFTER_COMMIT 이벤트가 실행되지 않음
위의 코드에서 eventPublisher.publishEvent(new ItemUpdateEvent) 를 확인할 수 있다.
문제는 @TransactionalEventListener(phase = AFTER_COMMIT)로 작성한 이벤트 리스너가 아예 실행되지 않는 것이었다.
원인을 추적해보니,
이벤트 발행 시점에 활성화된 트랜잭션이 없었기 때문이다!

내 코드는 이렇게 되어있었다.
- 락 획득 -> 내부 @Transactional 메서드를 실행 -> 트랜잭션 커밋
- 바깥 메서드에서 이벤트를 발행함
즉, 바깥 메서드에서는 이미 세션이 종료된 이후인 트랜잭션 외부였으므로,
AFTER_COMMIT이 붙은 이벤트는 동작할 수 없는 상황이었다.
Propagation(전파)에 따른 동작 정리
이 과정에서 트랜잭션 전파에 대하여 조사했다.
| 상황 | 이벤트를 발행한 트랜잭션 | 실제 after commit의 실행 타이밍 | 설명 |
| 기본(REQUIRED) | 바깥 트랜잭션 | 바깥 트랜잭션 커밋 직후 | 일반 케이스 |
| REQUIRES_NEW | 내부 트랜잭션 | 내부 트랜잭션 커밋 직후 | 바깥이 롤백돼도 이미 실행 |
| NESTED | 바깥 트랜잭션 | 바깥 트랜잭션 커밋 직후 | savepoint라 별도 아님 |
| 트랜잭션 없음 | 없음 | 즉시 실행 or 동기화 불가 | isSynchronizationActive() == false |
핵심은 이벤트가 발행된 시점의 트랜잭션에
AFTER_COMMIT이 묶인다는 점이었다.
해결: 서비스 분리 + REQUIRES_NEW
결론적으론,
트랜잭션에 대한 전파 수준을 수정하였다.
(트랜잭션이 필요하면 단순히 추가하는 것이 오히려 간단하긴 하다)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public DiaryResponseDto.CreateDto createDiary(DiaryRequestDto.CreateDto requestDto) {
// 생략...
eventPublisher.publishEvent(new ItemUpdateEvent(this, memberId));
// 생략...
}
이렇게 하면 내부의 새로운 트랜잭션의 "내부"에서 이벤트의 트랜잭션이 실행되는 것이다.
내부 트랜잭션의 커밋 직후에 이벤트는 실행이 된다.
이 때 문제점은 바깥이 롤백되어도,
이미 이벤트는 발행이 된 상태인 것이므로
아직은 완전히 해결한 사례라고는 할 수 없다. 이 때에는 Transaction의 동기화 관련을 통하여 해결을 해야하는데 더 깊게 조사해봐야할 것 같다
의도에 따라 다른 패턴도 고려해보기
의도에 따라 트랜잭션에 대하여 나누는 것도 필요하다.
1. 내부 트랜잭션 커밋 직후에 Event Publish가 목적이라면?
- 위의 상황처럼 이벤트를 REQUIRES_NEW 트랜잭션에서 발행한다.
- 리스너는 @TransactionalEventListener(AFTER_COMMIT) 사용
- 바깥이 롤백되어도 퍼블리쉬는 이미 발행
2. 최종적으로 바깥까지 성공한 뒤에 Publish하려면?
- 이벤트를 바깥 트랜잭션에서만 발행
- 혹은 버퍼에 쌓았다가 최종 커밋 후 한 번에 발행한다
- TransactionSynchronizationManager.registerSynchronization().afterCommit() 에 버퍼를 등록
- Outbox 패턴 사용: 메시지를 DB에 저장 후에 별도 worker가 commit된 이후에만 발행하도록 설계한다
이런식으로 동기화에 대한 별도의 클래스를 구현해주는 것도 하나의 방법이다.
정리
- @TransactionalEventListener(AFTER_COMMIT)는 이벤트가 발행된 트랜잭션의 커밋 시점에 맞춰 실행된다.
- REQUIRES_NEW는 내부 트랜잭션이 커밋되면 곧바로 실행된다.
- 바깥 트랜잭션의 성공 이후에만 푸시하려면 발행 시점을 바깥으로 옮기거나 버퍼/Outbox 패턴을 써야 한다.
솔직히 초기엔 Buffer 개념이 왜 필요한지 몰랐는데 ( 지금도 확실하게 와닿지는 않는다..ㅎㅎ) ,
이 과정을 겪으며 트랜잭션 관리 정책과 복구 전략(UNDO, REPO 복구 등)을 위해 버퍼가 존재한다는 걸 체감했고,
다음에는 세션에 대하여 좀더 조사 후에 패턴을 잘 적용해서
더 디벨롭 해야겠다...!!!!
'백엔드 > SpringBoot' 카테고리의 다른 글
| [SpringBoot] 스프링에서의 Service 와 Bean의 차이점 (1) | 2025.10.04 |
|---|---|
| [Spring Boot] Thread 설정 (Spring Boot) (1) | 2025.09.27 |
| [SpringBoot] UnitTest (1) | 2025.08.27 |
| [SpringBoot] Spring에서 비동기 작업과 스케줄링 - ThreadPoolTaskScheduler 편 (0) | 2025.08.22 |
| [Spring Boot] 동시성 제어하기 (5) | 2025.08.10 |