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

'Retroactive Consumer'를 통한 과거 토픽 메시지 수신법은?

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

'Retroactive Consumer'를 통한 과거 토픽 메시지 수신법은?

1. 토픽(Topic) 모델의 태생적 한계와 해결책

엔터프라이즈 메시지 브로커의 Publish/Subscribe(발행/구독) 모델에서 토픽(Topic)은 일종의 실시간 라디오 방송과 같이 동작합니다. 생산자(Publisher)가 특정 토픽으로 메시지를 브로드캐스팅하는 바로 그 찰나의 순간에, 브로커와 활성 상태로 연결되어 있는 소비자(Subscriber)들만이 해당 메시지를 수신할 수 있습니다.

이러한 특성으로 인해, 소비자가 배포 작업이나 일시적인 네트워크 장애로 잠시 오프라인 상태가 되었거나, 시스템이 늦게 기동되어 뒤늦게 토픽을 구독(Late Joiner)하기 시작했다면, 자신이 접속하기 직전까지 발행되었던 과거의 메시지 흐름은 영원히 유실됩니다.

이러한 휘발성 문제를 극복하고, 새롭게 접속한 소비자도 브로커에 캐싱된 과거의 메시지부터 거슬러 올라가 순차적으로 수신할 수 있도록 지원하는 강력한 기능이 바로 Retroactive Consumer(소급 소비자) 패턴입니다.

2. Retroactive Consumer의 동작 매커니즘: 복구 버퍼 (Recovery Buffer)

과거의 메시지를 늦게 접속한 클라이언트에게 다시 전달하려면, 브로커 내부에 지나간 메시지들을 임시로 붙잡아두는 저장 공간이 필수적입니다. 브로커는 특정 토픽들에 대하여 '구독 복구 정책(Subscription Recovery Policy)'을 설정하고, 이 정책에 따라 내부 메모리에 복구 버퍼(Recovery Buffer)를 생성하여 메시지를 캐싱(Caching)합니다.

클라이언트가 자신을 Retroactive Consumer로 선언하고 토픽에 접속하면, 브로커는 즉시 실시간 메시지 라우팅에 클라이언트를 투입하지 않습니다. 대신 메모리의 복구 버퍼를 먼저 열어, 보관되어 있던 과거 메시지들을 시간순으로 클라이언트에게 쏟아내어 전달합니다. 이 과거 메시지의 전송이 모두 완료되고 클라이언트의 상태가 최신으로 동기화되면, 그제야 실시간으로 유입되는 새로운 메시지들을 끊김 없이 이어서 전송합니다.

3. 브로커 측 설정: Subscription Recovery Policy의 4가지 유형

브로커의 JVM 힙 메모리는 한정되어 있으므로 모든 과거 메시지를 무한정 담아둘 수는 없습니다. 비즈니스 요구사항에 맞춰 캐싱할 데이터의 양과 시간을 브로커 설정 파일(activemq.xml 등)의 <destinationPolicy> 블록에 명확히 지정해야 합니다.

  • 고정 크기 정책 (Fixed Size Subscription Recovery Policy):
    복구 버퍼가 사용할 수 있는 물리적인 메모리 바이트(Byte) 크기를 엄격하게 제한합니다. 예를 들어 1MB로 설정하면 가장 최근 발행된 메시지들을 1MB까지만 보관하며, 한계에 도달하면 가장 오래된 메시지부터 버리는 FIFO(First-In-First-Out) 방식으로 동작합니다.
  • 고정 개수 정책 (Fixed Count Subscription Recovery Policy):
    메시지의 용량과 무관하게 보관할 메시지의 절대적인 '개수'를 기준으로 제한합니다.
    <subscriptionRecoveryPolicy>
    <fixedCountSubscriptionRecoveryPolicy maximumSize="100"/>
    </subscriptionRecoveryPolicy>
    

```

  • 시간 기반 정책 (Timed Subscription Recovery Policy):
    최근 특정 시간 동안 발행된 메시지들만 보관합니다. 예를 들어 보관 시간을 60,000ms(1분)로 설정하면, 1분이 경과한 메시지는 버퍼에서 즉시 만료되어 삭제됩니다. 데이터의 발행 주기가 비교적 일정한 실시간 차트나 대시보드 초기화 데이터에 적합합니다.
  • 마지막 이미지 정책 (Last Image Subscription Recovery Policy):
    실시간 주식 호가나 센서의 현재 온도처럼, 과거의 값들이 어떻게 변해왔는지에 대한 '이력'보다는 '가장 최근의 단일 상태 값' 한 개만이 중요할 때 사용합니다. 토픽당 딱 1개의 마지막 메시지만을 덮어쓰기 형태로 메모리에 보관하여 리소스 소모를 극단적으로 최소화합니다.

4. 클라이언트 측 설정: 소급 수신 명시

브로커에 복구 정책이 완벽하게 설정되어 있더라도, 큐(Queue)와 달리 토픽에 접속하는 모든 소비자가 강제로 과거 메시지를 받게 되는 것은 아닙니다.
클라이언트 애플리케이션은 브로커에 세션을 맺고 목적지(Destination)를 생성할 때, 자신이 과거 데이터부터 소급하여 받기를 원한다는 사실을 옵션을 통해 명시적으로 선언해야 합니다.

  • JMS 연결 URI 프로퍼티 활용:
    가장 직관적인 방법은 구독하고자 하는 토픽 이름 뒤에 파라미터를 붙이는 것입니다.
    topic://PRICE.STOCK.APPL?consumer.retroactive=true
  • 이 플래그를 달고 접속한 특정한 소비자에 한해서만 브로커가 내부 캐시 버퍼를 조회하여 과거 데이터를 밀어주게 됩니다.

5. Durable Subscriber와의 결정적 차이 및 아키텍처 한계

메시지 브로커 아키텍처를 설계할 때 개발자들이 가장 빈번하게 혼동하는 개념이 바로 Durable Subscriber(영속적 구독자)와 Retroactive Consumer의 차이입니다. 이 둘은 동작 목적과 리소스 사용 방식이 완전히 다릅니다.

  • Durable Subscriber (개별 맞춤형 보관):
    특정 소비자가 브로커에 '자신의 고유한 클라이언트 이름'을 등록해 두고 구독을 시작합니다. 이 소비자가 오프라인 상태가 되면, 브로커는 "이 특정 소비자가 나중에 돌아오면 밀린 데이터를 줘야지"라고 인지하고, 오직 그 소비자를 위해 메시지들을 KahaDB와 같은 '디스크'에 안전하게 누적 기록합니다.
  • Retroactive Consumer (공용 메모리 캐싱):
    특정한 누군가를 위해 데이터를 보관하는 개념이 아닙니다. 브로커가 자신의 '휘발성 메모리'에 최근 메시지들을 공용으로 캐싱해 두고, 새롭게 접속하는 불특정 다수의 소비자 누구에게나 제공하는 인메모리 캐시 서버와 같은 역할을 수행합니다. 만약 브로커 서버가 재시작되면 메모리 버퍼에 있던 과거 메시지 이력은 모두 날아갑니다.

아키텍처 설계 및 성능 최적화 주의사항:
Retroactive 버퍼는 디스크가 아닌 철저히 브로커의 한정된 힙 메모리(JVM Heap)를 사용합니다. 대용량 페이로드를 가진 메시지가 오가는 토픽에 Fixed Count를 수십만 개로 설정하거나 Timed Policy를 수 시간으로 설정하면, 브로커는 순식간에 심각한 OOM(Out of Memory) 장애를 일으키며 전체 시스템을 다운시킵니다.

따라서 Retroactive Consumer 기능은 절대 영구적인 데이터베이스나 이벤트 소싱(Event Sourcing) 스토리지를 대체하는 목적으로 남용되어서는 안 됩니다. 마이크로서비스 기동 시점에 필요한 최신 설정(Configuration) 값 1개를 받아오거나, 프론트엔드 대시보드의 초기 화면 렌더링을 위해 최근 몇 분간의 데이터만 빠르게 긁어오는 등 '크기가 엄격하게 제한된 단기 캐시 데이터'가 필요한 시나리오에만 매우 보수적이고 전략적으로 적용해야 합니다.

반응형