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

'Temporary Queue'의 생명주기와 브로커 재시작 시의 동작은?

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

'Temporary Queue'의 생명주기와 브로커 재시작 시의 동작은?

엔터프라이즈 메시징 시스템에서 비동기 통신 인프라를 구축하더라도, 시스템 간에 반드시 결과를 동기적으로 기다려야 하는 '요청-응답(Request-Reply)' 패턴이 필요한 순간이 있습니다. 이때 서버가 처리 결과를 클라이언트에게 돌려주기 위한 1회성 반송 주소로 가장 널리 사용되는 컴포넌트가 바로 Temporary Queue(임시 큐)입니다.

하지만 편리함 이면에는 매우 까다로운 생명주기(Lifecycle) 관리가 숨어있습니다. 이 가이드에서는 임시 큐의 탄생부터 소멸까지의 과정을 파헤치고, 특히 브로커 서버가 예기치 않게 재시작(Restart)되었을 때 클라이언트와 시스템에 미치는 파급 효과 및 방어 아키텍처를 상세히 분석합니다.


1. Temporary Queue의 본질적 특징과 생명주기

임시 큐는 관리자가 브로커 설정 파일(broker.xml 등)에 명시적으로 선언하는 정적(Static) 목적지가 아닙니다. 철저하게 클라이언트 애플리케이션의 런타임 요청에 의해 동적으로 생성되고 관리됩니다.

A. 생성 (Creation)
클라이언트가 session.createTemporaryQueue() API를 호출하는 순간, 브로커의 메모리상에 고유한 이름을 가진(예: ID:hostname-12345-1) 큐가 즉각적으로 생성됩니다.

B. 독점적 소비권 (Exclusive Consumption)
이 큐는 생성된 그 순간부터 이를 생성한 '커넥션(Connection)'에 완벽하게 종속됩니다. 다른 클라이언트는 이 임시 큐의 이름을 알아내어 메시지를 보낼(Send) 수는 있지만, 절대 메시지를 읽어갈(Receive/Consume) 수는 없습니다. 이러한 독점성 덕분에 다른 스레드나 인스턴스의 응답 메시지와 섞이지 않는 안전한 통신이 보장됩니다.

C. 정상적인 소멸 (Destruction)
임시 큐의 수명은 커넥션의 수명과 동일합니다.

  • 클라이언트가 명시적으로 temporaryQueue.delete()를 호출할 때.
  • 클라이언트 애플리케이션이 정상 종료되어 브로커와의 커넥션이 close() 될 때.
    위 두 가지 상황에서 브로커는 즉시 해당 임시 큐를 메모리에서 파기하고, 안에 남아있던 메시지들도 가비지 컬렉션(GC)을 통해 소멸시킵니다.

2. 브로커 재시작(Restart) 시의 치명적인 동작 매커니즘

운영 환경에서 브로커 서버의 OS 패치나 OOM(Out of Memory) 장애 등으로 인해 브로커 프로세스가 예기치 않게 종료되었다가 재시작된다면 어떤 일이 벌어질까요? 여기서 많은 개발자가 치명적인 오해를 범합니다.

"임시 큐에 영속성(Persistent) 모드로 메시지를 보냈으니, 브로커가 재시작되어도 큐와 메시지가 살아있을 것이다?"
절대 아닙니다.

  1. 메타데이터의 휘발성: 임시 큐라는 목적지 구조(Metadata) 자체는 브로커의 디스크(KahaDB, Journal 등)에 기록되지 않습니다. 오직 브로커의 힙 메모리상에만 존재합니다.
  2. 영속성 무시: 큐 자체가 메모리에서 증발하기 때문에, 그 큐를 향해 날아가던 메시지가 아무리 DeliveryMode.PERSISTENT로 설정되어 있었다 하더라도 디스크 복구 대상에서 완전히 제외됩니다. 브로커가 종료되는 찰나, 임시 큐와 그 안의 모든 데이터는 영구적으로 소멸합니다.
  3. 복구 불가: 브로커가 재부팅을 완료하더라도, 과거에 존재했던 임시 큐들의 목록은 복원되지 않습니다. 완전히 백지상태의 브로커가 기동됩니다.

3. Failover(장애 조치) 환경에서의 연쇄 장애 현상

클라이언트가 failover:(tcp://broker1,tcp://broker2) 트랜스포트를 사용 중이라면 상황은 더욱 복잡해집니다.

  1. 메인 브로커가 다운되면, 클라이언트의 Failover 메커니즘이 즉각 작동하여 백업 브로커(또는 재시작된 메인 브로커)로 TCP 커넥션을 다시 맺습니다.
  2. 이때 프레임워크는 세션(Session)과 일반 컨슈머들은 자동으로 복구해주려 시도하지만, 과거 커넥션에 묶여있던 임시 큐는 새 커넥션으로 승계되지 않습니다.
  3. 클라이언트 애플리케이션의 비즈니스 스레드는 예전에 만든 임시 큐에서 응답이 오기를 여전히 receive(timeout) 상태로 기다리고 있습니다.
  4. 그러나 서버 측 워커(Worker)가 로직을 처리하고 과거의 임시 큐 주소(JMSReplyTo)로 응답 메시지를 보내려 시도하면, 브로커는 해당 큐가 존재하지 않으므로 서버 측에 InvalidDestinationException을 발생시킵니다.
  5. 결국 클라이언트는 타임아웃 에러를 내뿜고, 서버 측 로직은 예외를 발생시키는 양방향 연쇄 장애가 발생합니다.

4. 복원력을 갖춘 아키텍처 설계 모범 사례 (Best Practices)

이러한 태생적 한계를 극복하고 견고한 메시징 파이프라인을 구축하기 위해서는 다음의 아키텍처 원칙을 적용해야 합니다.

A. 순수 JMS API 사용 시: ExceptionListener 기반 재생성
클라이언트 코드에서 브로커와의 연결 단절 이벤트를 감지할 수 있는 ExceptionListener를 반드시 구현해야 합니다. 네트워크가 끊어지고 재접속되는 이벤트가 발생하면, 과거의 임시 큐를 참조하던 객체들을 모두 폐기하고 새로운 TemporaryQueue를 생성하여 서버 쪽에 새로운 JMSReplyTo 주소를 알려주는 로직이 필요합니다.

B. Spring JmsTemplate의 비동기 래핑 활용
Spring Framework 환경이라면 날것의 임시 큐를 직접 제어하는 대신, 임시 목적지 생성과 파기, 그리고 타임아웃 처리를 프레임워크 레벨에서 안전하게 캡슐화해 둔 JmsTemplate.sendAndReceive 메서드나 @JmsListener를 활용하는 것이 엣지 케이스(Edge Case) 방어에 훨씬 유리합니다.

C. 고정 응답 큐(Fixed Reply Queue) 아키텍처로의 전환
사실 가장 권장되는 궁극적인 해결책입니다. 잦은 네트워크 단절이 예상되거나 재시작 후에도 요청-응답의 유실을 막아야 하는 크리티컬한 비즈니스라면, 휘발성 임시 큐를 버려야 합니다.
대신 클라이언트 인스턴스별로 영구적인 고정 응답 큐(예: REPLY.QUEUE.NODE01)를 브로커에 하나 미리 만들어 둡니다. 브로커가 재시작되어도 이 큐는 유지됩니다. 클라이언트는 이 고정 큐를 관찰하면서, 메시지 헤더의 JMSCorrelationID를 셀렉터로 활용하여 자신의 요청에 대한 응답만 쏙쏙 뽑아가는 아키텍처로 설계해야 완벽한 장애 내성(Fault Tolerance)을 갖출 수 있습니다.


요약하자면, 임시 큐(Temporary Queue)는 설정 비용이 들지 않는 빠르고 매력적인 기능이지만 브로커 재시작 시 완벽하게 증발한다는 한계를 지닙니다. 시스템의 요구사항이 강력한 데이터 보존과 복원력을 필요로 한다면, 임시 큐 대신 Correlation ID를 결합한 고정 목적지 라우팅으로 설계를 고도화하시기 바랍니다.

반응형