메시지 큐·비동기 — 백엔드 면접 (실제 기출 기반·심화)
"메시지 큐를 왜 쓰나", "Kafka와 RabbitMQ 차이", "exactly-once 가능한가", "순서 보장", "중복 처리", "dual-write(트랜잭셔널 아웃박스)" — MSA·비동기 설계 단골이다. 기업 기술블로그(우아한형제들·올리브영)가 실질 1차 소스다.
함정형 단골: ① Kafka가 Exactly-Once 보장? → end-to-end는 사실상 멱등 컨슈머 ② Kafka는 순서를 항상 보장? → 파티션 단위만 ③ MQ 쓰면 유실 0? → acks·복제·outbox 없이는 유실.
1. 왜 메시지 큐인가 (★ 단골)
실제 질문: "메시지 큐를 왜 쓰나요?" · "왜 메일을 동기로 안 보내고 큐로?"
동기 호출은 상대가 느리거나 죽으면 나도 막힌다(강결합). MQ는 사이에 버퍼를 둬:
- 비동기(보내고 바로 응답)·디커플링(서로 몰라도 됨)·부하 평탄화(폭주를 큐가 흡수)·확장(소비자 추가).
꼬리질문: "왜 메일을 큐로?" → 외부 SMTP 지연·장애가 주문 트랜잭션을 막음 → 이벤트 발행 후 비동기. / "단점은?" → 복잡도·결과적 일관성·중복·순서·운영 부담.
2. Kafka vs RabbitMQ (★★★ 단골)
실제 질문: "Kafka와 RabbitMQ의 차이는?" · "컨슈머가 죽으면 메시지는 어떻게 되나요?"
| Kafka | RabbitMQ | |
|---|---|---|
| 모델 | 로그 기반 pub/sub(덤 브로커·스마트 컨슈머·pull) | 전통 브로커(스마트 브로커·푸시) |
| 저장 | 로그 보존·재생(replay) | 큐(소비되면 사라짐) |
| 라우팅 | 토픽·파티션 | Exchange(direct/fanout/topic/headers) |
| 오프셋 | 컨슈머가 추적(여러 그룹 독립) | 브로커가 ack·전달 상태 관리 |
| 강점 | 이벤트 스트리밍·고처리량·재처리 | 작업 분배·복잡한 라우팅·낮은 지연 |
깊이: Kafka는 데이터를 쌓아 두고 여러 컨슈머 그룹이 각자 오프셋으로 독립 소비·재생 → 스트리밍·로그. RabbitMQ는 스마트 브로커가 Exchange로 유연하게 라우팅·즉시 소비 → 작업 큐.
꼬리질문: "컨슈머가 죽으면?" → Kafka는 오프셋만큼 보존(다시 읽음), RabbitMQ는 ack 안 하면 재큐. / "Kafka가 빠른 이유?" → 순차 디스크 쓰기·페이지 캐시·zero-copy·배치.
3. 전달 시맨틱·Exactly-Once (★★★ 단골·함정)
실제 질문: "Kafka의 전달 시맨틱은?" · "Exactly-Once를 정말 보장할 수 있나요?" · "중복 메시지를 어떻게 처리하나요?" (medium 김기현)
| 의미 | 위험 | |
|---|---|---|
| at-most-once | 처리 전 오프셋 커밋 | 유실 |
| at-least-once | 처리 후 커밋 | 중복 |
| exactly-once | 정확히 1번 | 복잡 |
- Kafka 기본 = at-least-once(처리 후 오프셋 커밋 전에 죽으면 재처리 → 중복 정상) → 소비자가 멱등해야.
- EOS(Exactly-Once Semantics): ① 멱등 프로듀서(
enable.idempotence=true— 브로커가 PID+시퀀스로 재전송 dedup) + ② 트랜잭션(다중 파티션 원자적·read-process-write). - ⚠️ 진짜 EOS 되나?(★): Kafka 내부(read-process-write)는 보장. 하지만 DB 커밋 성공 후 오프셋 커밋 실패 같은 외부(DB·API)까지면 깨질 수 있어 → 결국 **소비자 멱등(idempotent consumer)**이 현실 해법.
깊이 — 중복 제거 4전략: ① Upsert ② Unique Key 제약(DataIntegrityViolationException) ③ 인메모리 캐시(초기화 시 재발) ④ Redis Bloom Filter(false positive).
꼬리질문: "중복을 어떻게?" → 메시지 ID dedup(DB unique·Redis)·upsert. / "유실을 막으려면?" → 프로듀서 acks=all·복제·DB outbox·소비자 ack는 처리 후.
함정: ❌ "Kafka가 Exactly-Once를 완벽 보장" → 외부까지는 멱등 컨슈머가 정석.
4. 순서 보장 (★★ 단골·함정)
실제 질문: "Kafka는 메시지 순서를 보장하나요?" · "순서가 중요한데 처리량도 필요하면?"
- Kafka는 파티션 단위로만 순서 보장(오프셋 순). 전역 순서는 X(파티션 늘리면 라운드로빈으로 깨짐).
- 같은 키는 같은 파티션 → 키 기준 순서(예:
userId로 파티셔닝 → 한 사용자 이벤트 순서대로).
꼬리질문: "전역 순서 필요?" → 파티션 1개(병렬 포기) 또는 앱에서 정렬. / "처리량+순서?" → 메시지 키로 파티셔닝(키 단위 순서 + 파티션 병렬).
함정: ❌ "Kafka는 순서를 항상 보장" → 파티션 단위만.
5. Kafka 내부 — 파티션·ISR·acks·리밸런싱 (★ 심화)
- 토픽→파티션으로 나눠 병렬·확장. 각 메시지는 파티션 안 오프셋(순번).
- 컨슈머 그룹: 한 그룹 안에서 파티션을 나눠 소비(병렬·파티션 수가 상한). ⚠️ 리밸런싱(멤버 변동 시 파티션 재배분 — 그동안 처리 멈춤·중복 가능 → 멱등).
- 복제·ISR(In-Sync Replicas): 파티션을 여러 브로커에 복제(leader/follower)·ISR=리더와 동기화된 집합.
acks:0(안 기다림·유실)·1(리더만)·all/-1(ISR 전부 — 유실 방지).
꼬리질문: "컨슈머 lag이 쌓이면?" → 소비자 스케일아웃(파티션 수까지)·처리 최적화·백프레셔. / "파티션 수를 늘리면?" → 병렬↑·순서 단위↓·리밸런싱 비용.
6. dual-write·Transactional Outbox·DLQ (★★ 심화)
실제 질문: "Transactional Outbox 패턴이 무엇이고 왜 쓰나요?" · "DLQ는 언제 쓰나요?" (우아한형제들·올리브영)
- dual-write 문제: "DB 저장 + 메시지 발행"을 각각 하면 하나만 성공 시 불일치(DB는 저장됐는데 발행 실패·또는 반대).
- Transactional Outbox: 비즈니스 데이터와 메시지를 같은 트랜잭션으로 outbox 테이블에 쓰고, **릴레이/CDC(Debezium)**가 outbox를 읽어 발행 → 원자성 확보.
- DLQ(Dead Letter Queue): N번 재시도 후 실패 메시지 격리(독약 메시지가 전체를 막지 않게). 재시도: 지수 백오프·재시도 토픽. 백프레셔: 소비자 못 따라오면 prefetch 제한·스케일아웃.
꼬리질문: "outbox에서도 중복 발행은?" → CDC가 같은 행을 두 번 읽을 수 있어 소비자 멱등 필요. / "DLQ 메시지는?" → 원인 분석 후 수동/자동 재처리.
함정: ❌ "DB 저장과 발행을 그냥 둘 다 하면 됨" → dual-write 불일치 → outbox.
7. 이벤트 스키마 진화·계약 (★ 채용공고 보강·2026-07)
실제 질문: "이벤트 스키마가 자주 바뀌고 필드가 누락/의미 변경됩니다. 생산 환경에서 스키마 진화를 안전하게 하려면 어떤 계약·검증·배포 체계를 두겠습니까?" (prime-career 현대차·CJ대한통운 공고)
프로듀서와 컨슈머는 독립 배포되므로, 한쪽 변경이 상대를 깨지 않게 호환성 계약이 필요하다.
- 스키마 레지스트리(Confluent Schema Registry 등) + Avro/Protobuf/JSON Schema로 스키마를 중앙 관리하고, 이벤트에
schema_version을 포함. - 호환성 종류(★): backward(새 스키마로 옛 데이터 읽기 가능 → 소비자 먼저 배포), forward(옛 스키마로 새 데이터 읽기 가능 → 생산자 먼저 배포), full(양쪽). 어느 쪽을 먼저 배포하느냐로 선택.
- 안전 규칙: 새 필드는 optional + 기본값, 필드 삭제·타입 변경 금지(deprecate만), 의미가 바뀌면 같은 필드 재활용 말고 새 필드 추가.
- Tolerant Reader(Postel의 법칙): 파서는 관대하게 — 모르는 필드는 무시, 누락은 기본값 → 사소한 변경에 파이프라인이 죽지 않게.
- 배포 체계: ① CI에서 스키마 등록 시 호환성 검사 통과해야 머지, ② 소비자 주도 계약 테스트(CDC, Pact) 로 생산자 변경이 소비자를 깨는지 사전 검출, ③ 필수 필드 검증 실패분은 격리 큐(quarantine) 로.
꼬리질문: "소비자를 먼저 배포? 생산자를 먼저?" → backward 호환이면 소비자 먼저(새 코드가 옛·새 데이터 다 읽음), forward면 생산자 먼저. / "outbox 이벤트에도 스키마 진화가?" → 동일 적용, CDC(Debezium)가 읽는 테이블 변경도 계약.
함정: ❌ "필드 타입만 바꾸면 되지" → 소비자 역직렬화 깨짐. ❌ "스키마는 코드에만 두면 됨" → 레지스트리·계약테스트 없이는 배포 순서 사고.
(출처: Confluent — Schema Evolution·Compatibility·Pact — consumer-driven contracts·Kleppmann DDIA 4장(Encoding & Evolution) 교차검증. 실무 프레이밍: docs/04·05.)
흔한 오답·함정 정리
- Kafka가 Exactly-Once 완벽 보장 → 외부까지는 멱등 컨슈머.
- Kafka는 순서를 항상 보장 → 파티션 단위만.
- MQ 쓰면 유실 0 → acks=all·복제·outbox 없이는 유실.
- DB 저장+발행을 그냥 둘 다 → dual-write 불일치(outbox).
- 컨슈머 그룹에 컨슈머를 무한정 늘리면 빨라진다 → 파티션 수가 상한.
한국 면접 단골 Q&A (답변 골격)
| 질문 | 핵심 답 |
|---|---|
| MQ 왜 쓰나 | 비동기·디커플링·부하 평탄화·확장 |
| Kafka vs RabbitMQ | 로그 스트리밍 vs 작업 큐(Exchange) |
| 전달 시맨틱 | Kafka 기본 at-least-once·중복→멱등 |
| Exactly-Once | 멱등 프로듀서+트랜잭션·외부는 멱등 컨슈머 |
| 순서 보장 | 파티션 단위만·같은 키→같은 파티션 |
| acks | 0/1/all(ISR 전부·유실 방지) |
| 리밸런싱 | 멤버 변동 시 재배분·멈춤·중복 |
| 중복 처리 | ID dedup·upsert·unique·Bloom |
| outbox | dual-write 불일치→DB 트랜잭션+outbox+CDC |
| DLQ | 반복 실패 격리·후처리 |
꼬리질문 대비 (상 난이도)
- "메시지 유실 막으려면?" →
acks=all·복제·DB outbox·소비자 ack는 처리 후. - "전역 순서 필요?" → 파티션 1개(처리량 희생) 또는 키 파티셔닝.
- "컨슈머 lag?" → 스케일아웃(파티션 수까지)·처리 최적화·백프레셔.
- "Exactly-Once를 진짜?" → Kafka 내부만·외부는 멱등 컨슈머.
- "Kafka가 빠른 이유?" → 순차 디스크·페이지 캐시·zero-copy·배치.
한 줄 요약 — MQ는 비동기·디커플링·부하 평탄화. Kafka(로그·스트리밍·재생)↔RabbitMQ(Exchange·작업 큐). Kafka 기본 at-least-once(중복→소비자 멱등), EOS=멱등 프로듀서(PID+시퀀스)+트랜잭션이나 외부까지는 멱등 컨슈머,
acks=all+ISR로 유실 방지. 순서는 파티션 단위(같은 키→같은 파티션). dual-write는 Outbox+CDC, 반복 실패는 DLQ.
(출처 — 한국 면접 기출·교차검증 2026-06: medium 김기현 — Kafka 중복 메시지·Exactly-Once·우아한형제들 — 카프카+Transactional Outbox·올리브영 — Kafka 중복/유실·velog — Kafka vs RabbitMQ · Confluent — Delivery Semantics·Idempotent Producer·RabbitMQ — Exchanges·microservices.io(Transactional Outbox) 교차검증.)