RabbitMQ 깊게 보기: AMQP, exchange, ack, prefetch, DLX

Development

이 글은 메시징/이벤트 시리즈 4편이자 마지막 편입니다.

3편에서 Kafka를 "로그"라는 한 단어로 풀었습니다. RabbitMQ는 정반대 자리에 있습니다. Kafka가 "이벤트를 기록하고 다시 읽게 해주는 로그"라면, RabbitMQ는 **"메시지를 똑똑하게 라우팅해서 한 건씩 전달하고, 처리되면 지우는 브로커"**입니다.

이 정체성을 붙잡으면 RabbitMQ의 개념들 — exchange, binding, ack, prefetch, DLX — 이 전부 "어떻게 정확한 곳에, 정확히 한 건씩, 안전하게 전달하느냐"라는 한 질문의 답으로 보입니다.

한눈에 보면

  • RabbitMQ는 AMQP 모델을 따릅니다. 프로듀서는 큐가 아니라 exchange에 보내고, exchange가 binding 규칙으로 큐에 라우팅합니다.
  • exchange는 direct / topic / fanout / headers 네 종류이고, 이게 라우팅 유연성의 핵심입니다.
  • 전달 안전성은 수동 ack + publisher confirm + durable/persistent 조합으로 만듭니다.
  • **prefetch(QoS)**가 소비자별 동시 처리량을 정해 공정한 분배와 과부하 방지를 담당합니다.
  • 실패한 메시지는 **DLX(데드레터)**로 보내고, TTL·지연 메시지 같은 패턴을 세밀하게 다룰 수 있습니다.
  • HA는 quorum queue가 표준이고(과거 mirrored queue는 권장되지 않음), Kafka와의 대비가 RabbitMQ를 이해하는 가장 빠른 길입니다.

AMQP 모델: 프로듀서는 큐를 모른다

RabbitMQ에서 가장 먼저 깨야 할 직관은 "프로듀서가 큐에 메시지를 넣는다"입니다. 아닙니다. 프로듀서는 exchange에 보내고, 그 메시지가 어느 큐로 갈지는 exchange와 binding이 정합니다.

flowchart LR
  P[Producer] -->|publish<br/>routing key| X{Exchange}
  X -->|binding| Q1[(Queue A)]
  X -->|binding| Q2[(Queue B)]
  Q1 --> C1[Consumer]
  Q2 --> C2[Consumer]

구성 요소는 넷입니다.

  • Exchange: 메시지를 받아 규칙에 따라 큐로 분배하는 라우터.
  • Queue: 메시지가 실제로 쌓이고 소비를 기다리는 곳.
  • Binding: exchange와 큐를 잇는 규칙(보통 routing key 패턴).
  • Routing key: 프로듀서가 메시지에 붙이는 라벨. exchange가 이걸 보고 라우팅.

이 간접층(프로듀서 → exchange → 큐)이 RabbitMQ의 유연함의 원천입니다. 프로듀서를 건드리지 않고 binding만 바꿔서 "이 메시지를 새 큐에도 보내기"를 할 수 있습니다. 1편에서 말한 발행자-소비자 분리가 exchange라는 장치로 구현된 셈입니다.

exchange 4종: 라우팅의 문법

라우팅 유연성이 RabbitMQ의 간판인데, 그 문법이 exchange 타입입니다.

direct — routing key가 정확히 일치

routing key가 binding key와 정확히 같은 큐로 보냅니다. "log.error는 에러 큐로" 같은 단순 분기에 씁니다.

publish(routing_key="payment")  →  binding "payment" 인 큐로

topic — 패턴 매칭

routing key를 .로 구분된 패턴으로 매칭합니다. *는 단어 하나, #은 0개 이상을 의미합니다. 가장 많이 쓰는 유연한 타입입니다.

binding: "order.*.kr"     ← order.created.kr 매칭, order.created.us 불일치
binding: "order.#"        ← order. 로 시작하는 모든 것

order.created.kr, order.shipped.kr 같은 이벤트를 지역·종류별로 다른 큐에 라우팅하는 그림에 잘 맞습니다.

fanout — 무조건 전부

routing key를 무시하고 바인딩된 모든 큐에 복사합니다. 1편의 publish-subscribe(브로드캐스트)를 가장 단순하게 구현합니다.

publish → 바인딩된 큐 A, B, C 전부에 한 부씩

headers — 헤더 속성으로 매칭

routing key 대신 메시지 헤더의 키-값으로 매칭합니다. 라우팅 조건이 문자열 경로로 표현하기 어려울 때 씁니다. 실무 빈도는 낮은 편입니다.

exchange 라우팅 기준 대표 용도
direct key 정확 일치 단순 분기
topic key 패턴 매칭 유연한 이벤트 분배
fanout 무조건 전체 브로드캐스트
headers 헤더 속성 매칭 다차원 조건 라우팅

Kafka에는 이런 라우팅 문법이 없습니다. Kafka는 토픽·파티션·키가 전부이고, "조건에 따라 다른 곳으로"는 소비자 쪽 로직이나 별도 스트림 처리로 풉니다. 라우팅을 브로커가 하느냐(RabbitMQ), 소비자가 하느냐(Kafka) — 이게 두 도구의 큰 갈림입니다.

전달 안전성: ack, confirm, durability

RabbitMQ는 소비하면 메시지를 지웁니다(큐 모델). 그래서 "지우기 전에 정말 처리됐는지" 확인이 중요합니다. 안전성은 세 겹으로 만듭니다.

1. 소비자 측 — 수동 ack

자동 ack는 "메시지를 보내는 순간 처리됐다고 간주"하므로, 소비자가 처리 중 죽으면 유실됩니다. 안전하게는 수동 ack를 씁니다.

deliver → (소비자가 실제 처리) → basic.ack
처리 실패 → basic.nack(requeue=true)  → 다시 큐로 (재시도)

소비자가 ack 전에 죽으면 RabbitMQ는 그 메시지를 다른 소비자에게 다시 보냅니다(at-least-once). 그래서 3편·1편과 똑같이, 소비자는 멱등해야 중복을 안전하게 흡수합니다.

2. 프로듀서 측 — publisher confirm

프로듀서가 보낸 메시지가 브로커에 안전히 도달·저장됐는지 확인하는 장치입니다. confirm을 켜면 브로커가 "받았다"를 비동기로 알려줘, 유실 없이 보냈는지 보장할 수 있습니다. Kafka의 acks에 대응하는 개념입니다.

3. 저장 — durable + persistent

브로커가 재시작돼도 살아남게 하려면 둘 다 필요합니다.

  • durable queue: 큐 정의 자체가 재시작 후에도 유지.
  • persistent message: 메시지를 디스크에 기록.

이 둘이 빠지면 메모리에만 있다가 재시작 시 사라집니다. "confirm까지 받았는데 재시작에 날아갔다"는 보통 persistent를 빠뜨린 경우입니다.

prefetch(QoS): 공정한 분배의 핵심

RabbitMQ는 메시지를 소비자에게 밀어줍니다(push). 3편의 Kafka가 소비자가 당겨가는(pull) 것과 반대죠. push 모델에는 위험이 있습니다. 빠른 척하지만 실제론 느린 소비자에게 메시지를 잔뜩 밀어넣으면, 그 소비자 앞에 일이 쌓이고 다른 소비자는 놉니다.

이걸 막는 게 **prefetch(basic.qos)**입니다. "한 소비자에게 ack 안 된 메시지를 최대 몇 개까지만 미리 줄지"를 정합니다.

prefetch = 1   →  한 건 처리하고 ack해야 다음 한 건. 가장 공정(느린 작업에 적합)
prefetch = 10  →  최대 10건까지 미리. 처리량↑, 분배 균형은 약간↓

prefetch가 너무 크면 한 소비자가 메시지를 독식하고, 너무 작으면 왕복이 잦아 처리량이 떨어집니다. 작업 처리 시간이 길고 들쭉날쭉할수록 작게, 짧고 균일할수록 크게 잡는 게 보통입니다. push 모델인 RabbitMQ에서 prefetch는 "사실상의 백프레셔 손잡이"입니다.

실패와 지연을 다루는 패턴: DLX, TTL

RabbitMQ의 강점은 한 건 한 건을 세밀하게 다루는 데 있습니다. 대표적인 두 패턴입니다.

DLX (Dead Letter Exchange)

처리에 실패해 reject되거나, TTL이 만료되거나, 큐가 꽉 차서 밀려난 메시지를 다른 exchange로 보내는 장치입니다. "독이 든 메시지(poison message)"가 무한 재시도로 큐를 막는 걸 막습니다.

flowchart LR
  Q[(작업 큐)] -->|nack/만료| DLX{Dead Letter<br/>Exchange}
  DLX --> DLQ[(Dead Letter Queue)]
  DLQ --> Insp[수동 점검 / 재처리]

실패가 반복되는 메시지를 DLQ에 모아두고 나중에 분석·재처리하는 흐름은 작업 큐 운영의 기본기입니다.

TTL과 지연 메시지

메시지나 큐에 TTL을 걸어 "일정 시간 안 처리되면 만료(→ DLX)"를 만들 수 있습니다. 이걸 응용하면 지연 메시지(예: "5분 뒤에 처리")도 구현됩니다 — TTL 큐 + DLX 조합, 또는 delayed message 플러그인을 씁니다. Kafka에는 이런 per-message TTL/지연이 기본으로 없어서, RabbitMQ가 작업 큐 시나리오에서 더 편한 지점입니다.

HA: quorum queue

단일 노드 RabbitMQ는 가볍게 시작되지만, 노드가 죽으면 그 큐의 메시지가 위험합니다. 고가용성은 quorum queue로 만듭니다. Raft 합의 기반으로 여러 노드에 복제해, 일부 노드가 죽어도 큐와 메시지가 살아남습니다.

quorum queue: 3개 노드에 복제, 과반(2/3)이 살아 있으면 동작

과거에 쓰던 mirrored(classic HA) queue는 더 이상 권장되지 않고, 새로 구성한다면 quorum queue가 표준입니다. 다만 복제하는 만큼 단일 큐보다 무거우므로, HA가 필요한 큐에 선택적으로 적용합니다. 개념적으로는 3편 Kafka의 복제·ISR과 같은 목적(노드 장애에도 데이터 보존)을 다른 메커니즘으로 푸는 것입니다.

Kafka와 정면 대비

같은 시리즈를 닫으며, 두 도구를 같은 항목으로 마주 세우면 정체성 차이가 또렷합니다.

항목 Kafka (로그) RabbitMQ (브로커)
핵심 추상 추가 전용 로그 + offset exchange + 큐 + 라우팅
소비 후 보존 (replay 가능) 삭제 (ack 시)
전달 방식 소비자가 pull 브로커가 push (+ prefetch)
라우팅 소비자/스트림 처리가 담당 exchange가 브로커에서 담당
순서 파티션 단위 단일 큐/소비자 기준
강한 영역 대용량 스트림, 다수 구독, 분석 작업 큐, 복잡한 라우팅, 저지연
재시도/지연 직접 구현 nack·DLX·TTL로 풍부하게 지원
내구성 메커니즘 복제 + ISR persistent + publisher confirm + quorum

한 줄로 줄이면, Kafka는 "이벤트의 흐름을 기록"하고, RabbitMQ는 "메시지를 정확히 배달"합니다. "이 데이터를 여러 소비자가 각자, 나중에도 다시 읽어야 하는가"면 Kafka가, "이 작업을 정확한 워커에게 한 번 안전하게 시키고 끝내야 하는가"면 RabbitMQ가 자연스럽습니다.

정리하면

  • RabbitMQ는 AMQP 기반 스마트 브로커입니다. 프로듀서는 exchange에 보내고, binding이 큐로 라우팅합니다.
  • exchange 4종(direct/topic/fanout/headers)이 라우팅 문법이고, 이 라우팅을 브로커가 한다는 게 Kafka와의 핵심 차이입니다.
  • 전달 안전성은 수동 ack + publisher confirm + durable/persistent 세 겹으로 만들고, 소비자는 여전히 멱등해야 합니다.
  • prefetch가 push 모델의 백프레셔이자 공정한 분배 손잡이입니다.
  • DLX·TTL·지연 메시지·quorum queue로 작업 큐 운영과 HA를 세밀하게 다룹니다.
  • 정체성은 Kafka의 정반대 — 기록이 아니라 정확한 배달입니다.

이렇게 네 편으로 메시징의 개념부터 Kafka·RabbitMQ·SQS의 선택, 그리고 Kafka와 RabbitMQ의 내부까지 훑었습니다. 도구는 계속 바뀌지만, 1편에서 잡은 좌표축(큐 vs 로그, 전달 보장, 순서, 멱등성)은 어떤 메시징 시스템을 만나도 그대로 쓸 수 있는 지도입니다.

같이 보면 좋은 글