spring-jms는 한 가지 기술(Jakarta Messaging, 옛 JMS)을 감싸지만, 내부는 "보내는 쪽"과 "받는 쪽"이라는 두 축으로 갈린다. 이 장은 모듈 전체를 한눈에 조망하고, 모든 곳에서 공유되는 두 가지 토대 — **비검사 예외 변환 계층(JmsException)**과 공통 기반 클래스(JmsAccessor) — 를 먼저 짚는다. 이후 장들은 이 토대 위에 쌓인다.
JMS API의 두 가지 불편함이 이 토대의 존재 이유다. 첫째, 거의 모든 JMS 메서드가 검사 예외 jakarta.jms.JMSException을 던져 호출 코드가 try/catch에 강제로 묶인다. 둘째, ConnectionFactory에서 Connection/Session을 얻고 닫는 패턴이 어디서나 똑같이 반복된다. spring-jms는 전자를 런타임 예외 계층으로, 후자를 기반 클래스의 protected 메서드로 흡수한다.
┌─────────────────────────────────────────┐
│ 공통 토대 (support) │
│ JmsAccessor → JmsDestinationAccessor │
│ JmsUtils(예외변환/자원닫기), JmsException │
└───────────────┬─────────────────────────┘
│
┌───────────────────────┴────────────────────────┐
▼ 보내는 쪽 (동기) ▼ 받는 쪽 (비동기)
┌──────────────────────┐ ┌──────────────────────────┐
│ core.JmsTemplate │ │ listener.*Container │
│ core.JmsClient │ │ annotation.@JmsListener │
│ core.JmsMessagingTpl │ │ config.*Factory/Registry │
└──────────┬───────────┘ │ listener.adapter.* │
│ └────────────┬─────────────┘
▼ ▼
┌────────────────────────────────────────────────────────────────────┐
│ connection: ConnectionFactory 어댑터, JmsTransactionManager, │
│ ConnectionFactoryUtils (트랜잭션 동기화된 Session) │
└────────────────────────────────────────────────────────────────────┘
양쪽 모두 결국 connection 패키지를 통해 실제 JMS Connection/Session을 얻고, support 패키지의 컨버터·리졸버로 페이로드와 목적지를 다룬다.
루트 패키지(org.springframework.jms)에는 클래스 하나당 거의 한 파일씩 예외가 늘어서 있다. 이들은 전부 추상 클래스 JmsException(런타임 예외)을 상속한다. 핵심은 검사 예외를 비검사로 바꾸면서 의미를 보존하는 매핑이다.
RuntimeException
└── JmsException (추상)
├── UncategorizedJmsException 분류 안 되는 JMSException
├── InvalidDestinationException ← jakarta.jms.InvalidDestinationException
├── InvalidSelectorException ← jakarta.jms.InvalidSelectorException
├── MessageFormatException, MessageNotReadableException, ...
├── JmsSecurityException, TransactionRolledBackException, ...
└── (support 하위) MessageConversionException, DestinationResolutionException
이 매핑을 수행하는 정적 메서드가 JmsUtils.convertJmsAccessException(JMSException)이다. 들어온 JMSException의 구체 타입을 보고 대응하는 Spring 예외로 감싸 던지며, 매칭되는 것이 없으면 UncategorizedJmsException으로 떨어뜨린다. JmsAccessor.convertJmsAccessException(...)이 이 정적 메서드로 위임하므로, 모든 하위 클래스(JmsTemplate, 컨테이너)는 catch (JMSException ex) { throw convertJmsAccessException(ex); } 한 줄로 변환을 끝낸다.
JMS에 접근하는 모든 헬퍼의 뿌리다. JmsTemplate도, 모든 리스너 컨테이너도 결국 이 둘을 상속한다.
JmsAccessor(support 패키지, InitializingBean 구현)가 담는 것:
connectionFactory— 모든 작업의 출발점.obtainConnectionFactory()로 null 검증과 함께 꺼낸다.sessionTransacted,sessionAcknowledgeMode— Session을 만들 때의 트랜잭션/확인 모드.- protected 팩토리 메서드
createConnection(),createSession(Connection)—ConnectionFactory.createConnection()과Connection.createSession(transacted, ackMode)를 감싸 하위 클래스가 일관되게 자원을 만들게 한다.
JmsDestinationAccessor는 여기에 목적지 관련 상태를 더한다:
destinationResolver(기본SimpleDestinationResolver),pubSubDomain(Queue냐 Topic이냐).resolveDestinationName(session, name)— 이름 문자열을Destination객체로 해석.receiveFromConsumer(consumer, timeout)— timeout 부호에 따라receive(timeout)/receiveNoWait()/receive()를 갈라 호출.RECEIVE_TIMEOUT_NO_WAIT(-1),RECEIVE_TIMEOUT_INDEFINITE_WAIT(0)상수가 여기 정의된다.
JmsAccessor (ConnectionFactory, 트랜잭션/확인 모드, 예외 변환)
└── JmsDestinationAccessor (+ DestinationResolver, pubSubDomain, receive 타임아웃)
├── JmsTemplate (동기 — ch.02)
└── AbstractJmsListeningContainer → ... (비동기 — ch.04)
JmsTemplate.receive()를 예로 들면, 변환 경계가 어디인지 한눈에 보인다.
JmsTemplate.receive()
│
▼ execute(SessionCallback, startConnection=true)
│ try {
│ Session = ConnectionFactoryUtils.doGetTransactionalSession(...) // 트랜잭션 동기화 확인
│ ... action.doInJms(session) ... ← 실제 JMS 호출 (JMSException 던질 수 있음)
│ }
│ catch (JMSException ex) {
│ throw convertJmsAccessException(ex); ← 여기서 비검사 JmsException으로 변환
│ }
│ finally {
│ JmsUtils.closeSession(...); ConnectionFactoryUtils.releaseConnection(...);
│ }
▼
호출자는 JmsException(런타임)만 본다 — try/catch 강제 없음
finally의 자원 정리 역시 JmsUtils.closeSession/closeMessageProducer/closeMessageConsumer로 통일되어 있다. 이들은 닫는 도중 발생한 예외를 삼키고 로깅만 하므로, 정상 흐름의 예외가 자원 정리 예외에 가려지지 않는다.
JmsUtils.convertJmsAccessException(JMSException): 모듈 전체의 예외 변환 단일 창구. JMS 표준 예외 타입을 Spring 예외로 1:1 매핑한다.JmsAccessor.createSession(Connection):con.createSession(isSessionTransacted(), getSessionAcknowledgeMode()). 트랜잭션/확인 모드 설정이 실제 Session에 반영되는 단 한 곳.JmsAccessor.isClientAcknowledge(Session): 확인 모드가 표준 3종(AUTO/CLIENT/DUPS_OK)도 TRANSACTED도 아닌 "벤더 특화 클라이언트 확인"인지 판별. 이후 컨테이너가message.acknowledge()를 직접 부를지 결정하는 데 쓰인다.JmsDestinationAccessor.receiveFromConsumer(consumer, timeout): 타임아웃 의미(블로킹/즉시/무한대기)를 한 메서드로 통일.
- 템플릿 메서드 + 일관된 자원 패턴:
create*/close*가 전부 protected 메서드로 노출되어, 특수 JMS 드라이버 대응 시 하위 클래스가 자원 생성을 가로챌 수 있다. - 예외 변환의 중앙화: 변환 로직이
JmsUtils한 곳에 모여 있어, 새 JMS 예외 타입 추가나 매핑 변경이 국소적이다.convertJmsAccessException을 오버라이드하면 애플리케이션별 예외 정책도 끼울 수 있다. - AOT/네이티브 힌트:
connection.ConnectionFactoriesRuntimeHints가 프록시 기반(SingleConnectionFactory의 공유 Connection 프록시 등)에 필요한 리플렉션·프록시 힌트를 등록해 GraalVM 네이티브 이미지에서도 동작하게 한다.
spring-jms는 "동기로 보내기"와 "비동기로 받기"라는 두 책임을, JmsAccessor/JmsDestinationAccessor라는 공통 기반과 JmsException 비검사 예외 계층 위에 세운다. 어디서 JMS를 만지든 자원 생성·정리는 기반 클래스의 protected 메서드로, 예외 변환은 JmsUtils 단일 창구로 수렴한다. 다음 장부터는 이 토대 위의 두 축을 차례로 파고든다 — 먼저 보내는 쪽인 JmsTemplate이다.