1. 문서 제목
- Kafka 핵심 원리 정리 — 처리량의 진짜 이유, acks, 오토커밋 유실·중복, 리밸런싱까지
2. 기술 개요 요약
Kafka를 학습하다 보면 "파티션으로 병렬 처리한다"는 표면 지식까지는 금방 도달하지만, 한 겹 아래의 질문들 — 브로커 한 대는 왜 빠른가, 프로듀서 단에서도 순서가 꼬일 수 있는가, 오토커밋은 유실을 내는가 중복을 내는가 — 에서 막히기 쉽다. 이 문서는 그 한 겹 아래에 해당하는 6개 질문을 정리한다. 관통하는 원칙은 하나다: Kafka 파티션의 존재 이유는 "병렬 처리"와 "순서 보장" 두 가지이고, 모든 설정(acks, max.in.flight, 오토커밋, 리밸런싱, 파티션 증설)은 이 두 축 + "유실 vs 중복" 트레이드오프 위에서 해석하면 답이 나온다.
다루는 질문:
| # | 질문 | 핵심 키워드 |
|---|---|---|
| 1 | Kafka가 다른 MQ보다 처리량이 높은 이유는? ("RabbitMQ 여러 대면 똑같지 않나?") | 순차 디스크 I/O, OS 페이지 캐시, 제로카피 |
| 2 | acks 설정은 무엇을 결정하나? | acks=0/1/all, min.insync.replicas |
| 3 | 프로듀서 한 대가 병렬 전송하면 순서가 꼬일 수 있나? | max.in.flight, 멱등 프로듀서 |
| 4 | 오토커밋을 쓰면 유실과 중복 중 무엇이 발생하나? | 커밋 시점 vs 처리 시점의 분리 |
| 5 | 리밸런싱은 언제 일어나고 왜 문제인가? | eager vs cooperative, static membership |
| 6 | 파티션 증설은 왜 신중해야 하나? | 키 라우팅 변경, 순서 붕괴, 컨슈머 상한 |
3. 핵심 개념 정리
3-1. Kafka 처리량이 높은 이유 — 파티션 "말고" 세 가지
파티션 병렬 처리는 절반의 답이다. "RabbitMQ를 여러 대 두면 똑같다"는 반문에 대한 답은 브로커 한 대의 단일 머신 성능 자체가 다른 이유에서 나온다.
| 원리 | 설명 | 실무 포인트 |
|---|---|---|
| 순차 디스크 I/O (append-only log) | 파티션은 수정·삭제 없이 끝에만 붙이는(append-only) 로그 파일. 디스크는 랜덤 I/O에 약하지만 순차 쓰기는 매우 빠르며, HDD에서도 순차 접근은 랜덤 접근보다 수천 배 빠를 수 있다 | B+Tree 기반 저장소(전통적 MQ의 메시지 상태 갱신)와 달리 쓰기 경로에 랜덤 I/O가 없음. "디스크에 쓰는데 왜 빠르냐"는 꼬리질문의 답 |
| OS 페이지 캐시 | Kafka는 자체 캐시를 거의 두지 않고 파일 쓰기를 OS 페이지 캐시에 맡긴다. 쓰기는 페이지 캐시에 반영된 뒤 OS가 배치로 flush하고, 최근 데이터를 읽는 컨슈머는 디스크가 아니라 메모리(페이지 캐시)에서 읽는다 | JVM 힙에 캐시를 두지 않으므로 GC 부담이 없고, 프로세스를 재시작해도 캐시가 OS에 남아 있음. 컨슈머가 뒤처지지 않는 한 "디스크 기반인데 사실상 메모리 속도" |
| 제로카피 (zero-copy) | 컨슈머에게 데이터를 보낼 때 sendfile 시스템 콜(Java의 FileChannel.transferTo)로 페이지 캐시 → NIC 버퍼로 커널 안에서 직접 전달. 유저 공간으로의 복사와 컨텍스트 스위칭을 생략 |
일반 경로는 "디스크→커널버퍼→유저버퍼→소켓버퍼→NIC"로 복사 4회. 제로카피는 이를 크게 줄임. 단, TLS(SSL) 사용 시에는 암호화를 위해 유저 공간을 거쳐야 하므로 제로카피 이점이 사라진다는 것까지 알면 방어 완성 |
| (보조) 배치 + 압축 | 프로듀서는 레코드를 batch로 모아 압축해서 전송하고, 브로커는 압축된 배치를 그대로 저장·전달 | linger.ms, batch.size로 조절. 처리량 질문에 곁들이면 좋은 보너스 답변 |
면접 답변으로 압축하면: "파티션 병렬 처리에 더해, 브로커 한 대 자체가 빠릅니다. append-only 로그라 디스크 쓰기가 전부 순차 I/O이고, OS 페이지 캐시를 그대로 활용해 최근 데이터는 메모리에서 읽으며, 컨슈머 전송 시 sendfile 기반 제로카피로 유저 공간 복사를 생략하기 때문입니다."
3-2. acks — 프로듀서의 쓰기 내구성 설정 (TCP의 ACK와는 무관)
이름 때문에 TCP 핸드셰이크의 ACK를 떠올리기 쉽지만 전혀 다른 개념이다. acks는 프로듀서가 "이 전송을 성공으로 칠 것인가"를 판단하는 기준, 즉 브로커 복제 수준에 대한 확인 강도다.
| 값 | 의미 | 유실 가능성 | 용도 |
|---|---|---|---|
acks=0 |
응답을 기다리지 않음 | 브로커가 못 받아도 모름 → 유실 허용 | 메트릭, 로그 등 유실 허용 데이터 |
acks=1 |
리더 파티션에 쓰이면 성공 | 리더 장애 + 팔로워 미복제 시 유실 | 중간 수준 |
acks=all (-1) |
ISR(동기화된 레플리카)들이 복제를 마쳐야 성공 | min.insync.replicas와 조합 시 유실 최소화 |
결제·주문 등 유실 불가 데이터 |
- Kafka 3.0부터 프로듀서 기본값이
acks=all+enable.idempotence=true다(KIP-679). "디폴트가 뭐냐"는 질문에는 "3.0 이후 all"이 답. acks=all은min.insync.replicas(브로커/토픽 설정, 예: 2)와 함께 써야 의미가 있다. ISR이 1개로 쪼그라든 상태의acks=all은 사실상acks=1과 같아지기 때문.
3-3. 프로듀서 단에서 순서가 꼬이는 경우 — max.in.flight와 멱등 프로듀서
"순서 보장은 파티션 내에서만"이라는 명제조차 프로듀서가 그 파티션에 순서대로 적재해 준다는 전제 위에 있다. 컨슈머 쪽 순서만 신경 쓰다 보면 놓치기 쉬운 지점인데, 이 전제가 깨지는 시나리오가 있다.
- 프로듀서는 한 커넥션에서 여러 요청을 동시에 날릴 수 있다 —
max.in.flight.requests.per.connection(기본 5). - 배치 1 전송 실패 → 재시도(
retries) 하는 사이에 배치 2가 먼저 성공하면, 같은 파티션 안에서도 2가 1보다 앞에 적재된다. - 대응:
enable.idempotence=true(3.0+ 기본): 브로커가 프로듀서별 시퀀스 번호로 중복을 제거하고,max.in.flight가 5 이하면 재시도 상황에서도 순서를 보장한다.- 멱등성 없이 순서를 지키려면
max.in.flight=1로 줄여야 하는데, 이는 처리량을 희생한다.
면접 답변으로 압축하면: "프로듀서는 in-flight 요청을 여러 개 두고 병렬 전송하므로, 앞 배치의 재시도 중 뒤 배치가 먼저 성공하면 파티션 내에서도 순서가 꼬일 수 있습니다. 멱등 프로듀서를 켜면(3.0+ 기본) 시퀀스 번호 기반으로 max.in.flight 5 이하에서 순서가 보장됩니다."
3-4. 오토커밋의 유실과 중복 — 답은 "둘 다"
먼저 구조부터. 커밋 = 컨슈머가 "여기까지 읽었다"는 오프셋을 브로커에 기록하는 것이고, 이 오프셋은 브로커의 내부 토픽 __consumer_offsets에 저장된다(SSOT는 브로커. 0.9 이전에는 ZooKeeper였음).
오토커밋(enable.auto.commit=true, auto.commit.interval.ms 기본 5초)은 "처리 완료 여부"를 보지 않고, poll 호출 시점에 주기가 도래했으면 직전 poll까지 읽어온 오프셋을 커밋한다. 그래서:
| 시나리오 | 결과 |
|---|---|
| 읽어옴 → 커밋됨 → 처리 전/중에 컨슈머 사망 | 재시작 후 커밋된 오프셋 다음부터 읽음 → 처리 안 된 메시지를 건너뜀 → 유실 |
| 읽어옴 → 처리 완료 → 커밋 주기 전에 컨슈머 사망 | 재시작(또는 리밸런싱으로 다른 컨슈머가 인계) 후 같은 메시지를 다시 읽음 → 중복 |
"유실만 발생한다"고 생각하기 쉽지만 틀린 직관이다. 커밋 시점과 처리 시점이 분리되어 있으니, 커밋이 처리보다 빠르면 유실, 느리면 중복 — 이 한 문장으로 기억한다.
추가로 짚어야 할 두 가지:
- 명시(수동) 커밋으로 바꿔도 중복은 남는다. 처리 완료 → 커밋 직전 사망 시나리오는 그대로이기 때문. 이것이 at-least-once이며, Kafka 컨슈머의 기본 설계 목표다.
- 유실은 사후 감지가 사실상 불가능하다. 브로커는 파티션별 커밋 오프셋만 알지 "몇 번 메시지가 처리 안 됐는지"는 모른다. 그래서 설계 원칙은 "유실은 인프라 레벨에서 원천 차단(at-least-once 구성)하고, 대신 발생하는 중복은 컨슈머의 멱등 처리(멱등 키 + DB unique 제약 등)로 막는다". 오토커밋은 메트릭·로그처럼 유실이 허용되는 데이터에만 쓴다.
- 커밋과 오토커밋 설정의 주체는 컨슈머이고, 동작은 컨슈머 그룹 단위로 이뤄진다.
3-5. 리밸런싱 — 파티션-컨슈머 재배분
정의: 컨슈머 그룹 내에서 "어떤 컨슈머가 어떤 파티션을 담당할지"의 배정을 다시 하는 과정. 페일오버 복구와 혼동하기 쉽지만, 본질은 소유권 재배분이다(장애는 트리거 중 하나일 뿐).
발생 조건:
| 트리거 | 예시 |
|---|---|
| 컨슈머 추가 | 랙이 쌓여서 컨슈머를 늘림(스케일 아웃) |
| 컨슈머 이탈 | 배포·재시작, 크래시, session.timeout.ms(3.0+ 기본 45초) 내 하트비트 누락 |
| 처리 지연 | max.poll.interval.ms(기본 5분) 안에 다음 poll을 못 부름 → 그룹에서 추방 |
| 파티션 수 변경 | 토픽 파티션 증설 |
왜 문제인가:
- eager 방식(전통적 Range/RoundRobin/Sticky assignor)은 리밸런싱 시 그룹의 모든 컨슈머가 모든 파티션을 내려놓고(stop-the-world) 재배정받는다. 그동안 그룹 전체 소비가 멈춘다.
- 이때 "처리했지만 커밋 못 한" 메시지가 다른 컨슈머로 넘어가며 중복 소비가 발생할 수 있다. 평상시에는 파티션당 컨슈머가 1:1이라 컨슈머 간 중복이 없지만, 리밸런싱 순간만은 예외다.
대응:
- Cooperative(incremental) 리밸런싱 —
CooperativeStickyAssignor: 전체를 내려놓지 않고 이동이 필요한 파티션만 점진적으로 넘긴다. "새 컨슈머가 들어오면 각자 전부 반납 후 재배정"이 아니라 "각자 1개씩 떼어주는" 동작. Kafka 3.0+에서 기본 전략 목록은[RangeAssignor, CooperativeStickyAssignor]이지만, 이 기본값에서 실제 사용되는 프로토콜은 여전히 eager(Range) 다 — 목록의 CooperativeSticky는 무중단 전환용이며, cooperative를 실제로 쓰려면 명시 지정이 필요하다. - Static membership —
group.instance.id지정:session.timeout.ms이내에 같은 ID로 재조인하는 한, 재시작·배포 정도로는 리밸런싱을 유발하지 않게 함. - 참고: Kafka 4.0부터는 브로커 주도의 새 컨슈머 그룹 프로토콜(KIP-848)이 도입되어 리밸런싱이 점진적·서버 주도 방식으로 재설계됐다.
3-6. 파티션 수 상한과 증설의 리스크
- 컨슈머 수의 상한 = 파티션 수. 한 파티션에 (같은 그룹의) 컨슈머 두 개가 붙으면 순서 보장이 깨지므로, 파티션당 컨슈머는 최대 1개. 파티션 5개면 컨슈머는 5대까지만 의미가 있고, 초과분은 유휴 상태가 된다.
- 랙이 쌓여 컨슈머를 더 못 늘리면 파티션 증설이 떠오르지만, 증설은 순서 보장을 깨뜨린다:
- 키 기반 라우팅은
hash(key) % 파티션수인데 파티션 수가 바뀌면 같은 키의 새 메시지가 다른 파티션으로 가기 시작한다. 기존 메시지는 원래 파티션에 남아 있으므로(재배치되지 않음), 같은 키(예: 같은 주문 ID)의 신·구 메시지 간 순서가 깨진다. - 파티션 축소는 아예 지원되지 않는다.
- 키 기반 라우팅은
- 실무 원칙:
- 처음부터 파티션을 넉넉하게 만든다 — "부족하면 늘리자"가 아니라 "일단 여유 있게"가 일반 원칙. 증설은 순서가 무의미한 토픽이거나 불가피할 때만. (단, 파티션도 공짜는 아니다 — 파티션당 파일 핸들·메모리 오버헤드와 장애 시 리더 재선출 작업량이 함께 늘어나므로 "무조건 많이"가 아니라 "성장분을 감안해 여유 있게".)
- 파티션을 못 건드리면 컨슈머 내부 병렬 처리(워커 스레드 풀 등)로 소비량을 늘린다. 단, 이 경우 컨슈머 안에서 순서가 꼬일 수 있으므로 키 단위 직렬화 등 별도 제어가 필요하다.
면접 답변으로 압축하면: "랙이 쌓이면 파티션 수까지는 컨슈머를 늘리면 됩니다. 그 상한을 넘으면 선택의 문제인데, 파티션을 늘리면 키 라우팅이 바뀌어 순서 보장이 깨지고 리밸런싱 비용이 생기는 걸 감당해야 하고, 아니면 컨슈머 내부를 병렬화하되 그 안의 순서 제어를 별도로 해야 합니다."
4. 사용 예시 및 코드 스니펫
유실 불가 도메인(결제·주문)의 프로듀서/컨슈머 설정 예시:
# --- Producer: 유실 방지 + 순서 보장 ---
acks=all # ISR 복제 완료까지 대기 (3.0+ 기본)
enable.idempotence=true # 재시도 중복 방지 + 순서 보장 (3.0+ 기본)
max.in.flight.requests.per.connection=5 # 멱등성 켠 상태에서 5 이하면 순서 보장
# --- Broker/Topic ---
min.insync.replicas=2 # acks=all이 실효성을 갖는 최소 ISR
# --- Consumer: 중복은 허용하되 유실은 차단 ---
enable.auto.commit=false # 처리 완료 후 명시 커밋 (at-least-once)
partition.assignment.strategy=org.apache.kafka.clients.consumer.CooperativeStickyAssignor
group.instance.id=order-consumer-1 # static membership: 재시작 시 리밸런싱 회피
컨슈머 랙 확인(소비가 발행을 못 따라가는지 감지):
kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
--describe --group order-consumer-group
# LAG 컬럼 = (파티션 최신 오프셋) - (커밋된 오프셋)
오토커밋 유실/중복 시나리오 타임라인 (커밋은 다음 poll() 호출 시점에 나간다):
poll()#1 ──▶ [msg 100~109 수신, 워커에 처리 위임] ──▶ poll()#2 (5초 주기 도래 → 오프셋 110 자동 커밋)
│
커밋 후 워커가 처리 완료 전에 crash ────────────────────┘──▶ 재시작 시 110부터 재개 = 유실
처리 완료 후, 커밋 주기 도래 전에 crash ─────────────────▶ 100~109 다시 수신 = 중복- 유실 시나리오는 처리가 poll 루프와 분리된(비동기·워커 스레드 위임) 구조에서 성립한다. 단일 스레드 동기 루프라면 다음 poll 전에 처리가 끝나므로, 남는 문제는 중복뿐이다 — "동기 루프에서도 유실되나요?"라는 꼬리질문의 답.
5. 실제 사용 시 주의점 / Best Practice
- "파티션의 존재 이유 = 병렬 처리 + 순서 보장" 프레임으로 모든 질문을 환원할 것. 파티션 증설이 왜 위험한지, 컨슈머 상한이 왜 파티션 수인지, 리밸런싱이 왜 아픈지가 전부 이 두 축에서 유도된다.
- 유실 vs 중복은 대칭이 아니다: 중복은 멱등 키로 막을 수 있지만, 유실은 감지조차 어렵다. 따라서 인프라는 at-least-once로 고정하고 중복 방어를 애플리케이션에서 한다.
- 오토커밋은 "유실·중복을 감당할 수 있는 데이터"에만. 대신 커밋을 배치로 모아 네트워크 비용을 아끼는 이점은 있다.
acks=all을 믿으려면min.insync.replicas를 반드시 함께 볼 것.- 버전 표기 주의: acks 기본값(all)과 멱등성 기본 활성화는 Kafka 3.0 이후 기준이다. 그 이전 버전 기준으로 답하면
acks=1이 기본이었으므로, 면접에서는 "3.0 이후 기준"을 명시하는 게 안전하다. - 제로카피는 TLS 구간에서는 효과가 없다 — "그럼 SSL 쓰면요?"라는 꼬리질문 대비.
6. 참고자료 / 공식 문서 출처
- Apache Kafka 공식 문서 — Design: Persistence / Efficiency (순차 I/O, 페이지 캐시, sendfile): https://kafka.apache.org/documentation/#design
- Apache Kafka 공식 문서 — Producer Configs (
acks,enable.idempotence,max.in.flight.requests.per.connection): https://kafka.apache.org/documentation/#producerconfigs - Apache Kafka 공식 문서 — Consumer Configs (
enable.auto.commit,auto.commit.interval.ms,partition.assignment.strategy,group.instance.id): https://kafka.apache.org/documentation/#consumerconfigs - KIP-679: Producer will enable the strongest delivery guarantee by default (3.0 기본값 변경)
- KIP-429: Kafka Consumer Incremental Rebalance Protocol (cooperative sticky)
- KIP-848: The Next Generation of the Consumer Rebalance Protocol (Kafka 4.0)
'메세지 브로커 > Kafka' 카테고리의 다른 글
| [Kafka] 파티션키·샤딩키로 순서가 보장되는 원리 — 유저 단위로 쪼개는 이유 (0) | 2026.07.12 |
|---|---|
| [Kafka] 클러스터 구조와 파티션 설계 — 트랜잭셔널 프로듀서부터 토픽 분리 기준까지 (0) | 2026.07.12 |
댓글