해당글은 spring boot3.4.x, kotiln 2.1.x 환경에서 작성되었습니다.
굉장히 오랜만에 쓰는 글인데요.
오늘은 Spring Boot 의 @TransactionlEventListener 를 사용했을 때, 이벤트는 발행되지만 Listener 가 이벤트를 수신받아 동작을 정상적으로 수행하지 못했던 이슈에 대해 간단하게 정리해보고자 합니다.
[목차]
- 문제 상황
- 문제 원인 파악하기
- 결론 및 해결방안
1. 문제 상황
정확하게는 이벤트의 수행시점을 After Commit 으로 두었을 때 문제가 생겼습니다.
@TransactionlEventListener(phase = TransactionPhase.AFTER_COMMIT)
- 하나의 트랜잭션에서 묶임이 필요하지 않은 로직에서 이벤트를 발행
- 수신받은 리스너에서 로직 수행
- 다시 이벤트 발행 (phase = After Commit)
- 리스너에서 수신은 받지만, 동작을 수행하지 않음 (트리거 조건이 맞지않음)
1) 첫 번째 트랜잭션
@Transactional
fun firstTransaction(){
//...
eventPublisher.publish(event)
//...
}
2) 두 번째 트랜잭션
// Listener
public class CustomEventListener(){
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void litener(Event event) {
service.run(event.deliveryId());
}
}
// Service
public class ApiService(){
@Transactional
public void run(Event event) {
//...
publisher.publish(event)
//...
}
}
3) 마지막 리스너
- 이 부분이 실행되지 않았씁니다.
@Async
@TransactionalEventListener
fun on(event: Event) {
println("자라나라 머리머리");
}
2. 문제 원인 파악
문제의 원인은 생각보다 간단했습니다.
1) 첫 번째 시도
- 처음, 왜 안되는걸까 고민하다가 마지막 수신받는 부분을 @EventListner 로 수정하니 실행되었습니다.
2) 두 번째 시도
- 이벤트의 발행과 수신에 문제가 없다면 @TransactionalEventListner 의 트리거가 문제일까?
- phase 를 Before Commit 으로 수정하니 물론 되었습니다.
3) 세 번째 시도
- 스레드의 시작에서부터 이벤트가 2번 발행되니, 마지막으로 수신받는 리스너에서, 트랜잭션의 커밋 시점을 못 읽는게 아닐까?
- 두번째 트랜잭션이 시작되는 리스너에서 비동기(@Asyn) 로 끊으니
- 트랜잭션이 분리되면서 해당 스레드에서 발행하는 이벤트는 정상적으로 수행이 되었습니다.
4) ✔️ 네 번째 시도
- 비동기를 무의미하게 사용하는게 아닐까?
- 근본적인 원인은 트랜잭션의 커밋시점을 명확하게 잡지 못한다. (전파되지 못한다?)로 판단되어져서 내부 로직을 하나씩 디버깅으로 잡아가며 살펴보다가 아래와 같은 주석을 발견할 수 있었습니다.


NOTE: The transaction will have been committed already, but the transactional resources might still be active and accessible. As a consequence, any data access code triggered at this point will still "participate" in the original transaction, allowing to perform some cleanup (with no commit following anymore!), unless it explicitly declares that it needs to run in a separate transaction. Hence: Use PROPAGATION_REQUIRES_NEW for any transactional operation that is called from here.
참고: 트랜잭션은 이미 커밋되었지만 트랜잭션 리소스는 여전히 활성화되어 액세스할 수 있습니다. 따라서 이 시점에서 트리거된 모든 데이터 액세스 코드는 원래 트랜잭션에 여전히 "참여"하여 별도의 트랜잭션에서 실행해야 한다고 명시적으로 선언하지 않는 한(더 이상 커밋 팔로우 없이!) 정리를 수행할 수 있습니다. 따라서 여기서 호출되는 모든 트랜잭션 작업에는 PROGRAPION_REQUEED_NEW를 사용하세요.
3. 결론 및 해결방안
👏 즉, 하나의 스레드에서 이벤트가 수행될 때 트랜잭션이 1번 커밋되었다면, 이후의 커밋시점에 대해 이벤트를 트리거하기 위해서는 명시적으로 트랜잭션을 분리, 생성하여야 합니다.
1) 첫 번째 트랜잭션
@Transactional
fun firstTransaction(){
//...
eventPublisher.publish(event)
//...
}
2) 두 번째 트랜잭션 (여기서 트랜잭션을 분리해야 합니다.)
// Listener
public class CustomEventListener(){
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void litener(Event event) {
service.run(event.deliveryId());
}
}
// Service
public class ApiService(){
@Transactional
public void run(Event event) {
//...
publisher.publish(event)
//...
}
}
3) 마지막 리스너
- 정상 수행~
@Async
@TransactionalEventListener
fun on(event: Event) {
println("자라나라 머리머리");
}
잠깐 시간이 남아서 오랜만에 글을 썼는데, 많이 버겁네요 후
앞으로 열심히 써보겠습니다
열정열정열정~
[SpringBoot]
🚀 @TransactionalEventListener
"AFTER_COMMIT" 의 Transaction 처리 범위
'Spring > Spring Boot' 카테고리의 다른 글
| [Spring Boot] RestClient 와 HTTPInterface 로 통신하기 (0) | 2025.03.07 |
|---|---|
| Spring Bean 공부하기 (Been 생명주기, Scope, 권장 사용 방법) (0) | 2025.01.29 |
| [Spring Boot] 다중 인스턴스에서 스케줄링 중복 실행 제어 하기 (@Scheduled Lock - shed lock) (0) | 2024.10.10 |
| 동시성 제어 문제에 대한 고찰 (With. Spring, JAVA, MySQL, Redis, Kafka) (0) | 2024.08.30 |
| Repository는 어느 모듈에 위치해야할까? (DIP. 고수준모듈, 저수준 모듈) (4) | 2024.07.09 |