← all posts
DEV 2026.05.02 · 14 min read Intermediate

RabbitMQ는 왜 메시지 브로커인가

동기 호출 체인의 결합도 문제부터 Quorum Queue의 Raft 합의까지, RabbitMQ 설계 전반을 관통하는 '간접성을 통한 안정성' 철학을 추적한다.


RabbitMQ를 도입하면 코드가 복잡해진다. Exchange를 선언하고, Binding을 관리하고, Ack를 처리해야 한다. 그렇다면 왜 이 복잡성을 감수하는가? 그것이 해결하는 문제를 정확히 이해하지 못하면, RabbitMQ의 모든 설계 결정이 과잉처럼 보인다. RabbitMQ의 핵심 가치는 하나다 — Producer와 Consumer 사이에 간접 계층을 두는 것. 그리고 이 간접성이 어떻게 결합도, 연쇄 장애, 트래픽 집중이라는 세 가지 문제를 동시에 해결하는가?

동기 호출 체인이 만드는 세 가지 문제

주문 서비스가 결제, 재고, 알림 서비스를 차례로 HTTP로 호출한다고 하자. 세 서비스가 동시에 정상이어야만 주문이 성공한다. 이것이 결합도다.

알림 서비스가 일시적 OOM으로 30초 타임아웃을 유발하면, 주문 서비스의 스레드 풀이 순식간에 고갈된다. 주문 서비스도 응답하지 못하게 되고, 결제와 재고 서비스까지 연쇄 장애로 이어진다. 이것이 연쇄 장애다.

블랙프라이데이에 주문이 100배 폭증하면, 알림 서비스는 처리 한계를 초과해 요청을 거절하기 시작한다. 거절된 응답을 받은 주문 서비스는 결제가 정상인 주문을 실패로 처리한다. 이것이 트래픽 집중이다.

Before (동기):  OrderService ──→ NotificationService
                              (직접 호출, 강결합)

After (비동기):  OrderService ──→ [Queue] ──→ NotificationService
                              (간접 연결, 약결합)

메시지 큐는 이 세 문제를 Queue라는 버퍼를 중간에 두는 것만으로 해결한다. OrderService는 Queue에 메시지를 발행하고 즉시 반환한다. NotificationService가 다운되어도 메시지는 Queue에 쌓인다. 트래픽이 폭증해도 Queue가 속도 차이를 흡수한다.

AMQP의 3단계 라우팅 — 왜 Queue에 직접 발행하지 않는가

RabbitMQ가 “Producer → Queue 직접 발행”이 아닌 Exchange → Binding → Queue 3단계를 선택한 이유는 Producer의 독립성을 보장하기 위해서다.

직접 발행 방식이라면 OrderService는 NotificationQueue, InventoryQueue, AnalyticsQueue 이름을 전부 알아야 한다. 새 팀이 생겨 새 Queue를 추가할 때마다 OrderService 코드를 수정해야 한다.

Exchange 방식에서는 OrderService가 order.exchangeorder.placed라는 Routing Key만 안다. Queue 구조는 Exchange에 Binding을 추가하는 것으로 변경되고, OrderService는 전혀 건드리지 않아도 된다.

Producer: exchange="order.exchange", routingKey="order.payment.failed"

Exchange (Topic) → Binding 평가:
  "order.#"         → payment-queue   ✅
  "order.payment.*" → audit-queue     ✅
  "#.failed"        → alert-queue     ✅

→ 하나의 발행이 세 Queue에 복사 전달됨
라우팅 실패의 함정

mandatory=false(기본값)일 때 Binding이 없으면 메시지는 조용히 폐기된다. 에러도 없고 로그도 없다. mandatory=true로 설정하거나 Alternate Exchange를 두어야 라우팅 실패를 감지할 수 있다.

Channel 멀티플렉싱도 같은 철학의 연장이다. 하나의 TCP 연결 위에 여러 논리 채널을 두어, 스레드마다 채널을 분리하면서도 TCP 연결은 한 개만 유지한다. 단, Channel은 스레드 안전하지 않다 — 여러 스레드가 하나의 Channel을 공유하면 프레임이 뒤섞여 Channel이 강제로 닫힌다.

Queue의 내구성 — 재시작 후에도 살아있어야 한다

Queue 속성을 잘못 설정하면 브로커 재시작 후 모든 메시지가 증발한다. 올바른 조합은 두 가지 설정을 함께 해야 한다.

// durable=true: Queue 정의를 재시작 후에도 유지
// DeliveryMode=PERSISTENT: 메시지 내용을 디스크에 저장
@Bean
public Queue orderQueue() {
    return QueueBuilder
        .durable("order.queue")
        .quorum()  // 클러스터 환경이라면
        .build();
}

durable=true만으로는 부족하다. Queue의 정의는 살아있지만, DeliveryMode=TRANSIENT 메시지는 메모리에만 있으므로 재시작 시 소실된다. 반대로 메시지를 Persistent로 설정해도 Queue가 non-durable이면 Queue 자체가 삭제된다.

대용량 메시지 적재가 필요하다면 Lazy Queue를 선택한다. 일반 Queue는 메모리 우선 저장 후 메모리 압박 시 디스크로 Paging하지만, Lazy Queue는 메시지 도착 즉시 디스크에 저장한다. 메모리 사용량이 현저히 줄어드는 대신 디스크 I/O로 인한 처리량 감소가 따른다.

클러스터링 — “구성했으니 안전하다”는 착각

클러스터를 구성해도 Classic Queue의 메시지 데이터는 단일 마스터 노드에만 저장된다. 3노드 클러스터에서 마스터 노드가 장애나면 해당 Queue는 접근 불가다. Exchange/Binding/vHost 메타데이터는 전 노드에 복제되지만, 메시지 자체는 그렇지 않다.

Quorum Queue가 이 문제의 답이다. Raft 합의 알고리즘 기반으로 과반수 노드에 복제한 후 커밋한다.

3노드 Quorum Queue 쓰기:
  Node 1 (Leader): 메시지 수신 → Raft 로그 추가
  Node 1 → Node 2, 3: 복제 요청
  Node 2: 복제 완료 → "OK"
  → 과반수(2/3) 달성 → 커밋 → Producer에게 Ack

  Node 1 장애 → Node 2가 Leader 선출 (수 초)
  → 커밋된 메시지 전부 보존

네트워크 파티션이 발생해도 Raft는 과반수 없는 쪽의 쓰기를 거절한다. Split-Brain 데이터 불일치가 원천 차단된다. 소수 파티션은 일시적으로 서비스 불가 상태가 되지만, 데이터 손실은 없다.

트레이드오프

메시지 큐 도입의 대가는 명확하다.

트레이드오프

얻는 것: 결합도 제거, 장애 격리, 트래픽 버퍼링, Consumer 독립적 확장.

잃는 것: 즉시 일관성. Queue를 거치는 순간 처리 결과를 Producer가 즉시 알 수 없다. 알림·통계·배송 같은 지연 허용 처리에는 완벽하지만, 결제 승인처럼 “지금 됐는지 안 됐는지” 즉시 알아야 하는 경우에는 동기 호출이 여전히 필요하다. 또한 Exchange/Queue/Binding 설정, 브로커 운영과 모니터링, 메시지 유실 방어 로직이라는 운영 부담도 따른다.

RabbitMQ vs Kafka 선택도 이 프레임으로 이해하면 명확해진다. 메시지를 Consumer 처리 후 삭제해도 되고, 복잡한 라우팅이 필요하며, 낮은 지연이 중요하다면 RabbitMQ다. 메시지를 보관하며 재처리해야 하고, 초당 수십만 건의 처리량이 필요하다면 Kafka다.

정리

  • 동기 호출 체인의 세 문제(결합도, 연쇄 장애, 트래픽 집중)는 Queue라는 간접 계층 하나로 해결된다.
  • Exchange → Binding → Queue 3단계는 Producer가 Queue 구조를 모르게 하기 위한 설계다.
  • durable=true + DeliveryMode=PERSISTENT 조합이 재시작 후 메시지 보존의 최소 조건이다.
  • 클러스터의 안전성은 Classic Queue가 아닌 Quorum Queue에서 나온다. Raft가 Split-Brain을 방지한다.
  • 완전한 메시지 보장은 클러스터 구성만으로는 부족하다 — Publisher Confirm과 수동 Ack가 함께 필요하다.