본문으로 건너뛰기
yutils

message queue 는 어떻게 동작할까? (Kafka · RabbitMQ · SQS)

pub-sub · partition · queue vs log · at-least-once vs exactly-once · consumer group · 그리고 Kafka · RabbitMQ · SQS 의 차이와 선택 기준.

약 8분 읽기

"user signup 후 email 발송" 을 동기 HTTP 로 처리하면 — email provider 가 느릴 때 user 가 기다림. message queue 로 분리하면 — signup 즉시 응답 + email 은 background 에서. 단순 같지만 Kafka / RabbitMQ / SQS 의 동작은 다르고, "메시지 한 번만 처리" 를 보장하는 게 의외로 어렵다. 이 가이드는 pub-sub, partition, delivery guarantee, 그리고 어떤 system 을 언제 쓸지 정리한다.

왜 message queue 인가

동기 HTTP:
Client → POST /signup → API → email service → 응답
                              (느릴 수 있음)        ↑
                                  사용자가 다 기다림

Message queue:
Client → POST /signup → API → 즉시 응답 (200)
                            ↓
                     Queue: "send email to alice@..."
                            ↓
                     Worker (background)
                            ↓
                     email service

효과:

  • 응답 즉시 — 사용자 latency ↓
  • decoupling — API 와 email service 가 분리. email service down 이어도 signup 정상
  • load smoothing — peak 시 queue 가 버퍼
  • retry — worker 가 실패 시 자동 재시도

두 가지 모델 — Queue vs Log

Queue (RabbitMQ, SQS, Redis Streams)

Producer → [msg1, msg2, msg3, msg4, msg5] → Consumer
                       ↓
                  consumer 가 msg1 fetch
                       ↓
                  ack 후 queue 에서 제거
                       ↓
                  msg1 영원히 사라짐

→ 한 메시지를 한 consumer 만 처리
→ Work distribution 모델 — 부하 분산

Log (Kafka, Kinesis)

Producer → [msg1, msg2, msg3, msg4, msg5] → 영구 저장 (보통 N 일)
              ↑                          ↑
              ConsumerA의 cursor          ConsumerB의 cursor

→ 메시지는 안 사라짐. consumer 마다 본인 cursor (offset) 유지
→ 새 consumer 가 시간 거슬러 replay 가능
→ Pub-sub 모델 — 같은 메시지를 여러 consumer 가

근본 차이 — queue 는 "메시지 = 단발성 task", log 는 "메시지 = 영구 event stream".

Kafka — partitioned log

Topic: orders
  ├── Partition 0: [msg, msg, msg, ...]
  ├── Partition 1: [msg, msg, msg, ...]
  └── Partition 2: [msg, msg, msg, ...]

Producer 가 msg push:
  - partition key 가 있으면 hash(key) % N → 특정 partition
  - 없으면 round-robin 또는 random

Consumer Group: order-processor
  ├── Consumer A → Partition 0
  ├── Consumer B → Partition 1
  └── Consumer C → Partition 2

→ partition 마다 1 consumer (한 group 안)
→ partition 수 = 최대 parallel consumer 수
→ partition key 같으면 같은 consumer → 순서 보장

장점:

  • 매우 high throughput (수백만 msg/sec)
  • 같은 key 의 메시지는 같은 partition → 순서 보장
  • retention 길게 (수일~수년) — replay 가능

단점:

  • 운영 복잡 (Zookeeper / KRaft 클러스터)
  • partition 수 변경 어려움 (re-balance 시 key 매핑 변경)

RabbitMQ — flexible routing

Producer → Exchange → routing key 기반 분기
                       ↓
                       ├── Queue A (orders.payment)
                       ├── Queue B (orders.shipping)
                       └── Queue C (orders.audit)
                         ↓
                       Consumer (각 queue 마다)

Exchange types:
- direct: routing key exact match
- topic: pattern match (orders.*, orders.#)
- fanout: 모든 bound queue 로
- headers: header 기반 match

장점:

  • 유연한 routing — exchange type 으로 다양
  • per-message ack — 정확한 retry 제어
  • priority queue 지원

단점:

  • throughput Kafka 보다 낮음 (수만 msg/sec)
  • retention 짧음 (consumer ack 후 즉시 제거)

SQS (AWS) — managed queue

  • fully managed — 운영 부담 0
  • at-least-once delivery (Standard) 또는 exactly-once (FIFO)
  • retention 최대 14 일
  • auto-scaling — throughput 자동
  • 비싸지 않음 — pay per request

간단하지만 advanced feature 적음. AWS 안 default.

Delivery Guarantees — 3 종류

  • at-most-once — 메시지 손실 가능, 중복 X. fire-and-forget. 로그·analytics 등에 적합
  • at-least-once — 손실 X, 중복 가능. consumer 가 idempotent 해야 함. 대부분 system 의 default
  • exactly-once — 손실 X, 중복 X. 분산 시스템에서 매우 어려움

왜 exactly-once 가 어려운가

Consumer:
1. queue 에서 msg fetch
2. DB 에 처리 결과 저장
3. queue 에 ack (= 처리 완료)

network failure 시점:
- step 2 후 step 3 전: DB 에 저장됐지만 ack X
  → queue 가 같은 msg 를 다른 consumer 에 다시 → 중복 처리

- step 1 후 step 2 전: queue 는 in-flight 표시
  → consumer crash 시 timeout 후 다른 consumer 에 → 정상

해결:
1. Idempotent consumer — 같은 msg 두 번 처리해도 안전
   (예: "user X 의 balance 를 100 로 설정" vs "+100")
2. message_id 를 DB 에 저장하고 dedup
3. Two-phase commit (Kafka transaction) — 복잡하고 느림

실용 — at-least-once + idempotent consumer 가 표준 패턴. exactly-once 광고는 보통 같은 의미.

Consumer Group — Kafka 의 핵심

Topic 5 partitions, group "order-processor"

Scenario A — 2 consumer in group:
  Consumer A → Partition 0, 1
  Consumer B → Partition 2, 3, 4
  → 자동 re-balance

Scenario B — 5 consumer:
  각 consumer → 1 partition
  → 최대 parallel

Scenario C — 6 consumer:
  5 active, 1 idle
  → partition 수 = consumer 수 한도

다른 group 도 추가 가능:
  group "analytics" → 같은 topic 의 메시지를 별개 cursor 로 소비
  → 새 group 추가에 producer 영향 0

흔한 패턴

Outbox Pattern

문제 — DB transaction + queue publish 의 atomicity
  1. DB INSERT (success)
  2. queue publish (network failure) → 영원히 잃음

해결:
  1. DB transaction 안에 outbox 테이블에 msg 저장
  2. background worker 가 outbox 에서 읽어 queue 로 publish
  3. publish 성공 후 outbox row 삭제

→ DB 와 queue 의 atomicity 보장

Dead Letter Queue (DLQ)

Consumer 가 msg 처리 실패 → retry → 또 실패 → ...
무한 retry 위험. DLQ 로 보내고 alert.

queue: main
  msg 실패 → retry 3 회 후 dead letter queue 로 이동
  → main queue 는 다음 msg 로 진행
  → DLQ 의 msg 를 사람이 검토 후 재시도 또는 폐기

Backpressure

Producer 가 consumer 보다 빠르면 queue 폭주. 해결:

  • queue 크기 제한 → 초과 시 producer block 또는 reject
  • auto-scaling consumer (Kubernetes HPA)
  • priority queue — important msg 먼저

비교 — 어떤 system

KafkaRabbitMQSQS
모델Log (partitioned)Queue + flexible routingQueue (managed)
throughput매우 높음 (100K+ msg/sec)중간 (10K msg/sec)auto-scale
retention수일-수년ack 까지최대 14 일
replay✅ (cursor 이동)
운영복잡중간없음 (managed)
routingpartition keyflexible exchange단순 queue
비용self-host or MSKself-host or CloudAMQPpay per request

선택:

  • event stream / 분석 / replay — Kafka
  • complex routing / 작은 throughput — RabbitMQ
  • AWS 환경 + 단순 background job — SQS
  • 매우 작은 system — Redis Streams 또는 그냥 DB queue table

흔한 함정

1. 순서 가정

partition 다르면 순서 X. 같은 user 의 event 가 다른 partition 에 분산되면 순서 깨짐. user_id 를 partition key 로.

2. consumer 의 idempotency 부재

"+1 to counter" 처리 중 ack 실패 → 두 번 처리 → counter +2. idempotent 설계: "set counter to N" 또는 message_id dedup.

3. DLQ 모니터링 누락

DLQ 에 msg 쌓이는데 alert 없음 → 사용자 데이터 손실. CloudWatch / Prometheus 로 DLQ depth 알람.

4. queue 의 transaction 가정

"DB write + queue publish" 의 atomicity 보장 X. outbox pattern 또는 change data capture (Debezium).

5. Kafka partition 수 증가

partition 수 변경 시 key 의 hash mapping 바뀜 → 같은 user 의 과거 msg vs 미래 msg 다른 partition. 신중하게.

참고 자료

요약

  • MQ 가 API 와 background job 을 decouple. 사용자 latency ↓, retry, load smoothing.
  • Queue (RabbitMQ/SQS) vs Log (Kafka) — 한 번 처리 vs 영구 event stream.
  • Kafka partition 이 parallelism 단위. 같은 key 같은 partition 순서 보장.
  • Delivery — at-most-once / at-least-once / exactly-once. 분산에서 exactly-once 거의 불가능. at-least-once + idempotent 가 표준.
  • Outbox pattern — DB + queue 의 atomicity 보장.
  • DLQ — 실패한 msg 격리. 모니터링 필수.
  • 선택 — Kafka (stream/replay), RabbitMQ (routing), SQS (AWS managed).
가이드 목록으로