Kafka는 분산 이벤트 스트리밍 플랫폼이다. 대량의 이벤트를 실시간으로 발행하고 구독하는 구조를 제공한다. 실시간 데이터 파이프라인, 이벤트 기반 아키텍처, 로그 수집 등 다양한 영역에서 사용된다.

이 글은 Kafka의 핵심 개념을 정리하고, 최근 ZooKeeper 의존성을 제거한 KRaft 모드의 등장 배경을 설명한다.

토픽과 파티션

토픽

Kafka에서 메시지는 **토픽(Topic)**에 발행된다. 토픽은 메시지의 논리적 카테고리다. order-events, user-signups처럼 이벤트 유형별로 토픽을 생성한다.

토픽은 메시지를 보관하는 로그다. 한 번 기록된 메시지는 변경되지 않는다(append-only). 보존 기간(retention)이 지나면 삭제된다.

파티션

하나의 토픽은 여러 **파티션(Partition)**으로 나뉜다. 파티션은 Kafka의 병렬성과 순서 보장을 동시에 제공하는 핵심 단위다.

flowchart LR
    subgraph Topic["Topic: order-events"]
        P0["Partition 0
msg0, msg3, msg6..."] P1["Partition 1
msg1, msg4, msg7..."] P2["Partition 2
msg2, msg5, msg8..."] end

파티션 내에서 메시지는 순서가 보장된다. 파티션 간에는 순서가 보장되지 않는다. 같은 키를 가진 메시지는 같은 파티션에 할당되므로, 특정 엔티티(예: 특정 주문)에 대한 이벤트 순서를 보장할 수 있다.

파티션 수를 늘리면 처리량이 증가한다. 여러 컨슈머가 각 파티션을 병렬로 처리할 수 있기 때문이다.

오프셋

각 파티션 내에서 메시지는 고유한 오프셋(Offset) 번호를 가진다. 0부터 시작해서 순차적으로 증가한다. 오프셋은 컨슈머가 “어디까지 읽었는가"를 추적하는 기준이 된다.

프로듀서

**프로듀서(Producer)**는 토픽에 메시지를 발행한다.

프로듀서가 메시지를 보낼 때, 어느 파티션에 할당할지 결정해야 한다. 세 가지 방식이 있다.

키 기반 파티셔닝. 메시지에 키가 있으면 키의 해시값으로 파티션을 결정한다. 같은 키는 항상 같은 파티션에 할당된다. 특정 사용자나 주문에 대한 이벤트 순서를 보장할 때 사용한다.

라운드 로빈. 키가 없으면 파티션에 순서대로 분배한다. 순서 보장이 필요 없고 부하를 고르게 분산할 때 적합하다.

커스텀 파티셔너. 직접 파티셔닝 로직을 구현할 수도 있다. 특정 비즈니스 규칙에 따라 파티션을 선택해야 할 때 사용한다.

Acks

프로듀서는 메시지가 브로커에 기록되었는지 확인하는 수준을 설정할 수 있다.

  • acks=0: 확인 없이 전송. 가장 빠르지만 유실 가능성이 있다.
  • acks=1: 리더 브로커가 기록하면 확인. 리더 장애 시 유실 가능성이 있다.
  • acks=all: 모든 ISR(In-Sync Replica)이 기록하면 확인. 가장 안전하지만 지연이 증가한다.

컨슈머

**컨슈머(Consumer)**는 토픽에서 메시지를 읽는다. 프로듀서가 메시지를 “push"하는 것과 달리, 컨슈머는 직접 “pull"한다. 컨슈머가 자신의 처리 속도에 맞춰 메시지를 가져갈 수 있다.

컨슈머는 읽은 메시지의 오프셋을 **커밋(Commit)**한다. 커밋된 오프셋은 Kafka 내부 토픽(__consumer_offsets)에 저장된다. 컨슈머가 재시작되면 마지막 커밋된 오프셋부터 다시 읽는다.

컨슈머 그룹

여러 컨슈머를 하나의 **컨슈머 그룹(Consumer Group)**으로 묶을 수 있다. 같은 그룹 내에서 각 파티션은 하나의 컨슈머에만 할당된다.

flowchart LR
    subgraph Topic["Topic (3 Partitions)"]
        P0["P0"]
        P1["P1"]
        P2["P2"]
    end
    subgraph Group["Consumer Group A"]
        C1["Consumer 1"]
        C2["Consumer 2"]
        C3["Consumer 3"]
    end
    P0 --> C1
    P1 --> C2
    P2 --> C3

컨슈머 수가 파티션 수보다 많으면, 초과 컨슈머는 유휴 상태가 된다. 처리량을 늘리려면 파티션 수를 먼저 늘려야 한다.

그룹 내 컨슈머가 추가되거나 제거되면 **리밸런싱(Rebalancing)**이 발생한다. 파티션 할당을 재조정하는 과정이다. 리밸런싱 중에는 해당 그룹의 메시지 처리가 일시 중단된다.

서로 다른 컨슈머 그룹

서로 다른 컨슈머 그룹은 같은 토픽을 독립적으로 읽는다. 각 그룹이 자체 오프셋을 관리한다.

flowchart LR
    subgraph Topic["Topic (3 Partitions)"]
        P0["P0"]
        P1["P1"]
        P2["P2"]
    end
    subgraph GA["Group A (주문 처리)"]
        A1["Consumer A1"]
        A2["Consumer A2"]
    end
    subgraph GB["Group B (분석)"]
        B1["Consumer B1"]
    end
    P0 --> A1
    P1 --> A2
    P2 --> A1
    P0 --> B1
    P1 --> B1
    P2 --> B1

하나의 토픽에 여러 컨슈머 그룹이 구독하는 구조는 pub/sub 패턴이다. 주문 처리 시스템과 분석 시스템이 같은 이벤트를 독립적으로 소비하는 경우가 대표적이다.

브로커와 클러스터

브로커

**브로커(Broker)**는 Kafka 서버 인스턴스다. 메시지를 수신하고, 디스크에 저장하고, 컨슈머에게 전달한다. 여러 브로커가 모여 **클러스터(Cluster)**를 구성한다.

각 파티션은 하나의 브로커에 **리더(Leader)**로 할당된다. 프로듀서와 컨슈머는 리더 브로커와 통신한다.

복제

파티션은 여러 브로커에 **복제(Replication)**된다. 리더가 장애를 일으키면 팔로워 중 하나가 새 리더로 승격된다.

flowchart TB
    subgraph Cluster["Kafka Cluster"]
        subgraph B1["Broker 1"]
            P0L["P0 (Leader)"]
            P1F["P1 (Follower)"]
        end
        subgraph B2["Broker 2"]
            P0F["P0 (Follower)"]
            P1L["P1 (Leader)"]
        end
        subgraph B3["Broker 3"]
            P0F2["P0 (Follower)"]
            P1F2["P1 (Follower)"]
        end
    end
    P0L -.->|복제| P0F
    P0L -.->|복제| P0F2
    P1L -.->|복제| P1F
    P1L -.->|복제| P1F2

**ISR(In-Sync Replicas)**은 리더와 동기화된 복제본 집합이다. 팔로워가 리더를 따라잡지 못하면 ISR에서 제외된다. acks=all로 설정하면 ISR의 모든 복제본에 기록이 완료되어야 프로듀서에게 확인을 보낸다.

min.insync.replicas 설정으로 최소 ISR 수를 지정할 수 있다. replication factor가 3이고 min ISR이 2이면, 브로커 1대가 장애를 일으켜도 쓰기가 가능하다. 2대가 장애를 일으키면 쓰기가 거부되어 데이터 정합성을 보호한다.

ZooKeeper와 그 한계

Kafka 3.3 이전까지, Kafka 클러스터의 메타데이터 관리는 ZooKeeper가 담당했다. 브로커 목록, 토픽/파티션 설정, 컨트롤러 선출, ACL 정보 등을 ZooKeeper에 저장하고 조회했다.

ZooKeeper 기반 아키텍처에는 몇 가지 문제가 있었다.

별도 시스템 운영 부담. Kafka 클러스터와 별개로 ZooKeeper 클러스터(보통 3~5 노드)를 운영해야 한다. 모니터링, 업그레이드, 장애 대응 대상이 두 배가 된다.

메타데이터 전파 병목. 브로커가 ZooKeeper에서 메타데이터를 가져오는 구조이므로, 파티션 수가 늘어나면 메타데이터 동기화에 시간이 걸린다. 대규모 클러스터에서 컨트롤러 장애 복구가 느려지는 원인이 된다.

이중 합의 문제. ZooKeeper는 자체 합의 알고리즘(ZAB)으로 동작하고, Kafka는 별도로 ISR 기반 복제를 운영한다. 두 시스템의 상태가 일시적으로 불일치할 수 있다.

KRaft 모드

**KRaft(Kafka Raft)**는 ZooKeeper를 제거하고, Kafka 자체적으로 메타데이터를 관리하는 모드다. Kafka 3.3에서 프로덕션 사용이 가능해졌고, 4.0부터 ZooKeeper 모드가 제거되었다.

KRaft에서는 일부 브로커가 Controller 역할을 겸임한다. Controller 노드들이 Raft 합의 알고리즘으로 메타데이터 로그에 대해 합의한다. 메타데이터가 Kafka 내부 토픽에 저장되므로, 별도 시스템이 필요 없다.

ZooKeeper 모드와 비교한 주요 변화:

  • ZooKeeper 클러스터 제거. 운영 대상이 Kafka 하나로 줄어든다.
  • 메타데이터가 이벤트 로그로 관리된다. 브로커가 메타데이터 로그를 구독하여 자체 상태를 유지한다. ZooKeeper에서 풀링하는 방식보다 전파가 빠르다.
  • 컨트롤러 장애 복구가 빨라진다. Raft 프로토콜에 의해 새 리더가 선출되고, 메타데이터 로그를 이어받는다.

정리

Kafka의 핵심은 토픽, 파티션, 컨슈머 그룹이다. 파티션이 병렬성과 순서 보장을 제공하고, 컨슈머 그룹이 수평 확장을 가능하게 한다. 브로커 복제가 장애 내성을 보장한다.

KRaft 모드는 이 구조에서 ZooKeeper라는 외부 의존성을 제거했다. Kafka만으로 메타데이터 합의와 관리가 완결되는 아키텍처로 전환한 것이다.