RabbitMQ vs Kafka — 어떤 문제를 해결하려고 만들어졌는가
메시지 전달 후 삭제하는 브로커와 이벤트를 로그로 영구 보관하는 분산 로그의 철학적 차이부터, Push/Pull 소비 방식과 두 시스템을 함께 쓸 때의 아키텍처 분리까지 추적한다.
- 01 RabbitMQ는 왜 메시지 브로커인가
- 02 RabbitMQ Exchange는 어떻게 메시지를 찾아가게 하는가
- 03 RabbitMQ 메시지 유실은 왜 조용히 일어나는가
- 04 RabbitMQ 메시지 패턴의 공통 철학은 무엇인가
- 05 RabbitMQ 운영의 모든 실패는 어디서 오는가
- 06 RabbitMQ vs Kafka — 어떤 문제를 해결하려고 만들어졌는가
- 07 Spring AMQP의 컴포넌트들은 어떻게 연결되는가
“RabbitMQ vs Kafka 중 어떤 것을 써야 하나요?”는 가장 자주 받는 아키텍처 질문 중 하나다. 기능 목록을 나란히 펼쳐봐도 답이 나오지 않는다. 두 시스템은 해결하려는 문제 자체가 다르기 때문이다. 그렇다면 이 선택을 결정하는 가장 근본적인 질문은 무엇인가?
설계 목적의 차이
RabbitMQ의 설계 목적은 하나다 — “메시지를 올바른 Consumer에게 전달하고, 처리되면 삭제한다.” 메시지는 Exchange를 거쳐 Queue에 잠시 머물고, Consumer가 Ack를 보내는 순간 사라진다. 우편 시스템과 같다. 편지가 수신자에게 전달되면 우체국에 복사본은 없다.
Kafka의 설계 목적은 반대편에 있다 — “대량의 이벤트를 순서대로 영구 보관하고, 누구든지 읽을 수 있게 한다.” 이벤트는 Topic의 Partition에 append-only로 기록되고, Consumer가 처리한 뒤에도 보관 기간(retention)이 끝날 때까지 남아있다. 신문 구독 시스템에 가깝다. 어제 신문을 오늘 읽어도 되고, 창간호부터 읽어도 된다.
“메시지를 처리 후 삭제해도 되는가?” — YES면 RabbitMQ 가능. 과거 재처리나 여러 팀의 독립 소비가 필요하면 Kafka가 답이다.
Smart Broker vs Dumb Broker
설계 목적의 차이는 복잡성이 어디에 위치하는지를 결정한다.
RabbitMQ는 Smart Broker, Dumb Consumer 철학을 따른다. 브로커가 복잡한 라우팅 로직을 담당한다. Direct, Topic, Fanout, Headers Exchange를 통해 “VIP 고객의 결제 실패 이벤트만 긴급팀에게, 나머지는 CS팀에게”를 브로커 설정만으로 구현할 수 있다. Consumer는 Queue에서 메시지를 받아 처리하고 Ack를 보내면 끝이다.
Kafka는 반대다. Dumb Broker, Smart Consumer. 브로커는 Partition에 메시지를 append하고 오프셋 위치의 메시지를 반환할 뿐이다. 어느 Partition을 읽을지, 오프셋을 어떻게 관리할지, Consumer Group Rebalancing은 어떻게 할지 — 이 복잡성은 모두 Consumer(와 Client Library)가 담당한다. 대신 브로커가 단순하기 때문에 수평 확장이 쉽고 수백만 메시지/초의 처리량을 달성할 수 있다.
# RabbitMQ: 브로커가 라우팅 결정
Exchange Binding: "payment.failed.vip" → vip-alert.queue
"payment.failed.*" → cs.queue
# 새 팀 추가 = Queue + Binding 추가만으로 완료
# Kafka: Consumer가 라우팅 결정
Consumer: poll() → 전체 이벤트 수신 → "payment.failed"만 filter
# 새 팀 추가 = 새 Consumer Group 코드 배포 필요
Push vs Pull — 레이턴시의 근원
RabbitMQ는 Push 기반이다. 브로커가 준비된 Consumer에게 메시지를 밀어 넣는다. basicQos(prefetch=10)으로 한 번에 Push할 최대 개수를 제한할 수 있다. 메시지 도착 → 즉시 Consumer 수신. 일반적 레이턴시는 1~5ms다.
Kafka는 Pull 기반이다. Consumer가 poll(timeout=100ms)을 주기적으로 호출해 메시지를 가져온다. 메시지가 있으면 즉시 반환하지만, poll 호출 주기만큼의 지연이 구조적으로 존재한다. 기본 설정에서 레이턴시는 10~50ms 범위다. linger.ms=0, fetch.min.bytes=1 같은 최적화 설정으로 10ms 내외까지 줄일 수 있지만, 처리량과 트레이드오프가 따른다.
각자가 빛나는 사용 사례
두 시스템을 선택 기준으로 정리하면 명확해진다.
RabbitMQ가 답인 경우: 복잡한 라우팅(Exchange 패턴), 요청-응답 RPC(Reply-To Queue + Correlation ID, 레이턴시 1~5ms), 작업 큐(이미지 처리·PDF 생성에 DLX와 Prefetch 활용), 메시지 우선순위(x-max-priority), 실시간 알림(< 10ms 목표).
Kafka가 답인 경우: 이벤트 소싱(Consumer Group의 오프셋 리셋으로 과거부터 재처리), 로그 집계(여러 Consumer Group이 복사 없이 독립 소비), CDC/데이터 파이프라인(Debezium 통합), 초당 수십만 건 이상의 대용량 스트리밍.
잘못된 선택은 나중에 재설계 비용으로 돌아온다. “Kafka로 RPC를 구현하다 Topic 수가 폭발했다”거나 “RabbitMQ로 이벤트 소싱을 구현했는데 재처리가 안 돼서 Kafka로 마이그레이션했다”는 사례가 실무에서 반복된다.
두 시스템을 함께 쓸 때
실무에서는 두 시스템이 각자 잘 해결하는 영역이 다르기 때문에 함께 운영하는 아키텍처가 많다. 이때 핵심은 역할을 명확히 분리하는 것이다.
가장 흔한 패턴은 “Kafka 이벤트 허브 + RabbitMQ 작업 실행기”다. 모든 비즈니스 이벤트는 Kafka Topic에 영구 기록된다. Kafka Consumer(브리지 서비스)가 이를 읽어 RabbitMQ Exchange에 발행하면, RabbitMQ의 DLX, Prefetch, 우선순위 처리가 실제 작업을 담당한다.
운영 복잡도가 두 배가 된다. 두 시스템의 모니터링, 장애 대응, 학습 비용이 함께 증가한다. 규모가 작으면 하나로 시작하는 것이 단순성 면에서 낫다. 규모가 크고 요구사항이 명확히 분리될 때 병행이 오히려 더 단순해진다.
두 시스템 사이의 데이터 흐름은 At-Least-Once + Consumer 멱등성으로 보장하는 것이 현실적이다. RabbitMQ Ack와 Kafka 오프셋 커밋은 원자적이지 않기 때문에 Exactly-Once는 구조적으로 달성하기 어렵다.
정리
- RabbitMQ는 메시지를 Consumer에게 전달하고 삭제하는 브로커다. Smart Broker가 라우팅·상태를 담당하고, Push 방식으로 낮은 레이턴시를 제공한다.
- Kafka는 이벤트를 로그로 영구 보관하는 분산 로그다. Dumb Broker가 저장만 담당하고, Pull 방식의 배치 처리로 높은 처리량을 제공한다.
- 가장 중요한 판단 기준은 처리량이나 레이턴시가 아니다. “처리 후 메시지를 삭제해도 되는가”와 “재처리/과거 소급이 필요한가”다.
- 두 시스템을 함께 쓴다면 역할 경계를 먼저 문서화하라. 경계가 모호하면 중복과 불일치가 시작된다.
다음 글에서는 RabbitMQ의 Exchange 유형별 내부 라우팅 알고리즘과 Binding Key 매칭 성능을 추적한다.