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

저장소 수준의 'Duplicate ID Cache' 크기 최적화 방법은?

by 엉짱 2026. 4. 8.
반응형

저장소 수준의 'Duplicate ID Cache' 크기 최적화 방법은?

엔터프라이즈 분산 시스템에서 데이터의 무결성을 위협하는 가장 흔하고 치명적인 장애는 데이터 유실이 아니라 '메시지 중복 처리(Duplicate Message Processing)'입니다. 클라이언트가 결제 요청 메시지를 브로커로 전송한 직후, 일시적인 네트워크 단절로 인해 브로커의 성공(ACK) 응답을 받지 못했다면 클라이언트는 안전을 위해 동일한 결제 메시지를 다시 한번 전송(Retry)하게 됩니다.

이때 시스템에 멱등성(Idempotency) 방어 로직이 완벽하게 갖춰져 있지 않다면 고객의 계좌에서 돈이 두 번 빠져나가는 대형 사고로 직결됩니다. ActiveMQ Artemis와 같은 차세대 고성능 브로커는 이러한 중복 전송의 재난을 인프라 레벨에서 원천 차단하기 위해 'Duplicate ID Cache (중복 ID 캐시)'라는 강력한 방어선을 운영합니다.

하지만 이 캐시의 크기를 시스템 트래픽에 맞게 최적화하지 않으면, 오히려 브로커의 메모리를 고갈시키거나 디스크 I/O 병목을 유발하는 원인이 될 수 있습니다. 본 가이드에서는 저장소 수준에서 중복 ID 캐시가 어떻게 동작하는지 그 아키텍처를 해부하고, 트래픽 스펙에 맞춘 최적화 튜닝 방법을 상세히 분석합니다.


1. 'Duplicate ID Cache'의 동작 원리 및 영속성

중복 ID 캐시가 동작하려면 클라이언트(Producer)의 협조가 필수적입니다. 클라이언트가 메시지를 전송할 때 페이로드와 함께 고유한 식별자(주로 UUID나 트랜잭션 ID)를 _AMQ_DUPL_ID라는 특수한 속성(Property)에 담아 보내야 합니다.

A. 인메모리 필터링 메커니즘
브로커가 메시지를 수신하면, 가장 먼저 큐에 라우팅하기 전에 이 메시지의 _AMQ_DUPL_ID 값을 자신의 메모리에 있는 '중복 ID 캐시' 목록과 대조합니다.
만약 캐시에 동일한 ID가 이미 존재한다면, 브로커는 이 메시지를 악의적인 중복 전송이나 클라이언트의 재시도 트래픽으로 간주하여 즉시 버립니다(Discard). 그리고 클라이언트에게는 "정상적으로 수신되었다"는 가짜 ACK를 보내어 클라이언트가 더 이상 재시도를 하지 않도록 안심시킵니다.

B. 저장소 레벨의 영속성 (persist-id-cache)
단순히 메모리에만 캐시를 유지한다면, 브로커 서버가 재기동되었을 때 캐시가 모두 날아가 과거의 중복 메시지를 걸러내지 못하는 취약점이 생깁니다.
따라서 Artemis는 persist-id-cache="true" 옵션(기본값 활성화)을 통해 캐시에 등록되는 모든 ID를 물리적 디스크의 바인딩 저널(Bindings Journal) 또는 메시지 저널에 영구적으로 기록합니다. 서버가 다운되었다가 다시 켜져도 디스크에서 이 ID 목록을 읽어 들여 방어막을 완벽하게 재건축할 수 있습니다.


2. 캐시 크기(id-cache-size)가 시스템에 미치는 치명적 영향

브로커 설정 파일(broker.xml)의 <address-setting> 블록 내부에는 이 캐시의 크기를 결정하는 id-cache-size 파라미터가 존재합니다. 기본값은 보통 2000(또는 20000)으로 설정되어 있는데, 이는 트래픽 볼륨에 따라 시스템의 운명을 가를 수 있는 매우 민감한 수치입니다.

A. 크기가 너무 작을 때의 위험: 방어선 붕괴 (Eviction)
중복 ID 캐시는 무한정 데이터를 담을 수 없으므로, 설정된 크기를 초과하면 가장 오래된 ID부터 메모리에서 지워버리는 밀어내기(LRU Eviction) 방식으로 동작합니다.
만약 브로커가 초당 수천 건의 메시지를 처리하는 고트래픽 환경인데 캐시 사이즈가 고작 2000으로 잡혀있다면, 어떤 클라이언트가 5초 뒤에 지연 재시도(Retry)를 했을 때 그 메시지의 원본 ID는 이미 캐시에서 지워지고 없을 확률이 높습니다. 브로커는 이를 새로운 메시지로 착각하여 통과시키게 되고, 결국 컨슈머 애플리케이션으로 중복 데이터가 흘러가는 치명적인 정합성 파괴가 발생합니다.

B. 크기가 너무 클 때의 위험: 리소스 낭비와 I/O 병목
반대로 데이터 유실을 막겠다고 이 값을 수백만 단위로 무작정 크게 설정하는 것도 인프라의 안티 패턴입니다.
캐시 사이즈가 커지면 첫째로 JVM 힙 메모리 공간을 막대하게 점유하여 가비지 컬렉션(GC)의 부담을 가중시킵니다. 둘째로, 앞서 언급한 영속성 기능 때문에 캐시에 들어오고 나가는 수백만 개의 ID 메타데이터를 끊임없이 물리적 디스크에 동기화해야 하므로 메인 스토리지 엔진에 극심한 I/O 쓰기 부하(Write Overhead)를 유발합니다. 이는 전체적인 라우팅 스피드 저하로 이어집니다.


3. 아키텍처 최적화 튜닝 가이드 (Best Practices)

그렇다면 우리 비즈니스 시스템에 딱 맞는 id-cache-size는 어떻게 결정해야 할까요? 감에 의존하지 않고 시스템의 지표를 바탕으로 논리적인 튜닝을 진행해야 합니다.

1단계: 비즈니스의 최대 재시도(Retry) 윈도우 파악
클라이언트 애플리케이션이나 네트워크 로드 밸런서가 타임아웃을 인지하고 메시지를 다시 보내기까지 걸리는 '최대 대기 시간'을 파악해야 합니다. 예를 들어 네트워크 설정 상 최대 3번의 재시도를 수행하고 각 재시도 간격이 5초라면, 최초 전송 후 최대 15초 안에는 반드시 중복 메시지가 도달할 수 있다는 의미입니다.

2단계: 피크 타임 TPS(초당 처리량)와의 결합 산정
최적의 캐시 크기는 "초당 처리하는 최대 메시지 건수"에 "최대 재시도 대기 시간"을 곱한 값보다 여유롭게 커야 합니다.
만약 시스템이 초당 1000건의 메시지를 처리하고 중복 메시지가 15초 뒤에 올 수 있다면, 15000건 이상의 메시지가 흘러가는 동안 방어선이 유지되어야 합니다. 따라서 캐시 사이즈를 20000 이상으로 넉넉하게 설정해야 안전하게 중복을 걸러낼 수 있습니다. 이 기준을 통해 캐시가 너무 빨리 비워져 중복 데이터가 새어 나가는 현상을 완벽하게 방어할 수 있습니다.

3단계: 와일드카드를 통한 목적지(Destination)별 차등 적용
모든 큐가 동일한 방어 수준을 필요로 하는 것은 아닙니다. 로그를 수집하는 큐는 중복이 발생하더라도 비즈니스에 큰 타격이 없지만, 결제나 주문을 처리하는 큐는 단 한 건의 중복도 허용되어서는 안 됩니다.
broker.xml에서 와일드카드(#)를 활용하여 다음과 같이 중요도가 높은 큐에만 캐시 사이즈를 집중적으로 할당하고 영속성을 보장하는 것이 현명한 리소스 분배 전략입니다.

<address-settings>
    <address-setting match="queue.payment.#">
        <id-cache-size>50000</id-cache-size>
        <persist-id-cache>true</persist-id-cache>
    </address-setting>

    <address-setting match="queue.log.#">
        <id-cache-size>1000</id-cache-size>
        <persist-id-cache>false</persist-id-cache>
    </address-setting>
</address-settings>

결론적으로 'Duplicate ID Cache'는 메시지 브로커가 제공하는 가장 강력한 멱등성 보장 무기입니다. 하지만 무기가 강력할수록 메모리와 디스크라는 인프라 자원을 매섭게 소모합니다. 비즈니스의 트래픽 특성(TPS)과 네트워크의 재시도 주기를 면밀히 분석하여 저장소 레벨의 튜닝을 적용함으로써, 무겁지 않으면서도 어떠한 중복 트래픽도 허용하지 않는 견고한 방어 파이프라인을 구축하시기 바랍니다.

반응형