Skip to content

Latest commit

 

History

History
135 lines (107 loc) · 9.02 KB

File metadata and controls

135 lines (107 loc) · 9.02 KB

05. 애노테이션 기반 리스너 (annotation · config · adapter)

무엇을 / 왜

ch.04의 컨테이너는 강력하지만, 큐 하나당 DefaultMessageListenerContainer 빈을 직접 정의하고 MessageListener를 구현하는 것은 번거롭다. @JmsListener는 이를 POJO 메서드 한 개 + 애노테이션 한 줄로 줄인다.

@Component
class OrderListener {
    @JmsListener(destination = "orders", concurrency = "3-10")
    public String handle(@Payload Order order, @Header("priority") int p) { ... }
}

이 한 줄이 동작하려면 보이지 않는 세 패키지가 협업한다: annotation(스캔), config(엔드포인트·컨테이너 조립), adapter(메서드 호출 변환). 이 장은 부팅 시 @JmsListener가 어떻게 살아있는 컨테이너로 변신하는지, 그리고 런타임에 메시지가 어떻게 POJO 메서드 인자로 풀리는지를 따라간다.

핵심 타입

조립에 참여하는 빈들

@EnableJms ──Import──▶ JmsBootstrapConfiguration
                          ├─ JmsListenerAnnotationBeanPostProcessor  (BPP)
                          └─ JmsListenerEndpointRegistry             (레지스트리)

JmsListenerAnnotationBeanPostProcessor   @JmsListener 스캔 → 엔드포인트 생성
   │   (SmartInitializingSingleton, BeanFactoryAware, BeanPostProcessor)
   ▼
JmsListenerEndpointRegistrar             엔드포인트 수집 + 기본 팩토리 결정
   │
   ▼
JmsListenerEndpointRegistry              엔드포인트마다 컨테이너 생성·생명주기 관리
   │   (SmartLifecycle, ApplicationListener<ContextRefreshedEvent>)
   ▼
JmsListenerContainerFactory<C>           엔드포인트 → 컨테이너 인스턴스
   ├─ DefaultJmsListenerContainerFactory  → DefaultMessageListenerContainer
   ├─ SimpleJmsListenerContainerFactory   → SimpleMessageListenerContainer
   └─ DefaultJcaListenerContainerFactory  → JmsMessageEndpointManager

JmsListenerEndpoint                       "이 목적지에서 이 리스너를" 명세
   ├─ MethodJmsListenerEndpoint   @JmsListener 메서드 기반
   └─ SimpleJmsListenerEndpoint   리스너 객체 직접

어댑터 — 메서드를 MessageListener로

SessionAwareMessageListener
   ▲
AbstractAdaptableMessageListener      페이로드 추출, 반환값→응답 전송 (ch.04와 ch.06 연결)
   │   handleResult / buildMessage / getResponseDestination / sendResponse
   ▼
MessagingMessageListenerAdapter       spring-messaging InvocableHandlerMethod로 메서드 호출
                                       (@JmsListener의 실제 일꾼)

MessageListenerAdapter                레거시: 메서드 이름 규약 기반 어댑터
JmsResponse<T>                        반환값에 응답 목적지를 동봉

동작 흐름

1) 부팅: @JmsListener → 컨테이너

컨텍스트 기동
   │
   ▼ JmsListenerAnnotationBeanPostProcessor.postProcessAfterInitialization(bean)
       각 빈의 타입을 MethodIntrospector로 스캔
       → @JmsListener 메서드 발견 시 processJmsListener(annotation, method, bean)
           MethodJmsListenerEndpoint 생성 + 애노테이션 속성 주입
              (id, destination, selector, subscription, concurrency, ...)
           registrar.registerEndpoint(endpoint, factory)   // 일단 수집만
   │
   ▼ 모든 싱글톤 생성 후: afterSingletonsInstantiated()  (SmartInitializingSingleton)
       JmsListenerConfigurer 빈들에게 registrar 커스터마이즈 기회 제공
       기본 컨테이너 팩토리 이름("jmsListenerContainerFactory") 설정
       registrar.afterPropertiesSet() → registerAllEndpoints()
   │
   ▼ registrar → registry.registerListenerContainer(endpoint, factory)
       factory.createListenerContainer(endpoint)
          C instance = createContainerInstance()       // Default/Simple 컨테이너 생성
          initializeContainer(instance)                // 팩토리의 공통 설정 주입
          endpoint.setupListenerContainer(instance)    // 엔드포인트별 설정 + 리스너 부착
       registry가 컨테이너 보관 (앱 컨텍스트 빈이 아님!)
   │
   ▼ ContextRefreshedEvent → registry는 SmartLifecycle로서 start() → 각 컨테이너 start()

핵심 분담: BPP는 "발견과 명세화", registrar는 "수집과 기본값 결정", registry는 "컨테이너 제조와 생명주기". registry가 만든 컨테이너는 컨텍스트의 일반 빈이 아니어서 autowiring 대상이 아니다 — 관리하려면 registry.getListenerContainer(id)로 접근한다.

2) 엔드포인트 → 리스너 부착

endpoint.setupListenerContainerAbstractJmsListenerEndpoint에서 공통 속성(destination, selector, subscription, concurrency)을 컨테이너에 밀어넣은 뒤, 추상 메서드 createMessageListener(container)를 호출해 리스너를 만들고 container.setupMessageListener(...)로 붙인다. MethodJmsListenerEndpoint의 구현은 MessagingMessageListenerAdapter를 만들어, 대상 빈·메서드·MessageHandlerMethodFactory를 연결한다.

3) 런타임: 메시지 → 메서드 인자

메시지가 도착하면 컨테이너(ch.04)가 어댑터의 onMessage(message, session)를 부른다. 여기서 spring-messaging 인프라가 인자를 푼다.

MessagingMessageListenerAdapter.onMessage(jmsMessage, session)
   │
   ▼ toMessagingMessage(jmsMessage)
       MessagingMessageConverter로 JMS Message → org.springframework.messaging.Message<?>
       (페이로드는 LazyResolutionMessage로 지연 변환 — ch.06)
   │
   ▼ invokeHandler(jmsMessage, session, message)
       InvocableHandlerMethod가 @Payload/@Header/Session/Message 인자를 해석해 메서드 호출
   │
   ▼ (메서드가 값 반환 시) handleResult(result, jmsMessage, session)
       buildMessage(session, result)            // 반환 객체 → 응답 JMS Message
       postProcessResponse(request, response)   // JMSCorrelationID 세팅
       destination = getResponseDestination(...)// JMSReplyTo → @SendTo → 기본값 순
       sendResponse(session, destination, response)

반환값 기반 응답이 SessionAwareMessageListener가 필요한 이유다 — 응답을 보내려면 같은 Session이 있어야 한다. 응답 목적지는 요청 메시지의 JMSReplyTo를 1순위로, 없으면 @SendTo/기본 응답 목적지를, 그래도 없으면 예외를 던진다. JmsResponse를 반환하면 목적지를 코드로 직접 지정할 수 있다.

핵심 메서드

  • JmsListenerAnnotationBeanPostProcessor.processJmsListener(...): 애노테이션 속성을 MethodJmsListenerEndpoint로 변환하고 ${...} 플레이스홀더를 embeddedValueResolver로 해석. 발견의 종착점.
  • JmsListenerEndpointRegistry.createListenerContainer(endpoint, factory): 팩토리로 컨테이너를 만들고 afterPropertiesSet·phase 정합성을 처리. 명세가 실물이 되는 곳.
  • AbstractJmsListenerContainerFactory.createListenerContainer(endpoint): createContainerInstanceinitializeContainer(팩토리 공통 설정) → endpoint.setupListenerContainer(엔드포인트별 설정)의 3단 조립.
  • AbstractAdaptableMessageListener.handleResult(...): 리스너 반환값을 응답 메시지로 만들어 전송. 요청-응답 패턴의 수신측 구현.

설계 포인트 / 확장점

  • 명세(endpoint) ↔ 실물(container) 분리: 엔드포인트는 "무엇을 어디서"만 담는 가벼운 명세이고, 컨테이너 생성은 팩토리에 위임된다. 덕분에 같은 @JmsListener를 Default/Simple/JCA 어느 컨테이너로도 실현할 수 있다.
  • JmsListenerConfigurer: registrar에 직접 접근해 엔드포인트를 코드로 추가하거나 기본 팩토리·레지스트리·MessageHandlerMethodFactory를 교체. 애노테이션 밖의 프로그래밍 방식 등록 경로.
  • MessageHandlerMethodFactory: 인자 해석(@Payload/@Header/검증)을 spring-messaging에 위임. 커스텀 HandlerMethodArgumentResolver를 끼워 인자 종류를 확장한다.
  • XML 대안: JmsNamespaceHandler*Parser들이 <jms:annotation-driven>/<jms:listener-container>를 같은 인프라(BPP·registry)로 풀어, 애노테이션과 XML 구성이 동일 엔진을 공유한다.
  • 반복 애노테이션: @JmsListener@Repeatable(JmsListeners.class)이고 @MessageMapping 메타애노테이션이라, 한 메서드에 여러 목적지를 매핑하거나 커스텀 합성 애노테이션을 만들 수 있다.

정리

@JmsListener는 부팅 시 BPP가 발견하고, registrar가 수집·기본값을 정하고, registry가 팩토리로 컨테이너를 제조해 생명주기를 관리하는 파이프라인으로 실현된다. 런타임에는 MessagingMessageListenerAdapter가 JMS 메시지를 spring-messaging Message<?>로 바꿔 메서드 인자를 풀고, 반환값을 응답 메시지로 되돌린다. 이 모든 변환의 바닥에는 메시지 컨버터와 목적지 리졸버가 있다 — 마지막 장의 주제다.