컴포넌트 A가 "어떤 일이 일어났다"를 알리고 싶은데, 누가 그 일에 반응할지는 A가 몰라야 한다. 주문이 완료되면 메일을 보내고, 통계를 갱신하고, 캐시를 비울 수도 있다 — 하지만 주문 서비스가 메일·통계·캐시를 직접 참조하면 결합이 강해진다. Spring 이벤트는 이 발행/구독을 컨테이너가 중개해 느슨한 결합의 컴포넌트 간 통신을 제공한다.
이 챕터는 publishEvent() 한 번이 어떻게 적절한 리스너들에게 배달되는지, 그리고 인터페이스 구현(ApplicationListener) 대신 @EventListener 메서드를 쓸 때 무슨 일이 일어나는지를 다룬다.
발행 측 중개자 수신 측
───────── ─────── ─────────
ApplicationEventPublisher ApplicationEventMulticaster ApplicationListener<E>
publishEvent(Object|Event) multicastEvent(event, type) onApplicationEvent(E)
│ │ ▲
│ AbstractApplicationContext │ Simple...Multicaster │
│ 가 구현 │ (기본 구현) │
▼ │ - 타입 매칭으로 리스너 선별 │
PayloadApplicationEvent<T> │ - taskExecutor 있으면 비동기 │
(임의 객체를 이벤트로 래핑) │ - errorHandler로 예외 격리 │
▼
GenericApplicationListenerAdapter
├ ApplicationListener 빈을 감싸 타입 매칭
└ @EventListener 메서드 → ApplicationListenerMethodAdapter
ApplicationEvent: 모든 이벤트의 베이스(소스·타임스탬프 보유). 단 7.1에서는 임의 객체를 그냥 발행할 수 있고, 컨테이너가PayloadApplicationEvent<T>로 자동 래핑한다.ApplicationEventMulticaster: 리스너 등록부를 들고, 이벤트가 오면 매칭되는 리스너에게 뿌린다.applicationEventMulticaster라는 이름의 빈이 있으면 그것을, 없으면SimpleApplicationEventMulticaster를 쓴다(01장 refresh 8단계).SmartApplicationListener/GenericApplicationListener: 제네릭 이벤트 타입과 소스 타입을 런타임에 판별할 수 있게 하는 확장 인터페이스.
ctx.publishEvent(new OrderCreatedEvent(order))
│ AbstractApplicationContext.publishEvent(event, typeHint)
▼
1. event가 ApplicationEvent인가?
예 → 그대로 사용
아니오(POJO) → new PayloadApplicationEvent<>(this, payload, type) 로 래핑
│
▼
2. earlyApplicationEvents 버퍼가 살아있나? (멀티캐스터 초기화 전 = refresh 8단계 이전)
예 → 버퍼에 담아두고 나중에 일괄 발행
아니오 → multicaster.multicastEvent(event, eventType)
│
▼
3. 부모 컨텍스트가 있으면 부모에게도 전파 (계층 전체에 전달)
multicastEvent()의 본체는 단순하고 명확하다.
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
if (executor != null && listener.supportsAsyncExecution()) {
executor.execute(() -> invokeListener(listener, event)); // 비동기
}
else {
invokeListener(listener, event); // 동기(호출 스레드)
}
}getApplicationListeners(event, type)가 이벤트 타입과 호환되는 리스너만 골라낸다.ResolvableType으로 제네릭(ApplicationListener<OrderCreatedEvent>)까지 매칭하므로, 관심 없는 리스너는 애초에 호출되지 않는다.- 기본적으로 발행은 동기다. 즉
publishEvent()는 모든 리스너가 끝나야 반환한다.taskExecutor를 멀티캐스터에 설정하면 리스너 호출이 별도 스레드로 넘어간다. invokeListener()는errorHandler가 있으면 예외를 거기로 넘겨 한 리스너의 실패가 다른 리스너로 번지지 않게 격리한다.
인터페이스를 구현하는 대신 메서드에 어노테이션만 붙이는 방식.
@Component
class MailService {
@EventListener
void on(OrderCreatedEvent e) { ... }
@EventListener(condition = "#e.amount > 10000") // SpEL 조건
void onBig(OrderCreatedEvent e) { ... }
}이 메서드들을 리스너로 변환하는 것이 **EventListenerMethodProcessor**다. 이것은 SmartInitializingSingleton이라, 모든 싱글톤이 만들어진 직후(afterSingletonsInstantiated()) 한 번 호출된다.
afterSingletonsInstantiated()
└ 모든 빈을 순회하며 processBean(beanName, type)
└ @EventListener 메서드를 가진 빈인가?
└ EventListenerFactory(기본: DefaultEventListenerFactory)가
메서드마다 ApplicationListenerMethodAdapter 생성
└ 멀티캐스터에 ApplicationListener로 등록
ApplicationListenerMethodAdapter는 어댑터 패턴의 전형이다. 그것이 onApplicationEvent()를 받으면:
- 이벤트(또는
PayloadApplicationEvent의 payload)를 꺼내고, conditionSpEL을 평가해 거짓이면 건너뛰고(EventExpressionEvaluator),- 리플렉션으로 대상 메서드를 호출하며,
- 메서드가 값을 반환하면 그 반환값을 새 이벤트로 다시 발행한다(이벤트 체이닝). 컬렉션/배열을 반환하면 각 원소를 개별 이벤트로 발행한다.
AbstractApplicationContext.publishEvent(Object, ResolvableType)— POJO 래핑, early-event 버퍼링, 부모 전파를 처리하는 발행의 단일 입구.ApplicationEventPublisher.publishEvent()호출이 결국 여기로 모인다.SimpleApplicationEventMulticaster.multicastEvent()— 타입 매칭된 리스너에 동기/비동기로 디스패치.supportsAsyncExecution()이 false인 리스너(예: 트랜잭션 바운드 리스너)는 executor가 있어도 호출 스레드에서 실행된다.AbstractApplicationEventMulticaster.getApplicationListeners()— 이벤트 타입별로 매칭 결과를 캐시(ListenerCacheKey)해, 같은 타입 이벤트가 반복 발행돼도 매칭 비용을 한 번만 치른다.EventListenerMethodProcessor.afterSingletonsInstantiated()—@EventListener메서드를 스캔해 리스너로 등록하는 진입점. 싱글톤이 모두 준비된 뒤 도는 이유는, 리스너 빈이 완전히 초기화된 상태여야 하기 때문이다.
- 동기가 기본, 비동기는 선택: 트랜잭션 안에서 이벤트를 발행하면 같은 트랜잭션·스레드에서 리스너가 도는 것이 자연스럽다. 비동기가 필요하면 멀티캐스터에
taskExecutor를 주거나 리스너 메서드에@Async를 붙인다. - 타입 안전 디스패치:
ResolvableType기반 매칭으로 제네릭 이벤트까지 정확히 라우팅한다. 발행자는 수신자를, 수신자는 발행자를 몰라도 된다. - 컨텍스트 자체 이벤트:
ContextRefreshedEvent,ContextClosedEvent,ContextStartedEvent/ContextStoppedEvent등 라이프사이클 이벤트도 같은 채널로 흐른다. 애플리케이션 리스너는 인프라 이벤트와 도메인 이벤트를 동일하게 받는다. - 확장점:
EventListenerFactory(어노테이션→리스너 변환 전략 교체),ApplicationEventMulticaster빈 교체,conditionSpEL(@EventListener(condition=...)), 반환값 체이닝(리스너가 새 이벤트 발행). - 트랜잭션 바운드 리스너:
@TransactionalEventListener(spring-tx)는 이 메커니즘을 확장해 커밋/롤백 단계에서만 리스너를 발화시킨다 — 기반은 동일한ApplicationListenerMethodAdapter다.
이벤트는 publishEvent() → ApplicationEventMulticaster → 타입 매칭된 ApplicationListener로 흐르는 발행/구독 채널이다. POJO도 PayloadApplicationEvent로 래핑되어 발행되고, 기본은 동기 호출이며 executor/errorHandler로 비동기화·예외 격리가 가능하다. @EventListener 메서드는 EventListenerMethodProcessor가 싱글톤 완성 후 ApplicationListenerMethodAdapter로 변환·등록하며, 이 어댑터는 SpEL 조건 평가와 반환값 재발행(이벤트 체이닝)까지 지원한다.