메시지 큐·비동기 — 백엔드 면접 (실제 기출 기반·심화)
"메시지 큐를 왜 쓰나", "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 | |
|---|---|---|
| 모델 | 로그 기반 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.)
8. KRaft·Kafka 4.0 — 버전을 아는지 가르는 질문 (★★ 신규)
나오는 질문: "Kafka에서 ZooKeeper는 무슨 역할인가요?" · "지금도 쓰나요?" · "KRaft가 뭔가요?"
이건 어느 시점 자료로 공부했는지가 그대로 드러나는 질문이다. "ZooKeeper가 브로커 메타데이터를 관리합니다"까지만 답하면 2024년에 멈춘 지식이다.
ZooKeeper가 하던 일: 어느 브로커가 어느 파티션의 리더인지, 토픽·설정·ACL이 무엇인지 같은 클러스터 메타데이터 관리와 컨트롤러 선출. 즉 Kafka를 운영한다는 건 분산 시스템 두 개를 운영하는 일이었다.
KRaft(Kafka Raft, KIP-500)는 그 일을 Kafka 안으로 가져왔다. 발상은 단순하다 — 메타데이터도 로그로 관리하면 된다. Kafka가 제일 잘하는 게 로그니까.
깊이 — 왜 장애 복구가 빨라지나(★★). ZooKeeper 시절엔 컨트롤러가 죽으면 새 컨트롤러가 ZooKeeper에서 전체 상태를 다시 읽어 들여야 했고, 파티션이 많을수록 그 로딩이 길어졌다. KRaft에서는 팔로워 컨트롤러가 같은 메타데이터 로그를 이미 따라오며 메모리에 상태를 들고 있어서 거의 즉시 이어받는다. 로그가 무한정 커지지 않도록 주기적으로 **스냅샷**을 남기고, 브로커는 그 로그를 컨슈머처럼 당겨(pull) 읽는다. 결과적으로 파티션 수 한계도 크게 늘었다.
Kafka 4.0(2025-03)은 ZooKeeper 모드를 아예 제거했다. 실무에서 중요한 건 두 가지다.
| 항목 | 내용 |
|---|---|
| 마이그레이션 경로 | ZooKeeper 모드에서 4.0으로 직행 불가. 3.x(권장 3.9)에서 먼저 KRaft로 전환한 뒤 4.0으로 |
| 자바 요구 버전 | 클라이언트·Streams는 Java 11, 브로커·Connect·도구는 Java 17 이상 |
같은 4.0에서 KIP-848 차세대 리밸런스 프로토콜도 정식(GA)이 됐다. 5번에서 본 리밸런싱의 후속이다 — 예전에는 그룹 멤버 중 리더 컨슈머가 파티션 배정을 계산해서 멤버가 들고 날 때마다 그룹 전체가 동기화 지점을 거쳤는데, 새 프로토콜은 브로커의 그룹 코디네이터가 계산해 점진적으로 내려보낸다. 컨슈머가 많을수록 차이가 크고, group.protocol=consumer로 참여시킨다. 그리고 KIP-932 share groups(얼리 액세스)가 들어와, Kafka 토픽 위에서 큐처럼 나눠 소비하는 패턴도 가능해졌다.
꼬리질문: "KRaft를 쓰면 뭐가 좋아지나요?" → 운영 대상이 하나로 줄고, 컨트롤러 페일오버가 거의 즉시이며, 파티션 확장 한계가 올라간다. / "그럼 ZooKeeper는 이제 어디에 쓰나요?" → Kafka에서는 빠졌지만 분산 락·서비스 디스커버리 용도로는 여전히 쓰인다(etcd와 같은 자리).
함정: ❌ "Kafka는 ZooKeeper가 필요하다" → 4.0부터 아니다. ❌ "ZooKeeper 클러스터를 4.0으로 바로 올리면 된다" → 3.9를 거쳐야 한다.
9. 메시지는 언제까지 남나 — 리텐션·로그 컴팩션 (★★ 심화)
나오는 질문: "Kafka는 소비해도 메시지가 남는다고 했는데, 언제까지 남나요?" · "
__consumer_offsets는 계속 커지지 않나요?" · "특정 사용자 데이터만 지우려면요?"
"로그라서 재처리가 된다"는 답에는 언제까지라는 조건이 빠져 있다. 그걸 정하는 게 cleanup.policy 다.
delete(기본) — 오래된 세그먼트 파일을 통째로 버린다. retention.ms(기본 7일)나 retention.bytes 기준이다. 재처리가 가능한 창이 곧 이 값이라, "3개월 전 데이터를 다시 흘려 주세요"는 리텐션이 7일이면 애초에 불가능하다.
compact — 지우는 기준이 나이가 아니라 키다. 같은 키의 최신 값만 남긴다.
컴팩션 전: (u1,"서울") (u2,"부산") (u1,"대전") (u3,"광주") (u1,"제주")
↓ 같은 키의 옛 값 제거 (오프셋 번호는 그대로)
컴팩션 후: (u2,"부산") (u3,"광주") (u1,"제주")
그래서 컴팩션 토픽은 "이벤트 기록"이 아니라 "현재 상태 스냅샷" 이 된다. 처음부터 다 읽으면 모든 키의 최신 상태가 복원되므로 캐시 워밍·설정 배포·CDC 스냅샷에 쓴다. 질문에 나온 __consumer_offsets가 바로 컴팩션 토픽이다 — 필요한 건 "이 그룹의 현재 오프셋"이지 그 이력이 아니니까.
깊이 — 두 가지 전제(★). ① 키가 없는 메시지는 컴팩션 대상이 될 수 없다. 무엇의 최신인지 판단할 근거가 없기 때문이다. ② 삭제를 표현하려면 툼스톤(tombstone) — 키는 있고 값이 null 인 메시지 — 를 보낸다. 컨슈머들이 "이 키는 지워졌다"를 볼 시간을 준 뒤 delete.retention.ms가 지나면 툼스톤 자체도 정리된다. GDPR 같은 개인정보 삭제 요구가 이 방식으로 처리된다.
cleanup.policy=compact,delete로 둘을 함께 걸면 먼저 컴팩션하고 그다음 리텐션으로 오래된 것을 버린다.
꼬리질문: "컴팩션 토픽에서 특정 키를 완전히 지우려면?" → 툼스톤 발행 후 컴팩션·delete.retention.ms 경과를 기다린다. / "리텐션을 늘리면 되지 않나요?" → 디스크 비용이 선형으로 늘고, 정말 장기 보관이 필요하면 별도 저장소(레이크)로 싱크하는 게 정석이다.
함정: ❌ "Kafka는 메시지를 영구 보관한다" → 기본 7일이다. ❌ "컴팩션을 켜면 중복이 사라진다" → 같은 키의 옛 값만 정리하는 것이지 중복 제거가 아니다.
10. 컨슈머 랙 — 운영 1순위 지표 (★★ 실무)
나오는 질문: "메시지 처리가 밀리는 걸 어떻게 감지하나요?" · "컨슈머 랙이 뭔가요?" · "랙이 쌓이면 어떻게 대응하죠?"
정의는 뺄셈 하나다.
랙 = LEO(Log End Offset — 파티션에 쌓인 마지막 오프셋)
− 그룹이 커밋한 오프셋
예: LEO 10,000 / 커밋 9,500 → 랙 500 (읽어야 할 게 500건 남았다)
kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
--describe --group order-consumer
# TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG
# orders 0 9500 10000 500
깊이 — 랙은 "느리다"가 아니라 "관계"다(★★). 랙이 말해 주는 건 생산 속도를 소비가 못 따라간다는 사실이고, 어디에 쌓이는지가 원인을 가른다.
| 랙 패턴 | 원인 | 대응 |
|---|---|---|
| 특정 파티션에만 쏠림 | 키 분포 치우침(핫 파티션) | 키 설계 변경·파티션 재분배 |
| 모든 파티션에서 고르게 증가 | 컨슈머 부족 또는 처리 로직이 느림 | 컨슈머 스케일아웃(파티션 수가 상한)·처리 최적화 |
| 리밸런스·배포 직후 급증 후 회복 | 정상 | 대응 불필요 |
| 랙이 갑자기 0 | 오프셋이 리셋됐거나 리텐션으로 데이터가 날아감 | 오히려 경보 대상 |
마지막 두 줄 때문에 순간값으로 알림을 걸면 오탐이 쏟아진다. 배포마다 랙이 튀었다가 따라잡히기 때문이다. LinkedIn의 Burrow 같은 도구가 임계값 대신 시간에 따른 추세로 판단하는 이유가 이것이다 — 줄고 있으면 정상, 계속 늘면 경보.
꼬리질문: "컨슈머를 늘렸는데 랙이 안 줄어요" → 파티션 수가 상한이다. 남는 컨슈머는 논다. 파티션을 늘려야 한다. / "랙을 건수 대신 시간으로 보면?" → 더 유용하다. "500건 밀림"보다 "3분 뒤처짐"이 SLA와 직접 연결된다.
함정: ❌ "랙 = 지연 시간" → 건수다. 메시지 처리 속도에 따라 같은 랙도 체감 지연이 다르다.
11. RabbitMQ 내부 — AMQP 모델·prefetch·쿼럼 큐 (★ 심화)
나오는 질문: "RabbitMQ는 어떻게 라우팅하나요?" · "prefetch가 뭔가요?" · "고가용성은 어떻게 확보하죠?"
2번의 비교표를 실제로 설명할 수 있어야 하는 지점이다. Kafka와 모델 자체가 다르다 — Kafka에서 프로듀서는 토픽·파티션을 직접 겨냥하지만, AMQP에서 발행자는 큐를 모른다. 익스체인지(exchange) 에 보낼 뿐이고, 어느 큐로 갈지는 바인딩(binding) 규칙이 정한다.
| 익스체인지 | 라우팅 방식 |
|---|---|
| direct | 라우팅 키가 바인딩 키와 정확히 일치할 때만 |
| fanout | 라우팅 키 무시, 바인딩된 모든 큐에 복사(브로드캐스트) |
| topic | 점(.)으로 나뉜 패턴 매칭 — *는 단어 하나, #는 0개 이상의 단어 |
| headers | 라우팅 키 대신 헤더 속성으로 매칭(`x-match=any |
이 간접 계층이 RabbitMQ의 강점이다 — 발행자 코드를 안 고치고 바인딩만 추가해 새 구독자를 붙인다.
깊이 — prefetch는 왜 중요한가(★★). 소비자가 basic.ack를 보내야 브로커가 메시지를 지우고, 확인 없이 죽으면 다른 소비자에게 재전달된다. prefetch(QoS) 는 확인 안 된 메시지를 소비자당 몇 개까지 미리 밀어 줄지의 상한이다.
prefetch = 1 안전하지만 매 건 왕복 → 처리량 손해
prefetch = 1000 빠른 소비자가 1,000건을 쥔 채 느리게 처리
→ 다른 소비자는 놀고 부하 분산이 무너진다
즉 prefetch는 처리량과 공평한 분배 사이의 손잡이다. Kafka에는 이에 정확히 대응하는 개념이 없다 — 파티션이 이미 소비 단위를 나눠 두기 때문이다.
깊이 — 고가용성은 이제 쿼럼 큐뿐(★). RabbitMQ 4.0에서 classic mirrored queue가 제거돼, 복제 큐는 쿼럼 큐(quorum queue) 가 유일한 선택지다. Raft 합의 알고리즘으로 복제해 데이터 안전성과 처리량이 모두 낫고(같은 조건에서 미러링 큐의 약 3배), 항상 durable이다. 대신 exclusive 큐·global QoS는 지원하지 않는다. 실무에 바로 와닿는 건 delivery-limit(기본 20) — 배달 횟수를 헤더로 세다가 한계를 넘기면 버리거나 DLX(Dead Letter Exchange) 로 보내 무한 재큐 루프를 구조적으로 막는다. 6번의 DLQ가 RabbitMQ에서는 이 DLX로 구현된다.
꼬리질문: "메시지가 무한히 재시도되며 큐를 막습니다" → 쿼럼 큐의 delivery-limit + DLX. 클래식 큐라면 재시도 횟수를 헤더로 직접 세야 한다. / "fanout과 Kafka의 컨슈머 그룹 여럿은 같은 건가요?" → 결과는 비슷하지만 시점이 다르다. fanout은 발행 시점에 복사하고, Kafka는 하나의 로그를 그룹마다 각자 오프셋으로 읽는다.
함정: ❌ "RabbitMQ는 발행자가 큐를 지정한다" → 익스체인지에 보낸다. ❌ "미러링 큐로 HA를 구성한다" → 4.0에서 제거됐다.
12. 스프링에서의 구현 — 발행·소비·실패 처리 (★ 코드)
나오는 질문: "실패한 메시지는 어떻게 처리하셨나요?" · "재시도는 몇 번이고 그다음은요?"
// 발행 — 키를 주면 같은 키가 같은 파티션으로 간다(4번의 순서 보장)
@Service
class OrderEventPublisher {
private final KafkaTemplate<String, OrderPlaced> template;
void publish(OrderPlaced event) {
template.send("orders", event.orderId(), event); // (토픽, 키, 값)
}
}
// 소비 — concurrency는 이 인스턴스가 띄울 컨슈머 스레드 수
@KafkaListener(topics = "orders", groupId = "order-consumer", concurrency = "3")
void onOrderPlaced(OrderPlaced event) {
orderService.handle(event); // 예외가 나면 아래 에러 핸들러로
}
실패 처리가 답변의 핵심이다. 그냥 두면 같은 메시지에서 무한 재시도하며 그 파티션 전체가 막힌다(독약 메시지).
@Bean
DefaultErrorHandler errorHandler(KafkaTemplate<String, Object> template) {
var recoverer = new DeadLetterPublishingRecoverer(template); // "<원본토픽>.DLT" 로
return new DefaultErrorHandler(recoverer, new FixedBackOff(1000L, 3L)); // 1초 간격 3회 후 DLT
}
깊이 — 재시도하면 안 되는 예외가 있다(★). 역직렬화 실패는 재시도해도 절대 성공하지 않는다. 본문이 애초에 깨졌거나 스키마가 안 맞는 것이라(7번의 스키마 진화와 연결된다), 재시도는 시간 낭비이고 그동안 파티션이 막힌다. 이런 예외는 재시도 없이 즉시 DLT로 분류해야 한다. 반대로 일시적 네트워크 오류·DB 데드락은 재시도가 맞다. "재시도가 의미 있는 실패인가" 로 나누는 게 기준이다.
꼬리질문: "DLT에 쌓인 메시지는 어떻게 하나요?" → @DltHandler로 받아 로깅·알림하고, 원인 수정 후 원본 토픽으로 되돌려 재처리한다. 자동 재투입은 같은 이유로 다시 실패할 수 있어 보통 수동 승인 단계를 둔다. / "재시도 중에 그 파티션의 다음 메시지는요?" → 막힌다. 그래서 재시도 횟수·간격을 짧게 잡거나, 지연이 긴 재시도는 별도 재시도 토픽으로 뺀다.
흔한 오답·함정 정리
- Kafka가 Exactly-Once 완벽 보장 → 외부까지는 멱등 컨슈머.
- Kafka는 순서를 항상 보장 → 파티션 단위만.
- MQ 쓰면 유실 0 → acks=all·복제·outbox 없이는 유실.
- DB 저장+발행을 그냥 둘 다 → dual-write 불일치(outbox).
- 컨슈머 그룹에 컨슈머를 무한정 늘리면 빨라진다 → 파티션 수가 상한.
- Kafka는 ZooKeeper가 필요하다 → 4.0부터 아니다(KRaft 전용). ZK 모드에서 4.0 직행도 불가(3.9 경유).
- Kafka는 메시지를 영구 보관 →
retention.ms기본 7일. 재처리 창이 곧 그 값. - 컴팩션을 켜면 중복이 사라진다 → 같은 키의 옛 값만 정리. 중복 제거가 아니다.
- 랙 = 지연 시간 → 건수다. 시간으로 환산해야 SLA와 연결된다.
- RabbitMQ는 발행자가 큐를 지정한다 → 익스체인지에 보내고 바인딩이 경로를 정한다.
- 미러링 큐로 HA 구성 → RabbitMQ 4.0에서 제거. 쿼럼 큐뿐이다.
- 실패하면 계속 재시도하면 된다 → 역직렬화 실패는 영원히 실패하고 그동안 파티션이 막힌다.
한국 면접 단골 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 | 반복 실패 격리·후처리 |
| ZooKeeper 역할 | 메타데이터·컨트롤러 선출 — 4.0에서 제거 |
| KRaft | 컨트롤러 쿼럼 + __cluster_metadata 로그·즉시 페일오버 |
| Kafka 4.0 업그레이드 | ZK→4.0 직행 불가·3.9에서 KRaft 전환 후 |
| KIP-848 | 브로커가 배정 계산·점진 적용·group.protocol=consumer |
| 리텐션 | retention.ms 기본 7일 = 재처리 가능 창 |
| 로그 컴팩션 | 키별 최신 값만·상태 스냅샷·키 필수·툼스톤으로 삭제 |
| 컨슈머 랙 | LEO − 커밋 오프셋·추세로 판단(순간값은 오탐) |
| 랙이 안 줄어듦 | 파티션 수가 상한 → 파티션을 늘려야 |
| AMQP 라우팅 | 발행자는 큐를 모름 → 익스체인지 + 바인딩(direct/fanout/topic/headers) |
| prefetch | 미확인 메시지 상한 — 처리량 vs 공평 분배의 손잡이 |
| 쿼럼 큐 | Raft 복제·4.0부터 HA 유일·delivery-limit 기본 20 |
| 실패 처리 | 재시도 N회 후 DLT·역직렬화 실패는 즉시 DLT |
꼬리질문 대비 (상 난이도)
- "메시지 유실 막으려면?" →
acks=all·복제·DB outbox·소비자 ack는 처리 후. - "전역 순서 필요?" → 파티션 1개(처리량 희생) 또는 키 파티셔닝.
- "컨슈머 lag?" → 스케일아웃(파티션 수까지)·처리 최적화·백프레셔.
- "Exactly-Once를 진짜?" → Kafka 내부만·외부는 멱등 컨슈머.
- "Kafka가 빠른 이유?" → 순차 디스크·페이지 캐시·zero-copy·배치.
- "KRaft로 바꾸면 뭐가 좋아지나요?" → 운영 대상이 하나로 축소·컨트롤러 페일오버가 거의 즉시(팔로워가 메모리에 상태 보유)·파티션 확장 한계 상승.
- "3개월 전 이벤트를 재처리해 달라는 요청이 왔습니다" → 리텐션이 그보다 짧으면 Kafka에는 이미 없다. 장기 보관은 별도 저장소로 싱크해 두는 게 정석.
- "컴팩션 토픽에서 특정 사용자 데이터를 지우려면?" → 툼스톤(키 있고 값
null) 발행 → 컴팩션 →delete.retention.ms경과. - "랙 알림을 걸었는데 배포마다 울립니다" → 순간값이 아니라 추세로 판단(Burrow 방식). 또는 회복 시간을 조건에 넣는다.
- "역직렬화 실패 메시지가 파티션을 막습니다" → 재시도 대상에서 제외하고 즉시 DLT. 스키마 계약(7번)으로 재발 방지.
- "RabbitMQ에서 특정 소비자만 메시지를 독점합니다" → prefetch가 과도하다. 낮춰서 분배를 회복시킨다.
한 줄 요약 — MQ는 비동기·디커플링·부하 평탄화. Kafka(로그·스트리밍·재생)↔RabbitMQ(Exchange·작업 큐). Kafka 기본 at-least-once(중복→소비자 멱등), EOS=멱등 프로듀서(PID+시퀀스)+트랜잭션이나 외부까지는 멱등 컨슈머,
acks=all+ISR로 유실 방지. 순서는 파티션 단위(같은 키→같은 파티션). dual-write는 Outbox+CDC, 반복 실패는 DLQ. 클러스터 메타데이터는 KRaft(컨트롤러 쿼럼 +__cluster_metadata로그)가 관리하고 Kafka 4.0은 ZooKeeper를 제거했다(3.9 경유 필수·브로커 Java 17). 리밸런스는 KIP-848로 브로커가 계산해 점진 적용. 메시지 수명은cleanup.policy—delete면 리텐션 기간이 재처리 창(기본 7일),compact면 키별 최신 값만 남아 상태 스냅샷(키 필수·삭제는 툼스톤). 운영 1순위 지표는 컨슈머 랙(LEO − 커밋 오프셋)이고 순간값이 아니라 추세로 본다. RabbitMQ는 발행자가 큐를 모르고 익스체인지+바인딩이 경로를 정하며(direct/fanout/topic/headers), prefetch가 처리량과 분배의 손잡이, HA는 4.0부터 쿼럼 큐(Raft·delivery-limit20)뿐이다.
(출처 — 한국 면접 기출·교차검증 2026-06: medium 김기현 — Kafka 중복 메시지·Exactly-Once·우아한형제들 — 카프카+Transactional Outbox·올리브영 — Kafka 중복/유실·velog — Kafka vs RabbitMQ · Confluent — Delivery Semantics·Idempotent Producer·RabbitMQ — Exchanges·microservices.io(Transactional Outbox) 교차검증.)
*(8~12번 보강 2026-08 — 1차 자료: Apache Kafka 4.0 릴리스 공지·Confluent — KRaft·Confluent — Log Compaction·RabbitMQ — AMQP 0-9-1 Concepts·RabbitMQ — Quorum Queues·Spring for Apache Kafka — Handling Exceptions 교차검증.)