Raft 합의 알고리즘으로 복제되는 RabbitMQ의 큐 종류. 고가용성이 필요하면 이제 이것뿐이다.
무엇을 대체했나
예전에는 classic mirrored queue(클래식 큐를 여러 노드에 미러링)로 고가용성을 구성했다. 이 방식은 복제 일관성이 약해서 네트워크 분단 뒤 데이터가 어긋나는 문제가 있었다.
RabbitMQ 4.0에서 classic queue mirroring이 제거됐다. 복제 큐가 필요하면 쿼럼 큐(또는 스트림)를 써야 한다.
Raft — 과반이 동의해야 확정된다
쿼럼 큐 (3노드)
리더 노드 ← 모든 쓰기/읽기가 여기로
│ 로그 복제
├→ 팔로워 1
└→ 팔로워 2
과반(2/3)이 로그에 기록해야 "확정"으로 인정
→ 노드 1대가 죽어도 계속 동작, 2대가 죽으면 멈춘다
과반이라는 조건 때문에 노드 수는 홀수(3, 5)로 잡는다. 4대는 3대와 내성이 같으면서(둘 다 1대까지) 비용만 더 든다.
이 구조는 앞서 본 Kafka의 KRaft 컨트롤러 쿼럼과 같은 원리다 — 합의 알고리즘으로 로그를 복제한다는 발상이 분산 시스템 전반의 표준이 된 셈이다.
트레이드오프
| 쿼럼 큐 | 클래식 큐 | |
|---|---|---|
| 복제 | Raft·강한 일관성 | 없음(미러링은 제거됨) |
| 내구성 | 항상 durable(비durable 불가) | 선택 가능 |
| 처리량 | 미러링 큐 대비 약 3배 | — |
| 지연 | 디스크 I/O가 필수라 조금 더 김 | 더 낮을 수 있음 |
| 미지원 | exclusive 큐·global QoS | — |
"항상 durable"이라는 건 모든 메시지가 디스크를 거친다는 뜻이다. 지연에 극도로 민감하고 유실을 감수할 수 있는 워크로드라면 복제 없는 클래식 큐가 여전히 답일 수 있다.
실무에서 가장 유용한 기능 — delivery-limit
독약 메시지(처리하면 반드시 실패하는 메시지)가 무한 재큐되며 큐를 막는 문제를, 쿼럼 큐는 구조적으로 막는다.
메시지 헤더에 delivery-count 를 자동으로 센다 전달 → 실패 → 재큐 → delivery-count++ ... delivery-limit(4.0 기본 20) 초과
- 버리거나 DLX(Dead Letter Exchange)로 보낸다
클래식 큐에서는 이 카운트를 애플리케이션이 직접 헤더에 세야 했다. Kafka 쪽의 "재시도 N회 후 DLT"와 같은 역할을 브로커가 대신 해 주는 셈이다.
면접 함정
- ❌ "미러링 큐로 HA를 구성한다" → 4.0에서 제거됐다.
- ❌ "쿼럼 큐가 모든 면에서 낫다" → 항상 durable이라 지연이 더 들고, exclusive·global QoS를 못 쓴다.
선언과 설정
// 큐 타입을 인자로 지정한다. 나중에 바꿀 수 없으므로 만들 때 정해야 한다
Map<String, Object> args = Map.of(
"x-queue-type", "quorum",
"x-delivery-limit", 5, // 5회 실패하면 DLX 로
"x-dead-letter-exchange", "orders.dlx"
);
channel.queueDeclare("orders", true, false, false, args);
클래식 큐 → 쿼럼 큐로 "변경"은 불가능하다
- 새 큐를 만들고 → 소비자를 옮기고 → 남은 메시지를 이관하고 → 옛 큐 제거
노드 수는 홀수로
- 3노드: 과반 2 — → 1대 장애까지 견딤
- 4노드: 과반 3 — → 1대 장애까지 견딤 (3노드와 같은데 비용만 증가)
- 5노드: 과반 3 — → 2대 장애까지 견딤
짝수는 내성을 늘리지 못한다. Raft를 쓰는 시스템이 대체로 3 또는 5인 이유이고, 앞서 본 Kafka의 KRaft 컨트롤러도 같다.