RabbitMQ vs Kafka — 선택 기준과 공존 패턴
"우리 서비스에 RabbitMQ를 쓸 건가요, Kafka를 쓸 건가요?" — 이 질문을 받았을 때, "Kafka가 더 빠르니까요"라고 답하면 왜 감점일까?
RabbitMQ vs Kafka — 선택 기준과 공존 패턴
이전 글에서 RabbitMQ와 Kafka의 아키텍처 차이를 살펴봤습니다. 이번 글에서는 한 발 더 나아가서, **실제로 어떤 상황에서 무엇을 선택해야 하는지 **, 그리고 ** 둘을 함께 쓰는 하이브리드 패턴 **까지 정리해보겠습니다.
공부하다 보니, "어떤 게 더 좋은가?"보다 "우리 서비스가 메시지를 어떻게 다루는가?" 가 훨씬 중요한 질문이더라고요.
1. 판단 기준표
선택에 앞서, 핵심 항목별로 두 기술을 비교해보겠습니다.
| 항목 | RabbitMQ | Kafka |
|---|---|---|
| 메시지 보존 | 소비 후 삭제 (기본) | 설정한 보존 기간까지 유지 |
| ** 처리량** | 수만 msg/s (충분히 높음) | 수십만~수백만 msg/s |
| ** 라우팅 복잡도** | Exchange 타입별 유연한 라우팅 | 토픽 기반 단순 라우팅 |
| ** 순서 보장** | 단일 큐 내 보장 | 파티션 내 보장 |
| ** 소비 모델** | Push (브로커 → 소비자) | Pull (소비자 → 브로커) |
| ** 재처리(Replay)** | 불가 (소비 후 삭제) | 오프셋 이동으로 가능 |
| ** 프로토콜** | AMQP, MQTT, STOMP 등 다양 | 자체 바이너리 프로토콜 |
| ** 학습 곡선** | 상대적으로 낮음 | 파티션, 오프셋 등 개념 학습 필요 |
| ** 운영 복잡도** | 단독 운영 가능 | ZooKeeper/KRaft 클러스터 필요 |
처리량만 보고 Kafka를 선택하면 오버 엔지니어링이 될 수 있습니다. 일일 메시지가 수만 건 수준이라면 RabbitMQ로도 충분하고, 운영 부담은 훨씬 적습니다.
2. 선택 플로우차트
텍스트 기반으로 판단 흐름을 정리해봤습니다.
메시지를 소비 후 버려도 되는가?
├── YES → 복잡한 라우팅이 필요한가?
│ ├── YES → ✅ RabbitMQ
│ └── NO → 일일 메시지량이 수십만 건 이상인가?
│ ├── YES → ✅ Kafka
│ └── NO → ✅ RabbitMQ (운영이 더 간단)
│
└── NO (보존·재처리 필요) → 여러 소비자가 독립적으로 읽어야 하는가?
├── YES → ✅ Kafka
└── NO → 이벤트 소싱/감사 로그가 목적인가?
├── YES → ✅ Kafka
└── NO → ✅ 상황에 따라 판단 (하이브리드 고려)
핵심은 ** 첫 번째 분기 **입니다. 메시지 보존 여부가 가장 근본적인 선택 기준이라는 점, 기억해두면 좋습니다.
3. 시나리오별 추천
RabbitMQ가 더 적합한 경우
| 시나리오 | 이유 |
|---|---|
| ** 작업 큐 (Task Queue)** | 작업 분배 + ACK 기반 신뢰성. 실패 시 재전송이 자연스러움 |
| ** 실시간 알림/푸시** | 낮은 지연 시간 + 메시지 소비 후 삭제해도 무방 |
| RPC 패턴 | Reply Queue를 활용한 요청-응답 패턴 지원 |
| ** 복잡한 라우팅** | Topic/Headers Exchange로 조건별 분기 가능 |
| ** 소규모~중규모 서비스** | 클러스터 없이 단독 운영 가능, 학습 곡선 낮음 |
Kafka가 더 적합한 경우
| 시나리오 | 이유 |
|---|---|
| ** 이벤트 소싱** | 모든 이벤트를 보존하고 상태를 재구성 가능 |
| ** 로그 수집/분석** | 대용량 로그를 파티션별로 병렬 처리 |
| ** 스트림 처리** | Kafka Streams로 실시간 데이터 변환·집계 |
| ** 여러 소비자 독립 소비** | Consumer Group별로 같은 메시지를 독립적으로 읽음 |
| ** 감사 추적 (Audit Trail)** | 메시지 보존으로 히스토리 추적 가능 |
| IoT 대량 데이터 수집 | 초당 수십만 건 이상의 센서 데이터 처리 |
"작업을 분배하고 완료 확인을 받아야 한다" → RabbitMQ. "이벤트를 기록하고 여러 시스템이 독립적으로 소비해야 한다" → Kafka. 이 한 줄이 가장 실용적인 판단 기준입니다.
4. 하이브리드 아키텍처 — 둘 다 쓰는 패턴
실무에서는 하나만 쓰는 것이 아니라, ** 역할을 나눠서 공존 **시키는 경우가 꽤 많습니다.
전형적인 하이브리드 구성
[주문 서비스]
│
├──(작업 처리)──→ RabbitMQ ──→ [결제 서비스] (1:1 작업 큐)
│ [재고 서비스] (1:1 작업 큐)
│
└──(이벤트 발행)──→ Kafka ──→ [분석 서비스] (독립 소비)
[알림 서비스] (독립 소비)
[감사 로그] (장기 보존)
역할 분담 원칙
- RabbitMQ: 명령(Command) 처리, 작업 분배, 재시도가 필요한 흐름
- Kafka: 이벤트(Event) 기록, 데이터 파이프라인, 여러 소비자가 읽어야 하는 흐름
실무 사례: 이커머스 주문 시스템
1. 사용자가 주문 생성
2. 주문 서비스 → RabbitMQ로 "결제 처리" 작업 전송
- 결제 서비스가 ACK/NACK로 처리 결과 확인
- 실패 시 DLX로 이동 → 재시도 또는 수동 처리
3. 주문 서비스 → Kafka로 "주문 생성됨" 이벤트 발행
- 분석 서비스: 매출 집계에 활용
- 추천 서비스: 사용자 행동 분석에 활용
- 감사 로그: 장기 보존
이렇게 하면 ** 작업 처리의 신뢰성 **과 ** 이벤트의 확장성 **을 동시에 확보할 수 있습니다.
하이브리드 구성 시 주의할 점
- ** 운영 부담 증가 **: 두 개의 인프라를 모두 모니터링·관리해야 합니다
- ** 데이터 일관성 **: 동일한 이벤트가 두 곳에 기록될 때 정합성 관리 필요
- ** 팀 역량 **: 두 기술 모두에 대한 이해가 필요하므로, 팀 규모가 작다면 하나만 선택하는 것이 나을 수 있습니다
하이브리드는 "둘 다 잘 알아서" 쓰는 게 아니라, "각각의 역할이 명확히 다를 때" 쓰는 겁니다. 역할 구분 없이 둘 다 도입하면 복잡도만 올라갑니다.
5. Spring Cloud Stream — 브로커 추상화
만약 "지금은 RabbitMQ인데, 나중에 Kafka로 바꿀 수도 있다"는 상황이라면 Spring Cloud Stream 을 고려해볼 수 있습니다.
Binder 추상화란?
Spring Cloud Stream은 Binder 라는 추상화 계층을 통해, 비즈니스 코드를 브로커 구현체로부터 분리합니다.
[비즈니스 로직]
│
[Spring Cloud Stream]
│
[Binder 추상화]
┌───┴───┐
RabbitMQ Kafka
Binder Binder
코드 예시 — 주문 이벤트 처리
// 주문 이벤트 발행자 — 브로커 종류와 무관한 코드
@Configuration
public class OrderEventPublisher {
@Bean
public Supplier<OrderEvent> orderCreated() {
// 주문 생성 이벤트를 주기적으로 발행
return () -> new OrderEvent("ORDER-001", "CREATED");
}
}
// 주문 이벤트 소비자 — 마찬가지로 브로커 독립적
@Configuration
public class OrderEventConsumer {
@Bean
public Consumer<OrderEvent> processOrder() {
// 주문 이벤트를 수신하여 처리
return event -> {
System.out.println("주문 처리: " + event.orderId());
System.out.println("상태: " + event.status());
};
}
}
RabbitMQ 설정 (application.yml)
spring:
cloud:
stream:
# RabbitMQ 바인더 사용
defaultBinder: rabbit
bindings:
orderCreated-out-0:
destination: order-events # RabbitMQ에서는 Exchange 이름
processOrder-in-0:
destination: order-events # 같은 Exchange에서 소비
Kafka로 전환할 때 — 코드 변경 없이 설정만 수정
spring:
cloud:
stream:
# Kafka 바인더로 전환
defaultBinder: kafka
bindings:
orderCreated-out-0:
destination: order-events # Kafka에서는 Topic 이름
processOrder-in-0:
destination: order-events
kafka:
binder:
brokers: localhost:9092
비즈니스 코드는 한 줄도 바꾸지 않았습니다. defaultBinder와 브로커 접속 정보만 변경하면 됩니다.
Spring Cloud Stream의 한계
물론 만능은 아닙니다.
- **브로커 고유 기능 사용 제한 **: RabbitMQ의 DLX, Priority Queue나 Kafka의 Streams API 같은 고유 기능을 쓰려면 추상화를 벗어나야 합니다
- ** 성능 오버헤드 **: 추상화 계층이 있으므로 네이티브 클라이언트 대비 약간의 오버헤드 존재
- ** 디버깅 복잡도 **: 문제 발생 시 추상화 계층과 실제 브로커 양쪽을 모두 살펴야 합니다
Spring Cloud Stream은 "브로커를 바꿀 가능성이 있을 때" 보험처럼 쓰는 도구입니다. 처음부터 Kafka 고유 기능(Streams, Exactly-once)을 써야 한다면, 네이티브 클라이언트가 더 적합합니다.
6. 마이그레이션 고려사항
RabbitMQ → Kafka 전환이 필요한 신호
- 메시지 재처리(Replay) 요구가 늘어남
- 소비자가 계속 추가되는데, 같은 메시지를 독립적으로 읽어야 함
- 일일 메시지량이 급격히 증가하여 RabbitMQ 스케일링이 한계에 도달
Kafka → RabbitMQ 전환이 필요한 신호
- 복잡한 라우팅 로직이 필요해졌는데, Kafka의 토픽 기반 라우팅으로는 부족
- 메시지별 ACK/NACK와 세밀한 재시도 정책이 필요
- 클러스터 운영 부담이 서비스 규모 대비 과도
마이그레이션 전략
- ** 병렬 운영 (Strangler Fig 패턴)**: 새 기능부터 새 브로커를 사용하고, 기존 기능은 점진적으로 이관
- ** 브릿지 패턴 **: 두 브로커 사이에 중계 서비스를 두고, 양쪽 모두에 메시지를 전달
- **Spring Cloud Stream 활용 **: Binder만 교체하여 전환 (단, 브로커 고유 기능을 쓰지 않을 때만 유효)
7. "우리 서비스에 뭘 쓸 건가요?" — 대응법
이 질문을 받았을 때, 단순히 기술 비교만 나열하면 부족합니다. 다음과 같은 흐름으로 답변하면 설득력이 있습니다.
답변 프레임워크
- ** 요구사항 확인 **: "메시지를 소비 후 버려도 되는지, 보존이 필요한지부터 확인하겠습니다"
- ** 규모 파악 **: "일일 메시지량과 소비자 수를 기준으로 판단합니다"
- ** 기술 선택 근거 **: "이 서비스는 [작업 분배 / 이벤트 스트리밍]이 핵심이므로 [RabbitMQ / Kafka]가 적합합니다"
- ** 트레이드오프 언급 **: "대신 [운영 복잡도 / 라우팅 유연성]은 감수해야 합니다"
- ** 확장 가능성 **: "추후 [이벤트 분석 / 작업 큐]이 필요해지면 하이브리드 구성을 고려할 수 있습니다"
피해야 할 답변
- "Kafka가 더 빠르니까 Kafka요" → 처리량이 선택의 유일한 기준이 아님
- "요즘 다 Kafka 쓰니까요" → 기술 트렌드가 아니라 요구사항이 기준
- "RabbitMQ가 쉬우니까요" → 학습 곡선만으로 판단하면 나중에 한계에 부딪힐 수 있음
기술 선택의 핵심은 "무엇이 더 좋은가"가 아니라 "우리 서비스의 메시지 흐름에 무엇이 더 자연스러운가"입니다.
정리
| 기준 | RabbitMQ | Kafka |
|---|---|---|
| 핵심 역할 | 메시지 브로커 (작업 분배) | 이벤트 스트리밍 (로그 보존) |
| 선택 신호 | 소비 후 삭제 OK, 복잡한 라우팅, 재시도 | 보존·재처리, 대량 처리, 다수 소비자 |
| 하이브리드 | 명령(Command) 처리 담당 | 이벤트(Event) 기록 담당 |
| 추상화 도구 | Spring Cloud Stream Binder | Spring Cloud Stream Binder |
주의할 점:
- ** 하나만 골라야 하는 건 아닙니다** — 역할이 다르면 공존이 자연스럽습니다
- Spring Cloud Stream은 보험이지 필수가 아닙니다 — 브로커 고유 기능이 필요하면 네이티브 클라이언트를 쓰세요
- ** 기술 선택은 요구사항에서 출발합니다** — 트렌드나 성능 수치가 아니라, 서비스의 메시지 흐름이 기준입니다
다음 글에서는 RabbitMQ의 ** 클러스터링과 고가용성 구성 **을 다뤄보겠습니다.