본문 바로가기
1. 개발/1.8. ActiveMQ

메시지 헤더의 'JMSCorrelationID'를 이용한 RPC 패턴 구현법은?

by 엉짱 2026. 3. 18.
반응형

메시지 헤더의 'JMSCorrelationID'를 이용한 RPC 패턴 구현법은?

엔터프라이즈 환경에서 메시지 브로커(ActiveMQ, Amazon MQ 등)를 도입하는 주된 목적은 시스템 간의 결합도를 낮추는 비동기(Asynchronous) 통신입니다. 생산자는 메시지를 큐에 던지고 자신의 작업을 계속하며, 소비자는 나중에 여유가 될 때 이를 처리합니다.

하지만 실무를 하다 보면, 결제 승인 결과나 재고 확인처럼 요청을 보낸 후 반드시 그에 대한 응답(결과)을 즉시 받아야만 다음 비즈니스 로직을 진행할 수 있는 동기식(Synchronous) 처리가 필요한 순간이 반드시 찾아옵니다.

이처럼 비동기 메시징 인프라 위에서 동기식 요청-응답 처리를 흉내 내는 기법을 요청-응답 패턴(Request-Reply Pattern) 또는 RPC(Remote Procedure Call) 패턴이라고 부릅니다. 이 패턴을 완벽하게 구현하기 위한 핵심 열쇠가 바로 메시지 표준 헤더인 JMSCorrelationIDJMSReplyTo입니다.


1. RPC 패턴 구현의 핵심 원리: 두 개의 헤더

단방향 메시징을 양방향 통신으로 바꾸려면, 수신자가 응답을 "어디로" 보낼지, 그리고 송신자가 수많은 응답 중 "어떤 것이 내 요청에 대한 답"인지 알아야 합니다.

  • JMSReplyTo (회신 주소):
    요청자(Client)가 메시지를 보낼 때, "작업을 다 마치면 이 주소(Queue 또는 Topic)로 응답을 보내주세요"라고 명시하는 헤더입니다.
  • JMSCorrelationID (상관관계 식별자):
    요청자가 자신이 보낸 요청을 고유하게 식별하기 위해 부여하는 ID입니다. 응답자(Server)는 응답 메시지를 만들 때 이 값을 그대로 복사하여 헤더에 넣습니다. 요청자는 회신용 큐에서 이 ID를 가진 메시지만 필터링하여 자신의 응답을 찾아냅니다.

2. 단계별 구현 워크플로우 (Step-by-Step)

메시지 브로커를 경유하는 RPC 호출은 다음과 같은 정교한 6단계의 라이프사이클을 거칩니다.

  1. 임시 목적지 생성: 요청자는 응답을 받기 위한 자신만의 임시 큐(Temporary Queue)를 생성하거나, 사전에 합의된 전용 응답 큐를 준비합니다.
  2. 요청 메시지 조립: 요청 메시지를 생성하고, JMSReplyTo 헤더에 앞서 만든 응답 큐를 지정합니다.
  3. 식별자 주입: 고유한 UUID를 생성하여 요청 메시지의 JMSCorrelationID에 세팅합니다. (때로는 브로커가 자동 생성하는 JMSMessageID를 그대로 활용하기도 합니다.)
  4. 전송 및 대기: 요청 메시지를 메인 작업 큐(Request Queue)로 전송합니다. 그 직후, 요청자는 응답 큐에 컨슈머를 달고 JMSCorrelationID = '생성한UUID' 조건으로 메시지 셀렉터(Message Selector)를 걸어 동기적으로 대기(Block)합니다.
  5. 서버 측 처리 및 회신: 메인 작업 큐에서 대기하던 워커(서버)가 요청 메시지를 수신하여 비즈니스 로직을 수행합니다. 완료 후, 새로운 응답 메시지를 생성합니다. 이때 원본 요청 메시지의 JMSCorrelationID 값을 추출하여 응답 메시지에 그대로 복사하고, JMSReplyTo에 적힌 주소로 전송합니다.
  6. 응답 수신 및 재개: 셀렉터를 걸고 대기하던 요청자가 자신의 JMSCorrelationID와 일치하는 응답을 낚아챕니다. 응답 데이터를 파싱하고 중단되었던 비즈니스 로직을 재개합니다.

3. Java 클라이언트 코드 구현 예시

순수 JMS API를 사용하여 이 패턴을 구현하는 핵심 로직은 다음과 같습니다.

요청자 (Client/Producer) 로직:

// 1. 임시 큐 생성
TemporaryQueue replyQueue = session.createTemporaryQueue();
MessageConsumer responseConsumer = session.createConsumer(replyQueue);

// 2. 요청 메시지 생성 및 헤더 세팅
TextMessage requestMessage = session.createTextMessage("OrderData:12345");
requestMessage.setJMSReplyTo(replyQueue);
String correlationId = UUID.randomUUID().toString();
requestMessage.setJMSCorrelationID(correlationId);

// 3. 전송
MessageProducer producer = session.createProducer(requestQueue);
producer.send(requestMessage);

// 4. 동기적 대기 (타임아웃 5초) - 특정 CorrelationID만 수신하도록 셀렉터 적용
Message responseMessage = responseConsumer.receive(5000);

if (responseMessage != null) {
    System.out.println("응답 수신 완료: " + ((TextMessage) responseMessage).getText());
} else {
    System.out.println("응답 타임아웃 발생!");
}

응답자 (Server/Consumer) 로직:

public void onMessage(Message requestMessage) {
    try {
        // 1. 요청 처리
        String requestPayload = ((TextMessage) requestMessage).getText();
        String result = processOrder(requestPayload);

        // 2. 응답 메시지 생성
        TextMessage responseMessage = session.createTextMessage(result);

        // 3. CorrelationID 복사 (매우 중요)
        responseMessage.setJMSCorrelationID(requestMessage.getJMSCorrelationID());

        // 4. JMSReplyTo 목적지로 전송
        Destination replyDestination = requestMessage.getJMSReplyTo();
        MessageProducer replyProducer = session.createProducer(replyDestination);
        replyProducer.send(responseMessage);

        replyProducer.close();
    } catch (JMSException e) {
        e.printStackTrace();
    }
}

4. 아키텍처 설계 시 핵심 주의사항 (Best Practices)

이 패턴을 프로덕션 환경에 적용할 때 브로커의 성능 저하를 막기 위해 다음 요소들을 반드시 고려해야 합니다.

A. 임시 큐(Temporary Queue) vs 고정 응답 큐(Fixed Queue)

  • 임시 큐: RPC 호출이 일어날 때마다 session.createTemporaryQueue()를 호출하면, 브로커 내부적으로 목적지를 생성하고 지우는 극심한 오버헤드가 발생합니다. 동시 요청이 적은 환경에서만 적합합니다.
  • 고정 응답 큐: 대규모 트래픽 환경에서는 요청자(클라이언트)마다 고정된 1개의 전용 응답 큐를 미리 만들어 두고 재사용해야 합니다. 수많은 스레드가 하나의 응답 큐를 공유할 경우, JMSCorrelationID 기반의 셀렉터를 사용하여 각 스레드가 자신의 응답만 정확히 솎아내도록 설계해야 합니다.

B. 타임아웃(Timeout)과 자원 누수 방지

서버의 장애로 인해 영원히 응답이 오지 않을 수 있습니다. 요청자는 receive(timeout) 메서드를 사용하여 반드시 최대 대기 시간을 명시해야 합니다. 또한, 타임아웃이 발생하여 요청자가 대기를 포기한 후 뒤늦게 도착한 응답 메시지들은 응답 큐에 영원히 방치되는 쓰레기(Orphan) 데이터가 됩니다. 이를 방지하기 위해 서버가 응답 메시지를 보낼 때 짧은 TimeToLive(TTL)를 설정하여 브로커가 스스로 만료된 메시지를 삭제하도록 유도해야 합니다.

C. 브로커의 셀렉터 성능 병목 (Full Scan)

하나의 거대한 공유 응답 큐에서 수천 개의 스레드가 동시에 JMSCorrelationID = '...' 셀렉터를 걸고 대기하면, 브로커는 응답 메시지가 큐에 들어올 때마다 조건에 맞는 컨슈머를 찾기 위해 큐를 풀 스캔(Full Scan)해야 합니다. 이는 막대한 CPU 부하를 일으키므로, 가급적이면 애플리케이션 인스턴스 단위로 응답 큐를 물리적으로 분리하는 것이 바람직합니다.

반응형