반응형
드디어 분산 시스템의 성배(Holy Grail)라고 불리는 메시지 전송 보장(Delivery Guarantees)의 영역에 발을 들이셨군요! 🫡
카프카를 운영하다 보면 반드시 마주하게 되는 질문입니다. "데이터가 중복되어도 괜찮은가, 아니면 절대로 단 하나라도 틀리면 안 되는가?" 이 결정에 따라 시스템의 복잡도와 성능이 천차만별로 달라집니다. 특히 주식 종목이나 금융 데이터라면 '중복'은 곧 '재앙'이 될 수 있죠.
카프카가 이 까다로운 전송 보장을 어떻게 구현하는지 그 내부 메커니즘을 낱낱이 파헤쳐 보겠습니다!

🚚 메시지 전송 보장의 세 가지 단계
카프카와 컨슈머 사이에는 크게 세 가지의 약속 방식이 있습니다.
1. 최대 한 번 (At-most-once): "잃어버려도 난 몰라"
- 원리: 컨슈머가 메시지를 읽자마자 "나 이거 읽었어!"라고 오프셋을 먼저 저장(Commit)해버립니다. 그 후 실제 데이터를 처리합니다.
- 문제점: 만약 오프셋을 저장한 직후, 실제 데이터를 DB에 넣기 전에 컨슈머가 죽어버리면? 다시 살아난 컨슈머는 다음 번호부터 읽기 시작하므로, 그 사이의 데이터는 유실됩니다.
- 사용 사례: 데이터가 누락되어도 시스템 전체에 큰 지장이 없는 로그 수집 등.
2. 최소 한 번 (At-least-once): "중복은 돼도 유실은 안 돼"
- 원리: 컨슈머가 데이터를 가져와서 처리를 완전히 끝낸 후에(DB 저장 완료 등) 오프셋을 저장합니다. 카프카의 기본 설정이자 가장 많이 쓰이는 방식입니다.
- 문제점: 데이터를 처리하고 DB에 저장까지 성공했는데, 오프셋을 저장하기 직전에 컨슈머가 죽으면 어떻게 될까요? 다시 살아난 컨슈머는 "어? 아까 그 데이터 아직 안 읽었네?"라고 판단하여 똑같은 데이터를 다시 처리하게 됩니다. 즉, 데이터 중복이 발생합니다.
- 사용 사례: 대부분의 일반적인 서비스. (중복 처리를 로직으로 방어할 수 있는 경우)
3. 딱 한 번 (Exactly-once): "완벽한 결벽증"
- 원리: 데이터의 유실도, 중복도 허용하지 않습니다. 시스템이 어떤 장애를 겪더라도 결과적으로 데이터는 단 한 번만 처리된 것과 동일한 효과를 냅니다.
- 방법: 카프카의 트랜잭션(Transaction) API를 사용하거나, 멱등성(Idempotence) 설정을 통해 구현합니다.
🛠️ '딱 한 번(Exactly-once)'은 어떻게 가능한가? (Deep Dive)
사실 분산 시스템에서 '물리적으로 딱 한 번' 메시지를 보내는 것은 거의 불가능에 가깝습니다. 네트워크 오류는 언제든 발생하니까요. 그래서 카프카는 "결과적으로 딱 한 번만 반영되게 한다"는 전략을 씁니다.
① 프로듀서의 멱등성 (Idempotent Producer)
프로듀서 설정에서 enable.idempotence=true를 주면 마법이 시작됩니다.
- 카프카는 각 프로듀서에게 고유한 PID(Producer ID)를 부여하고, 메시지마다 시퀀스 번호(Sequence Number)를 붙입니다.
- 만약 프로듀서가 메시지를 보냈는데 응답을 못 받아서 재전송했을 때, 브로커가 "어? 이 번호 아까 받은 건데?"라고 판단하면 중복된 메시지를 저장하지 않고 버립니다. 쓰기 단계에서의 중복을 원천 차단하는 것이죠.
② 카프카 트랜잭션 (Transactions)
여러 토픽에 데이터를 쓰거나, 데이터를 읽고 쓰는 과정(Read-Process-Write)을 하나의 원자적 단위로 묶는 기능입니다.
- Transaction Coordinator: 브로커 내부에서 트랜잭션을 관리하는 특수 프로세스입니다.
- 동작: 모든 작업이 성공하면
Commit을 찍고, 하나라도 실패하면Abort를 찍어 전체 작업을 무효화합니다. 컨슈머는isolation.level=read_committed설정을 통해 커밋된 데이터만 읽게 됩니다.
⚖️ 실전 선택 가이드
"그럼 무조건 '딱 한 번'이 좋은 거 아니야?"라고 물으실 수 있습니다. 하지만 세상에 공짜는 없죠.
| 구분 | 최소 한 번 (At-least-once) | 딱 한 번 (Exactly-once) |
|---|---|---|
| 성능 (Throughput) | 매우 높음 (오버헤드 거의 없음) | 낮음 (트랜잭션 관리 오버헤드 발생) |
| 복잡도 | 낮음 (기본 설정으로 가능) | 높음 (코딩 및 설정이 까다로움) |
| 데이터 정합성 | 중복 발생 가능 (로직 대응 필요) | 완벽 보장 |
| 추천 상황 | 대용량 로그, 트래픽이 몰리는 서비스 | 결제, 뱅킹, 주식 체결, 정산 시스템 |
💡 팁: "멱등적 컨슈머(Idempotent Consumer)"
실제로 '딱 한 번'을 구현하기 위해 카프카의 무거운 트랜잭션 API를 쓰는 대신, 엔지니어들은 컨슈머 쪽에서 중복을 걸러내는 전략을 많이 씁니다.
- Unique Key 활용: DB의
Primary Key나Unique Index를 메시지의 고유 ID로 설정합니다. 중복 데이터가 들어와도 DB 수준에서 거절되므로 결과적으로 '딱 한 번' 처리된 효과를 봅니다. - Upsert 처리: 데이터를 새로 넣는 대신, 있으면 업데이트하고 없으면 넣는
Upsert로직을 사용합니다. 여러 번 실행해도 최종 결과는 동일하므로 안전합니다. (이것을 멱등성이라고 합니다.)
📊 요약: "어떤 그릇을 선택하시겠습니까?"
- 배달 사고가 나도 괜찮으니 빨리 배달해라:
At-most-once - 배달 사고는 절대 안 되지만, 같은 물건이 두 번 와도 네가 알아서 처리해라:
At-least-once(가장 추천!) - 배달 사고도, 중복 배달도 절대 안 된다. 비용이 많이 들어도 완벽해야 한다:
Exactly-once
반응형
'1. 개발 > 1.4. 데이터 분석' 카테고리의 다른 글
| Spark를 이용한 '데이터 가공' 단계 (1) | 2026.02.06 |
|---|---|
| Kafka Streams는 일반 Consumer랑 뭐가 다를까? (1) | 2026.02.06 |
| Kafka - 성능 모니터링의 핵심 지표인 '컨슈머 랙(Consumer Lag)' (0) | 2026.02.06 |
| Kafka - Cleanup Policy: Delete vs Compact (0) | 2026.02.06 |
| Kafka는 어떻게 부하를 분산할까? (Consumer Group) (1) | 2026.02.05 |