"우리 서비스에 RabbitMQ를 쓸 건가요, Kafka를 쓸 건가요?" — 이 질문을 받았을 때, "Kafka가 더 빠르니까요"라고 답하면 왜 감점일까?

RabbitMQ vs Kafka — 선택 기준과 공존 패턴

이전 글에서 RabbitMQ와 Kafka의 아키텍처 차이를 살펴봤습니다. 이번 글에서는 한 발 더 나아가서, **실제로 어떤 상황에서 무엇을 선택해야 하는지 **, 그리고 ** 둘을 함께 쓰는 하이브리드 패턴 **까지 정리해보겠습니다.

공부하다 보니, "어떤 게 더 좋은가?"보다 "우리 서비스가 메시지를 어떻게 다루는가?" 가 훨씬 중요한 질문이더라고요.


1. 판단 기준표

선택에 앞서, 핵심 항목별로 두 기술을 비교해보겠습니다.

항목RabbitMQKafka
메시지 보존소비 후 삭제 (기본)설정한 보존 기간까지 유지
** 처리량**수만 msg/s (충분히 높음)수십만~수백만 msg/s
** 라우팅 복잡도**Exchange 타입별 유연한 라우팅토픽 기반 단순 라우팅
** 순서 보장**단일 큐 내 보장파티션 내 보장
** 소비 모델**Push (브로커 → 소비자)Pull (소비자 → 브로커)
** 재처리(Replay)**불가 (소비 후 삭제)오프셋 이동으로 가능
** 프로토콜**AMQP, MQTT, STOMP 등 다양자체 바이너리 프로토콜
** 학습 곡선**상대적으로 낮음파티션, 오프셋 등 개념 학습 필요
** 운영 복잡도**단독 운영 가능ZooKeeper/KRaft 클러스터 필요

처리량만 보고 Kafka를 선택하면 오버 엔지니어링이 될 수 있습니다. 일일 메시지가 수만 건 수준이라면 RabbitMQ로도 충분하고, 운영 부담은 훨씬 적습니다.


2. 선택 플로우차트

텍스트 기반으로 판단 흐름을 정리해봤습니다.

PLAINTEXT
메시지를 소비 후 버려도 되는가?
├── 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. 하이브리드 아키텍처 — 둘 다 쓰는 패턴

실무에서는 하나만 쓰는 것이 아니라, ** 역할을 나눠서 공존 **시키는 경우가 꽤 많습니다.

전형적인 하이브리드 구성

PLAINTEXT
[주문 서비스]

    ├──(작업 처리)──→ RabbitMQ ──→ [결제 서비스] (1:1 작업 큐)
    │                              [재고 서비스] (1:1 작업 큐)

    └──(이벤트 발행)──→ Kafka ──→ [분석 서비스] (독립 소비)
                                  [알림 서비스] (독립 소비)
                                  [감사 로그]   (장기 보존)

역할 분담 원칙

  • RabbitMQ: 명령(Command) 처리, 작업 분배, 재시도가 필요한 흐름
  • Kafka: 이벤트(Event) 기록, 데이터 파이프라인, 여러 소비자가 읽어야 하는 흐름

실무 사례: 이커머스 주문 시스템

PLAINTEXT
1. 사용자가 주문 생성
2. 주문 서비스 → RabbitMQ로 "결제 처리" 작업 전송
   - 결제 서비스가 ACK/NACK로 처리 결과 확인
   - 실패 시 DLX로 이동 → 재시도 또는 수동 처리
3. 주문 서비스 → Kafka로 "주문 생성됨" 이벤트 발행
   - 분석 서비스: 매출 집계에 활용
   - 추천 서비스: 사용자 행동 분석에 활용
   - 감사 로그: 장기 보존

이렇게 하면 ** 작업 처리의 신뢰성 **과 ** 이벤트의 확장성 **을 동시에 확보할 수 있습니다.

하이브리드 구성 시 주의할 점

  • ** 운영 부담 증가 **: 두 개의 인프라를 모두 모니터링·관리해야 합니다
  • ** 데이터 일관성 **: 동일한 이벤트가 두 곳에 기록될 때 정합성 관리 필요
  • ** 팀 역량 **: 두 기술 모두에 대한 이해가 필요하므로, 팀 규모가 작다면 하나만 선택하는 것이 나을 수 있습니다

하이브리드는 "둘 다 잘 알아서" 쓰는 게 아니라, "각각의 역할이 명확히 다를 때" 쓰는 겁니다. 역할 구분 없이 둘 다 도입하면 복잡도만 올라갑니다.


5. Spring Cloud Stream — 브로커 추상화

만약 "지금은 RabbitMQ인데, 나중에 Kafka로 바꿀 수도 있다"는 상황이라면 Spring Cloud Stream 을 고려해볼 수 있습니다.

Binder 추상화란?

Spring Cloud Stream은 Binder 라는 추상화 계층을 통해, 비즈니스 코드를 브로커 구현체로부터 분리합니다.

PLAINTEXT
[비즈니스 로직]

  [Spring Cloud Stream]

   [Binder 추상화]
    ┌───┴───┐
 RabbitMQ   Kafka
  Binder    Binder

코드 예시 — 주문 이벤트 처리

JAVA
// 주문 이벤트 발행자 — 브로커 종류와 무관한 코드
@Configuration
public class OrderEventPublisher {

    @Bean
    public Supplier<OrderEvent> orderCreated() {
        // 주문 생성 이벤트를 주기적으로 발행
        return () -> new OrderEvent("ORDER-001", "CREATED");
    }
}
JAVA
// 주문 이벤트 소비자 — 마찬가지로 브로커 독립적
@Configuration
public class OrderEventConsumer {

    @Bean
    public Consumer<OrderEvent> processOrder() {
        // 주문 이벤트를 수신하여 처리
        return event -> {
            System.out.println("주문 처리: " + event.orderId());
            System.out.println("상태: " + event.status());
        };
    }
}

RabbitMQ 설정 (application.yml)

YAML
spring:
  cloud:
    stream:
      # RabbitMQ 바인더 사용
      defaultBinder: rabbit
      bindings:
        orderCreated-out-0:
          destination: order-events  # RabbitMQ에서는 Exchange 이름
        processOrder-in-0:
          destination: order-events  # 같은 Exchange에서 소비

Kafka로 전환할 때 — 코드 변경 없이 설정만 수정

YAML
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와 세밀한 재시도 정책이 필요
  • 클러스터 운영 부담이 서비스 규모 대비 과도

마이그레이션 전략

  1. ** 병렬 운영 (Strangler Fig 패턴)**: 새 기능부터 새 브로커를 사용하고, 기존 기능은 점진적으로 이관
  2. ** 브릿지 패턴 **: 두 브로커 사이에 중계 서비스를 두고, 양쪽 모두에 메시지를 전달
  3. **Spring Cloud Stream 활용 **: Binder만 교체하여 전환 (단, 브로커 고유 기능을 쓰지 않을 때만 유효)

7. "우리 서비스에 뭘 쓸 건가요?" — 대응법

이 질문을 받았을 때, 단순히 기술 비교만 나열하면 부족합니다. 다음과 같은 흐름으로 답변하면 설득력이 있습니다.

답변 프레임워크

  1. ** 요구사항 확인 **: "메시지를 소비 후 버려도 되는지, 보존이 필요한지부터 확인하겠습니다"
  2. ** 규모 파악 **: "일일 메시지량과 소비자 수를 기준으로 판단합니다"
  3. ** 기술 선택 근거 **: "이 서비스는 [작업 분배 / 이벤트 스트리밍]이 핵심이므로 [RabbitMQ / Kafka]가 적합합니다"
  4. ** 트레이드오프 언급 **: "대신 [운영 복잡도 / 라우팅 유연성]은 감수해야 합니다"
  5. ** 확장 가능성 **: "추후 [이벤트 분석 / 작업 큐]이 필요해지면 하이브리드 구성을 고려할 수 있습니다"

피해야 할 답변

  • "Kafka가 더 빠르니까 Kafka요" → 처리량이 선택의 유일한 기준이 아님
  • "요즘 다 Kafka 쓰니까요" → 기술 트렌드가 아니라 요구사항이 기준
  • "RabbitMQ가 쉬우니까요" → 학습 곡선만으로 판단하면 나중에 한계에 부딪힐 수 있음

기술 선택의 핵심은 "무엇이 더 좋은가"가 아니라 "우리 서비스의 메시지 흐름에 무엇이 더 자연스러운가"입니다.


정리

기준RabbitMQKafka
핵심 역할메시지 브로커 (작업 분배)이벤트 스트리밍 (로그 보존)
선택 신호소비 후 삭제 OK, 복잡한 라우팅, 재시도보존·재처리, 대량 처리, 다수 소비자
하이브리드명령(Command) 처리 담당이벤트(Event) 기록 담당
추상화 도구Spring Cloud Stream BinderSpring Cloud Stream Binder

주의할 점:

  • ** 하나만 골라야 하는 건 아닙니다** — 역할이 다르면 공존이 자연스럽습니다
  • Spring Cloud Stream은 보험이지 필수가 아닙니다 — 브로커 고유 기능이 필요하면 네이티브 클라이언트를 쓰세요
  • ** 기술 선택은 요구사항에서 출발합니다** — 트렌드나 성능 수치가 아니라, 서비스의 메시지 흐름이 기준입니다

다음 글에서는 RabbitMQ의 ** 클러스터링과 고가용성 구성 **을 다뤄보겠습니다.

댓글 로딩 중...