Kafka Topic·Partition·Offset 개념 — 구조·동작 원리·실전 설계


“카프카 쓰는 프로젝트에 들어가게 됐는데, Topic이 뭐고 Partition이 뭔지부터 모르겠어요.” 처음 Kafka를 접하면 낯선 용어들이 한꺼번에 쏟아집니다. Kafka Topic Partition Offset은 Kafka의 모든 동작이 시작되는 핵심 개념 3종 세트입니다. 이 세 가지의 관계를 제대로 이해하지 못하면 Kafka로 무언가를 만들 수는 있어도 왜 이렇게 설계되어 있는지, 왜 파티션 수를 신중하게 정해야 하는지, 왜 오프셋 커밋이 중요한지를 알 수 없습니다. 이 글에서는 Topic·Partition·Offset의 개념과 구조부터 컨슈머 그룹과의 관계, 오프셋 커밋 전략, 파티션 수 설계 기준, 면접 단골 질문까지 완전히 정리합니다.


목차

  1. Kafka란 무엇인가 — 분산 커밋 로그로 이해하는 전체 구조
  2. Topic — 메시지가 발행되는 논리적 카테고리
  3. Partition — 토픽을 실제로 저장하는 물리적 단위
  4. Offset — 파티션 안에서 메시지의 고유 위치 번호
  5. 컨슈머 그룹과 리밸런싱 — Partition·Offset이 실제로 작동하는 방식
  6. 오프셋 커밋 전략과 파티션 수 설계 기준

1. Kafka란 무엇인가 — 분산 커밋 로그로 이해하는 전체 구조

Kafka의 핵심 정체 — 분산 커밋 로그

카프카의 문서나 관련 서적에서 Kafka를 분산 커밋 로그(Distributed Commit Log)라고 부릅니다. 분산 커밋 로그에서의 로그가 Topic과 일치하는 개념이라고 보면 됩니다. Daishin

일반적인 메시지 큐는 소비자가 메시지를 읽으면 메시지가 사라집니다. Kafka는 다릅니다. 메시지는 읽혀도 삭제되지 않고, 설정된 보존 기간(기본 7일) 동안 그대로 유지됩니다. 이 특성이 Kafka를 단순한 메시지 큐가 아닌 이벤트 스트리밍 플랫폼으로 만들어줍니다.

Kafka의 전체 구조 한눈에 보기

Kafka는 Producer · Broker · Consumer로 구성됩니다.

[Producer] → [Kafka Broker Cluster] → [Consumer]
               │
               ├── Topic A
               │   ├── Partition 0  [0][1][2][3][4]
               │   ├── Partition 1  [0][1][2][3]
               │   └── Partition 2  [0][1][2][3][4][5]
               │
               └── Topic B
                   ├── Partition 0  [0][1][2]
                   └── Partition 1  [0][1][2][3]
  • Producer: 메시지를 만들어 Kafka에 보내는 주체 (앱 서버, 데이터 수집기 등)
  • Broker: 메시지를 저장하고 전달하는 Kafka 서버 (클러스터 형태로 운영)
  • Consumer: 메시지를 읽어 처리하는 주체 (앱 서버, 분석 시스템 등)
  • Topic·Partition·Offset: Broker 안에서 메시지가 저장·관리되는 구조 단위

2. Topic — 메시지가 발행되는 논리적 카테고리

Topic의 정의

Topic은 메시지가 발행되는 카테고리 혹은 Feed로서, 메시지들의 스트림으로 볼 수 있습니다. 다양한 생산자(Producer)와 소비자(Consumer)들이 Topic에 메시지를 발행하거나, Topic으로부터 발행된 메시지를 가져와 사용합니다. Daishin

쉽게 말하면 Topic은 Kafka의 ‘채널’ 또는 ‘폴더’ 같은 개념입니다. 주문이 발생하면 order-events 토픽에, 결제가 완료되면 payment-events 토픽에 메시지를 씁니다. 각 관심사별로 토픽을 분리해 메시지 흐름을 관리합니다.

Topic의 규칙과 특성

토픽의 이름은 249자 미만으로 영문, 숫자, ‘.’, ‘_’, ‘-‘를 조합하여 만들 수 있습니다. 토픽은 1개 이상의 파티션으로 구성되어 있습니다. Daishin

Topic 이름 설계에서 실무 권장 컨벤션은 .을 구분자로 사용하는 것입니다. 예: order.created, payment.completed, user.signup. 단, _.을 혼용하면 내부 메트릭 충돌 문제가 생길 수 있어 둘 중 하나만 일관되게 사용하는 것이 좋습니다.


3. Partition — 토픽을 실제로 저장하는 물리적 단위

Partition의 정의와 존재 이유

토픽은 메시지를 받을 수 있도록 논리적으로 묶은 개념이고, 파티션은 토픽을 구성하는 물리적 데이터 저장소이며 수평 확장이 가능한 단위입니다. Daishin

Partition이 필요한 이유는 두 가지입니다.

이유 ① 수평 확장 (Scale-Out)

하나의 Topic이 하나의 서버에 종속되는 구조라면 Topic을 구성하는 데이터의 크기는 종속된 서버의 저장소 크기보다 작아야 합니다. 파티션으로 나누면 각 파티션을 다른 브로커(서버)에 분산 저장할 수 있어 토픽의 크기가 서버 한 대의 용량을 초과해도 됩니다. Daishin

이유 ② 병렬 처리 (Parallel Processing)

파티션마다 컨슈머를 하나씩 붙이면 여러 파티션을 동시에 읽을 수 있습니다. 파티션이 1개라면 아무리 컨슈머를 늘려도 한 번에 하나씩만 읽을 수 있습니다. 파티션 3개라면 컨슈머 3개가 동시에 읽어 처리 속도가 3배 빨라집니다.

Partition의 핵심 특성 — append-only

Partition 0:  [msg0] → [msg1] → [msg2] → [msg3] → [새 메시지 추가만 가능]
Partition 1:  [msg0] → [msg1] → [msg2] → [새 메시지 추가만 가능]

파티션은 append-only로 동작하며 새 메시지는 파티션 맨 뒤에 추가됩니다. 기존 메시지를 수정하거나 중간에 끼워 넣는 것은 불가능합니다. 이 불변성(immutability)이 Kafka의 높은 처리 속도와 재현 가능성의 근원입니다. Daishin

파티션과 메시지 순서 — 핵심 주의사항

카프카에서는 오프셋을 이용하여 메시지의 순서를 보장합니다. 그렇기 때문에 파티션 내에서는 순서를 보장하지만 파티션 간에는 데이터 순서를 보장하지 않습니다. 그렇기 때문에 순서를 보장해야 하는 메시지의 경우 파티션 설정에 주의해야 합니다. Daishin

“같은 사용자의 주문 이벤트는 반드시 순서대로 처리되어야 한다”는 요구사항이 있다면, 해당 사용자의 모든 메시지가 동일한 파티션으로 가야 합니다. 이때 Producer가 메시지를 보낼 때 **파티션 키(Partition Key)**로 사용자 ID를 지정합니다. 같은 키의 메시지는 항상 같은 파티션으로 라우팅되어 순서가 보장됩니다.

java

// 파티션 키 지정 — 같은 userId는 항상 같은 파티션으로
ProducerRecord<String, String> record = new ProducerRecord<>(
    "order-events",      // 토픽
    userId.toString(),   // 파티션 키 ← 같은 키는 같은 파티션 보장
    orderJson            // 메시지 값
);
producer.send(record);

키를 지정하지 않으면 Kafka는 기본적으로 Round-Robin 방식으로 파티션에 분산합니다.


4. Offset — 파티션 안에서 메시지의 고유 위치 번호

Offset의 정의

오프셋은 파티션 내에서 메시지를 식별하는 유니크한 값이며 순차적으로 증가하는 64비트 정수 형태로 되어 있으며, 0부터 시작합니다. Daishin

책의 페이지 번호 같은 것입니다. Partition 0의 offset 5번은 “Partition 0에서 다섯 번째로 들어온 메시지”를 의미하며, 이 번호는 절대 바뀌지 않습니다. 메시지가 더 많이 쌓이면 offset은 0, 1, 2, 3, 4, 5, 6, 7… 계속 증가합니다.

오프셋의 중요한 특성

Topic: order-events
│
├── Partition 0: [0:주문A] [1:주문B] [2:주문C] [3:주문D] [4:주문E]
│                                         ↑
│                              Consumer가 여기까지 읽음 (offset 2 커밋)
│
├── Partition 1: [0:주문F] [1:주문G] [2:주문H]
│                    ↑
│         Consumer가 여기까지 읽음 (offset 0 커밋)
│
└── Partition 2: [0:주문I] [1:주문J] [2:주문K] [3:주문L]
                                               ↑
                                Consumer가 여기까지 읽음 (offset 3 커밋)

토픽 기준으로 보면, 각 파티션마다 오프셋이 동일하게 존재하지만, 파티션별로 보면 오프셋은 유일합니다. 즉, Partition 0의 offset 5와 Partition 1의 offset 5는 서로 다른 메시지입니다. 오프셋은 ‘파티션 내에서만’ 고유합니다. Daishin

Apache Kafka에서 Offset은 각 Partition 내 메시지의 순서를 나타내는 고유 번호로, 메시지가 들어오는 순서대로 증가합니다. Consumer는 마지막으로 읽은 Offset을 저장한 이후에 데이터를 처리하며, 이를 통해 중복 처리와 데이터 손실을 방지합니다. Thinkpool

auto.offset.reset — 처음 연결하거나 오프셋이 없을 때

만약 Consumer가 Offset 정보를 잃어버리거나 처음 Kafka에 연결된다면, auto.offset.reset 설정에 따라 데이터를 처음부터(earliest) 또는 마지막부터(latest) 읽게 됩니다. Thinkpool

yaml

# application.yml — Spring Kafka 설정
spring:
  kafka:
    consumer:
      auto-offset-reset: earliest  # earliest: 처음부터 / latest: 최신부터
  • earliest: 해당 파티션의 가장 오래된 메시지(offset 0)부터 읽기 시작. 누락 없이 모든 메시지를 처리해야 할 때.
  • latest: 새로 들어오는 메시지부터 읽기 시작. 실시간 처리 우선, 이전 메시지 불필요 시.

5. 컨슈머 그룹과 리밸런싱 — Partition·Offset이 실제로 작동하는 방식

컨슈머 그룹(Consumer Group)이란

여러 컨슈머를 하나의 논리적 그룹으로 묶은 단위입니다. 컨슈머 그룹이 중요한 이유는 파티션과 컨슈머의 대응 관계를 결정하기 때문입니다.

핵심 규칙:

파티션은 컨슈머 그룹 내에서 오직 1개의 컨슈머에게만 배치될 수 있습니다. 한 개의 컨슈머가 여러 개의 파티션을 가질 수는 있지만, 한 개의 파티션이 여러 개의 컨슈머에 배정될 수는 없습니다. tistory

이 규칙에서 중요한 결론이 나옵니다.

파티션 3개, 컨슈머 그룹의 컨슈머 3개 (이상적 구성):
  Partition 0 → Consumer 1
  Partition 1 → Consumer 2
  Partition 2 → Consumer 3

파티션 3개, 컨슈머 그룹의 컨슈머 2개:
  Partition 0 → Consumer 1
  Partition 1 → Consumer 1  ← Consumer 1이 2개 담당
  Partition 2 → Consumer 2

파티션 3개, 컨슈머 그룹의 컨슈머 4개:
  Partition 0 → Consumer 1
  Partition 1 → Consumer 2
  Partition 2 → Consumer 3
  Consumer 4  → 유휴 상태 (아무 파티션도 없음)

파티션의 갯수보다 컨슈머 그룹 내 컨슈머의 갯수가 많으면 유휴 컨슈머가 발생합니다. 따라서 컨슈머를 아무리 늘려도 파티션 수 이상의 처리량 향상은 얻을 수 없습니다. tistory

컨슈머 그룹 격리 — 독립적인 오프셋 관리

오프셋은 Consumer Group당 그리고 Topic당 관리됩니다. 따라서 Consumer Group이 다르면 Topic이 같아도 서로 각자 오프셋을 관리합니다. tistory

이것이 Kafka의 강력한 장점입니다. 같은 토픽의 메시지를 여러 서비스가 동시에 독립적으로 소비할 수 있습니다.

Topic: order-events
│
├── Consumer Group A (주문 처리 서버)
│   → Partition 0: offset 100 커밋
│   → Partition 1: offset 87 커밋
│
└── Consumer Group B (분석 서버)
    → Partition 0: offset 50 커밋  ← 독립적으로 다른 위치에서 읽는 중
    → Partition 1: offset 40 커밋

같은 메시지를 두 그룹이 각자의 속도로 처리합니다. 그룹 A가 메시지를 이미 읽었다고 그룹 B에게 영향이 없습니다.

리밸런싱(Rebalancing) — 파티션 소유권 재분배

리밸런싱이란 특정 토픽을 구독하던 컨슈머 그룹에 변동이 있는 경우에 해당 그룹 안의 파티션을 재분배하는 행위입니다. Stocktwits

리밸런싱이 발생하는 조건:

  • 컨슈머 그룹에 새로운 컨슈머가 추가될 때
  • 기존 컨슈머가 종료되거나 장애가 발생할 때
  • session.timeout.ms 시간 내에 하트비트가 오지 않을 때
  • max.poll.interval.ms를 초과해 poll()이 늦어질 때

리밸런싱 중에는 컨슈머가 데이터를 읽지 못합니다. 따라서 리밸런싱이 잦으면 성능에 매우 큰 문제를 일으킵니다. 무거운 처리 로직이 있다면 max.poll.interval.ms를 적절히 늘리거나, 처리 로직을 비동기로 분리하는 것이 중요합니다. tistory


6. 오프셋 커밋 전략과 파티션 수 설계 기준

오프셋 커밋의 핵심 원리

컨슈머가 오프셋을 커밋하면 내부적으로 __consumer_offsets라는 이름의 특별한 토픽에 메시지를 씁니다. 이 토픽은 모든 컨슈머의 오프셋을 가집니다. tistory

커밋은 “나는 이 오프셋까지 처리 완료했다”를 Kafka에 기록하는 행위입니다. 컨슈머가 재시작되거나 리밸런싱 후에 어디서부터 읽어야 하는지 이 커밋 정보를 기반으로 결정됩니다.

오프셋 커밋의 두 가지 함정

마지막으로 커밋된 오프셋이 컨슈머가 가장 최근에 읽고 처리한 메시지의 오프셋보다 작으면, 그 사이의 메시지들이 두 번 처리됩니다. 마지막으로 커밋된 오프셋이 가장 최근에 읽고 처리한 메시지의 오프셋보다 크다면, 그 사이의 메시지들은 컨슈머 그룹에서 누락됩니다. tistory

처리한 메시지: offset 0, 1, 2, 3, 4
커밋한 오프셋: 2

→ 컨슈머 재시작 시: 3, 4가 다시 처리됨 (중복 처리)

처리한 메시지: offset 0, 1, 2, 3, 4
커밋한 오프셋: 7 (아직 처리하지 않은 미래 오프셋)

→ 컨슈머 재시작 시: 5, 6이 누락됨 (메시지 손실)

자동 커밋 vs 수동 커밋

java

// 자동 커밋 — 설정 (기본값)
enable.auto.commit=true
auto.commit.interval.ms=5000  // 5초마다 자동 커밋

// ⚠️ 문제: 처리 완료 전에 자동 커밋이 일어나면 리밸런싱 시 누락 가능

java

// 수동 커밋 — 처리 완료 후 직접 커밋 (권장)
@KafkaListener(topics = "order-events", groupId = "order-group")
public void consume(ConsumerRecord<String, String> record,
                    Acknowledgment ack) {
    try {
        processOrder(record.value());  // 비즈니스 로직 처리
        ack.acknowledge();              // 처리 완료 후 수동 커밋
    } catch (Exception e) {
        // 실패 시 커밋하지 않으면 다음 poll 때 재처리
        log.error("처리 실패: {}", e.getMessage());
    }
}

yaml

# Spring Kafka 수동 커밋 설정
spring:
  kafka:
    listener:
      ack-mode: manual_immediate  # 수동 커밋 모드
    consumer:
      enable-auto-commit: false

파티션 수 설계 기준 — 실전 체크리스트

파티션 수는 나중에 줄일 수 없습니다(늘리는 것만 가능). 처음 설계가 매우 중요합니다.

고려 사항가이드라인
목표 처리량파티션 1개당 처리량(Throughput)을 측정하고 목표 TPS ÷ 단일 파티션 TPS로 계산
컨슈머 수최대로 운영할 컨슈머 수 이상의 파티션을 확보
메시지 순서순서 보장이 필요한 단위(사용자 ID 등)의 최대 동시 처리 단위 수 고려
초기 설정 권장처음엔 낮게 시작(6~12), 모니터링 후 증설 (Lag 증가 시)
절대 파티션 수브로커 수 × 복제 인수를 넘지 않는 범위에서 결정

파티션의 갯수는 항상 컨슈머 그룹의 컨슈머 갯수보다 많이 설정해야 합니다. 최소한 최대 예상 컨슈머 수와 동일하게 설정하고, 여유분을 두어 스케일 아웃에 대비하는 것이 실무 권장 사항입니다. tistory

면접 핵심 질문 Q&A

Q. Topic, Partition, Offset의 관계를 한 문장으로 설명하면?

“Topic은 메시지를 담는 논리적 채널이고, Partition은 그 채널을 물리적으로 분산 저장하는 단위이며, Offset은 각 Partition 안에서 메시지를 순서대로 식별하는 고유 번호입니다.”

Q. 파티션이 많을수록 항상 좋은가?

아닙니다. 파티션 수가 증가하면 병렬 처리 성능은 올라가지만, 파일 핸들러·메모리·리밸런싱 비용도 함께 증가합니다. 또한 파티션 간 순서는 보장되지 않아 순서가 중요한 메시지에서 버그가 생길 수 있습니다.

Q. 왜 파티션 수보다 컨슈머가 많으면 안 되는가?

파티션 1개는 동시에 1개의 컨슈머만 읽을 수 있습니다. 컨슈머가 파티션보다 많으면 초과분의 컨슈머는 아무 파티션도 할당받지 못해 유휴 상태가 됩니다. 리소스만 차지하고 실제로 아무 일도 하지 않습니다.


결론

Kafka Topic Partition Offset의 관계를 세 줄로 압축하면 이렇습니다. Topic은 메시지의 논리적 분류 단위, Partition은 그것을 물리적으로 분산·저장하는 단위로 병렬 처리의 기반이 됩니다. Offset은 파티션 내 메시지의 절대 위치 번호로, 컨슈머가 어디까지 읽었는지를 추적하는 핵심 메커니즘입니다. 처음 Kafka를 설계할 때 파티션 수는 신중하게 정해야 합니다. 나중에 늘릴 수는 있어도 줄일 수는 없으며, 파티션 수가 최대 병렬 처리 성능의 상한선을 결정하기 때문입니다. 오프셋 커밋 전략(자동 vs 수동)은 중복 처리와 메시지 누락을 방지하는 첫 번째 방어선입니다. 처리 보장이 중요한 시스템이라면 반드시 수동 커밋을 선택하세요.


ℹ️ 참고 안내 본 글은 Apache Kafka 3.x 기준으로 작성되었습니다. Spring for Apache Kafka(spring-kafka) 3.x를 기준으로 코드 예시가 작성됐습니다. KRaft 모드(ZooKeeper 제거, Kafka 3.3+)를 사용하는 환경에서는 일부 내부 동작 방식이 다를 수 있습니다. 더 깊은 내용은 Apache Kafka 공식 문서(kafka.apache.org/documentation)를 참고하시기 바랍니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다