bedaily.me
Backend2026년 6월 23일14분 읽기

Kafka 3.x 아키텍처 — ZooKeeper에서 KRaft로 넘어가는 과도기

Kafka 3.x는 ZooKeeper와 KRaft를 모두 지원하는 마지막 버전대입니다. 전체 구조부터 복제·프로듀서·컨슈머·저장·전달 보증, 그리고 실무 튜닝까지 다이어그램과 함께 정리합니다.

KafkaKRaft메시징아키텍처Spring Kafka

개요

Kafka 3.x는 메타데이터 관리를 ZooKeeper와 KRaft 둘 다 지원하는 마지막 메이저 버전대다. 4.0(2025년 3월)부터 ZooKeeper가 완전히 제거되어 KRaft가 유일한 모드가 됐기 때문에, 3.x를 이해한다는 건 사실상 "ZooKeeper에서 KRaft로 넘어가는 과도기 아키텍처"를 이해하는 일과 같다.

아래에서 전체 구조 → 메타데이터(KRaft) → 복제/ISR → 프로듀서 → 컨슈머 → 저장 → 전달 보증 → 실무 튜닝 순으로, 옵션은 빠짐없이 정리한다.

전체 그림

카프카는 발행한 쪽(프로듀서)과 구독하는 쪽(컨슈머)이 직접 연결되지 않고, 그 사이에 브로커 클러스터가 로그를 저장·중계하는 구조다.

📄전체 아키텍처
Producers
   │  토픽(topic)으로 메시지 발행

Broker Cluster  —  Broker A · B · C ...
   ·  파티션(partition)을 append-only 로그로 저장·중계
   ·  Controller 가 클러스터 메타데이터 총괄 (3.x에서 ZooKeeper → KRaft)


Consumer Groups
      오프셋(offset) 기준으로 구독·소비
  • 토픽(Topic) — 메시지를 논리적으로 분류하는 이름 단위. 모든 발행·구독은 토픽을 기준으로 이뤄진다.
  • 파티션(Partition) — 토픽을 물리적으로 쪼갠 단위이자 병렬 처리·순서 보장의 최소 단위. 파티션 안에서만 순서가 보장된다.
  • 브로커(Broker) — 파티션 데이터를 디스크 로그로 저장하고 클라이언트 요청을 처리하는 서버. 여러 대가 모여 클러스터를 이룬다.
  • 오프셋(Offset) — 파티션 내 각 메시지의 일련번호. 컨슈머는 "어디까지 읽었는지"를 오프셋으로 관리한다.
  • 컨트롤러(Controller) — 어떤 브로커가 어떤 파티션의 리더인지 등 클러스터 메타데이터를 총괄. 카프카 3.x에서 이 역할의 구현이 ZooKeeper에서 KRaft로 넘어갔다.

메타데이터 관리 — ZooKeeper에서 KRaft로

메타데이터 관리 방식의 변화가 카프카 3.x를 정의하는 가장 큰 사건이다. 두 모드를 나란히 두면 차이가 분명하다.

📄ZooKeeper vs KRaft
[ ZooKeeper 모드 ]  (3.x까지 지원, 4.0에서 제거)
   Brokers  ◀── RPC 전체 푸시 ──  단일 Active Controller
                                       │  메타데이터 read/write

                                 ZooKeeper 앙상블  (외부 시스템)

[ KRaft 모드 ]  (3.3 GA, 4.0부터 유일)
   Brokers  ◀── 증분(오프셋) 전파 ──  Controller Quorum (Raft 합의)
                                       │  복제된 이벤트 로그

                                 __cluster_metadata 토픽  (카프카 내부)
  • KRaft 정의 — Kafka Raft의 줄임말로, Raft 합의 알고리즘을 카프카 내부에 구현한 메타데이터 관리 방식. ZooKeeper라는 외부 시스템 없이 컨트롤러 노드들이 복제된 로그(__cluster_metadata 토픽)로 메타데이터를 관리한다.
  • 버전 타임라인 — KRaft는 3.3에서 프로덕션 레디로 표시됐고, 3.5에서 ZooKeeper 모드가 deprecated, 4.0에서 완전히 제거됐다. ZooKeeper→KRaft 마이그레이션 도구는 3.6에 도입되어 3.7에서 프로덕션 레디가 됐다.
  • 3.9의 위치 — 3.9가 ZooKeeper 모드를 지원하는 마지막 버전이며, 4.0으로 가는 사실상의 "브리지 릴리스"다. ZooKeeper 모드 클러스터는 4.0으로 직행할 수 없고, 반드시 3.x에서 KRaft로 먼저 전환해야 한다.
  • KRaft의 실익 — 운영 컴포넌트가 둘에서 하나로 줄어 모니터링·온콜이 단순해지고, 컨트롤러 장애 복구가 거의 즉시 이뤄지며, 대규모 파티션 확장에 유리하다.
  • 실무 시사점 — 신규 클러스터는 KRaft로 시작하는 게 정답. 기존 ZooKeeper 클러스터를 운영 중이라면 3.9를 경유한 마이그레이션을 미리 계획해두는 게 좋다. 프로덕션은 브로커와 컨트롤러를 분리한 전용 노드 구성이 권장된다.

KRaft의 세 가지 실익은 모두 같은 설계 변화에서 나온다. 메타데이터를 "외부 저장소 + 단일 컨트롤러 + RPC 전체 푸시"에서 "복제된 이벤트 로그 + 컨트롤러 쿼럼 + 오프셋 기반 증분 전파"로 바꾼 것이다. 자세한 메커니즘은 접어둔다.

① KRaft가 모니터링·온콜을 단순하게 만드는 이유

핵심은 "관리 대상 시스템이 둘에서 하나로 줄었다"는 점이다.

  • 시스템 통합 — ZooKeeper는 카프카와 완전히 다른 분산 시스템이라 별도로 배포·관리·트러블슈팅해야 한다. KRaft는 그 역할을 브로커/컨트롤러 안으로 흡수해 운영 대상이 하나가 된다.
  • 운영 표면 통합 — ZooKeeper는 별도의 JVM 튜닝, 별도의 디스크 I/O 모니터링, 분리된 보안 설정을 요구한다. KRaft는 카프카 데이터 플레인과 동일한 설정·장애 처리·보안 메커니즘을 사용해 배우고 운영하기가 더 쉽다.
  • 교차 장애 추적 제거 — 두 시스템을 모두 이해해야 하던 온콜 부담이 준다. ZK의 문제가 카프카로 전이되는 식의 "어느 쪽이 원인인지" 교차 디버깅이 사라지고, 런북·대시보드·알람도 하나의 시스템 기준으로 통합된다.
  • split-brain 제거 — 두 분산 시스템 사이에서 생길 수 있는 상태 불일치(split-brain) 시나리오가 없어진다.
  • 상태 가시성 — 쿼럼·메타데이터 상태를 kafka-metadata-quorum.sh describe --status 같은 카프카 자체 도구와 ActiveControllerCount 같은 KRaft 전용 메트릭으로 확인한다.
② 컨트롤러 장애 복구가 빨라진 이유

복구 경로에 "외부 저장소에서 전체 상태를 다시 읽는 단계"가 있느냐 없느냐의 차이다.

  • ZK 방식의 복구 경로 — 액티브 컨트롤러는 /controller 임시(ephemeral) znode를 먼저 만든 브로커가 맡는다. 그 컨트롤러가 죽으면 새로 선출된 컨트롤러는 액티브로 동작하기 전에 모든 토픽 정보를 포함한 메타데이터를 ZooKeeper에서 다시 가져와야 한다. 이 적재량은 파티션 수에 비례한다.
  • 그래서 느리다 — 현장 사례에서 ZooKeeper 기반 컨트롤러 페일오버는 보통 5~10초가 걸렸고, 파티션이 수백만 규모이면 이 적재가 수 분까지 늘었다. 이 시간 동안 리더 선출 같은 메타데이터 변경이 멈춘다.
  • KRaft 방식 — 팔로워 컨트롤러들이 평소에 액티브 컨트롤러의 데이터를 복제하며 웜 스탠바이로 대기한다. 리더십이 바뀌면 새 액티브 컨트롤러는 이미 커밋된 모든 메타데이터 레코드를 메모리에 갖고 있어 외부에서 상태를 적재할 필요가 없다.
  • 결과 — ZooKeeper의 5~10초 대비 서브초 단위의 컨트롤러 복구. ZK의 불리함은 알고리즘이 아니라 "복구할 때마다 외부 저장소에서 전체 메타데이터를 다시 읽어야 한다"는 경로 그 자체에서 온다.
③ 대규모 파티션 확장에 KRaft가 유리한 이유

같은 "외부 저장소 + RPC 전체 푸시" 구조가 파티션이 많아질수록 더 무거워지기 때문이다.

  • ZK 전파 비용의 증가 — 메타데이터 변경을 RPC로 다른 브로커에 전파하는데, 그 전파량이 관련 파티션 수에 비례해 커진다. 또 파티션 상태 변경 하나하나가 앙상블 합의를 거치는 동기 ZooKeeper 쓰기였다.
  • 복구 시 폭증 — 대규모 클러스터에서 컨트롤러가 복구할 때 수천 건의 순차적 ZooKeeper 쓰기를 해야 했고, 파티션이 수백만이면 복구가 몇 시간까지 늘어날 수 있었다.
  • 실질 한계 — 약 20만 파티션이라는 한계는 임의의 값이 아니라 ZooKeeper가 대량 메타데이터를 다룰 때의 성능 특성에서 비롯됐다.
  • KRaft의 증분 전파 — 메타데이터가 로그라서 브로커는 오프셋으로 자기 상태를 추적하고, 재접속 시 처음부터 전부 읽는 대신 마지막 오프셋 이후만 따라잡는다. 로그가 무한히 커지지 않도록 스냅샷으로 압축하고, 컨트롤러 쿼럼이라는 일부 노드만 메타데이터를 처리한다.
  • 결과 — Confluent 실험에서 200만 파티션(ZooKeeper 최대치의 약 10배)을 운영했고, 테스트 클러스터는 500만 파티션 이상을 돌렸으며 메타데이터 연산이 클러스터 크기와 무관하게 거의 일정한 지연을 보였다.

복제와 ISR

카프카의 신뢰성을 떠받치는 핵심은 파티션 복제다. 하나의 파티션은 여러 브로커에 복제되며, 그중 하나가 리더(Leader) 역할을 한다.

📄파티션 복제와 ISR
Partition 0   (replication.factor = 3)
   Broker A : Leader     ◀── 모든 read / write 는 리더로만
   Broker B : Follower   ── 리더를 복제만 (클라이언트 요청 안 받음)
   Broker C : Follower   ── 리더를 복제만

   ISR (In-Sync Replicas) = 리더와 충분히 동기화된 복제본 집합
      └ 리더 장애 시 ISR 안에서만 새 리더가 선출됨  ← 유실 방지의 경계
  • 리더 단일 처리 — 한 파티션의 모든 읽기·쓰기는 리더 복제본 한 곳으로만 들어온다. 팔로워는 리더를 따라 복제만 하며 클라이언트 요청은 받지 않는다.
  • ISR — 리더와 충분히 동기화된 복제본 집합. 리더가 죽으면 ISR 안에서만 새 리더가 선출되므로, ISR이 데이터 유실 방지의 핵심 경계다.
  • replication.factor — 파티션을 몇 벌 복제할지. 운영 환경은 보통 3을 권장한다. 1이면 브로커 한 대 장애로 데이터가 사라진다.
  • min.insync.replicasacks=all일 때 쓰기를 인정받기 위해 필요한 최소 ISR 수. 복제 계수 3에 이 값을 2로 두면 브로커 한 대가 죽어도 쓰기가 계속된다.
  • unclean.leader.election.enable — ISR 밖 복제본을 리더로 허용할지. true면 가용성↑·유실 위험↑, false(권장)면 안전성↑. 정합성이 중요한 토픽은 false로 둔다.

프로듀서

send() 한 번이 곧바로 네트워크 전송이 되는 게 아니라, 직렬화·파티셔닝·배치를 거쳐 별도 스레드가 묶어서 보낸다.

📄프로듀서 내부 흐름
send(record)


Serializer       key/value 직렬화


Partitioner      키 있으면 murmur2 해시로 같은 파티션, 없으면 sticky


RecordAccumulator   파티션별 배치 버퍼에 누적
   │   batch.size 가 차거나 linger.ms 가 지나면

Sender 스레드  ── 배치를 묶어 전송 ──▶  Broker(Leader)
   ▲                                        │
   └────────────── acks 응답 ───────────────┘
  • 파티셔너 — 키가 있으면 murmur2 해시 기반으로 항상 같은 파티션에 보내 키 단위 순서를 보장하고, 키가 없으면 sticky 방식으로 한 배치를 채운 뒤 다음 파티션으로 넘어간다.
  • 배치·linger.ms — 레코드를 곧바로 보내지 않고 batch.size만큼 모으거나 linger.ms만큼 기다렸다가 묶어 보낸다. linger를 늘리면 처리량↑·지연↑의 트레이드오프가 생긴다.
  • acks — 쓰기 인정 기준. 값은 세 가지가 전부다.
    • 0 — 응답을 기다리지 않음. 유실 가능, 가장 빠름.
    • 1 — 리더에만 기록되면 인정.
    • all(=-1, 별칭) — ISR 전체에 기록돼야 인정. 가장 안전.
  • enable.idempotencetrue면 재시도 시 중복 쓰기를 브로커가 걸러내 정확히 한 번 기록을 보장한다. 카프카 3.x부터 기본값이 true다. 내부적으로 acks=all, retries가 0보다 큼, max.in.flight.requests.per.connection가 5 이하를 함께 요구하며, 이 조건이 충족돼야 순서까지 보장된다.
  • compression.type — 배치를 압축해 네트워크·디스크를 절약. 코덱 선택이 처리량·압축률을 좌우하는 핵심 변수다(아래 상세).

압축 — 모든 코덱과 레벨

프로듀서 레벨 compression.type의 유효 값은 다섯 가지이고, 토픽·브로커 레벨에서는 두 개가 더 있다.

  • none — 압축하지 않음. CPU 비용 0, 네트워크·디스크 사용량 최대. 메시지가 매우 작거나 이미 압축된 데이터(이미지·동영상)일 때 적합. (토픽 레벨의 uncompressed가 none과 동일한 의미)
  • gzip — 압축률은 가장 높지만 CPU 비용도 가장 큼. 저장·대역폭 절감이 최우선이고 처리량 여유가 있을 때. 비슷한 압축률을 더 적은 CPU로 원하면 zstd가 대안.
  • snappy — 속도가 빠르고 압축률은 보통. Google이 만든 코덱으로, CPU 여유가 적고 적당한 절감만 원할 때. 레벨 설정을 지원하지 않음.
  • lz4 — 속도와 압축률의 균형이 가장 좋아 가장 무난한 기본 선택. 처리량이 최우선이면 1순위.
  • zstd — 압축률은 gzip급인데 속도는 훨씬 빠름. 레벨 조정 폭이 가장 넓고 Kafka 2.1부터 지원. 압축률과 성능을 모두 원할 때 균형 잡힌 선택.
  • producer (토픽·브로커 레벨 전용, 기본값) — 프로듀서가 설정한 원본 압축 코덱을 그대로 유지. 브로커가 재압축하지 않아 브로커 CPU를 아낌.
  • uncompressed (토픽·브로커 레벨) — none과 동일하게 압축하지 않음.

운영 권장 패턴은 "프로듀서에서 압축 + 토픽은 producer로 유지"다. 토픽의 코덱이 producer가 아니고 프로듀서가 쓴 코덱과도 다르면, 브로커가 배치를 압축 해제했다가 토픽 설정대로 재압축하면서 CPU 부담이 커지기 때문이다.

압축 레벨(KIP-390)과 코덱별 벤치마크

압축 강도를 더 세밀하게 조정하려면 레벨 설정을 쓴다. KIP-390으로 compression.gzip.level, compression.lz4.level, compression.zstd.level 세 가지가 프로듀서·토픽·브로커 설정에 추가됐다. snappy는 레벨을 지원하지 않아 제외되고, none·snappy일 때는 레벨 값이 무시된다.

  • gzip — 레벨 1~9, 기본값 6. 숫자가 높을수록 압축률↑·CPU↑.
  • zstd — 레벨 범위가 음수(최고 속도)부터 22(최고 압축률)까지로 가장 넓고, 기본값은 3.
  • lz4 — 정수 레벨을 받으며 기본값은 라이브러리 기본을 따름.
  • 효과의 크기 — 압축 크기를 좌우하는 주 변수는 코덱 선택이고, 레벨이 압축 크기에 주는 영향은 상대적으로 작다. 레벨을 낮출수록 초당 처리 메시지 수는 늘어난다. 즉 레벨 미세조정보다 코덱 선택이 먼저다.

코덱별 트레이드오프(약 50만 건 규모의 예시 벤치마크이며, 실제 수치는 데이터 특성에 따라 크게 달라진다):

코덱압축률처리량CPU레벨 조정
none1.0x가장 높음없음
snappy약 3.4x높음낮음불가
lz4약 4.0x높음낮음가능
zstd약 6.1x중간중간가능(넓음)
gzip약 7.7x낮음높음가능

예시 측정값: none 약 85만 msg/s·476MB, snappy 약 68만 msg/s·142MB(3.4배), lz4 약 78만 msg/s·118MB(4.0배), zstd 약 52만 msg/s·78MB(6.1배), gzip 약 32만 msg/s·62MB(7.7배).

압축은 배치 단위로 적용되므로 배치를 잘 채울수록 효율이 좋아진다. batch.size·linger.ms를 키워 배치를 키우면 압축률과 처리량이 함께 올라간다. 반대로 100바이트 미만의 작은 단건은 압축 효과가 미미하거나 오히려 손해이고, 컨슈머도 압축 해제에 CPU를 쓴다는 점을 고려해야 한다.

컨슈머와 컨슈머 그룹

읽는 쪽은 컨슈머 그룹 단위로 동작한다. 토픽의 파티션들이 그룹 내 컨슈머에게 나뉘어 배정되는 구조다.

📄컨슈머 그룹 배정
Topic T (파티션 4개)            Consumer Group G  (group.id = G)
   P0 ──────────────────────▶  Consumer 1
   P1 ──────────────────────▶  Consumer 1
   P2 ──────────────────────▶  Consumer 2
   P3 ──────────────────────▶  Consumer 3
                               Consumer 4   ← 배정 파티션 없음 (유휴)

   · 한 파티션은 그룹 내 한 컨슈머에게만 배정 (중복 소비 방지)
   · 유효 병렬성의 상한 = 파티션 수
   · 진행 위치는 __consumer_offsets 토픽에 커밋
  • 컨슈머 그룹 — 같은 group.id를 가진 컨슈머들의 묶음. 하나의 파티션은 그룹 내에서 정확히 한 컨슈머에게만 배정되어 중복 소비를 막는다.
  • 병렬성의 상한 — 그룹의 유효 병렬 처리량은 파티션 수가 결정한다. 컨슈머가 파티션보다 많으면 남는 컨슈머는 놀게 된다.
  • 오프셋 커밋 — 어디까지 읽었는지를 __consumer_offsets 토픽에 저장. 자동 커밋(enable.auto.commit)은 편하지만 처리 전 커밋으로 유실 위험이 있어, 처리 완료 후 수동 커밋이 안전하다.
  • auto.offset.reset — 커밋된 오프셋이 없을 때의 동작. 세 가지다.
    • earliest — 파티션의 처음부터 읽음.
    • latest — 최신 메시지부터 읽음.
    • none — 커밋된 오프셋이 없으면 예외를 던짐. 오프셋 유실을 조용히 넘기지 않고 명시적으로 잡아야 할 때 쓴다.
  • 컨슈머 생존 감지 — 리밸런싱 오작동을 막으려면 세 값을 함께 봐야 한다.
    • heartbeat.interval.ms — 백그라운드 스레드가 보내는 생존 신호 주기.
    • session.timeout.ms — 이 시간 동안 신호가 없으면 그룹에서 제외.
    • max.poll.interval.mspoll() 사이 처리 시간 상한. 처리가 느려 이 값을 넘기면 그룹에서 쫓겨난다.

리밸런싱과 파티션 할당 전략

컨슈머가 들어오거나 빠지면 파티션을 재배정(리밸런싱)한다. partition.assignment.strategy는 네 가지다.

  • RangeAssignor — 토픽별로 파티션 범위를 연속 배정. (eager: 리밸런싱 시 전체 중단)
  • RoundRobinAssignor — 모든 파티션을 라운드로빈으로 고르게 배정. (eager)
  • StickyAssignor — 기존 배정을 최대한 유지하며 균형을 맞춤. (eager)
  • CooperativeStickyAssignor — 유일하게 점진적(cooperative) 재배정을 한다. 전체 중단(stop-the-world) 없이 옮길 파티션만 단계적으로 회수·재배정해 멈춤이 줄어든다.

차세대 컨슈머 리밸런스 프로토콜(KIP-848)은 3.x에서 preview였다가 4.0에서 GA가 됐고, 업그레이드 완료 시 서버에서 자동 활성화된다. 조정 로직을 클라이언트에서 브로커 측 그룹 코디네이터로 옮겨 기존 stop-the-world를 없애 대규모 그룹의 리밸런싱 안정성을 크게 높인다. 다만 이 프로토콜을 한 번 쓰면 클러스터는 3.4.1 이상으로만 다운그레이드 가능하니 운영 전 확인이 필요하다.

저장 구조

카프카는 파티션을 디스크에 추가 전용(append-only) 로그로 쌓고, 일정 크기·시간마다 세그먼트 파일로 끊어 관리한다.

📄로그 저장 구조
Partition 0 (append-only 로그)
   ├ segment 0   off 0    .. 999    (closed)
   ├ segment 1   off 1000 .. 1999   (closed)
   └ segment 2   off 2000 ..  ▶     (active, 계속 append)

   · retention(.ms / .bytes) 만료 시 오래된 세그먼트부터 통째로 삭제
   · cleanup.policy=compact 면 같은 키의 최신 값만 남김
   · tiered storage(KIP-405): 오래된 세그먼트를 원격(S3 등)으로 이전
  • 세그먼트 — 파티션 로그를 잘라놓은 실제 파일 단위. 오래된 세그먼트부터 통째로 삭제하므로 retention 정책이 세그먼트 단위로 동작한다.
  • retention(보존)retention.ms(시간) 또는 retention.bytes(용량) 기준으로 오래된 데이터를 삭제. 토픽 단위로 다르게 줄 수 있다.
  • cleanup.policy — 세 가지를 지정할 수 있다.
    • delete(기본) — 보존 기간이 지나면 삭제.
    • compact — 키별 최신 값만 유지. 최종 상태 스냅샷 같은 상태성 데이터에 적합.
    • compact,delete — 압축하되 전체 보존 기간도 함께 적용.
  • tiered storage — KIP-405. 오래된 세그먼트를 S3 같은 원격 저장소로 내려 브로커 디스크 부담을 줄이는 기능으로, 3.6 얼리액세스로 도입됐다. 장기 보존이 필요할 때 검토할 가치가 있다.
  • 제로카피 — 컨슈머에게 데이터를 보낼 때 커널 영역에서 바로 네트워크로 전송해 CPU·메모리 복사를 줄이는 게 카프카 고성능의 한 축이다.

전달 보증 (Delivery Semantics)

위 설정들의 조합으로 전달 보증 수준이 결정된다. 세 가지다.

  • at-most-once (최대 1회) — 처리 전에 오프셋을 먼저 커밋. 유실 가능, 중복 없음. 손실을 감수할 수 있는 로그성 데이터에 쓴다.
  • at-least-once (최소 1회) — 처리 후 커밋. 재처리로 인한 중복은 생길 수 있으나 유실은 없다. 가장 흔한 기본 전략이며, 컨슈머 로직을 멱등하게 짜서 중복을 흡수한다.
  • exactly-once (정확히 1회) — 프로듀서 멱등성 + 트랜잭션(transactional.id)을 묶어 "읽기-처리-쓰기"를 원자적으로 처리. Kafka Streams의 processing.guarantee=exactly_once_v2가 대표 사례다. 오버헤드가 있으니 정말 필요한 구간에만 적용한다.

실무 튜닝 체크리스트

  • 파티션 수 설계 — 목표 처리량 ÷ 파티션당 처리량으로 산정하되, 파티션은 늘리기는 쉬워도 줄이기 어렵고 키 기반 순서가 깨질 수 있으니 처음부터 여유를 둔다. 과도하면 리밸런싱·메타데이터 부담이 커진다.
  • 키 설계 — 순서 보장이 필요한 단위(예: 주문 ID, 딜러 ID)를 키로 삼아 같은 파티션으로 모은다. 단, 특정 키에 트래픽이 몰리면 핫 파티션이 생기므로 분포를 확인한다.
  • 프로듀서 튜닝 — 안전이 우선이면 acks=all + enable.idempotence=true, 처리량이 우선이면 linger.ms·batch.size를 키우고 compression.type=lz4 또는 zstd를 적용한다.
  • 컨슈머 튜닝max.poll.records와 처리 시간을 함께 보고 max.poll.interval.ms를 넉넉히 잡아 리밸런싱 오작동을 막는다. fetch.min.bytes·fetch.max.wait.ms로 폴링 효율을 조정한다.
  • 모니터링 — consumer lag(records-lag-max)이 가장 중요한 건강 지표다. 그 외 under-replicated partitions, ISR 축소, 디스크 사용률, KRaft의 ActiveControllerCount 등을 함께 본다.
  • 운영 안정성replication.factor=3, min.insync.replicas=2, unclean.leader.election.enable=false를 안전 기본값으로 삼고, 토픽별로 보존·압축 정책을 명시한다.

Spring Kafka 매핑

Spring Kafka를 쓰는 환경이라면 위 개념들이 다음과 같이 연결된다.

  • @KafkaListener + ConcurrentKafkaListenerContainerFactoryconcurrency가 그룹 내 컨슈머 수에 해당하므로 파티션 수와 맞춘다.
  • AckMode — 오프셋 커밋 시점을 제어해 at-least-once를 명확히 구현한다. 일곱 가지다.
    • RECORD — 레코드 하나 처리 후 커밋.
    • BATCH(기본) — poll()로 받은 배치 처리 후 커밋.
    • TIME — 지정 시간마다 커밋.
    • COUNT — 지정 건수마다 커밋.
    • COUNT_TIME — 건수 또는 시간 중 먼저 도달한 쪽에서 커밋.
    • MANUAL — 애플리케이션이 ack를 호출하고 다음 poll 시 커밋.
    • MANUAL_IMMEDIATEack 호출 즉시 커밋.
  • DefaultErrorHandler + DeadLetterPublishingRecoverer — 재시도·DLT(데드레터 토픽)를 구성해 독성 메시지(poison message)가 컨슈머를 막지 않게 한다.
  • KafkaTransactionManager — 트랜잭션 프로듀서를 묶어 exactly-once 구간을 처리한다.
  • 그룹 ID 전략 — 고정 그룹은 오프셋 이어받기, 랜덤 그룹은 매번 처음부터(broadcast성) 소비에 쓰며, auto.offset.reset과 함께 동작을 결정한다.

마무리

카프카 3.x는 전체 아키텍처 → 메타데이터(KRaft) → 복제/ISR → 프로듀서 → 컨슈머 → 저장 → 전달 보증 → 실무 튜닝이 하나의 흐름으로 이어진다. 그 중심에는 메타데이터를 외부 시스템에 두던 구조를 카프카 안의 복제 로그로 끌어들인 KRaft 전환이 있다.

신규 클러스터라면 KRaft로 시작하고, 기존 ZooKeeper 클러스터라면 3.9를 브리지로 삼은 마이그레이션을 미리 그려두는 것 — 그게 3.x를 이해하는 이유이자 4.0을 맞이하는 준비다.

주간 기술 뉴스레터

Backend · AI · Java 핵심 내용을 매주 이메일로 보내드립니다.