프로듀서와 컨슈머 설계
데이터 전달 보장 수준
Kafka를 실제로 쓸 때 가장 먼저 결정해야 하는 것은 "메시지를 얼마나 안전하게 보낼 것인가"다. 이는 acks 설정과 컨슈머 오프셋 커밋 전략 두 가지로 결정된다.
전달 보장은 세 단계로 나뉜다.
| 보장 수준 | 의미 | 적합한 상황 |
|---|---|---|
| At-most-once | 재전송 없음. 유실 가능 | 로그처럼 일부 손실이 허용될 때 |
| At-least-once | 재전송으로 중복 가능 | 처리가 idempotent할 때 (기본값) |
| Exactly-once | 중복 없이 정확히 한 번 | 결제, 재고 등 중복이 치명적일 때 |
Producer: 메시지를 브로커에 쓰기
acks 설정
프로듀서가 produce() 요청을 보내면 브로커는 설정한 acks 수준에 따라 응답 시점이 달라진다.
- acks=0: 프로듀서가 응답을 기다리지 않는다. 가장 빠르지만 브로커 장애 시 유실.
- acks=1: 리더가 로컬에 쓴 뒤 응답. 리더 장애 전 팔로워 복제 완료 전이면 유실 가능.
- acks=all (또는 -1): 모든 ISR이 복제한 뒤 응답. 가장 안전. Kafka 3.0부터 기본값.
acks=all과 함께 min.insync.replicas=2를 설정하면, "적어도 두 브로커에 기록됐을 때만 성공으로 본다"는 보장이 생긴다.
멱등 프로듀서 (Idempotent Producer)
네트워크 오류 후 재시도할 때 같은 메시지가 두 번 브로커에 기록될 수 있다. 이를 막는 것이 멱등 프로듀서다.
Kafka 3.0부터 enable.idempotence=true가 기본값이다. 브로커는 프로듀서에게 Producer ID(PID)를 부여하고, 파티션별 시퀀스 번호를 추적한다. 재전송된 메시지의 PID+시퀀스가 이미 기록된 것과 같으면 중복으로 처리해 무시한다.
중요한 한계: 멱등성은 단일 프로듀서 세션 안에서만 보장된다. 프로듀서가 재시작되면 PID가 바뀌어 중복을 감지할 수 없다. 프로세스 재시작을 넘는 exactly-once가 필요하다면 트랜잭션을 써야 한다.
파티셔닝 전략
메시지가 어느 파티션으로 갈지는 파티셔너가 결정한다.
- 키 없음 (null key): Kafka 2.4 이전에는 round-robin, 이후에는 Sticky Partitioner가 기본. 배치가 채워질 때까지 한 파티션으로 모아 보내 브로커 측 압축·쓰기 효율을 높인다.
- 키 있음:
hash(key) % numPartitions로 같은 키는 항상 같은 파티션으로. 파티션 내 순서 보장이 필요할 때 사용. - 커스텀 파티셔너:
Partitioner인터페이스를 구현해 특정 키를 특정 파티션에 고정할 수 있다.
파티션 수를 늘리면 기존 키-파티션 매핑이 깨진다. 순서가 중요하다면 파티션 수를 초기에 넉넉하게 잡는다.
Consumer: 메시지를 브로커에서 읽기
오프셋 커밋 전략
컨슈머는 메시지를 읽은 후 처리 완료를 __consumer_offsets에 오프셋 커밋으로 기록한다. 재시작이나 리밸런싱 후 이 커밋 지점부터 이어서 읽는다.
자동 커밋 (enable.auto.commit=true, 기본값): auto.commit.interval.ms마다 마지막 poll 오프셋을 자동으로 커밋한다. 간단하지만, 커밋된 오프셋 이후 처리 도중 컨슈머가 죽으면 다음 재시작에서 그 구간을 건너뛴다(at-most-once 위험).
수동 커밋: 처리가 완료된 뒤 직접 commitSync() 또는 commitAsync()를 호출한다. 처리 전 커밋하면 유실, 처리 후 커밋하면 중복 위험이 각각 남는다. 일반적으로 처리 후 커밋을 권장한다(at-least-once).
while (true) {
records = consumer.poll(timeout)
for record in records:
process(record) // 먼저 처리
consumer.commitSync() // 처리 후 커밋
}exactly-once가 필요하면 오프셋을 외부 저장소(DB 등)에 처리 결과와 원자적으로 함께 쓴다.
Consumer Group 리밸런싱
컨슈머 그룹에 새 인스턴스가 합류하거나 기존 인스턴스가 떠나면 리밸런싱(rebalancing) 이 일어나 파티션이 재분배된다.
Eager 리밸런싱 (기본): 모든 컨슈머가 파티션을 일제히 반납하고 재할당. 리밸런싱 동안 전체 그룹이 소비를 멈춘다(stop-the-world).
Cooperative Sticky 리밸런싱 (partition.assignment.strategy=CooperativeStickyAssignor): 실제로 이동이 필요한 파티션만 점진적으로 재할당. 이동하지 않는 파티션은 계속 소비할 수 있어 처리 중단 시간을 최소화한다. Kafka 2.4+, 클라이언트 2.4+ 이상에서 사용 가능.
흐름 요약
멱등·트랜잭션 파티셔너
key hash / sticky
Async
커밋 기록 Group Coordinator
멤버십·리밸런싱
Producer 주요 설정 요약
| 설정 | 기본값 (Kafka 3.0+) | 역할 |
|---|---|---|
acks | all | 쓰기 내구성 |
enable.idempotence | true | 재시도 중복 방지 |
retries | MAX_INT | 재시도 횟수 |
max.in.flight.requests.per.connection | 5 | 순서 보장과 처리량의 균형 |
compression.type | none | snappy/lz4/zstd로 대역폭 절감 가능 |
linger.ms | 0 | 배치 지연 허용 시간 |
batch.size | 16384 bytes | 배치 최대 크기 |
Consumer 주요 설정 요약
| 설정 | 기본값 | 역할 |
|---|---|---|
enable.auto.commit | true | 자동 커밋 여부 |
auto.commit.interval.ms | 5000 | 자동 커밋 주기 |
auto.offset.reset | latest | 커밋 없을 때 시작 위치 (earliest/latest) |
max.poll.records | 500 | poll 1회 최대 레코드 수 |
session.timeout.ms | 45000 | 하트비트 없을 때 컨슈머 제거 기준 |
partition.assignment.strategy | RangeAssignor | 리밸런싱 전략 |
Open Questions
- 트랜잭션 API(
initTransactions,beginTransaction,commitTransaction)를 사용한 exactly-once producer-consumer 파이프라인은 다음 장에서 다룰 예정이다. - KIP-848(Next Generation Consumer Rebalance Protocol)은 Kafka 4.0에서 early-access로 포함됐으나, 기본 활성화 여부는 배포판별로 확인 필요.
References
- https://kafka.apache.org/documentation/#producerconfigs
- https://kafka.apache.org/documentation/#consumerconfigs
- https://cwiki.apache.org/confluence/display/KAFKA/KIP-848%3A+The+Next+Generation+of+the+Consumer+Rebalance+Protocol
- https://www.confluent.io/blog/cooperative-rebalancing-in-kafka-streams-consumer-ksqldb/
- https://oneuptime.com/blog/post/2026-01-24-kafka-consumer-group-rebalancing/view
- https://eryilmaz0.medium.com/designing-event-consumers-everything-about-commit-offsets-in-kafka-23d3f88472bd
- https://www.lydtechconsulting.com/blog/kafka-idempotent-producer
- https://oneuptime.com/blog/post/2026-01-21-kafka-exactly-once-semantics/view