RabbitMQ vs Kafka — 메시지 브로커 vs 이벤트 스트리밍
"RabbitMQ랑 Kafka 중에 뭘 써야 하나요?"라는 질문은, 사실 "택시와 버스 중에 뭘 타야 하나요?"와 비슷합니다. 목적지가 같더라도 용도가 다릅니다.
RabbitMQ vs Kafka — 메시지 브로커 vs 이벤트 스트리밍
먼저 알아야 할 것: 둘은 근본적으로 다른 물건이다
RabbitMQ와 Kafka를 비교하는 글은 많지만, 공부하다 보니 가장 중요한 포인트는 둘이 같은 카테고리의 제품이 아니라는 것 이었습니다.
- RabbitMQ — 전통적인 ** 메시지 브로커** (Message Broker)
- Kafka — ** 이벤트 스트리밍 플랫폼** (Event Streaming Platform)
이름부터가 다릅니다. 하나는 "브로커"고 하나는 "플랫폼"입니다. 이 차이를 모르면 비교 자체가 의미 없어집니다.
1. 설계 철학 — 큐 모델 vs 분산 로그 모델
RabbitMQ: 스마트 브로커, 심플 컨슈머
RabbitMQ는 AMQP 프로토콜 기반의 메시지 브로커입니다. 브로커가 메시지의 라우팅, 필터링, 전달을 모두 책임집니다.
Producer → Exchange → Binding → Queue → Consumer
↑
브로커가 라우팅 결정
- 메시지가 소비자에게 전달되고 ACK되면 ** 큐에서 삭제 **됩니다
- 브로커가 "똑똑한" 역할을 하고, 소비자는 단순히 받아서 처리하면 됩니다
Kafka: 심플 브로커, 스마트 컨슈머
Kafka는 분산 커밋 로그(Distributed Commit Log)입니다. 브로커는 로그를 저장하는 역할만 하고, 소비자가 자신의 오프셋을 관리합니다.
Producer → Topic (Partition 0) → [msg0][msg1][msg2][msg3]...
↑
Consumer가 오프셋 관리
- 메시지가 소비되어도 ** 삭제되지 않습니다** (보존 기간까지 유지)
- 소비자가 "똑똑한" 역할을 하고, 브로커는 단순히 로그를 저장합니다
이 차이가 나머지 모든 차이의 원인입니다. "소비 후 삭제 vs 소비 후 보존"이라는 한 줄로 기억하면, 나머지 특성들이 자연스럽게 따라옵니다.
2. 핵심 비교표
| 비교 항목 | RabbitMQ | Kafka |
|---|---|---|
| ** 모델** | 메시지 큐 (Queue) | 분산 커밋 로그 (Log) |
| ** 프로토콜** | AMQP 0-9-1 | 자체 TCP 프로토콜 |
| ** 전달 방식** | Push (브로커 → 소비자) | Pull (소비자 → 브로커) |
| ** 메시지 소비 후** | 큐에서 삭제 | 보존 (retention 기간까지) |
| ** 라우팅** | Exchange + Binding (유연) | Topic + Partition (단순) |
| ** 순서 보장** | 큐 단위 | 파티션 단위 |
| ** 소비자 확장** | Competing Consumers | Consumer Group |
| ** 처리량** | 수만 msg/s | 수백만 msg/s |
| ** 메시지 재처리** | 기본적으로 불가 (이미 삭제) | 오프셋 리셋으로 가능 |
| ** 주요 언어** | Erlang | Scala + Java |
3. 메시지 소비 방식 — Push vs Pull
RabbitMQ: Push 모델
[브로커] --push--> [Consumer A]
--push--> [Consumer B]
--push--> [Consumer C]
브로커가 소비자에게 메시지를 ** 밀어넣습니다 **. 메시지가 도착하면 즉시 전달되므로 ** 지연(latency)이 낮습니다 **.
하지만 소비자의 처리 속도보다 빠르게 밀어넣으면 과부하가 발생할 수 있습니다. 이를 방지하기 위해 prefetch count로 한 번에 받을 메시지 수를 제한합니다.
// RabbitMQ — prefetch로 소비자 과부하 방지
channel.basicQos(10); // 최대 10개까지만 미리 전달
Kafka: Pull 모델
[Consumer A] --poll--> [브로커]
[Consumer B] --poll--> [브로커]
[Consumer C] --poll--> [브로커]
소비자가 자신의 속도에 맞춰 브로커에서 메시지를 ** 가져갑니다 **. 처리가 느려도 과부하가 발생하지 않습니다.
대신, 새 메시지가 없을 때도 계속 폴링하면 리소스 낭비가 발생합니다. Kafka는 이를 long polling으로 해결합니다.
// Kafka — 소비자가 자신의 속도로 메시지를 가져감
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
// 1초 동안 새 메시지가 없으면 빈 결과 반환
Push 모델은 실시간성이 중요할 때, Pull 모델은 대량 데이터를 안정적으로 처리할 때 유리합니다. 면접에서 단순히 "Push vs Pull"만 말하면 절반만 아는 겁니다. ** 트레이드오프까지 설명해야 합니다.**
4. 메시지 삭제 vs 보존
이 차이가 실무에서 가장 큰 영향을 미칩니다.
RabbitMQ: ACK하면 삭제
Queue: [msg1][msg2][msg3]
↓ Consumer가 msg1을 ACK
Queue: [msg2][msg3] ← msg1 사라짐
- 한 번 소비된 메시지는 큐에서 사라집니다
- 다른 소비자가 같은 메시지를 읽을 수 없습니다 (Fanout Exchange로 복제하면 가능)
- 메시지 재처리가 필요하면? 프로듀서가 다시 보내야 합니다
Kafka: 오프셋 기반 보존
Partition: [msg0][msg1][msg2][msg3][msg4][msg5]
↑ Consumer A 오프셋 = 2
↑ Consumer B 오프셋 = 4
- 소비해도 메시지가 삭제되지 않습니다 (retention 기간까지 보존)
- 여러 Consumer Group이 독립적으로 같은 메시지를 읽을 수 있습니다
- 오프셋을 되감으면 과거 메시지를 재처리할 수 있습니다
// Kafka — 오프셋을 처음으로 되감아 전체 재처리
consumer.seekToBeginning(consumer.assignment());
이 특성 때문에 ** 이벤트 소싱 **, ** 감사 로그 , ** 데이터 파이프라인 같은 용도에서 Kafka가 압도적으로 유리합니다.
5. Consumer Group vs Competing Consumers
소비자를 수평 확장하는 방식도 다릅니다.
RabbitMQ: Competing Consumers
┌→ [Consumer 1] ← msg1, msg3, msg5
[Queue] ──────┼→ [Consumer 2] ← msg2, msg4, msg6
└→ [Consumer 3] ← msg7, msg8, msg9
- 하나의 큐를 여러 소비자가 경쟁적으로 소비합니다
- 각 메시지는 하나의 소비자만 받습니다 (라운드 로빈)
- 같은 메시지를 여러 소비자가 받으려면? Fanout Exchange 로 큐를 복제해야 합니다
Kafka: Consumer Group
Topic (3 파티션)
├── Partition 0 → [Consumer 1] ← Group A
├── Partition 1 → [Consumer 2] ← Group A
├── Partition 2 → [Consumer 3] ← Group A
├── Partition 0 → [Consumer X] ← Group B (독립적)
├── Partition 1 → [Consumer X] ← Group B
├── Partition 2 → [Consumer Y] ← Group B
- **같은 그룹 내 **: 파티션이 컨슈머에게 분배됩니다 (하나의 파티션은 하나의 컨슈머만 소비)
- ** 다른 그룹 간 **: 같은 메시지를 독립적으로 소비합니다
- 같은 그룹 내 컨슈머 수가 파티션 수를 초과하면? 초과분은 ** 유휴 상태 **가 됩니다
| 패턴 | RabbitMQ | Kafka |
|---|---|---|
| 1:1 (하나의 메시지를 하나의 소비자가) | 기본 큐 소비 | 같은 Consumer Group |
| 1:N (하나의 메시지를 여러 소비자가) | Fanout Exchange | 서로 다른 Consumer Group |
| 수평 확장 | 소비자 추가 (제한 없음) | 파티션 수 이하로 소비자 추가 |
6. 메시지 순서 보장
RabbitMQ: 큐 단위 순서 보장
하나의 큐 내에서는 메시지 순서가 보장됩니다. 하지만 여러 소비자가 경쟁적으로 소비하면, 처리 완료 순서는 보장되지 않습니다.
Queue: [A][B][C]
Consumer 1이 A를 받고 3초 걸림
Consumer 2가 B를 받고 1초 걸림
→ B가 A보다 먼저 처리 완료됨
Kafka: 파티션 단위 순서 보장
하나의 파티션 내에서는 메시지 순서가 ** 절대적으로 보장 **됩니다. 같은 키를 가진 메시지는 항상 같은 파티션에 들어가므로, 키 기반 순서 보장이 가능합니다.
// 같은 orderId를 가진 메시지는 항상 같은 파티션으로
producer.send(new ProducerRecord<>("orders", orderId, orderEvent));
하지만 ** 파티션 간** 순서는 보장되지 않습니다. 글로벌 순서가 필요하면 파티션을 1개로 제한해야 하는데, 그러면 병렬 처리를 포기하는 것이므로 신중하게 판단해야 합니다.
7. 전달 보장 — At-most-once, At-least-once, Exactly-once
| 전달 보장 수준 | 의미 | RabbitMQ | Kafka |
|---|---|---|---|
| At-most-once | 최대 한 번 (유실 가능) | Auto ACK | 오프셋 자동 커밋 |
| At-least-once | 최소 한 번 (중복 가능) | Manual ACK + Publisher Confirm | 오프셋 수동 커밋 + acks=all |
| Exactly-once | 정확히 한 번 | 네이티브 미지원 | Idempotent Producer + Transactional API |
Exactly-once는 정말 가능한가?
공부하다 보니 이 부분이 가장 많이 헷갈렸습니다.
Kafka 는 0.11 버전부터 Idempotent Producer와 Transactional API를 통해 브로커 레벨의 exactly-once 를 지원합니다.
// Kafka — Idempotent Producer 활성화
props.put("enable.idempotence", "true");
// Transactional API로 원자적 쓰기
producer.initTransactions();
producer.beginTransaction();
producer.send(new ProducerRecord<>("topic", "key", "value"));
producer.commitTransaction();
하지만 이것은 프로듀서 → 브로커 구간의 exactly-once입니다. ** 브로커 → 컨슈머 → 외부 시스템** 구간까지 포함한 엔드투엔드 exactly-once는 소비자 측에서도 ** 멱등성(idempotency)**을 보장해야 합니다.
"Exactly-once가 가능한가?"라는 질문에 "Kafka에서 가능합니다"라고만 대답하면 부족합니다. 브로커 내부 exactly-once와 엔드투엔드 exactly-once를 구분해서 설명할 수 있어야 합니다.
RabbitMQ 는 네이티브 exactly-once를 지원하지 않습니다. Publisher Confirm + Manual ACK 조합으로 at-least-once를 구현하고, 소비자 측 멱등성으로 중복을 제거하는 방식이 일반적입니다.
8. 처리량(Throughput) 차이
Kafka가 높은 처리량을 달성하는 이유는 설계 자체가 다르기 때문입니다.
| 요소 | RabbitMQ | Kafka |
|---|---|---|
| I/O 방식 | 메시지별 개별 처리 | 시퀀셜 I/O + 배치 쓰기 |
| ** 압축** | 메시지별 (선택) | 배치 단위 압축 |
| ** 데이터 전송** | 브로커가 복사 후 전달 | Zero-copy (sendfile 시스템콜) |
| ** 저장 구조** | 큐 (인메모리 + 디스크) | 파티션별 세그먼트 파일 (디스크) |
RabbitMQ 처리량:
├── 단일 큐: ~50,000 msg/s
└── 클러스터: ~수십만 msg/s
Kafka 처리량:
├── 단일 파티션: ~수십만 msg/s
└── 클러스터: ~수백만 msg/s
다만 처리량이 높다고 항상 좋은 것은 아닙니다. 메시지 하나하나의 ** 라우팅 유연성 **이 필요하면 RabbitMQ가 적합하고, 대량의 이벤트를 빠르게 ** 스트리밍 **해야 하면 Kafka가 적합합니다.
9. 프로토콜 — AMQP vs Custom TCP
RabbitMQ: AMQP 0-9-1
- 표준 프로토콜이라 다양한 언어/클라이언트가 존재합니다
- Exchange, Queue, Binding 등의 개념이 프로토콜 레벨에서 정의되어 있습니다
- STOMP, MQTT 등 플러그인으로 다른 프로토콜도 지원합니다
Kafka: 자체 TCP 프로토콜
- Kafka 전용 바이너리 프로토콜입니다
- 표준이 아니므로 Kafka 클라이언트 라이브러리가 반드시 필요합니다
- 대신 배치, 압축, 파티셔닝에 최적화되어 있습니다
최종 정리
| 기준 | RabbitMQ를 선택 | Kafka를 선택 |
|---|---|---|
| ** 메시지 패턴** | 복잡한 라우팅이 필요할 때 | 단순 pub/sub, 대량 스트리밍 |
| ** 메시지 보존** | 처리 후 삭제해도 될 때 | 이벤트 재처리, 감사 로그가 필요할 때 |
| ** 처리량** | 수만 msg/s 이하 | 수백만 msg/s 이상 |
| ** 순서 보장** | 큐 단위면 충분할 때 | 키 기반 파티션 순서가 필요할 때 |
| ** 전달 보장** | At-least-once면 충분할 때 | 브로커 레벨 exactly-once가 필요할 때 |
| ** 지연(Latency)** | 밀리초 단위 실시간 필요 | 약간의 지연 허용 가능 |
| ** 소비자 패턴** | 작업 큐 (Task Queue) | 이벤트 소싱, 데이터 파이프라인 |
| ** 운영 복잡도** | 상대적으로 단순 | ZooKeeper/KRaft 등 추가 구성 필요 |
둘 다 각자의 영역에서 최고의 도구입니다. "어떤 게 더 좋냐"가 아니라 "어떤 문제를 풀어야 하냐" 로 접근해야 올바른 선택을 할 수 있습니다.
다음 편에서는 RabbitMQ와 Kafka를 함께 사용하는 공존 패턴 과 실무 선택 기준 체크리스트 를 정리합니다.