[카프카 핵심 가이드] Chapter 01 — 카프카는 왜 분산 메시징의 표준이 되었는가
이 글에 대해
이 글은 “카프카 핵심 가이드(Kafka: The Definitive Guide)”를 읽고 Chapter 1의 내용을 정리한 것입니다. 단순 요약이 아니라, 각 개념이 왜 그렇게 설계되었는지에 초점을 맞춰 정리했습니다.
Kafka는 LinkedIn에서 시작된 프로젝트로, 대규모 실시간 데이터 파이프라인과 스트리밍 처리를 위해 설계되었습니다. 현재는 Apache 재단의 최상위 프로젝트로, 분산 메시징 시스템의 사실상 표준(de facto standard)이 되었습니다.
메시지(Message) — Kafka의 데이터 단위
Kafka에서 데이터의 최소 단위는 메시지입니다. 데이터베이스의 row, 테이블의 record에 대응하는 개념입니다.
메시지는 선택적으로 key라는 메타데이터를 포함할 수 있습니다. key는 단순한 식별자가 아니라 파티션 배치 전략에 직접적으로 관여합니다.
파티션 결정 방식:
hash(key) % partition_count = 저장될 파티션 번호
이 설계 덕분에 동일한 key를 가진 메시지는 항상 같은 파티션에 저장됩니다. 예를 들어, 주문 ID를 key로 사용하면 같은 주문의 모든 이벤트가 하나의 파티션에 순서대로 쌓이게 됩니다.
배치(Batch) — 처리량과 지연의 트레이드오프
Kafka는 메시지를 하나씩 전송하지 않고 배치 단위로 모아서 전송합니다.
| 배치 크기 작음 | 배치 크기 큼 | |
|---|---|---|
| 처리량 | 낮음 | 높음 |
| 지연 시간 | 짧음 | 길어짐 |
| 네트워크 효율 | 오버헤드 큼 | 오버헤드 줄어듦 |
배치 크기를 키우면 네트워크 왕복 횟수가 줄어 전체 처리량이 증가하지만, 개별 메시지 입장에서는 배치가 채워질 때까지 기다려야 하므로 지연이 늘어납니다. 실시간성이 중요한 서비스에서는 이 트레이드오프를 신중하게 조정해야 합니다.
토픽(Topic)과 파티션(Partition)
토픽은 메시지를 논리적으로 분류하는 단위이고, 파티션은 토픽을 물리적으로 분할하는 단위입니다.
Topic: order-events
├── Partition 0: [msg1] [msg3] [msg5] → append-only
├── Partition 1: [msg2] [msg6] → append-only
└── Partition 2: [msg4] [msg7] → append-only
파티션의 핵심 특성
1. Append-Only 구조
메시지는 파티션 끝에만 추가됩니다. 수정이나 삽입이 없기 때문에 디스크 순차 쓰기(sequential write)가 가능하고, 이것이 Kafka의 높은 처리량의 기반입니다.
2. 순서 보장 범위
- 단일 파티션 내에서는 메시지 순서가 보장됩니다
- 파티션 간에는 순서가 보장되지 않습니다
이 때문에 순서가 중요한 메시지는 같은 key를 사용해 같은 파티션으로 보내야 합니다.
3. 복제(Replication)
각 파티션은 여러 브로커에 복제되어 저장됩니다. 하나의 브로커가 다운되더라도 다른 브로커에 복제본이 있으므로 데이터 유실 없이 서비스를 지속할 수 있습니다.
프로듀서(Producer)와 컨슈머(Consumer)
프로듀서 — 메시지를 만드는 쪽
프로듀서는 메시지를 생성하여 토픽에 전송합니다.
- key가 없으면 — 라운드 로빈으로 파티션에 균등 분배
- key가 있으면 — 파티셔너(Partitioner)가 hash 기반으로 파티션 결정
// key 없이 전송 → 라운드 로빈
producer.send(new ProducerRecord<>("order-events", orderJson));
// key 지정 전송 → 같은 orderId는 항상 같은 파티션
producer.send(new ProducerRecord<>("order-events", orderId, orderJson));
컨슈머 — 메시지를 읽는 쪽
컨슈머는 하나 이상의 토픽을 구독하고 메시지를 읽습니다. 핵심 개념은 오프셋(offset)입니다.
Partition 0: [0] [1] [2] [3] [4] [5] [6]
↑
현재 오프셋 = 3
(여기까지 읽었음)
오프셋은 파티션 내에서 메시지의 위치를 나타내는 순차적인 번호입니다. 컨슈머는 자신이 어디까지 읽었는지를 오프셋으로 관리하기 때문에, 장애 후 재시작해도 마지막으로 읽은 위치부터 이어서 처리할 수 있습니다.
컨슈머 그룹(Consumer Group) — 수평 확장의 핵심
컨슈머 그룹은 Kafka가 수평 확장을 지원하는 핵심 메커니즘입니다.
Consumer Group: order-service
├── Consumer A → Partition 0, Partition 1
├── Consumer B → Partition 2, Partition 3
└── Consumer C → Partition 4
규칙: 하나의 파티션은 그룹 내 오직 하나의 컨슈머에만 할당
왜 이렇게 설계했을까?
하나의 파티션을 여러 컨슈머가 동시에 읽으면 메시지 처리 순서를 보장할 수 없고, 중복 처리 가능성도 생깁니다. “하나의 파티션 = 하나의 컨슈머” 규칙 덕분에 순서 보장과 정확히 한 번 처리가 가능해집니다.
장애 시 자동 리밸런싱
컨슈머 하나가 다운되면 해당 컨슈머가 담당하던 파티션이 나머지 컨슈머에게 자동으로 재할당됩니다. 별도의 수동 작업 없이 장애 복구가 이루어집니다.
주의: 컨슈머 수가 파티션 수보다 많으면 초과된 컨슈머는 유휴 상태가 됩니다. 따라서 파티션 수를 설계할 때 예상되는 최대 컨슈머 수를 고려해야 합니다.
브로커(Broker)와 클러스터
브로커는 Kafka 서버의 단일 인스턴스입니다. 프로듀서로부터 메시지를 수신하고, 오프셋을 할당한 뒤, 디스크에 저장하는 역할을 합니다.
Kafka 클러스터
├── Broker 0 (Leader: P0, P3) (Follower: P1, P4)
├── Broker 1 (Leader: P1, P4) (Follower: P2, P0)
└── Broker 2 (Leader: P2) (Follower: P3)
리더와 팔로워
- 리더(Leader) — 해당 파티션의 모든 읽기/쓰기를 처리합니다
- 팔로워(Follower) — 리더의 데이터를 복제합니다. 리더가 다운되면 팔로워 중 하나가 새로운 리더로 승격됩니다
이 구조 덕분에 브로커 하나가 장애를 겪어도 클러스터 전체는 정상 운영됩니다.
Kafka의 네 가지 설계 철학
1. 다중 프로듀서 지원
여러 프로듀서가 동일한 토픽에 동시에 메시지를 보낼 수 있습니다. 서로 다른 마이크로서비스에서 발생하는 이벤트를 하나의 토픽으로 모을 수 있어, 이벤트 통합이 자연스럽습니다.
2. 다중 컨슈머 그룹 지원
같은 토픽을 여러 컨슈머 그룹이 독립적으로 읽을 수 있습니다. 주문 이벤트를 결제 서비스, 알림 서비스, 분석 서비스가 각각 독립적으로 소비할 수 있습니다. 한 그룹이 느려지더라도 다른 그룹에 영향을 주지 않습니다.
3. 디스크 기반 저장
메시지를 메모리가 아닌 디스크에 저장합니다. 브로커가 재시작되어도 데이터가 유지되고, 보존 기간(retention period) 동안 메시지를 재소비할 수 있습니다. 장애 복구나 데이터 재처리 시나리오에서 큰 장점입니다.
4. 고성능
순차 쓰기, 배치 처리, zero-copy 전송 등의 기법으로 높은 처리량을 달성합니다. 디스크 기반임에도 메모리 기반 시스템에 준하는 성능을 보여줍니다.
마치며
Chapter 1을 읽으며 느낀 점은, Kafka의 각 구성 요소가 독립적으로 존재하는 것이 아니라 서로 맞물려 동작하도록 설계되었다는 것입니다.
- key → 파티션 결정 → 순서 보장
- 파티션 → 컨슈머 그룹 → 수평 확장
- 복제 → 리더/팔로워 → 고가용성
단순히 “메시지를 보내고 받는 시스템”으로 이해하면 Kafka의 절반만 아는 것입니다. 다음 장에서는 프로듀서의 내부 동작과 설정 옵션에 대해 정리해 보겠습니다.