TransactionalEventListener 정리
이번 글에서는 TransactionalEventListener를 보면서 들었던 질문과 답을 정리해 볼 예정이다. 특히 TransactionalEventListener의 동작 중 'AFTER_COMMIT' 동작에 대해서 알아볼 예정이다
분석의 기반이 되는 코드는 아래와 같다
@Service
class CoffeeService(
private val coffeeRepository: CoffeeRepository,
private val eventPublisher: ApplicationEventPublisher
) {
@Transactional
fun createCoffee(name: String): Coffee {
val coffee = Coffee(name = name)
coffeeRepository.save(coffee)
eventPublisher.publishEvent(CoffeeCreatedEvent.of(coffee))
return coffee
}
}
@Component
class CoffeeEventListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
fun handleCoffeeCreatedEvent(event: CoffeeCreatedEvent) {
println("커피가 생성됨: ${event.name}")
}
}
1. TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)는 어떻게 transaction commit 후 실행될 수 있는 걸까?
CoffeeService의 'eventPublisher.publishEvent(~)' 부분에 break point를 걸어서 내부 동작을 확인해 봤다
결론부터 말하면 아래와 같은 방식으로 처리된다
- 발행된 'CoffeeCreatedEvent'와 이벤트 핸들러인 'handleCoffeeCreatedEvent'가 바로 실행되지 않고, 이벤트와 핸들러를 포함한 객체로 감싸져서 스레드 로컬에 저장된다
- 트랜잭션 커밋 후 후처리 할 때 등록된 이벤트와 이벤트 핸들러를 실행시킨다. 이를 통해 트랜잭션 커밋 후 작업을 수행할 수 있다
이제부터 실제 코드 상에서 위의 내용이 어떻게 구현됐는지 간단히 살펴보고자 한다
- 'CoffeeCreatedEvent'는 'AbstractApplicationContext' 클래스의 'publishEvent' 메서드 내부에서 'PayloadApplicationEvent'로 한 번 감싸진 후 'SimpleApplicationEventMultiCaster'의 'multicastEvent' 메서드로 보내진다
- 우리의 이벤트를 'PayloadApplicationEvent'로 감싸는 이유는 다형성을 활용하기 위해서 인 것 같다
- 'SimpleApplicationEventMultiCaster' 의 'multicastEvent' 메서드는 이름처를 이벤트를 멀티캐스팅 하기 위한 클래스이다. 이벤트를 핸들링할 수 있는 listener 클래스들을 가져온 후에 listener 클래스들을 순회하며 'invokeListener(listener, event)' 메서드를 호출한다. 우리의 예시에서는 'handleCoffeeCreatedEvent'가 listener이고, 'CoffeeCreatedEvent'가 이벤트이다
- listener 클래스를 가져오는 로직은 'AbstractApplicationEventMulticaster' 클래스에서 확인할 수 있다
- 'AbstractApplicationEventMulticaster'의 'defaultRetriever'에는 '@EventListener' 어노테이션을 달고 있는 bean들이 등록되어 있으며, 이 bean 들은 spring application context를 만드는 과정에서 등록된다
- 'AbstractApplicationEventMulticaster'의 'defaultRetriever'에서 현재 이벤트 ('CoffeeCreatedEvent')를 핸들링할 수 있는 listener를 가져오고 & 캐싱처리한다
- 'SimpleApplicationEventMultiCaster'의 'invokeListener'에서는 사전에 등록한 에러핸들러가 있는지 확인 후 에러핸들러가 있다면 try catch 구문을 통해 에러가 날 경우 에러핸들러를 실행할 수 있도록 한다. 만약 에러핸들러가 없다면 단순히 'doInvokeListener(listener, event)'를 호출한다
- 'doInvokeListener(listener, event)'에서는 'listener.onApplicationEvent(event)'를 통해 이벤트 리스너가 이벤트를 처리하는 함수를 호출한다
- 'handleCoffeeCreatedEvent'는 이벤트 리스너 중에서도 트랜잭셔널 이벤트 리스너이기 때문에 'TransactionalApplicationListenerMethodAdapter'의 'onApplicationEvent' 메서드가 호출된다
- 'onApplicationEvent'에서는 'TransactionApplicationListenerSynchronization' 클래스의 'register' 메서드를 통해 이벤트를 등록해 준다
- 'register' 메서드는 'isSynchronizationActive()'가 true이고, 'isActualTransactionActive()'가 true 이면 이벤트와 리스너를 PlatformSynchronization 객체로 감 써서 'TransactionSynchronizationManager' 클래스의 'registerSynchronization'를 호출한다
- 'isSynchronizationActive()''isSynchronizationActive()'는 transaction manager에 의해 transaction 이 시작되면 true를 return 하고, transaction manager 가 transaction 종료를 위해 clean up 하면 false를 return 한다
- 'isActualTransactionActive()'는 'isSynchronizationActive()' 가 true 일 때도 false를 return 할 수 있다. ' isActualTransactionActive()'는 실제 코드가 트랜잭션 안에서 실행되는지를 나타낸다. 예를 들어 코드가 PROPAGATION_SUPPORTS 전파방식을 가진 트랜잭션 내부에서 호출됐다면 '@Transactional'에 의해 'isSynchronizationActive()'는 true를 return 하지만, 실제 transaction 내부에서 처리되는 코드가 아니므로 'isActualTransactionActive()'는 false를 return 할 것이다 (상위 코드에서 트랜잭션이 시작되지 않았다는 가정하에)
- 'registerSynchronization'는 'synchronizations' 라는 set 타입의 스레드 로컬에 [7]에서 만든 PlatformSynchronization 객체를 저장한다. 이벤트와 listener를 스레드 로컬에 등록하는 작업이라고 생각하면 된다
- 위에서 등록된 listener는 transaction 종료되고 후처리 작업을 할 때 'AbstractPlatformTransactionManager' 클래스의 'triggerAfterCompletion' 메서드에서 호출된다. 'triggerAfterCommit' 메서드가 아닌 것에 주의하라
- 최종적으로 PlatformSynchronization는 스레드 로컬에서 삭제된다
2. Transaction 밖에서 발행된 이벤트를 TransactionalEventListener는 감지할 수 있을까?
실제 테스트를 돌려보기 전 위의 분석을 통해 추측을 해보자면, 감지할 수 없을 것이다
[7]을 보면 TransactionalEventListener와 이벤트(이걸 감싸는 PlatformSynchronization)는 '@Transactional'을 통해 트랜잭션이 열리고, 현재 스레드가 실제로 트랜잭션에 참여중일 때만 스레드 로컬에 저장된다고 했다. 따라서 Transaction 밖에서 발행된 이벤트는 스레드 로컬에 아예 저장 자체기 되지 않고, 당연히 실행 또한 안될 것이다. 아래는 테스트 결과이다
fun createCoffeeNoTransaction(name: String): Coffee {
val coffee = Coffee(name = name)
coffeeRepository.save(coffee)
eventPublisher.publishEvent(CoffeeCreatedEvent.of(coffee))
return coffee
}
/*
어떤 결과도 출력되지 않는다
*/
3. TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)와 Async 어노테이션을 함께 사용해도 문제없을까?
마찬가지로 위의 분석을 통해 추측을 해보자면, @Async와 함께 사용할 경우 문제가 없을 것이다
@Async를 사용하면 트랜잭션 commit 후, 후처리 과정에서 'handleCoffeeCreatedEventAsync'를 비동기로 호출할 것이다. 하지만 'handleCoffeeCreatedEventAsync'의 반환 값을 사용할 필요도 없고, 순차적으로 호출할 필요도 없기 때문에 @Async를 사용해도 문제가 없다
한 가지 주의할 점은 @Async 를 사용해 비동기로 처리하게 되면 기존의 persistence context를 사용할 수 없고, 이로 인해 lazy loading을 할 경우 'LazyInitializationException'이 발생할 수 있다
'AFTER_COMMIT' 외의 'TransactionPhase'에 대해서는 상황에 맞게 고민이 필요할 것 같다
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
fun handleCoffeeCreatedEvent(event: CoffeeCreatedEvent) {
println("Thread: ${Thread.currentThread().name}, 커피가 생성됨: ${event.name}")
}
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
fun handleCoffeeCreatedEventAsync(event: CoffeeCreatedEvent) {
println("Thread: ${Thread.currentThread().name}, 커피가 비동기적으로 생성됨: ${event.name}")
}
/*
결과
Thread: http-nio-8080-exec-2, 커피가 생성됨: americano
Thread: task-1, 커피가 비동기적으로 생성됨: americano
*/
알게 된 사실들
- 'AbstractApplicationEventMulticaster'을 구현한 event mulitcaster를 만들 때 taskExecutor를 지정해 주면, @Async를 사용하지 않고도 일반 '@EventListener'가 비동기로 동작하게 할 수 있다
- 'TransactionalEventListener'는 트랜잭션이 열리고, 현재 코드가 실제 트랜잭션에 참여한 상태에서만 이벤트가 등록되고 추후에 처리될 수 있다
- ApplicationContext에서 bean을 가져올 때 탐색 결과를 캐싱해서 성능을 최적화할 수도 있다