Skip to content

Latest commit

 

History

History
118 lines (93 loc) · 8.72 KB

File metadata and controls

118 lines (93 loc) · 8.72 KB

03. 이벤트 발행과 리스너

무엇을 / 왜

컴포넌트 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: 제네릭 이벤트 타입과 소스 타입을 런타임에 판별할 수 있게 하는 확장 인터페이스.

동작 흐름 1: publishEvent → 배달

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가 있으면 예외를 거기로 넘겨 한 리스너의 실패가 다른 리스너로 번지지 않게 격리한다.

동작 흐름 2: @EventListener 메서드 등록

인터페이스를 구현하는 대신 메서드에 어노테이션만 붙이는 방식.

@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()를 받으면:

  1. 이벤트(또는 PayloadApplicationEvent의 payload)를 꺼내고,
  2. condition SpEL을 평가해 거짓이면 건너뛰고(EventExpressionEvaluator),
  3. 리플렉션으로 대상 메서드를 호출하며,
  4. 메서드가 값을 반환하면 그 반환값을 새 이벤트로 다시 발행한다(이벤트 체이닝). 컬렉션/배열을 반환하면 각 원소를 개별 이벤트로 발행한다.

핵심 메서드

  • 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 빈 교체, condition SpEL(@EventListener(condition=...)), 반환값 체이닝(리스너가 새 이벤트 발행).
  • 트랜잭션 바운드 리스너: @TransactionalEventListener(spring-tx)는 이 메커니즘을 확장해 커밋/롤백 단계에서만 리스너를 발화시킨다 — 기반은 동일한 ApplicationListenerMethodAdapter다.

정리

이벤트는 publishEvent()ApplicationEventMulticaster → 타입 매칭된 ApplicationListener로 흐르는 발행/구독 채널이다. POJO도 PayloadApplicationEvent로 래핑되어 발행되고, 기본은 동기 호출이며 executor/errorHandler로 비동기화·예외 격리가 가능하다. @EventListener 메서드는 EventListenerMethodProcessor가 싱글톤 완성 후 ApplicationListenerMethodAdapter로 변환·등록하며, 이 어댑터는 SpEL 조건 평가와 반환값 재발행(이벤트 체이닝)까지 지원한다.