이번에 이벤트 테스트를 하면서 다음과 같은 상황이 있었다.
@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() 시점에 우리가 원하는 작업을 수행하게 된다.
위 과정을 간략하게 정리하면 다음과 같다.
- @TransactionalEventListener 가 달린 메서드는 초기에 TransactionalApplicationListenerMethodAdapter로 감싸져 등록됨
- 트랜잭션 내에서 이벤트 발행
- TransactionalApplicationListenerMethodAdapter.onApplicationEvent() 호출 → 이벤트 리스너 작업을 TransactionSynchronization로 등록
- 트랜잭션 커밋
- AbstractPlatformTransactionManager가 트랜잭션 훅 수행
- 등록된 TransactionSynchronization를 순회하면서 각각의 작업 수행
- 이때 우리가 등록한 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 |