RabbitMQ Exchange는 어떻게 메시지를 찾아가게 하는가
Direct의 O(1) 해시 매칭부터 Topic 와일드카드, Fanout 브로드캐스트, Headers 다차원 조건, DLX 안전망까지 — Exchange 설계 결정의 통일된 철학을 추적한다.
- 01 RabbitMQ는 왜 메시지 브로커인가
- 02 RabbitMQ Exchange는 어떻게 메시지를 찾아가게 하는가
- 03 RabbitMQ 메시지 유실은 왜 조용히 일어나는가
- 04 RabbitMQ 메시지 패턴의 공통 철학은 무엇인가
- 05 RabbitMQ 운영의 모든 실패는 어디서 오는가
- 06 RabbitMQ vs Kafka — 어떤 문제를 해결하려고 만들어졌는가
- 07 Spring AMQP의 컴포넌트들은 어떻게 연결되는가
RabbitMQ에는 네 가지 Exchange 유형이 있다. 각각 라우팅 방식이 다르고, 성능 특성이 다르고, 적합한 상황이 다르다. 그런데 이 네 유형을 관통하는 단 하나의 질문이 있다 — “메시지가 어느 Queue에 도달해야 하는가를 누가, 어떤 기준으로 결정하는가?” 그 결정 방식의 차이가 Exchange 유형을 가른다.
정확 매칭 — Direct Exchange
Direct Exchange는 가장 단순한 라우팅 규칙을 따른다. Routing Key와 Binding Key가 문자열로 정확히 일치할 때만 전달한다. 내부적으로는 O(1) 해시 테이블 조회다.
Exchange: order.exchange (direct)
┌──────────────────┬──────────────────────┐
│ Binding Key │ Queue │
├──────────────────┼──────────────────────┤
│ "payment" │ payment.queue │
│ "payment" │ payment-audit.queue │ ← 같은 Key, 다른 Queue
│ "inventory" │ inventory.queue │
└──────────────────┴──────────────────────┘
같은 Binding Key에 여러 Queue를 연결하면 1:N 라우팅이 된다. routingKey="payment"로 발행하면 payment.queue와 payment-audit.queue 모두에 복사 전달된다. 반대로 하나의 Queue에 여러 Binding Key를 연결하면 OR 조건으로 수집할 수 있다.
Direct가 적합하지 않은 순간은 명확하다. Binding Key가 5개를 넘기 시작하고 패턴이 보이면 Topic으로 갈 신호다.
와일드카드 매칭 — Topic Exchange
Topic Exchange는 Routing Key를 점(.)으로 구분된 세그먼트 배열로 분리하고, 두 가지 와일드카드로 패턴 매칭한다.
*— 세그먼트 정확히 하나#— 0개 이상의 세그먼트
Routing Key: "order.payment.failed"
"order.#" → ✅
"order.payment.*" → ✅
"#.failed" → ✅
"order.*" → ❌ (* 는 하나, 세그먼트가 3개)
다섯 패턴이 동시에 매칭되면 다섯 Queue에 복사 전달된다. 이것이 Topic의 핵심 가치다 — Producer는 order.payment.failed라는 이벤트 이름만 알고 발행하면, 감사 서비스는 order.#으로, 결제 모니터링은 order.payment.*으로, 운영 알림은 #.failed로 각자 구독한다.
Topic Exchange의 가치는 Routing Key 설계에 달려 있다. <도메인>.<엔티티>.<행동> 형식의 계층 구조, 소문자, 점 구분, 과거형 동사(placed, failed, completed)가 권장 패턴이다. 계층 없이 orderpaymentfailed처럼 설계하면 Topic의 이점이 전혀 없다.
내부적으로 RabbitMQ는 Binding Key를 Trie 구조로 관리한다. 라우팅 비용은 O(B × L) — Binding 수 × Key 길이. Direct의 O(1)보다 비싸지만, Binding이 수백 개 이하인 실무에서는 무시할 수준이다.
조건 없는 브로드캐스트 — Fanout Exchange
Fanout Exchange는 Routing Key를 완전히 무시한다. Binding된 모든 Queue에 메시지를 복사 전달한다. 라우팅 연산이 없으므로 O(B) — Binding 수만큼 순회만 한다.
핵심 패턴은 Pub/Sub이다. 올바른 구조는 Consumer마다 자신만의 Queue를 갖는 것이다.
order.fanout Exchange
├── → inventory.queue → InventoryService
├── → notification.queue → NotificationService
└── → analytics.queue → AnalyticsService
세 서비스가 하나의 Queue를 공유하면 경쟁 소비(Work Queue)가 된다. 이것은 Fanout이 아니다.
Fanout의 결정적 특성은 구독자 추가가 Publisher에 완전히 투명하다는 점이다. 새 Queue와 Binding을 추가하기만 하면, OrderService는 코드 한 줄 건드리지 않고 새 서비스로 이벤트가 흘러간다.
Fanout Exchange에 Binding된 Queue가 없으면 메시지는 조용히 폐기된다. 에러도 없다. 서비스 배포 중 Consumer가 아직 떠 있지 않을 때 발행된 메시지가 유실되는 원인이 여기에 있다. 영구 Queue와 Binding은 Exchange보다 먼저 생성해두는 것이 안전하다.
다차원 조건 — Headers Exchange와 Dead Letter Exchange
Routing Key는 단일 문자열이다. “결제 방법이 카드이면서 VIP 고객인 주문”을 Topic으로 표현하면 payment.card.vip처럼 Routing Key가 조합되기 시작한다. 조건이 늘수록 관리 불가 상태가 된다.
Headers Exchange는 Routing Key 대신 메시지 헤더의 키-값 쌍으로 라우팅한다.
// Binding: x-match=all, paymentMethod=card, customerType=vip
// 발행 시 헤더에 해당 값이 모두 있으면 매칭
props.setHeader("paymentMethod", "card");
props.setHeader("customerType", "vip");
x-match=all은 AND, x-match=any는 OR다. 타입이 중요하다 — "1000000" (String)과 1000000 (Integer)은 다른 값이다.
Headers Exchange는 실무에서 드물게 쓰인다. Topic으로 표현 가능한 것은 Topic을 쓰는 것이 더 단순하고 가시성이 높다. Headers는 진짜 다차원 조건이 필요할 때만 꺼낸다.
Dead Letter Exchange(DLX)는 “실패한 메시지를 버리지 않는다”는 안전망이다. 메시지가 Dead Letter가 되는 조건은 세 가지다.
basicNack(requeue=false)— Consumer가 처리를 거절x-message-ttl만료 — Queue에 너무 오래 머묾x-max-length초과 — Queue가 가득 참
order.queue → Consumer 처리 실패
→ basicNack(requeue=false)
→ x-dead-letter-exchange: "dlx.exchange" 설정 있음
→ dlx.exchange → order.dlq (x-death 헤더 추가)
DLQ로 이동한 메시지의 x-death 헤더에는 원본 Queue 이름, 실패 이유(rejected/expired/maxlen), 횟수, 시각이 자동으로 기록된다. DLX 없이 requeue=false로 Nack하면 메시지는 영구 폐기된다. requeue=true로 무한 재시도하면 이벤트 루프가 같은 메시지를 수천 번 처리하며 CPU가 포화된다. DLX는 이 두 극단 사이의 세 번째 선택지다.
트레이드오프
| Exchange | 라우팅 기준 | 성능 | 유연성 |
|---|---|---|---|
| Direct | Routing Key 정확 매칭 | O(1) | 낮음 |
| Topic | 와일드카드 패턴 | O(B×L) | 높음 |
| Fanout | 조건 없음 | O(B) | 없음 (전체) |
| Headers | 헤더 AND/OR | O(B×H) | 다차원 |
Binding이 수백 개 이하인 실무에서 성능 차이는 대부분 무시할 수준이다. 선택 기준은 성능이 아니라 **“라우팅 의도를 얼마나 명확하게 표현하는가”**다.
Exchange 설계의 현실적 지침은 간단하다. 도메인당 Exchange 하나, Exchange 유형은 Topic이 기본값이고, 조건 없는 전체 브로드캐스트만 Fanout, 단일 서비스 지정이 명확할 때만 Direct. 모든 Queue에 DLX를 달고, DLQ 메시지 수를 모니터링한다.
정리
- Direct는 O(1) 해시 매칭, 정확한 1:1 또는 1:N 라우팅. 패턴이 보이면 Topic으로 전환 신호다.
- Topic은
*(세그먼트 하나)와#(0개 이상)으로 유연한 구독 표현. Routing Key 계층 설계가 전부다. - Fanout은 조건 없는 브로드캐스트. Consumer마다 자신만의 Queue가 올바른 Pub/Sub이다.
- Headers는 다차원 AND/OR 조건. Topic으로 가능하면 Topic을 쓴다.
- DLX는 실패 메시지 안전망.
requeue=false폐기와 무한 루프 사이의 세 번째 선택지다.
다음 글에서는 이 Exchange 위에서 메시지 유실이 실제로 발생하는 세 지점 — Publisher Confirm, Queue 내구성, Consumer Ack — 을 추적한다.