스프링 애플리케이션에서 이벤트를 발행하고 처리할 때, 두 가지 타입의 어노테이션을 사용할 수 있다.
@EventListener@TransactionalEventListener다만 어떤 유스케이스와 어떤 목적에 따라 어떻게 써야하는지 매 번 헷갈려서 정리해보았다.
Subscriber side

Publisher side

public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : ResolvableType.forInstance(event));
Executor executor = getTaskExecutor();
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
if (executor != null && listener.supportsAsyncExecution()) {
try {
executor.execute(() -> invokeListener(listener, event));
}
catch (RejectedExecutionException ex) {
// Probably on shutdown -> invoke listener locally instead
invokeListener(listener, event);
}
}
else {
invokeListener(listener, event);
}
}
}
☝
TL;DR;
@EventListener로 등록한 리스너가 처리 중 예외를 던지면, 이벤트 발행 지점까지 예외가 전파된다.
비즈니스 로직 코드가 아래와 같다고 해보자.
@Override
public CreateOrderResponse createOrder(Long userIdx, CreatOrderRequest creatOrderRequest) {
User buyer = userInfra.findUser(userIdx);
Product product = productInfra.findProduct(creatOrderRequest.prdIdx());
Order order = orderInfra.save(Order.createFrom(buyer, product,creatOrderRequest.quantity()));
applicationEventPublisher.publishEvent(new OrderCreatedEvent(order));
return createOrderUsecaseMapper.toCreateOrderResponse(order);
}
이 때 예외를 일부러 터트리는 테스트 코드를 짜보았다.
RuntimeException("boom") 을 던지면createOrder 메서드에도 예외가 발생하는 것 확인@SpringBootTest
class CreateOrderUsecaseImplTest {
@Autowired
CreateOrderUsecaseImpl publisher;
@MockitoSpyBean
AddOwnerBalance listener;
@Test
@DisplayName("이벤트 수신자 예외 발생 시 이벤트 발행자로 예외 전파합니다.")
void 이벤트수신자예외발생시_이벤트발행자예외전파() {
// GIVEN
Long userId = 1L;
Long productId = 10L;
long quantity = 1L;
CreatOrderRequest creatOrderRequest
= new CreatOrderRequest(productId, quantity);
// 이벤트 수신자가 예외를 던짐
BDDMockito.doThrow(new RuntimeException("boom"))
.when(listener)
.addOwnerBalance(BDDMockito.any(OrderCreatedEvent.class));
// WHEN
// THEN
// 이벤트 발행자에서 예외를 잡음
assertThrows(RuntimeException.class,
() -> publisher.createOrder(userId, creatOrderRequest)
);
}
}
해당 테스트가 통과하는 것을 볼 수 있다.
즉, 이벤트 수신자의 예외가 이벤트 발행하는 응용 계층까지 전파되는 것을 확인할 수 있었다.

phase 속성으로 실행 시점을 조절 가능BEFORE_COMMIT)AFTER_COMMIT)AFTER_ROLLBACK)AFTER_COMPLETION)ApplicationEventPublisher.publishEvent(...) 호출TransactionSynchronization 을 등록phase 에 맞는 리스너만 호출@Async 사용@Transactional(propagation = Propagation.REQUIRES_NEW) 사용@Component
public class SimpleListener {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
// Order 생성 직후, 같은 트랜잭션 내에서 실행
log.info("OrderCreatedEvent 처리: {}", event.getOrder().getId());
}
}
@Component
public class TxListener {
// 커밋이 성공한 후에만 실행
@TransactionalEventListener
public void onOrderCreatedAfterCommit(OrderCreatedEvent event) {
// 메시지 큐에 발행하거나, 외부 API 호출
notifyExternalService(event.getOrder());
}
// 커밋 전에 실행
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void beforeCommit(OrderCreatedEvent event) {
log.info("트랜잭션 커밋 직전: {}", event.getOrder().getId());
}
// 롤백 후 실행
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void afterRollback(OrderCreatedEvent event) {
log.warn("트랜잭션 롤백됨: {}", event.getOrder().getId());
}
}