카프카를 처음 배울 때 가장 많이 헷갈리는 지점은 “토픽에 메시지를 넣었는데 왜 순서가 뒤죽박죽으로 나오지?”다. 답은 카프카의 저장 구조에 있다. 카프카 클러스터는 브로커라는 서버 위에, 토픽이라는 논리 단위를 두고, 그 안에 파티션이라는 물리 단위로 메시지를 나눠 저장한다. 이 세 겹을 정확히 구분하면 순서 보장, 파티션 개수 설계, 메시지 형식 선택까지 대부분의 의문이 풀린다. 이 글에서는 카프카 클러스터의 내부 구조를 브로커에서 시작해 파티션의 순서 보장, 그리고 ZooKeeper의 퇴장까지 순서대로 정리한다
브로커는 카프카 서버 그 자체다
브로커(Broker)는 카프카의 실질적인 서버, 즉 클러스터 안의 서버 인스턴스 하나다. 프로듀서와 컨슈머의 요청을 처리하고 메시지를 저장·관리하는 주체이므로, 사실상 “카프카 프로그램 그 자체”라고 봐도 된다. 이 브로커가 여러 대 모인 집합이 카프카 클러스터다
카프카는 장애를 대비한 분산 복제 구조를 지향하는 고가용성 시스템이다. 그래서 브로커 한 대가 아니라 여러 대를 묶은 클러스터 단위가 설계의 기본 전제다. 브로커를 추가하면 처리 용량을 늘리는 스케일 아웃, 줄이면 스케일 인이 된다
각 브로커 안에는 파티션(Partition)이라는 작은 단위가 있어 실제 메시지를 저장하고, 이 파티션을 논리적으로 묶은 토픽(Topic)이 존재한다. 모든 발행자와 수신자는 이 토픽을 통해 메시지를 주고받는다
토픽은 논리 단위, 파티션은 물리 단위다
토픽은 카프카에 발행되는 메시지가 기록되는 논리적 단위다. 메시지의 카테고리처럼 동작한다. 프로듀서는 토픽 단위로 메시지를 발행하고, 컨슈머는 특정 토픽에서 메시지를 읽는다. 예를 들어 주문 완료 메시지를 order-topic에 발행하면, order-topic을 구독하는 컨슈머가 그 메시지를 가져간다
여기서 핵심 구분이 나온다. 토픽은 논리 단위이고, 파티션은 메시지가 실제로 저장되는 물리 단위다. 하나의 토픽은 여러 개의 파티션으로 구성되며, 그 파티션들은 여러 브로커에 걸쳐 분산될 수 있다
Kafka Cluster
┌─────────────┬─────────────┬─────────────┐
│ Broker 1 │ Broker 2 │ Broker 3 │
│ [order-P0] │ [order-P2] │ [order-P4] │
│ [order-P1] │ [order-P3] │ [order-P5] │
└─────────────┴─────────────┴─────────────┘
└──── order-topic (파티션 6개) ────┘
위 구조에서 order-topic은 브로커 3대에 걸쳐 총 6개의 파티션으로 구성돼 있다. 파티션 개수는 개발자가 서비스 특성에 맞춰 지정한다. 주문 메시지가 order-topic에 발행되면, 그중 한 파티션이 선택돼 거기에 저장된다
| 구분 | 토픽 | 파티션 |
|---|---|---|
| 성격 | 논리 단위 | 물리 단위(실제 저장소) |
| 개수 | 1개 | 여러 개(개발자 지정) |
| 위치 | 여러 브로커에 걸침 | 특정 브로커에 존재 |
순서 보장은 토픽이 아니라 파티션 안에서만 된다
파티션은 메시지를 발행된 순서대로 이어 붙여(append) 저장한다. 그래서 한 파티션 안에서는 시간 순서가 반드시 지켜진다. 문제는 토픽 전체 관점이다 토픽에 들어온 메시지는 여러 파티션에 분산 저장된다. 컨슈머는 특정 파티션 하나가 아니라 토픽 전체에서 메시지를 읽으므로, 파티션별로는 순서가 맞아도 토픽 전체로 보면 순서가 섞여 나온다
발행 순서: 1 → 2 → 3 → 4 Partition-0: [1][2] ... (파티션 내부는 순서 유지) Partition-1: [3] ... Partition-2: [4] ... 컨슈머가 토픽에서 읽으면: 1, 3, 4, 2 처럼 뒤섞일 수 있음
정리하면, 순서 보장은 파티션 단위로만 성립한다. 토픽 전체의 시간 순서는 보장되지 않는다. 이 한 문장이 카프카 순서 이해의 전부다
키로 파티션을 고정해 순서를 보장한다
특정 메시지들의 순서를 반드시 지켜야 한다면, 메시지에 키(key)를 부여한다. 같은 키를 가진 메시지는 항상 같은 파티션에 저장되므로, 그 키에 한해서는 순서가 보장된다. 원리는 해시다. 해시 함수는 같은 입력에 항상 같은 출력을 내놓는다. 프로듀서가 메시지에 키를 부여하면, 브로커는 그 키의 해시값을 구하고 파티션 수로 나눈 나머지를 파티션 번호로 정한다
파티션 번호 = hash(key) % 파티션_개수
예) 파티션 3개, hash("삼성") = 100 → 100 % 3 = 1 → 항상 Partition-1
hash 값이 120인 키 → 120 % 3 = 0 → 항상 Partition-0
한 가지 바로잡을 점이 있다. 해시의 “같은 입력 → 같은 출력” 성질을 설명할 때 SHA-256 같은 알고리즘을 예로 드는 경우가 많은데, 카프카의 기본 파티셔너가 실제로 쓰는 건 SHA-256이 아니라 murmur2다. 정확한 공식은 murmur2(key) % 파티션_개수이며, murmur2가 부호 있는 32비트 정수를 반환하기 때문에 양수로 변환한 뒤 나머지를 구한다. 알고리즘 이름만 다를 뿐, “같은 키는 같은 파티션으로 간다”는 결론은 동일하다
이 원리를 활용하면 도메인 단위 순서 보장이 쉬워진다. 주식 주문이라면 종목명(“삼성”, “LG”)을 키로 쓰면 종목별로 시간 순서가 지켜진다. 채팅이라면 채팅방 ID를 키로 쓰면 채팅방별로 순서가 지켜진다. 반대로 키를 주지 않으면 메시지는 파티션에 고르게 분산되고 순서는 보장되지 않는다
파티션 개수는 세 가지를 보고 정한다
“파티션을 1개만 두면 순서도 안 꼬이고 간단하지 않나?” 이 생각이 첫 번째 함정이다. 파티션이 1개면 컨슈머를 아무리 늘려도 그 파티션을 점유하는 컨슈머는 하나뿐이라, 병렬 소비가 안 되고 처리 성능이 떨어진다. 파티션 개수는 세 가지 기준으로 정한다
첫째, 시스템 규모. 트래픽이 크면 단일 파티션으로는 처리량이 부족하다. 파티션을 여러 개 두고 컨슈머도 여러 개 붙여 병렬 처리해야 한다. 파티션 수가 곧 병렬 처리의 상한이다
둘째, 키의 종류. 파티션을 많이 만들어도 키 종류가 적으면 몇몇 파티션에만 메시지가 쏠린다. 나머지 파티션은 놀고 있는 유휴 자원이 된다. 예를 들어 종목이 몇 개뿐이거나 채팅방이 몇 개뿐이라면 파티션을 많이 둘 필요가 없다. 반대로 키 종류가 많고 파티션이 적은 건 문제가 되지 않는다. “삼성”과 “LG”가 같은 파티션에 쌓여도, 컨슈머가 키로 구분할 수 있고 각 키의 순서는 그대로 유지되기 때문이다
셋째, 미래 확장 계획. 카프카는 다른 장비와 달리 파티션을 나중에 늘리기가 어렵다. 특히 키를 쓰는 토픽은 구조적으로 확장이 까다롭다. 이유는 앞의 공식에 있다. hash(key) % N에서 N(파티션 수)이 3에서 4로 바뀌면 나머지가 달라져, 같은 키가 이전과 다른 파티션으로 매핑된다. 기존 순서 보장이 깨지는 것이다. 그래서 트래픽 증가가 예상되면 초기에 파티션을 넉넉히 잡아야 한다
| 고려 요소 | 판단 기준 |
|---|---|
| 시스템 규모 | 트래픽 크면 파티션·컨슈머 늘려 병렬 처리 |
| 키의 종류 | 키 종류보다 파티션이 많으면 유휴 자원 발생 |
| 미래 확장 | 키 기반 토픽은 확장 시 매핑이 깨짐 → 초기에 여유 있게 |
메시지는 세그먼트 단위로 저장되고 삭제된다
메시지는 파티션에 저장되지만, 개별 메시지 단위가 아니라 세그먼트(Segment)라는 물리 단위로 관리된다. 메시지는 세그먼트에 계속 append되다가, 세그먼트가 꽉 차면 다음 세그먼트가 생성되고 거기에 이어 쌓인다
Partition A1 [Seg 1: 97~100][Seg 2: 101~103] ... [Seg N: 8476~8479] 가득 참 가득 참 현재 쓰는 중
삭제도 세그먼트 단위로 이뤄진다. 카프카는 기본값으로 7일이 지난 세그먼트를 삭제한다. 그래서 카프카는 영구 저장소가 아니다. 설정된 기간이 지나면 메시지를 정기적으로 지운다. 메시지 기본 크기는 1MB이며, 더 큰 메시지를 보내려면 별도 설정이 필요하다. 텍스트 기반 메시지는 1MB를 넘기기 어려워 보통 손댈 일이 없다
메시지 형식 – JSON이 기본, 대규모에선 Avro
카프카는 JSON, XML, 단순 문자열 등을 지원한다. Key-Value 기반의 유연한 구조라 JSON이 가장 많이 쓰인다. HTTP 통신의 사실상 표준이기도 해서 카프카에서도 기본 선택지다. 대규모 시스템에서는 Avro가 함께 많이 쓰인다. Avro는 카프카에 특화된 메시지 형식으로, 두 가지 특징만 기억하면 된다
첫째, 바이너리 기반이라 전송 성능이 빠르다
둘째, 표준 스키마를 제공한다. 이게 핵심이다. JSON 방식에서는 프로듀서와 컨슈머가 데이터 구조를 서로 맞추지 않으면 에러가 난다. 프로듀서가 회원 정보를 name, email, password로 발행하다가 name을 age로 바꾸면, 컨슈머도 그에 맞춰 코드를 고쳐야 한다. 문제는 이 합의가 오직 개발자 간 의사소통에 의존한다는 점이다. 한쪽이 바꾸고 말을 안 하면 그대로 장애로 이어진다. 구조적으로 실수를 막을 방법이 없다
Avro는 사전에 정의된 스키마(Schema)를 두고, 그 스키마로 DTO를 자동 생성하며, 직렬화·역직렬화 시 타입을 강제한다. 스키마에 맞지 않으면 아예 동작하지 않게 만들어 실수를 원천 차단한다. 스키마는 보통 스키마 레지스트리(Schema Registry)에서 중앙 관리한다. 서버가 많은 대규모 환경에서 메시지 구조를 강제로 통일하고 빠르게 전송하기에 적합하다
정리하면 Avro의 강점은 둘이다. 바이너리 전송으로 빠르고, 표준 스키마로 프로듀서·컨슈머의 DTO 구조를 강제해 안정성을 높인다. 실습은 JSON으로 하더라도 실무에서 Avro를 만나면 이 둘을 떠올리면 된다
ZooKeeper는 KRaft로 대체됐다
오래된 카프카 구조도에는 클러스터 밖에 ZooKeeper가 그려져 있다. ZooKeeper는 분산 아키텍처를 위한 코디네이션 서비스로, 카프카 클러스터의 상태 정보(메타데이터)를 관리하는 역할을 했다. 어떤 브로커가 살아 있고 어떤 브로커가 죽었는지 추적하고, 클러스터 구성 정보를 전체에 일관되게 동기화했다. 그래서 과거에는 카프카를 설치할 때 ZooKeeper를 항상 함께 설치해 운영했다. 하지만 이제 옛것이다
카프카 3.x부터 자체 메타데이터 관리 모드인 KRaft가 도입됐고, 카프카 재단은 ZooKeeper 제거를 권고해 왔다. KRaft는 클러스터 내부의 컨트롤러 노드가 메타데이터를 직접 관리하므로 외부 ZooKeeper가 필요 없다. 그리고 2025년 3월 릴리스된 카프카 4.0에서 ZooKeeper 모드가 완전히 제거됐다. 이제 KRaft가 유일한 운영 모드다
기준을 정리하면 이렇다. 카프카 2.x를 쓰는 현장이라면 여전히 ZooKeeper를 별도로 설치해 운영한다. 3.x라면 KRaft 모드를 선택할 수 있다(마이그레이션 도구는 3.7부터 실무 적용 가능). 4.0 이상은 ZooKeeper 자체가 없다. 지금 새로 배운다면 ZooKeeper는 배경지식으로만 알아 두고 KRaft를 전제로 이해하면 된다
정리
정리하면 카프카 클러스터는 브로커(서버) 위에 토픽(논리 단위)을 두고, 그 안의 파티션(물리 단위)에 메시지를 나눠 저장하는 구조다. 순서는 파티션 안에서만 보장되므로, 도메인 단위 순서가 필요하면 그 도메인 식별자를 키로 삼아 파티션을 고정한다. 파티션 개수는 나중에 늘리기 어려우니 규모·키 종류·확장 계획을 보고 처음부터 여유 있게 잡는 것이 가장 중요한 설계 판단이다
출처 – 인프런 강의 중 [핵심만 빠르게 끝내는 실전 카프카(kafka)]