JMS의 정의와 역할
JMS(Java Message Service)는 자바 플랫폼에서 메시지 지향 미들웨어(MOM)와 상호작용하기 위한 표준 API입니다. 이 API는 특정 벤더에 종속되지 않으며, 분산 환경에 있는 두 개 이상의 애플리케이션 간에 신뢰할 수 있는 비동기 메시지 통신을 가능하게 합니다. Apache ActiveMQ, RabbitMQ, Kafka와 같은 다양한 메시지 브로커들이 JMS 표준을 구현한 JMS Provider로 사용됩니다.
동기식 통신의 한계와 JMS의 필요성
전통적인 RPC(Remote Procedure Call)나 RMI와 같은 동기식 통신 방식은 클라이언트가 서버의 응답을 기다려야 하는 블로킹(blocking) 특성을 가집니다. 이는 시스템 간의 강한 결합(Tight Coupling)을 유발하며, 서버 장애나 네트워크 지연 시 전체 시스템의 성능 저하와 오류를 초래할 수 있습니다. 반면 JMS는 메시지 큐를 매개로 하는 비동기 통신 모델을 제공하여, 발신자와 수신자의 생명주기를 분리하고 시스템의 확장성과 유연성을 크게 향상시킵니다.
JMS 아키텍처 및 핵심 구성 요소
JMS 기반 시스템은 다음과 같은 핵심 구성 요소로 이루어집니다.
- JMS Provider: JMS 표준을 구현한 메시지 브로커 서버.
- Producer / Consumer: 메시지를 생성하여 전송하는 클라이언트와 메시지를 수신하여 처리하는 클라이언트.
- Destination: 메시지가 라우팅되는 목적지로, 큐(Queue) 또는 토픽(Topic) 형태를 띱니다.
- Connection Factory: 클라이언트가 JMS Provider와 물리적 연결을 맺기 위해 사용하는 팩토리 객체.
- Session: 메시지 생산 및 소비를 위한 단일 스레드 컨텍스트로, 트랜잭션 관리를 지원합니다.
- Message Listener: 비동기 방식으로 도착하는 메시지를 처리하기 위한 이벤트 기반 인터페이스.
메시지 전송 모델
1. 지점간 통신 (Point-to-Point, P2P)
P2P 모델은 큐(Queue)를 사용합니다. 하나의 메시지는 오직 하나의 소비자(Consumer)에 의해서만 처리됩니다. 메시지가 큐에 적재되면 소비자가 이를 가져와 처리할 때까지 보관됩니다. 작업 큐나 순차적 처리가 필요한 비즈니스 로직에 적합합니다.
2. 발행/구독 모델 (Publish/Subscribe, Pub/Sub)
Pub/Sub 모델은 토픽(Topic)을 사용합니다. 발행자(Publisher)가 토픽에 메시지를 전송하면, 해당 토픽을 구독하고 있는 모든 구독자(Subscriber)에게 메시지가 브로드캐스팅됩니다. 이벤트 기반 아키텍처나 실시간 알림 시스템에 주로 사용됩니다.
JMS 메시지의 구조
JMS에서 전송되는 메시지는 크게 세 부분으로 구성됩니다.
- Header (헤더): 라우팅 및 식별을 위한 메타데이터 (예: JMSMessageID, JMSTimestamp, JMSDeliveryMode, JMSPriority).
- Properties (속성): 애플리케이션 레벨에서 정의하는 사용자 지정 키-값 쌍으로, 메시지 선택기(Message Selector)를 통한 필터링에 활용됩니다.
- Body (본문): 실제 전송되는 데이터 페이로드. TextMessage, MapMessage, ObjectMessage, BytesMessage, StreamMessage 등 다양한 타입을 지원합니다.
핵심 프로그래밍 인터페이스 및 구현
JMS를 활용한 메시지 생산 및 소비 과정을 코드로 구현해 보겠습니다. 아래 예시는 JNDI를 통해 리소스를 조회하고, Try-with-resources 구문을 사용하여 리소스 누수를 방지하도록 재구성되었습니다.
메시지 생산자 (Producer) 구현
Context jndiContext = new InitialContext();
ConnectionFactory msgConnFactory = (ConnectionFactory) jndiContext.lookup("java:/comp/env/jms/MyConnectionFactory");
Destination targetQueue = (Destination) jndiContext.lookup("java:/comp/env/jms/OrderProcessingQueue");
try (Connection connection = msgConnFactory.createConnection()) {
// 트랜잭션을 사용하지 않으며, 자동 확인(Auto-Acknowledge) 모드로 세션 생성
Session msgSession = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageProducer sender = msgSession.createProducer(targetQueue);
// 텍스트 메시지 페이로드 생성 및 전송
TextMessage payload = msgSession.createTextMessage("New Order Placed: ORD-9921");
payload.setStringProperty("OrderType", "Standard"); // 사용자 정의 속성 추가
sender.send(payload);
System.out.println("Message successfully dispatched.");
} catch (Exception e) {
e.printStackTrace();
}
메시지 소비자 (Consumer) 구현
메시지를 비동기적으로 수신하기 위해 MessageListener를 등록하는 방식을 사용합니다.
Context jndiContext = new InitialContext();
ConnectionFactory msgConnFactory = (ConnectionFactory) jndiContext.lookup("java:/comp/env/jms/MyConnectionFactory");
Destination targetQueue = (Destination) jndiContext.lookup("java:/comp/env/jms/OrderProcessingQueue");
Connection connection = msgConnFactory.createConnection();
Session msgSession = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageConsumer receiver = msgSession.createConsumer(targetQueue);
// 람다 표현식을 활용한 비동기 메시지 리스너 등록
receiver.setMessageListener((Message incomingMsg) -> {
if (incomingMsg instanceof TextMessage) {
TextMessage textPayload = (TextMessage) incomingMsg;
try {
String orderData = textPayload.getText();
String orderType = textPayload.getStringProperty("OrderType");
System.out.println("Processing " + orderType + " order: " + orderData);
} catch (JMSException e) {
System.err.println("Error while extracting message payload: " + e.getMessage());
}
}
});
// 리스너가 메시지를 수신할 수 있도록 연결 시작
connection.start();
// 애플리케이션 종료를 방지하기 위한 대기 로직 (실제 환경에서는 컨테이너가 관리)
Thread.currentThread().join();