"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
| Kafka | RabbitMQ | SQS | |
|---|---|---|---|
| 모델 | Log (partitioned) | Queue + flexible routing | Queue (managed) |
| throughput | 매우 높음 (100K+ msg/sec) | 중간 (10K msg/sec) | auto-scale |
| retention | 수일-수년 | ack 까지 | 최대 14 일 |
| replay | ✅ (cursor 이동) | ✗ | ✗ |
| 운영 | 복잡 | 중간 | 없음 (managed) |
| routing | partition key | flexible exchange | 단순 queue |
| 비용 | self-host or MSK | self-host or CloudAMQP | pay 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. 신중하게.
참고 자료
- Designing Data-Intensive Applications (Martin Kleppmann) — dataintensive.net
- Kafka — official docs — kafka.apache.org
- RabbitMQ tutorials — rabbitmq.com
- Outbox Pattern — microservices.io
요약
- 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).