Kafka 깊게 보기: 분산 로그, 파티션, 컨슈머 그룹, exactly-once
이 글은 메시징/이벤트 시리즈 3편입니다.
- 1편: 메시징과 이벤트 깊게 보기: 큐, 로그, 전달 보장, 그리고 이벤트 기반 아키텍처
- 2편: Kafka vs RabbitMQ vs SQS: 세 메시징 시스템을 같은 표 위에 올려놓기
- 4편: RabbitMQ 깊게 보기: AMQP, exchange, ack, prefetch, DLX
2편에서 Kafka를 "추가 전용 분산 로그"라고 한 문장으로 그렸습니다. 이번 편은 그 한 문장을 끝까지 풀어냅니다. Kafka의 거의 모든 개념은 **로그(append-only log)**라는 단 하나의 추상에서 파생됩니다. 이걸 붙잡고 있으면 파티션도, offset도, 컨슈머 그룹도, exactly-once도 같은 그림의 다른 면으로 보입니다.
한눈에 보면
- Kafka의 토픽은 여러 파티션으로 나뉘고, 각 파티션은 순서가 있는 추가 전용 로그입니다.
- 메시지의 위치는 offset(파티션 내 증가하는 번호)이고, 소비 진행도 offset으로 관리됩니다.
- 같은 키는 같은 파티션으로 가므로 순서는 파티션 단위로만 보장됩니다.
- 컨슈머 그룹: 그룹 안에서는 파티션을 나눠 가지며(작업 분배), 그룹이 다르면 같은 데이터를 각자 읽습니다(브로드캐스트).
- 내구성은 복제 + ISR + acks로 만들어지고, exactly-once는 멱등 프로듀서 + 트랜잭션으로 특정 범위 안에서 달성됩니다.
- 소비해도 메시지는 retention 동안 남아, replay와 다수 구독이 자연스럽습니다.
모든 것은 로그에서 시작한다
로그는 단순합니다. 끝에 계속 이어 붙이고, 각 항목에 번호를 매깁니다.
partition 0: [0][1][2][3][4][5][6] → (계속 append)
↑
offset = 3이 단순함이 Kafka의 성능과 모델을 동시에 설명합니다.
- 쓰기가 빠르다: 끝에 붙이기만 하니 순차 디스크 I/O. 랜덤 액세스가 거의 없습니다.
- 읽기가 독립적이다: 소비자는 "어디까지 읽었는지(offset)"만 기억하면 됩니다. 브로커가 소비자별로 메시지를 따로 관리하지 않습니다.
- 소비가 파괴적이지 않다: 읽어도 로그는 그대로입니다. 그래서 여러 소비자가 같은 로그를 각자 읽고, 되감아 다시 읽을 수 있습니다.
1편에서 본 큐 vs 로그의 "로그" 쪽 성질이 전부 여기서 나옵니다.
토픽, 파티션, offset
토픽은 논리적인 이름(예: orders)이고, 실제로는 여러 파티션으로 쪼개집니다. 파티션이 Kafka의 병렬성·순서·확장의 기본 단위입니다.
flowchart LR
P[Producer] -->|key=user-7| P0[(Partition 0)]
P -->|key=user-3| P1[(Partition 1)]
P -->|key=user-9| P2[(Partition 2)]- 메시지에 키가 있으면
hash(key) % 파티션수로 파티션이 정해집니다. 그래서 같은 키는 항상 같은 파티션으로 갑니다. - 키가 없으면 라운드로빈 등으로 분산됩니다.
여기서 1편의 순서 원칙이 구체화됩니다. 순서는 파티션 안에서만 보장됩니다. user-7의 이벤트를 항상 같은 파티션으로 보내면 그 사용자의 이벤트 순서는 지켜지지만, 서로 다른 파티션 간에는 전역 순서가 없습니다. 그래서 "순서가 필요한 단위"를 키로 잡는 게 설계의 핵심입니다.
파티션 수는 처리량과 직결됩니다. 소비 병렬성의 상한이 파티션 수이기 때문입니다(뒤에서 설명). 다만 한 번 늘린 파티션은 줄이기 어렵고, 늘리면 키-파티션 매핑이 바뀌어 기존 순서 보장이 깨질 수 있으니 초기 설계에서 신중해야 합니다.
프로듀서: acks와 멱등성
프로듀서가 메시지를 보낼 때 "어디까지 확인받고 성공으로 칠지"를 acks로 정합니다. 이게 내구성과 성능의 트레이드오프입니다.
acks=0 # 보내고 확인 안 받음. 빠르지만 유실 가능
acks=1 # 리더가 받으면 성공. 리더 장애 시 유실 가능
acks=all # 리더 + ISR 복제본까지 받아야 성공. 가장 안전내구성이 중요하면 acks=all에 min.insync.replicas를 함께 잡습니다(뒤의 복제 절 참고).
또 하나 중요한 게 멱등 프로듀서입니다. 네트워크 재시도 때문에 같은 메시지가 두 번 쓰일 수 있는데,
enable.idempotence=true를 켜면 프로듀서가 메시지에 시퀀스 번호를 붙이고 브로커가 중복을 걸러내, 재시도로 인한 파티션 내 중복을 막습니다. 1편에서 "exactly-once는 공짜가 아니다"라고 했는데, 멱등 프로듀서가 그 비용의 일부를 도구가 대신 내주는 장치입니다.
복제와 ISR: 내구성의 뼈대
파티션은 여러 브로커에 복제됩니다. 하나가 리더, 나머지가 팔로워입니다. 쓰기·읽기는 리더가 받고, 팔로워는 리더를 따라 복제합니다.
flowchart TB
Pr[Producer] --> L[Leader<br/>broker 1]
L --> F1[Follower<br/>broker 2]
L --> F2[Follower<br/>broker 3]
subgraph ISR["ISR (in-sync replicas)"]
L
F1
F2
end여기서 핵심 개념이 **ISR(In-Sync Replicas)**입니다. 리더를 충분히 따라잡고 있는 복제본 집합이죠. 리더가 죽으면 ISR 안에서 새 리더가 뽑힙니다. ISR 밖으로 뒤처진 복제본은 리더가 될 수 없어, 데이터 유실을 줄입니다.
내구성을 제대로 보장하려면 세 가지를 같이 맞춰야 합니다.
acks=all: ISR 전부가 받아야 성공min.insync.replicas=2: 최소 2개의 복제본이 살아 있어야 쓰기 허용replication.factor=3: 복제본 3벌
이 조합이면 브로커 하나가 죽어도 데이터가 살아남고 쓰기도 계속됩니다. 반대로 min.insync.replicas를 너무 높게 잡으면 복제본 장애 시 쓰기가 막히니, 가용성과 내구성 사이에서 균형을 잡아야 합니다.
참고로 과거 Kafka는 클러스터 메타데이터 관리에 ZooKeeper를 썼지만, 최근 버전은 KRaft로 자체 관리해 ZooKeeper 의존을 없앴습니다. 운영 구성이 한결 단순해졌습니다.
컨슈머 그룹: 분배와 브로드캐스트를 한 모델로
Kafka가 1편의 두 모델(작업 분배 + 브로드캐스트)을 동시에 표현하는 비결이 컨슈머 그룹입니다.
flowchart TB
subgraph T["Topic: orders (3 partitions)"]
P0[(P0)]
P1[(P1)]
P2[(P2)]
end
subgraph G1["Group: billing"]
C1[Consumer 1]
C2[Consumer 2]
end
subgraph G2["Group: analytics"]
C3[Consumer 1]
end
P0 --> C1
P1 --> C1
P2 --> C2
P0 --> C3
P1 --> C3
P2 --> C3규칙은 둘입니다.
- 한 그룹 안에서: 파티션을 소비자들이 나눠 가집니다. 파티션 하나는 그룹 내 한 소비자만 읽습니다(작업 분배). 그래서 그룹 내 병렬성의 상한이 파티션 수입니다. 소비자가 파티션보다 많으면 남는 소비자는 놉니다.
- 그룹이 다르면: 같은 파티션을 각 그룹이 독립적으로 읽습니다(브로드캐스트).
billing그룹과analytics그룹은 같은 주문 이벤트를 각자 처음부터 자기 속도로 읽습니다.
이 한 모델로 "워커들이 일 나눠 갖기"와 "여러 시스템이 같은 이벤트 구독하기"를 동시에 표현합니다. RabbitMQ가 exchange와 큐 구성으로 푸는 걸, Kafka는 그룹 개념 하나로 풉니다.
소비자가 추가·제거되면 파티션을 다시 나누는 리밸런싱이 일어납니다. 이때 잠깐 소비가 멈추므로, 리밸런싱이 잦으면 성능에 영향이 있습니다. 그래서 협력적 리밸런싱, 정적 멤버십 같은 옵션으로 영향을 줄입니다.
offset 커밋: 전달 보장이 갈리는 지점
소비자는 "어디까지 처리했는지"를 offset으로 커밋합니다. 이 커밋 시점이 1편에서 본 at-least-once / at-most-once를 가릅니다.
처리 전에 커밋 → 처리 도중 죽으면 그 메시지 유실 (at-most-once)
처리 후에 커밋 → 커밋 전에 죽으면 다시 처리 (at-least-once, 중복 가능)대부분은 **처리 후 커밋(at-least-once)**을 택하고, 1편의 결론대로 소비자를 멱등하게 만들어 중복을 흡수합니다. 자동 커밋(enable.auto.commit=true)은 편하지만 "처리 전에 커밋돼 유실"이 생길 수 있어, 정확성이 중요하면 수동 커밋으로 처리 완료 후 커밋하는 패턴을 많이 씁니다.
exactly-once는 어디까지 진짜인가
Kafka는 "exactly-once semantics(EOS)"를 말합니다. 1편에서 분산 환경의 exactly-once가 어렵다고 했는데, Kafka는 그걸 특정 경계 안에서 달성합니다. 두 장치의 조합입니다.
- 멱등 프로듀서: 재시도로 인한 파티션 내 중복 제거.
- 트랜잭션: 여러 파티션에 대한 쓰기와 offset 커밋을 원자적으로 묶습니다. "메시지를 읽어서(consume) → 처리하고 → 결과를 쓰고(produce) → offset 커밋"을 하나의 트랜잭션으로 처리하는 consume-process-produce 패턴이 대표적입니다.
beginTransaction
produce(결과 토픽, ...)
sendOffsetsToTransaction(읽은 offset)
commitTransaction ← 결과 쓰기와 offset 커밋이 함께 성공/실패소비자 쪽은 isolation.level=read_committed로 커밋된 메시지만 읽게 합니다.
여기서 정확히 이해해야 할 것: 이 exactly-once는 Kafka에서 읽어 Kafka로 쓰는 처리 파이프라인 안에서 성립합니다. 처리 결과가 Kafka 밖(외부 DB, 외부 API)으로 나가는 순간, 그 외부 시스템까지 포함한 진짜 exactly-once는 다시 멱등성/트랜잭션 설계의 몫입니다. 그래서 실무에서는 EOS를 켜더라도 외부 부수효과는 멱등하게 짜는 원칙이 여전히 유효합니다.
retention과 compaction
소비해도 메시지가 남는다는 건, "언제 지울지"를 따로 정해야 한다는 뜻입니다. Kafka는 두 가지 retention 정책이 있습니다.
- 시간/크기 기반 삭제:
retention.ms,retention.bytes로 "7일", "100GB"처럼 오래되거나 넘치면 지웁니다. 일반 이벤트 스트림의 기본입니다. - log compaction: 같은 키에 대해 최신 값만 남기고 과거 값을 정리합니다. "현재 상태"를 키별로 보존하는 데 적합해서, 변경 로그(changelog)나 상태 토픽에 씁니다.
삭제 정책: 오래된 offset부터 통째로 제거
compaction: key=A의 v1,v2,v3 중 v3만 남김 (키별 최신 상태)retention이 길수록 replay 가능 기간이 늘지만 디스크를 더 씁니다. "새 소비자가 과거 얼마나 거슬러 읽어야 하는가"가 이 값을 정하는 기준이 됩니다.
왜 Kafka가 이런 모양인가: 다시 로그로
마지막으로 처음 질문으로 돌아가면, Kafka의 특성은 전부 "로그를 진실의 원천으로 삼는다"는 선택에서 나옵니다.
- 순차 쓰기 → 높은 처리량
- offset 기반 독립 읽기 → 다수 구독, replay
- 파티션 → 병렬성과 부분 순서
- 보존 → 이벤트 소싱·재처리·스트림 처리의 토대
그래서 Kafka는 "메시지를 전달하는 도구"라기보다 **"이벤트의 흐름을 기록하고 다시 읽게 해주는 분산 로그"**에 가깝습니다. 이 정체성이 다음 편에서 볼 RabbitMQ와 정반대입니다. RabbitMQ는 "메시지를 똑똑하게 라우팅해서 전달하고 지우는" 브로커이고, 그 대비가 두 도구를 이해하는 가장 빠른 길입니다.
정리하면
- Kafka의 모든 개념은 추가 전용 로그라는 하나의 추상에서 파생됩니다.
- 토픽은 파티션으로 쪼개지고, 순서는 파티션 단위, 같은 키는 같은 파티션으로 갑니다.
- 내구성은 복제 + ISR + acks=all + min.insync.replicas 조합으로 만듭니다.
- 컨슈머 그룹이 작업 분배(그룹 내)와 브로드캐스트(그룹 간)를 한 모델로 표현합니다.
- 전달 보장은 offset 커밋 시점이 가르고, 보통 at-least-once + 멱등 소비자입니다. EOS는 Kafka 내부 파이프라인 범위에서 성립합니다.
- retention/compaction 덕에 replay와 다수 구독이 자연스럽고, 이게 Kafka를 로그답게 만듭니다.
다음 편에서는 정반대 철학의 RabbitMQ를 봅니다. exchange로 라우팅하고, ack로 한 건씩 제어하고, 소비하면 지우는 — 스마트 브로커의 세계입니다.
