Skip to main content

Command Palette

Search for a command to run...

카프카 입문 시리즈 1편: 카프카의 구성 요소

Updated
10 min read

이 글은 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)**는 카프카에 메시지를 발행하는 클라이언트 애플리케이션입니다.

프로듀서의 동작은 직관적입니다.

  1. 보내고 싶은 **토픽(Topic)**을 지정합니다

  2. 메시지를 구성합니다 (키, 값, 타임스탬프)

  3. 카프카 브로커로 전송합니다

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 합의 알고리즘을 사용하여 컨트롤러 리더를 선출합니다.

동작 방식은 다음과 같습니다.

  1. 클러스터가 시작되면 모든 컨트롤러 노드가 **투표(vote)**를 진행합니다
  2. 과반수 이상의 표를 얻은 노드가 **활성 컨트롤러(Active Controller)**가 됩니다
  3. 활성 컨트롤러가 장애로 응답하지 않으면, 나머지 컨트롤러들이 새로운 투표를 시작합니다
  4. 새 리더는 이미 메타데이터 로그를 복제하고 있었으므로, 즉시 역할을 이어받습니다

핵심은 "가장 최신의 메타데이터 로그를 가진 컨트롤러"가 리더로 선출될 수 있다는 점입니다. 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 공식 문서를 기반으로 작성되었습니다.

More from this blog

JVM은 컨테이너의 CPU와 메모리 한계를 어떻게 알아낼까

8코어 노드에 컨테이너를 띄웠는데 ForkJoinPool이 스레드를 한두 개만 만들어요. 메모리는 넉넉히 줬는데 컨테이너가 자꾸 OOMKilled로 죽고요. 분명히 같은 JAR인데 로컬에서는 멀쩡하다가 쿠버네티스에만 올리면 이상해져요. 이 글은 "왜 컨테이너 속 JVM은 다르게 행동하는가"를 cgroup이라는 진짜 경계선과, JVM이 그 경계를 읽어내는 내

May 21, 202615 min read

ThreadPoolExecutor는 언제 스레드를 새로 만들까 — execute()의 3단계

Executors.newFixedThreadPool(10) 한 줄을 쓰면서도, 11번째 작업이 오면 스레드가 11개로 늘어날 거라고 막연히 기대해 본 적 없으신가요. 실제로는 큐가 먼저 무한히 쌓이고 스레드는 영원히 10개에 머물러요. 이 글은 ThreadPoolExecutor가 작업을 받았을 때 "스레드를 새로 만들지, 큐에 넣을지, 거부할지"를 결정하는

May 21, 202617 min read

자바 synchronized는 어떻게 동작할까 — 모니터, 락 인플레이션, 그리고 사라진 biased locking

synchronized 키워드 하나로 스레드 안전을 얻는 동안, JVM 안에서는 객체 헤더의 비트를 뒤집고, 스택에 락 레코드를 쌓고, 경합이 생기면 네이티브 모니터로 승격하는 일이 벌어져요. 이 글은 그 한 번의 잠금이 객체 헤더부터 ObjectMonitor까지 어떤 경로를 거치는지, 그리고 한때 있었다가 JDK 18에서 사라진 biased locking

May 19, 202616 min read

JVM 객체 할당의 비밀 — TLAB, Bump-the-Pointer, 그리고 할당이 거의 공짜인 이유

Java에서 new를 호출하면 무슨 일이 벌어질까요? "힙에 메모리를 잡는다"는 한 문장 뒤에는 스레드마다 자기만의 분양 구역을 나눠 갖는 정교한 설계가 숨어 있어요. 이 글은 HotSpot JVM이 객체 할당을 어떻게 "거의 공짜"로 만드는지 그 내부를 따라가 보려는 글이에요. JVM 메모리 동작 원리에 관심 있는 분께 권해요. 자바를 쓰다 보면 객체를

May 15, 202614 min read

Java Zero-Copy — FileChannel.transferTo, sendfile, 그리고 Kafka가 디스크를 네트워크로 흘려보내는 방법

"파일을 읽어서 소켓으로 보낸다." 한 줄짜리 요구사항이에요. 그런데 이 한 줄 뒤에서 데이터는 메모리를 네 번이나 복사하고, CPU는 커널과 유저 공간을 네 번이나 들락거려요. Kafka처럼 초당 수십만 건을 흘려보내야 하는 시스템에서 이 비용은 그냥 넘길 수가 없어요. 이 글은 그 복사를 한 겹씩 벗겨내는 zero-copy의 동작 원리를 따라가요. 전통

May 15, 202617 min read

끄적끄적 테크 블로그

165 posts

물류 회사에 다니고 있는 개발자 블로그입니다. 개발을 너무 좋아해서 정신없이 작업하다가 중간에 끄적거리며 내용들을 몇개 적어봅니다 ㅎㅎ