🌏 인트로
Kafka와 같은 메시징 시스템에서는 메시지가 언제, 어떻게 소비되는지를 보장하는 수준이 중요합니다. (데이터베이스의 트랜잭션 격리 수준과 비슷한 포지션이랄까…?)
네트워크 지연, 장애, 재시도 같은 상황에서 메시지가 유실되거나 중복 처리될 수 있기 때문에, 전송 보장 방식(Delivery Semantics)을 우리 서비스의 특성에 맞게 잘 설정해야 합니다.
🌏 메시지 전송 방식
먼저 간단하게나마 카프카가 어떻게 메세지를 애플리케이션으로부터 받는지에 대해 알아보겠습니다
Kafka에서 “메시지 전송”
= 사건(Event)이 애플리케이션에서 발생했을 때,
애플리케이션 코드가 Kafka Producer API를 호출해서 Broker에 메시지를 보내는 것
- 애플리케이션에서 사건 발생
- 예: 주문 서비스에서 “주문 생성됨” 이벤트 발생
- 애플리케이션이 Producer 코드 실행
- 애플리케이션은 Kafka Producer 클라이언트 라이브러리를 사용해서 메시지를 Kafka Broker로 전송
- 메시지에는 Topic 이름, Key(선택), Value 등이 포함됨
ProducerRecord<String, String> record = new ProducerRecord<>("order-topic", "orderId=12345", "주문 생성됨"); // topic, key, value producer.send(record);
- Broker 저장
- 메시지는 지정된 Topic의 Partition에 순차적으로 저장됨
*acks설정에 따라, Leader/Follower가 확인 응답*
- Consumer가 꺼내 처리
- Consumer Group이 해당 Topic을 구독 중이라면, 메시지를 가져오고 보장 방식 설정에 따라 메세지 처리 전/후에 Offset 을 커밋한다
🌏 메시지 전송 보장 방식
✅ At most once (최대 한 번)
- 메시지가 0번 또는 1번 전달되는 모델
- 중복은 절대 발생하지 않지만, 메시지가 유실될 수 있음
동작 방식
- Consumer가 메시지를 가져오면 바로 Offset 커밋 (“읽었다” 표시)
- 처리 도중 장애 발생한다면 메시지는 이미 읽은 걸로 표시됐으니 재처리 불가
특징
- 장점: 빠름, 중복 없음
- 단점: 장애 시 데이터 손실 발생 가능
- 활용 사례: 로그 수집, 모니터링 (조금 빠져도 큰 문제 없는 경우)
✅ At least once (최소 한 번)
- 메시지가 최소 1번 이상 전달되는 모델
- 유실은 없지만, 중복 처리될 수 있음
동작 방식
- Consumer가 메시지를 가져옴
- 처리를 끝낸 뒤에 Offset을 커밋
- 만약 처리 후 커밋 전에 장애가 나면 같은 메시지를 다시 읽어와서 중복 처리함
특징
- 장점: 데이터 손실 없음
- 단점: 중복 처리 가능하기 때문에 애플리케이션에서 Idempotency(멱등성) 처리 필요
- 활용 사례: 결제, 포인트 적립 (중복 적립을 막기 위해 DB에서 unique key 등으로 보정)
✅ Exactly once (정확히 한 번)
- 메시지가 유실도 없고, 중복도 없음 ⇒
단 한 번만처리 - 가장 이상적인 모델이지만 구현이 가장 복잡함
특징
- 장점: 유실이나 중복 없는 완벽한 처리
- 단점: 복잡도와 성능 비용 ↑
- 활용 사례: 금융, 송금, 재고 관리 (중복/유실이 치명적인 경우)
🌏 카프카에서는 Exactly Once를 어떻게 구현하나?
최대한 예시를 들어 설명해 보겠습니다.
카프카의 Exactly-once 옵션은 크게 2가지 트랜잭션을 조율하는 과정입니다.
- Producer 애플리케이션의 로컬 트랜잭션
- Consumer 애플리케이션의 로컬 트랜잭션
✅ Producer 측 (Idempotent Producer)
문제: 네트워크 장애나 재시도로 인해 같은 메시지가 중복 전송될 수 있음.
주문 애플리케이션 서버(=Producer)가 "주문 생성됨(orderId=12345)" 메시지를 보냈지만 네트워크 지연으로 인해 Producer가 응답(ACK)을 못 받았다면 Producer는 "메시지가 실패했나?" 하고 같은 메시지를 재전송하게 됩니다.
Broker는 그대로 두 번 저장하게 되고, 그러면 결제 서비서(=Consumer)가 두 번 읽게 되어, 중복 결제 발생 위험이 발생합니다
해결: Idempotent Producer 사용 (enable.idempotence=true)
- Producer가 메시지를 보낼 때
(PID=111, seq=1, orderId=12345)라는 메타데이터를 함께 전송합니다- PID: Producer가 브로커와 연결될 때 카프카가 부여하는 고유 ID (프로듀서별로 고정됨)
- Sequence number: Producer가 각 Partition에 메시지를 보낼 때마다 자동 증가하는 번호 (auto-increment처럼 관리)
- 정상적으로 저장된 메시지는 seq가 증가하지만, ACK 못 받아서 재전송하는 경우는 seq를 유지한 채로 보내게 됩니당
- Broker는 메세지를 저장하기 전에 항상
(PID, seq)조합이 중복되는지 확인합니다- 만약에 Producer가 두번 메세지를 전송하게 된다면 두 번째로 온 건 중복이니까 무시
결과: 정확성 보장
Kafka에 "orderId=12345" 메시지가 한 번만 저장되고, Consumer(결제 서비스)도 한 번만 처리할 수 있습니다
✅ 2. Transactional Producer
문제: Producer가 자신의 로컬 DB에 처리를 했는데 메세지를 전송하는 과정에서 에러가 난다면, 데이터 불일치가 발생할 수 있음.
- Producer가 자신의 비즈니스 로직을 처리하면서 DB에 변경을 함.
- Producer가 Kafka에 메세지를 전송함
- Kafka는 브로커에 메세지를 저장함.
여기서 만약에 2번까지는 해서 Producer DB에는 변화가 생겼는데, 3번 전에 장애가 발생했다면???
데이터 불일치(inconsistency)가 발생할 수 있습니다.
해결: *Transactional Producer***
(메세지 쓰기 + Producer 서버 내 DB 커밋)을 하나의 트랜잭션으로 묶어야만 합니다.
- 트랜잭션 시작Producer가 트랜잭션 안에서 메세지를 보내겠다고 선언합니다
(애플리케이션 코드에서 직접 설정해야 합니다) Properties props = new Properties(); props.put("bootstrap.servers", "localhost:9092"); props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); // 트랜잭션 활성화 props.put("enable.idempotence", "true"); props.put("transactional.id", "order-tx-1"); // 반드시 고유해야 함producer.initTransactions(); producer.beginTransaction();- Producer의 메시지 전송
- 예: 주문 서비스에서 DB insert 후
"주문 생성됨"이벤트 Kafka 전송한다면 DB 트랜잭션과 Kafka 메시지 전송이 원자적으로 묶여야 함 - 메시지가 브로커에 기록되지만, 아직 Producer의 트랜잭션이
commit될지abort될지 모르기 때문에 Consumer에 노출하지 않습니다. (= prepare 상태)- Consumer는 "확정된 데이터만 본다"라는 보장(원자성)을 위해 잠시 숨겨둡니다.
- Producer의 DB 트랜잭션 커밋 시점에 맞춰서 같이 Kafka
commitTransaction()호출하면 브로커가 메시지를 확정(commit) 처리하고, 이제부터는 Consumer가 읽을 수 있습니당- 만약에 DB에서 rollback 되면 Kafka도
abortTransaction()호출해 메시지는 폐기됩니다
- 만약에 DB에서 rollback 되면 Kafka도
- 예: 주문 서비스에서 DB insert 후
✅ 3. Transactional Consumer
문제: Consumer가 메시지를 처리한 뒤 커밋 전에 장애가 나면, 다시 같은 메시지를 읽어 중복 처리될 수 있음
- consumer가 메세지를 읽음
- consumer가 자신의 처리를 하고 DB에 저장 (예를 들어 주문했다는 정보를 Insert)
- Kafka에 offset 커밋 (이 메세지는 다 읽었다는 표시)
여기서 만약에 2번까지는 해서 서비 DB에는 변화가 생겼는데, 3번 전에 장애가 발생했다면???
데이터 불일치(inconsistency)가 발생할 수 있습니다.
해결: Transactional Consumer (read-process-write 패턴에서 EOS 지원)
Offset을 +1 해서 "여기까지 읽었다"는 표시를 하는 주체는 Consumer 입니다.
다만 일반적인 commitSync()나 commitAsync()로 단독으로 커밋하는 게 아니라,
- Consumer가 메시지를 처리함
- 그 offset 커밋 작업을 새로운 Producer 트랜잭션 안에 포함시켜서 (
sendOffsetsToTransaction) Broker에 전달합니다.
*Consumer가 "offset 저장"이라는 이벤트를 Kafka에 보내는 순간, 그 행위는 Producer처럼 메시지를 보내는 것과 똑같이 동작합니다.*
평소의 세팅에서는 Consumer는 "읽기 전용"으로, 토픽에서 메시지만 가져오는 존재이지만, 빡빡한 exactly once 세팅을 구현하기 위해서는 처리 이후에 Producer로 offset 커밋 메세지를 브로커에 보내야 합니다.
예를 들어서 offset=101의 메세지를 처리했다면, "offset=101까지 읽음"이라는 특별한 메시지를 __consumer_offsets 토픽에 써 넣는 Producer 역할을 하게 됩니다.
✅ 구현방법을 정리하자면!
- Producer 실패 케이스
- 같은 메시지를 여러 번 보내는 문제 ⇒
Idempotent Producer(PID + seq)로 해결 - 반쪽짜리 쓰기*(DB는 커밋됐는데 Kafka는 실패 or 반대)* ⇒
Transactional Producer로 DB 트랜잭션과 Kafka 쓰기를 묶어서 해결DB 저장 + 메세지 발행이 원자적으로 보장
- 같은 메시지를 여러 번 보내는 문제 ⇒
- Consumer 실패 케이스
- 메시지를 처리했지만 offset 커밋 전에 죽으면 같은 메시지를 또 읽어서 중복 처리 위험
- Consumer가 offset 커밋 자체를 Kafka 트랜잭션 안에 포함 (이때 offset 기록은 특별한 Producer 행위처럼 동작)
메시지 처리 + offset 커밋이 원자적으로 보장
🌏 정리
| 보장 수준 | 의미 | 장점 | 단점 | 활용 사례 |
|---|---|---|---|---|
| At most once | 최대 1번 (중복 X, 유실 O) | 빠름 | 데이터 손실 가능 | 로그, 모니터링 |
| At least once | 최소 1번 (중복 O, 유실 X) | 유실 없음 | 중복 가능 → 멱등성 필요 | 결제, 포인트 |
| Exactly once | 정확히 1번 (중복 X, 유실 X) | 완벽 | 복잡도/성능 비용 ↑ | 금융, 재고 |

도움이 되었다면, 공감/댓글을 달아주면 깃짱에게 큰 힘이 됩니다!🌟
비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!
'아키텍처+MSA' 카테고리의 다른 글
| [아키텍처] 로드밸런싱 완전 정복: L4/L7 구조부터 SPOF 해결까지 (0) | 2025.11.20 |
|---|---|
| [아키텍처/Kafka] Kafka로 강한 결합 탈출하기: 회원가입 비동기 처리 미니 프로젝트 (0) | 2025.11.05 |
| [아키텍처/Kafka] acks(Acknowledgement) 설정: acks=0,1,all (0) | 2025.09.17 |
| [아키텍처/Kafka] 메세지 복제(Replication)와 브로커의 Leader-Follower 구조 (0) | 2025.09.17 |
| [아키텍처/Kafka] Kafka만의 특징: 분산 로그 저장, Queue vs Topic (기존 메세징 시스템과 차이) (1) | 2025.09.17 |