메시지 브로커 선택은 시스템 설계에서 중요한 결정이다. Kafka와 RabbitMQ의 차이를 개념부터 실전 패턴까지 정리했다.

아키텍처 차이

메시지 소비 방식

RabbitMQKafka
메시지 소비Consumer가 가져가면 삭제됨Consumer가 읽어도 유지됨
비유택배 수령 (가져가면 끝)게시판 열람 (누가 봐도 글은 그대로)
저장 방식메모리 버퍼링 + 디스크 영속화 (Quorum Queue는 Raft 기반 디스크 복제)디스크에 로그처럼 저장
전달 모델Push — 브로커가 Consumer에게 밀어줌Pull — Consumer가 브로커에서 당겨감

Push vs Pull

┌─────────────────────────────────────────────────────────────────┐
│                     RabbitMQ (Push)                             │
│  ┌──────────┐      ┌──────────┐      ┌──────────┐              │
│  │ Producer │ ──── │  Broker  │ ════>│ Consumer │              │
│  └──────────┘      └──────────┘      └──────────┘              │
│                         │                  │                    │
│                    브로커가 주도     "받아라!" (수동적)          │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                       Kafka (Pull)                              │
│  ┌──────────┐      ┌──────────┐      ┌──────────┐              │
│  │ Producer │ ──── │  Broker  │ <════│ Consumer │              │
│  └──────────┘      └──────────┘      └──────────┘              │
│                         │                  │                    │
│                    로그만 저장       "줘!" (능동적)              │
└─────────────────────────────────────────────────────────────────┘

RabbitMQ (Smart Broker, Dumb Consumer)

  • 브로커가 큐에 메시지가 들어오자마자 즉시 밀어 넣는다
  • 지연 시간 최소화. prefetch_count로 흐름 제어

Kafka (Dumb Broker, Smart Consumer)

  • 컨슈머가 자신의 처리 능력에 맞춰 속도 조절
  • 장애 발생해도 브로커에 데이터가 그대로 있어 나중에 다시 가져올 수 있음

RabbitMQ: Exchange와 Queue

복잡한 라우팅을 구현하는 핵심 요소들이다.

Producer -> [Exchange] --binding--> [에러 로그 큐] --> 알림 서비스
                       --binding--> [전체 로그 큐] --> 아카이빙 서비스
  • Exchange: 메시지를 어떤 큐로 보낼지 결정하는 라우팅 로직. 저장하지 않음
  • Queue: 메시지가 소비되기 전까지 실제로 저장되는 버퍼
  • Binding: Exchange와 Queue를 연결하는 규칙 (Routing Key 기반)

Exchange 타입

Exchange 타입라우팅 방식
Directrouting key가 정확히 일치하는 큐로 전달
Topic와일드카드 패턴 매칭 (*: 한 단어, #: 0개 이상)
Fanout바인딩된 모든 큐에 브로드캐스트
Headers메시지 header 속성 기반 매칭

AMQP 프로토콜

RabbitMQ는 AMQP(Advanced Message Queuing Protocol) 0-9-1을 사용한다. ISO/IEC 국제 표준으로, 메시지 전송 형식뿐 아니라 브로커 내부 동작 방식(Exchange, Queue, Binding)까지 정의한다.


Kafka: Consumer Group과 Partition

Consumer Group

같은 클릭 이벤트를 여러 시스템이 필요로 하는 상황:

광고 클릭 이벤트 발생 -> 여러 시스템이 필요로 함:
- 정산 시스템: 광고주에게 비용 청구
- 분석 시스템: 클릭률(CTR) 집계
- 실시간 대시보드: 광고주가 보는 현황판

정산 서버가 3대인데 같은 클릭 이벤트를 3대 모두 처리하면 100원짜리 클릭이 300원으로 과청구된다.

클릭 이벤트 -> [ Kafka Topic: ad-clicks ]
                    |
        +-----------+-----------+
        v           v           v
    +-----------------------------+
    |  Consumer Group: "정산"      |  <- 3대가 이벤트를 나눠서 처리
    |  서버A  서버B  서버C         |     (한 이벤트는 딱 한 대만)
    +-----------------------------+

    +-----------------------------+
    |  Consumer Group: "분석"      |  <- 별도 그룹이라 같은 이벤트를 또 받음
    +-----------------------------+

핵심 규칙

  • 같은 Consumer Group 내에서는 -> 이벤트를 나눠 가짐 (중복 처리 방지)
  • 다른 Consumer Group끼리는 -> 같은 이벤트를 각자 받음

Offset

메시지가 순서대로 쌓이므로, 각 메시지에 번호(offset)가 붙는다: 0, 1, 2, 3…

"정산 그룹은 offset 1542까지 읽었다"

Kafka는 Consumer의 현재 위치를 __consumer_offsets 내부 토픽에 저장한다. 메시지가 삭제되지 않으므로 offset을 과거로 되돌리면 재처리가 가능하다.

Partition

초당 10만 건이 하나의 줄에 순서대로 쌓이면 쓰기 병목이 발생한다. 여러 Partition으로 나눈다.

                    +- Partition 0: [0, 1, 2, 3, ...]
                    |
클릭 -> Topic ------+- Partition 1: [0, 1, 2, 3, ...]
                    |
                    +- Partition 2: [0, 1, 2, 3, ...]
hash(ad_id) % 파티션_수  # -> 해당 Partition으로

같은 ad_id의 클릭은 항상 같은 Partition -> 같은 광고 내에서 순서 보장

Consumer와 Partition의 관계

상황결과
Partition 3개, Consumer 2개한 Consumer가 2개 담당
Partition 3개, Consumer 3개딱 맞게 1:1
Partition 3개, Consumer 5개2개는 놀고 있음

-> Partition 수가 병렬 처리의 상한선


전달 보장 (Delivery Guarantees)

분산 시스템에서 메시지를 얼마나 확실하게 처리할 것인지에 대한 약속이다.

┌───────────────┬─────────────┬─────────────┬─────────────────────┐
│    보장 수준   │  메시지 유실 │  메시지 중복 │       사용 예시      │
├───────────────┼─────────────┼─────────────┼─────────────────────┤
│ At-most-once  │     O       │     X       │ 로그, 메트릭        │
│ At-least-once │     X       │     O       │ 알림, 이벤트 처리    │
│ Exactly-once  │     X       │     X       │ 결제, 정산          │
└───────────────┴─────────────┴─────────────┴─────────────────────┘

At-most-once (최대 한 번)

메시지를 보내고 확인(Ack)을 받지 않는다. 유실 가능, 중복 없음. (Fire and forget)

At-least-once (적어도 한 번)

메시지를 보내고 확인(Ack)을 받는다. 못 받으면 재전송. 유실 없음, 중복 가능.

Producer: "메시지 보냈다!"
Consumer: 처리 완료, Ack 전송 -> (네트워크 끊김) -> Producer: Ack 못 받음
Producer: "Ack 안 왔네? 다시 보낸다!"
결과: Consumer가 같은 메시지를 두 번 받음

Exactly-once (정확히 한 번)

가장 어렵다. 유실도 없고 중복도 없다.

  • Kafka: 멱등성 프로듀서(enable.idempotence=true)와 트랜잭션으로 Kafka-to-Kafka 파이프라인(consume-transform-produce)에서 보장. 외부 시스템(DB, API)과의 exactly-once는 별도 멱등성 처리가 필요하다
  • RabbitMQ: 기본 미지원. 컨슈머단에서 중복 처리 방지 로직(Idempotency) 구현 필요

저장 정책

Kafka는 메시지를 소비해도 지우지 않는다. 정책(Policy)에 따라 파일 시스템에 저장한다.

정책설정 예시설명
시간 기반log.retention.hours=1687일 동안 보관 후 삭제
크기 기반log.retention.bytes=1GB파티션 크기 초과 시 오래된 것부터 삭제

이 정책 덕분에 Kafka는 과거 데이터 재생(Replay)이 가능한 스토리지 시스템처럼 동작한다.

RabbitMQ는 소비되면 삭제된다. (단, RabbitMQ Streams 3.9+는 Kafka처럼 append-only 로그 지원)


처리량 vs 지연시간

Kafka (높은 처리량)

  • 배치 처리: 메시지를 모아서 한 번에 전송/저장
  • 순차 I/O: 디스크에 append-only로 순차적으로 쓴다
  • Zero-copy: 커널 레벨에서 데이터를 복사 없이 직접 네트워크로 전송
  • 단점: 배치 대기 시간만큼 개별 메시지 지연 증가

RabbitMQ (낮은 지연시간)

  • 즉시 전달: 배치 없이 바로 Push
  • 메모리 중심: 빠른 접근
  • 단점: 대량 처리 시 메모리 한계, 개별 메시지 오버헤드
Kafka:   [msg1, msg2, msg3, msg4, msg5] --batch--> Consumer
         ~~~~~~~~~~~~ 배치 대기 ~~~~~~~~~~~~~
         처리량: 높음, 지연시간: 배치 간격만큼

RabbitMQ: msg1 --> Consumer (즉시)
          msg2 --> Consumer (즉시)
          처리량: 낮음, 지연시간: 최소

DLQ(Dead Letter Queue) 패턴

처리에 실패한 메시지를 별도 큐/토픽으로 격리하는 패턴이다.

RabbitMQ: 브로커 레벨 지원

Worker: PDF 생성 실패 -> ACK 안 보냄 -> 메시지 자동 재전달

큐 선언 시 x-dead-letter-exchangex-dead-letter-routing-key 설정하면, 거부(reject/nack)되거나 TTL 만료된 메시지가 자동으로 DLX로 라우팅된다. 별도 코드 없이 브로커 설정만으로 동작.

Kafka: 직접 구현

Kafka는 “dumb pipes, smart endpoints” 철학. 브로커는 append-only 로그로 단순하게 유지하고, 에러 처리는 클라이언트가 담당.

[ad-events] --> Consumer --> 외부 API 호출
                   |
                   +-- 성공 -> commit
                   |
                   +-- 실패 -> [ad-events-dlq] 로 전송
                                    |
                                    v
                            DLQ Consumer (재시도)

DLQ Best Practices

원본 메시지 key/value는 유지하고, 실패 원인은 Kafka message header에 기록:

headers = [
    ("error.cause", b"TimeoutException"),
    ("error.message", b"External API timeout after 5000ms"),
    ("error.origin.topic", b"ad-events"),
    ("error.origin.partition", b"3"),
    ("error.origin.offset", b"1542"),
]
producer.send("ad-events-dlq", key=original_key, value=original_value, headers=headers)

순서 보장 DLQ 패턴

광고 이벤트처럼 순서가 중요한 경우:

offset 10: "광고 123 시작" -> 실패 -> DLQ로
offset 11: "광고 123 종료" -> ???
offset 12: "광고 123 시작" -> ???

순서가 중요하면 offset 11, 12도 처리하면 안 된다.

blocked_ad_ids: set[str] = set()

def process_event(event):
    if event.ad_id in blocked_ad_ids:
        send_to_dlq(event, reason="blocked_by_previous_failure")
        return

    success = call_external_api(event)

    if not success:
        send_to_dlq(event, reason="api_failure")
        blocked_ad_ids.add(event.ad_id)  # 이 ad_id blocked 처리

Zookeeper 제거 (KRaft)

Zookeeper의 원래 역할

  • 브로커 등록/발견
  • Controller 선출
  • 토픽/파티션 메타데이터 저장
  • ACL(권한) 정보

왜 제거했을까?

문제점설명
운영 복잡성Kafka와 Zookeeper 두 개의 분산 시스템을 운영
확장성 한계Zookeeper가 병목, 파티션 수 제한 (~수만 개)
장애 복구 지연Controller 재선출에 시간 소요
메타데이터 불일치Kafka-Zookeeper 간 동기화 문제

KRaft 모드 (Kafka 3.0+)

Kafka 브로커들이 자체적으로 Raft 프로토콜로 합의를 수행한다.

Before (Zookeeper 방식):
[Broker 1] ---> [Zookeeper Cluster] <--- [Broker 2]

After (KRaft 방식):
[Controller 1] <--Raft--> [Controller 2] <--Raft--> [Controller 3]
      |                         |                         |
[Broker 1]              [Broker 2]                [Broker 3]
  • 빠른 복구: Controller 장애 시 밀리초 단위로 새 Controller 선출
  • 확장성 향상: 수백만 개의 파티션까지 지원
  • Kafka 4.0에서 Zookeeper 지원이 완전히 제거되었다

언제 뭘 쓸까?

상황선택이유
광고 클릭/노출 스트림Kafka대용량, 재처리, 여러 시스템이 같은 데이터 필요
썸네일 생성 작업RabbitMQ소규모, 한 번 처리하면 끝, 작업 분배
정산 리포트 PDF 생성RabbitMQ실패 시 재시도 간단 (ACK 기반)
이벤트 소싱/로그 수집Kafka순서 보장 + 영구 저장 + 재처리
마이크로서비스 간 RPC성 통신RabbitMQ요청-응답 패턴, 유연한 라우팅

성능 벤치마크

아래 수치는 일반적으로 알려진 대략적인 참고치이며, 실제 성능은 하드웨어, 메시지 크기, 배치 설정, 영속성 모드에 따라 크게 달라진다 (3노드 클러스터, 1KB 메시지 기준):

지표KafkaRabbitMQ
처리량 (Producer)100만 msg/sec20,000 msg/sec
처리량 (Consumer)100만 msg/sec30,000 msg/sec
지연시간 (p99)5~15ms (배치)1~2ms
지연시간 (p99, 배치 없음)2~5ms1~2ms

Kafka는 linger.ms=5, batch.size=16KB 기본값 기준. 배치를 줄이면 지연시간이 줄지만 처리량도 감소한다.


정리 비교표

항목KafkaRabbitMQ
메시지 보관디스크에 유지 (재처리 가능)소비되면 삭제
여러 시스템이 같은 메시지 필요Consumer Group으로 자연스럽게Exchange 설정 필요
순서 보장Partition 내 보장기본 보장 (단일 큐)
병렬 처리 확장Partition 수가 상한선Worker 수 자유롭게
작업 단위 재시도DLQ 패턴 직접 구현ACK 기반으로 간단
처리량~100만 msg/sec~2만 msg/sec
지연시간5~15ms1~2ms
라우팅Topic 기반Exchange 기반 (유연함)

Redis를 메시지 브로커로 쓸 수 있을까?

Kafka/RabbitMQ 도입이 과할 때, Redis로 가벼운 메시징을 구현할 수 있다.

모델저장신뢰성적합한 경우
Pub/Sub안 함없음실시간 알림, 유실 허용되는 신호
List + BRPOPLPUSH직접 관리수동소규모 비동기 작업 큐
Streamsappend-onlyat-least-onceConsumer Group, ACK, 재처리가 필요한 경우
신뢰성 필요 없음 ──────────────────────────────► 신뢰성 필요
     │                                              │
  Pub/Sub ────────► List 큐 ────────► Streams ────► Kafka

Redis Streams는 XREADGROUP/XACK/XAUTOCLAIM으로 “작은 Kafka” 역할을 할 수 있지만, 대규모 파이프라인이나 정확한 파티셔닝이 필요하면 전문 브로커를 써야 한다.


참고자료