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 리스너 객체 직접
SessionAwareMessageListener
▲
AbstractAdaptableMessageListener 페이로드 추출, 반환값→응답 전송 (ch.04와 ch.06 연결)
│ handleResult / buildMessage / getResponseDestination / sendResponse
▼
MessagingMessageListenerAdapter spring-messaging InvocableHandlerMethod로 메서드 호출
(@JmsListener의 실제 일꾼)
MessageListenerAdapter 레거시: 메서드 이름 규약 기반 어댑터
JmsResponse<T> 반환값에 응답 목적지를 동봉
컨텍스트 기동
│
▼ 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)로 접근한다.
endpoint.setupListenerContainer는 AbstractJmsListenerEndpoint에서 공통 속성(destination, selector, subscription, concurrency)을 컨테이너에 밀어넣은 뒤, 추상 메서드 createMessageListener(container)를 호출해 리스너를 만들고 container.setupMessageListener(...)로 붙인다. MethodJmsListenerEndpoint의 구현은 MessagingMessageListenerAdapter를 만들어, 대상 빈·메서드·MessageHandlerMethodFactory를 연결한다.
메시지가 도착하면 컨테이너(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):createContainerInstance→initializeContainer(팩토리 공통 설정) →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<?>로 바꿔 메서드 인자를 풀고, 반환값을 응답 메시지로 되돌린다. 이 모든 변환의 바닥에는 메시지 컨버터와 목적지 리졸버가 있다 — 마지막 장의 주제다.