Spring EventListener: 사용 사례와 한계
TOC
Overview
@EventListener는 간단한 후속 작업을 분리하는 데 유용하지만, 모든 작업을 이벤트 listener로 옮겨야 한다는 뜻은 아니다.
동작 흐름
publishEvent()가 호출되면 Spring은 해당 이벤트 타입을 처리하는 listener를 찾아 호출한다.
동기 방식
기본 listener는 이벤트를 발행한 thread에서 동기 실행되므로 listener가 끝나야 publishEvent()가 반환된다.
publishEvent()
-> listener 실행
-> listener 종료
-> publishEvent() 반환
따라서 외부 API 호출이나 무거운 계산을 listener에서 수행하면 원래 요청의 응답 시간이 늘어난다.
하나의 이벤트를 처리하는 여러 Listener
UserRegistered 이벤트 하나를 WelcomeMessageListener, AuditLogListener, CacheEvictListener가 각각 처리할 수 있다.
특정 호출 순서가 필요하면 @Order를 사용할 수 있다.
@Order(1)
@EventListener
public void writeAuditLog(UserRegistered event) {
auditLog.write(event.userId());
}
@Order는 호출 순서만 지정하며 listener 간 workflow, transaction, 실패 복구를 보장하지 않는다.
주의점
@EventListener는 동기 실행과 예외 전파 때문에 publisher의 처리 흐름에 영향을 줄 수 있다.
Listener 예외가 미치는 영향
동기 listener에서 예외가 발생하면 예외가 publisher의 호출 흐름으로 전파될 수 있으므로, 부가 작업의 실패가 원래 요청을 실패시켜도 되는지 먼저 결정해야 한다.
@EventListener
public void sendNotification(UserRegistered event) {
notificationClient.send(event.userId());
}
알림 실패가 회원가입 실패를 의미하지 않는다면 예외를 무조건 삼키기보다 실패를 기록하고 재처리할 방법을 함께 마련해야 한다.
Listener가 오래 걸릴 때
오래 걸리는 작업은 동기 listener에 넣지 말고 별도 작업 큐나 비동기 처리로 분리하는 편이 요청 latency와 장애 전파를 줄인다.
@Async
@EventListener
public void generateReport(ReportRequested event) {
reportService.generate(event.reportId());
}
@Async는 transaction context를 다른 thread로 전달하지 않으므로 원래 transaction을 공유하지 않는다. async 메서드에 @Transactional이 있으면 별도 transaction으로 동작할 수 있다.
한계
@EventListener는 이벤트를 발행 시점에 호출할 뿐이며 이벤트를 영속 저장하거나 애플리케이션 재시작 후 복구하거나 자동 재시도하지 않는다.
또한 이벤트 타입이라는 계약은 publisher와 listener가 공유하고, 호출 흐름은 직접 메서드 호출보다 추적하기 어렵다.
차선책
후속 작업이 핵심 비즈니스 불변식이면 같은 transaction에서 직접 처리하고, transaction 성공 이후에만 실행해야 하면 @TransactionalEventListener를 검토한다.
반드시 전달되어야 하거나 재시도·중복 처리가 필요하면 @EventListener보다 Transactional Outbox나 메시지 브로커를 선택한다.
요약
@EventListener는 짧고 실패를 별도로 처리할 수 있는 부가 작업을 분리할 때 유용하다.
기본적으로 동기 실행되므로 응답 시간과 예외 전파를 고려해야 하며, 비동기 실행은 transaction 경계를 분리한다.
전달 보장, 재시도, 핵심 상태 변경이 필요하다면 listener보다 transaction 또는 영속 메시징 구조를 사용해야 한다.