카프카 입문 시리즈 1편: 카프카의 구성 요소
이 글은 Apache Kafka를 처음 접하는 분들을 위한 입문 시리즈의 첫 번째 글입니다. 카프카를 구성하는 핵심 요소들을 하나씩 살펴보며, 전체 구조를 머릿속에 그려보겠습니다.
카프카란?
Apache Kafka는 이벤트 스트리밍 플랫폼입니다. 단순한 메시지 큐가 아니라, 이벤트를 **발행(publish)하고, 저장(store)하고, 처리(process)**할 수 있는 분산 시스템입니다.
"쇼핑몰에서 사용자가 주문 버튼을 눌렀다"는 이벤트가 발생하면, 이 이벤트를 결제 시스템, 재고 시스템, 알림 시스템이 각각 받아서 처리해야 합니다. 카프카는 이런 시스템 간의 데이터 흐름을 연결하는 중앙 허브 역할을 합니다.
flowchart LR
subgraph Producers
A[Order Service]
B[Payment Service]
C[User Service]
end
K[Apache Kafka]
subgraph Consumers
D[Notification Service]
E[Analytics Service]
F[Inventory Service]
end
A --> K
B --> K
C --> K
K --> D
K --> E
K --> F
이제 카프카를 구성하는 핵심 요소들을 하나씩 살펴보겠습니다.
Broker: 카프카의 서버
**브로커(Broker)**는 카프카 클러스터에서 **저장소 계층(storage layer)**을 구성하는 서버입니다. 데이터를 받아서 저장하고, 요청이 오면 데이터를 내려주는 역할을 합니다.
브로커의 핵심 역할은 네 가지입니다.
메시지 수신: Producer가 보낸 메시지를 받아 디스크에 저장합니다
메시지 제공: Consumer의 요청에 따라 저장된 메시지를 전달합니다
데이터 복제: 다른 브로커와 데이터를 복제하여 장애에 대비합니다
메타데이터 제공: 클라이언트에게 파티션 리더 위치 등 클러스터 상태 정보를 알려줍니다
하나의 브로커가 하나의 카프카 서버라고 이해하면 됩니다. 실제 운영 환경에서는 여러 대의 브로커를 묶어서 사용하는데, 이것이 바로 **클러스터(Cluster)**입니다.
Cluster: 브로커의 집합
**클러스터(Cluster)**는 여러 브로커가 모여 하나의 카프카 시스템을 구성한 것입니다.
flowchart TB
subgraph Kafka Cluster
B1[Broker 1]
B2[Broker 2]
B3[Broker 3]
end
P[Producer] --> B1
P --> B2
B1 <--> B2
B2 <--> B3
B1 <--> B3
B1 --> C[Consumer]
B3 --> C
왜 브로커를 여러 대 사용할까요?
고가용성: 브로커 한 대가 죽어도 다른 브로커가 데이터를 제공합니다
확장성: 데이터가 많아지면 브로커를 추가하여 부하를 분산합니다
데이터 안전성: 데이터를 여러 브로커에 복제하여 유실을 방지합니다
운영 환경에서는 일반적으로 복제 계수(replication factor)를 3으로 설정합니다. 데이터 사본을 3개 유지한다는 의미이므로, 이를 위해 최소 3대의 브로커가 필요합니다. 이렇게 하면 1대가 장애가 나더라도 나머지 2대가 정상 동작을 유지할 수 있습니다.
Producer: 메시지를 보내는 쪽
**프로듀서(Producer)**는 카프카에 메시지를 발행하는 클라이언트 애플리케이션입니다.
프로듀서의 동작은 직관적입니다.
보내고 싶은 **토픽(Topic)**을 지정합니다
메시지를 구성합니다 (키, 값, 타임스탬프)
카프카 브로커로 전송합니다
flowchart LR
P[Producer] -->|1. Send message| B[Broker]
B -->|2. Write to disk| D[(Log)]
B -->|3. Acknowledge| P
프로듀서는 브로커에 메타데이터를 조회하여 어떤 브로커가 어떤 파티션의 리더인지 파악하고, 해당 브로커에 직접 메시지를 전송합니다. 중간 라우터 같은 것이 따로 필요 없습니다.
여기서 "토픽"이라는 용어가 나왔습니다. 토픽은 메시지를 분류하는 논리적 단위인데, 자세한 내용은 2편에서 다루겠습니다. 지금은 "메시지가 저장되는 카테고리"라고 이해하면 충분합니다.
Consumer: 메시지를 받는 쪽
**컨슈머(Consumer)**는 카프카에서 메시지를 읽어가는 클라이언트 애플리케이션입니다.
카프카에서 데이터의 흐름은 방향에 따라 다릅니다. 프로듀서는 브로커에게 메시지를 밀어넣고(Push), 컨슈머는 브로커에서 메시지를 당겨옵니다(Pull). 특히 컨슈머의 Pull 방식은 카프카의 중요한 설계 결정입니다.
왜 Pull 방식인가?
Push 방식은 브로커가 컨슈머에게 메시지를 보내는 것이고, Pull 방식은 컨슈머가 브로커에게 메시지를 달라고 요청하는 것입니다.
| 구분 | Push 방식 | Pull 방식 |
|---|---|---|
| 속도 제어 | 브로커가 결정 | 컨슈머가 결정 |
| 느린 컨슈머 | 과부하 위험 | 자기 속도로 처리 |
| 배치 처리 | 어려움 | 쌓인 메시지를 한번에 가져오기 가능 |
Pull 방식 덕분에 컨슈머는 자신의 처리 능력에 맞춰 메시지를 가져올 수 있습니다. 빠른 컨슈머는 빠르게, 느린 컨슈머는 느리게 — 각자의 속도로 동작합니다.
오프셋(Offset)
컨슈머는 **오프셋(offset)**이라는 숫자로 "다음에 읽을 메시지의 위치"를 관리합니다. 책갈피와 같은 개념이라고 보면 됩니다.
flowchart LR
subgraph Partition
M0["msg 0"]
M1["msg 1"]
M2["msg 2"]
M3["msg 3"]
M4["msg 4"]
end
M0 --> M1 --> M2 --> M3 --> M4
CO["Consumer Offset = 3"]
CO -.->|"next read"| M3
오프셋 덕분에 컨슈머가 중간에 죽었다가 다시 살아나더라도, 마지막으로 읽었던 지점부터 이어서 읽을 수 있습니다. 과거의 오프셋으로 되감아 메시지를 재처리하는 것도 가능합니다. 오프셋에 대해서도 2편에서 더 자세히 다루겠습니다.
Controller: 클러스터의 관리자
**컨트롤러(Controller)**는 카프카 클러스터 전체를 관리하는 역할을 합니다. 비유하자면 브로커들을 지휘하는 관제탑입니다.
컨트롤러가 하는 일은 다음과 같습니다.
파티션 리더 선출: 어떤 브로커가 어떤 파티션의 리더가 될지 결정합니다
브로커 감시: 브로커가 정상 동작하는지 모니터링하고, 장애가 발생하면 대응합니다
메타데이터 관리: 토픽, 파티션, 브로커 정보 등 클러스터의 모든 상태 정보를 관리합니다
설정 관리: 토픽 설정 변경 등 관리 작업을 처리합니다
카프카 클러스터에는 항상 **하나의 활성 컨트롤러(Active Controller)**가 존재합니다. 이 컨트롤러가 죽으면, 대기 중인 다른 컨트롤러가 즉시 역할을 이어받습니다.
KRaft: 카프카의 두뇌
여기서 중요한 질문이 나옵니다. "컨트롤러는 클러스터 상태를 어디에 저장할까?"
ZooKeeper 시절 (과거)
과거에는 ZooKeeper라는 별도의 분산 시스템이 이 역할을 담당했습니다. 카프카를 운영하려면 ZooKeeper 클러스터를 별도로 구축하고 관리해야 했습니다.
flowchart TB
subgraph Before["Before: Kafka + ZooKeeper"]
direction TB
subgraph ZK["ZooKeeper Cluster"]
Z1[ZK Node 1]
Z2[ZK Node 2]
Z3[ZK Node 3]
end
subgraph BK["Kafka Cluster"]
KB1[Broker 1]
KB2[Broker 2]
KB3[Broker 3]
end
KB1 <--> Z1
KB2 <--> Z2
KB3 <--> Z3
end
이 구조에는 문제가 있었습니다.
운영 복잡성: 카프카와 ZooKeeper, 두 개의 분산 시스템을 동시에 관리해야 합니다
확장 한계: ZooKeeper에 저장할 수 있는 메타데이터 양에 제한이 있어, 파티션 수에 한계가 있었습니다
장애 복구 지연: 컨트롤러 장애 시 ZooKeeper에서 상태를 다시 읽어와야 해서 복구에 시간이 걸렸습니다
KRaft 시대 (현재)
**KRaft(Kafka Raft)**는 ZooKeeper 의존성을 완전히 제거하고, 카프카 자체적으로 메타데이터를 관리하는 방식입니다. Kafka 4.0부터 KRaft가 유일한 메타데이터 관리 방식이 되었습니다.
flowchart TB
subgraph After["After: Kafka with KRaft"]
direction TB
subgraph Controllers["Controller Quorum"]
C1[Controller 1]
C2[Controller 2]
C3[Controller 3]
end
subgraph Brokers
KB1[Broker 1]
KB2[Broker 2]
KB3[Broker 3]
end
C1 <--> C2
C2 <--> C3
C1 <--> C3
C1 --> KB1
C2 --> KB2
C3 --> KB3
end
KRaft의 핵심 개념을 살펴보겠습니다.
컨트롤러 쿼럼 (Controller Quorum)
KRaft에서는 여러 컨트롤러 노드가 **쿼럼(quorum)**을 구성합니다. 쿼럼이란 "의사결정을 위해 필요한 최소 인원"이라는 뜻입니다.
3대 구성 → 1대 장애 허용
5대 구성 → 2대 장애 허용
공식: N대 중 (N/2 + 1)대가 살아 있으면 정상 동작
이 컨트롤러들은 Raft 합의 알고리즘을 사용하여 리더를 선출하고, 메타데이터 변경사항을 동기화합니다.
메타데이터 관리 방식
KRaft는 __cluster_metadata라는 내부 토픽에 모든 클러스터 메타데이터를 이벤트 로그로 기록합니다.
flowchart LR
subgraph Metadata Log["__cluster_metadata"]
E1["Topic Created"]
E2["Partition Assigned"]
E3["Broker Registered"]
E4["Leader Changed"]
end
E1 --> E2 --> E3 --> E4
Leader["Active Controller"] -->|"write"| E1
Follower1["Follower Controller"] -->|"replicate"| E4
Follower2["Follower Controller"] -->|"replicate"| E4
이 방식의 장점은 명확합니다.
빠른 장애 복구: 새 리더는 이미 모든 메타데이터를 메모리에 가지고 있으므로, 외부 시스템에서 다시 읽어올 필요가 없습니다
운영 단순화: ZooKeeper 없이 카프카만 관리하면 됩니다
확장성 향상: 메타데이터 관리에 카프카 자체의 로그 구조를 사용하므로 더 많은 파티션을 지원합니다
전체 구조 한눈에 보기
지금까지 살펴본 구성 요소들을 하나의 그림으로 정리하면 다음과 같습니다.
flowchart TB
subgraph Clients
P1[Producer]
P2[Producer]
C1[Consumer]
C2[Consumer]
end
subgraph Kafka Cluster
subgraph Controller Quorum
CT1["Controller 1 (Leader)"]
CT2[Controller 2]
CT3[Controller 3]
end
subgraph Brokers
B1["Broker 1"]
B2["Broker 2"]
B3["Broker 3"]
end
CT1 <--> CT2
CT2 <--> CT3
CT1 <--> CT3
CT1 -.->|"metadata"| B1
CT1 -.->|"metadata"| B2
CT1 -.->|"metadata"| B3
end
P1 -->|"send"| B1
P2 -->|"send"| B2
B1 -->|"fetch"| C1
B3 -->|"fetch"| C2
| 구성 요소 | 역할 | 비유 |
|---|---|---|
| Broker | 메시지를 저장하고 전달하는 서버 | 우체국 |
| Cluster | 브로커들의 집합 | 우체국 네트워크 |
| Producer | 메시지를 보내는 클라이언트 | 편지를 보내는 사람 |
| Consumer | 메시지를 읽는 클라이언트 | 편지를 받는 사람 |
| Controller | 클러스터 상태를 관리하는 관리자 | 우체국 관제탑 |
| KRaft | 컨트롤러의 합의 메커니즘 | 관제탑의 의사결정 규칙 |
정리
이번 글에서는 카프카를 구성하는 핵심 요소들을 살펴보았습니다.
브로커가 메시지를 저장하고, 여러 브로커가 모여 클러스터를 이룹니다
프로듀서가 메시지를 보내고, 컨슈머가 메시지를 가져갑니다
컨트롤러가 클러스터를 관리하며, KRaft로 메타데이터를 자체 관리합니다
하지만 아직 답하지 못한 질문들이 있습니다.
메시지는 브로커 안에서 어떤 구조로 저장될까?
토픽과 파티션은 정확히 무엇이고, 왜 필요할까?
컨슈머의 오프셋은 어떻게 동작할까?
이 질문들에 대한 답은 다음 편인 **"토픽, 파티션, 오프셋"**에서 이어집니다.
부록: Q&A
Q1. 컨트롤러는 어떻게 선출되는가?
답변 보기
KRaft에서는 Raft 합의 알고리즘을 사용하여 컨트롤러 리더를 선출합니다.
동작 방식은 다음과 같습니다.
- 클러스터가 시작되면 모든 컨트롤러 노드가 **투표(vote)**를 진행합니다
- 과반수 이상의 표를 얻은 노드가 **활성 컨트롤러(Active Controller)**가 됩니다
- 활성 컨트롤러가 장애로 응답하지 않으면, 나머지 컨트롤러들이 새로운 투표를 시작합니다
- 새 리더는 이미 메타데이터 로그를 복제하고 있었으므로, 즉시 역할을 이어받습니다
핵심은 "가장 최신의 메타데이터 로그를 가진 컨트롤러"가 리더로 선출될 수 있다는 점입니다. Raft 알고리즘은 로그가 뒤처진 노드에게는 투표하지 않도록 보장합니다.
Q2. 컨슈머와 브로커는 어떤 프로토콜로 통신하는가?
답변 보기
카프카는 TCP 기반의 자체 바이너리 프로토콜을 사용합니다. HTTP나 gRPC가 아닌 카프카만의 전용 프로토콜입니다.
주요 특징은 다음과 같습니다.
- 요청-응답 구조: 클라이언트가 요청을 보내면 브로커가 응답합니다
- 순서 보장: 하나의 TCP 연결에서 요청은 보낸 순서대로 처리됩니다
- 버전 협상: 클라이언트와 브로커가 서로 지원하는 프로토콜 버전을 맞춰 통신합니다
주요 요청 타입을 살펴보면:
| 요청 | 역할 |
|---|---|
Produce |
메시지 발행 |
Fetch |
메시지 조회 |
Metadata |
클러스터 정보 조회 |
OffsetCommit |
오프셋 저장 |
JoinGroup |
컨슈머 그룹 참여 |
자체 바이너리 프로토콜을 사용하는 이유는 성능과 효율성 때문입니다. HTTP 같은 텍스트 기반 프로토콜에 비해 오버헤드가 적고, 메시지 배치 전송에 최적화되어 있습니다.
Q3. 카프카가 높은 처리량을 가질 수 있는 이유는?
답변 보기
카프카의 높은 처리량은 하나의 비결이 아니라, 여러 설계 결정이 합쳐진 결과입니다.
1. 순차적 디스크 I/O (Sequential I/O)
카프카는 데이터를 로그 파일 끝에 순서대로 추가합니다. 디스크의 순차 쓰기 속도는 약 600MB/s로, 랜덤 쓰기(약 100KB/s)보다 수천 배 빠릅니다.
2. OS 페이지 캐시 활용
데이터를 JVM 힙 메모리가 아닌 OS의 페이지 캐시에 저장합니다. GC 오버헤드 없이 수십 GB의 캐시를 활용할 수 있고, 브로커를 재시작해도 캐시가 유지됩니다.
3. Zero-Copy 전송
일반적인 데이터 전송은 디스크 → 커널 → 애플리케이션 → 커널 → 네트워크로 4번 복사가 발생합니다. 카프카는 sendfile 시스템 콜로 디스크에서 네트워크 카드로 직접 전송하여 복사 횟수를 최소화합니다.
4. 배치 처리
메시지를 하나씩 보내지 않고, 여러 메시지를 묶어서(batch) 한 번에 전송합니다. 네트워크 왕복 횟수를 줄이고, 압축 효율도 높아집니다.
5. 파티션 기반 병렬 처리
토픽을 여러 파티션으로 나누어 여러 브로커가 동시에 읽기/쓰기를 처리합니다.
이 요소들이 결합되어 카프카는 초당 수백만 건의 메시지를 처리할 수 있습니다.
Q4. 브로커가 죽으면 어떤 일이 벌어지는가?
답변 보기
브로커 장애 시 카프카는 다음과 같은 순서로 대응합니다.
1. 장애 감지
컨트롤러가 브로커의 하트비트를 모니터링합니다. 일정 시간 응답이 없으면 해당 브로커를 "죽은 것"으로 판단합니다.
2. 리더 재선출
죽은 브로커가 리더로 있던 파티션들에 대해 새로운 리더를 선출합니다. 같은 파티션의 데이터를 복제하고 있던 다른 브로커(ISR 목록에 있는 브로커) 중 하나가 새 리더가 됩니다.
3. 클라이언트 리디렉션
프로듀서와 컨슈머는 메타데이터를 갱신하여 새 리더 브로커로 요청을 보냅니다. 이 과정은 자동으로 이루어집니다.
4. 데이터는 안전한가?
복제 계수가 3이라면, 데이터가 3개의 브로커에 복제되어 있으므로 1대가 죽어도 데이터 유실은 없습니다. 죽었던 브로커가 복구되면 자동으로 클러스터에 재합류하고, 뒤처진 데이터를 따라잡습니다.
Q5. 하나의 서버에서 브로커와 컨트롤러를 동시에 실행할 수 있는가?
답변 보기
가능합니다. KRaft에서는 process.roles 설정으로 서버의 역할을 지정합니다.
process.roles=broker— 브로커 전용process.roles=controller— 컨트롤러 전용process.roles=broker,controller— 결합 모드(Combined Mode)
결합 모드는 하나의 프로세스가 브로커와 컨트롤러 역할을 동시에 수행합니다. 서버 수를 줄일 수 있어 개발 환경이나 소규모 클러스터에서 유용합니다.
하지만 프로덕션 환경에서는 역할을 분리하는 것이 권장됩니다. 브로커의 높은 I/O 부하가 컨트롤러의 메타데이터 처리에 영향을 줄 수 있기 때문입니다.
Q6. 카프카는 메시지 순서를 보장하는가?
답변 보기
파티션 단위로 보장합니다.
하나의 파티션 내에서 메시지는 발행된 순서 그대로 저장되고, 같은 순서로 소비됩니다. 하지만 서로 다른 파티션 간에는 순서가 보장되지 않습니다.
예를 들어 주문 이벤트를 처리한다면:
- 같은 사용자의 주문을 같은 파티션에 넣으면 → 해당 사용자의 이벤트 순서가 보장됩니다
- 서로 다른 사용자의 주문이 다른 파티션에 들어가면 → 사용자 간 순서는 보장되지 않습니다
이를 위해 프로듀서는 **메시지 키(key)**를 사용합니다. 같은 키를 가진 메시지는 항상 같은 파티션으로 전송되므로, 키가 같은 메시지끼리는 순서가 보장됩니다. 파티션과 키에 대한 자세한 내용은 2편에서 다루겠습니다.
이 글은 Apache Kafka 4.2 공식 문서를 기반으로 작성되었습니다.

