> ## Documentation Index
> Fetch the complete documentation index at: https://blog.nvim.me/llms.txt
> Use this file to discover all available pages before exploring further.

# Topic은 왜 partition으로 나뉠까요?

> 두 주문의 record를 key로 두 partition에 배치하며 partition, offset, ordering의 정확한 범위를 연결해요.

> 결제 완료가 먼저였는데 배송 시작 record가 먼저 처리되면 어떡하죠?

주문 41번은 `PAID` 다음에 `SHIPPING`으로 바뀌어야 해요. 주문 45번은 `PAID` 다음에 `CANCELLED`가 됐고요. 두 주문이 동시에 들어오면 Kafka가 topic 전체를 하나의 긴 줄로 세워줄까요?

그렇지 않아요. Kafka에서 순서를 이야기할 때는 먼저 **어느 partition 안의 순서인지** 물어야 해요.

[앞 글](/messaging/kafka/run-local-kafka-and-send-first-record)에서 `orders.paid` topic의 partition 하나에 첫 record를 남겼어요. 이번에는 partition을 두 개로 늘린 새 topic을 만들고, 두 주문의 key가 각각 다른 줄로 가는 모습을 확인해볼게요.

<Note title="적용 버전">
  이 글의 동작 설명과 CLI 예제는 **Apache Kafka 4.3.1**을 기준으로 해요. Docker는 `apache/kafka:4.3.1`, Ubuntu 직접 설치는 OpenJDK 21과 `kafka_2.13-4.3.1.tgz`를 사용했어요. 아래의 정확한 partition 번호는 topic의 partition 수, 기본 partitioner, key serializer가 같을 때의 예제 결과예요.
</Note>

***

## Topic 하나에 기록 줄을 두 개 둬볼게요

Topic은 record를 묶는 논리적인 이름이에요. 실제 append와 읽기의 기본 단위는 그 안의 partition이에요.

```mermaid theme={null}
flowchart LR
    P[Producer]
    T[order-sequence topic]

    subgraph K[Kafka log]
        direction TB
        P0[partition 0<br />offset 0, 1, 2...]
        P1[partition 1<br />offset 0, 1, 2...]
    end

    P -->|record와 key| T
    T --> P0
    T --> P1
```

Partition마다 offset은 `0, 1, 2...`처럼 따로 증가해요. `partition 0의 offset 1`과 `partition 1의 offset 1`은 서로 다른 위치예요.

그래서 offset은 이 세 가지가 아니에요.

* Topic 전체에서 하나뿐인 record ID가 아니에요.
* 모든 partition을 합친 전역 순번이 아니에요.
* Business event의 식별자도 아니에요.

Record 위치를 가리키려면 최소한 **topic, partition, offset**을 함께 봐야 해요. 업무상 중복을 판별하려면 별도의 `eventId` 같은 business identifier가 필요하고요.

***

## 두 partition에 두 key를 보내봐요

앞 글의 `working-first-record` 상태에서 broker를 실행하세요. 이번 실습은 두 VM 모두 `orders.paid`가 있고 `order-sequence`는 아직 없는 상태에서 시작해요.

<Tabs>
  <Tab title="Docker">
    <Steps>
      <Step title="기존 broker와 topic을 먼저 확인해요">
        ```bash theme={null}
        sudo docker start aha-kafka

        sudo docker exec aha-kafka \
          /opt/kafka/bin/kafka-topics.sh \
          --bootstrap-server localhost:9092 \
          --describe \
          --topic orders.paid
        ```

        `orders.paid`의 partition 0이 보이면 앞 글의 broker에 연결된 거예요.
      </Step>

      <Step title="Partition이 두 개인 topic을 만들어요">
        ```bash theme={null}
        sudo docker exec aha-kafka \
          /opt/kafka/bin/kafka-topics.sh \
          --bootstrap-server localhost:9092 \
          --create \
          --topic order-sequence \
          --partitions 2 \
          --replication-factor 1

        sudo docker exec aha-kafka \
          /opt/kafka/bin/kafka-topics.sh \
          --bootstrap-server localhost:9092 \
          --describe \
          --topic order-sequence
        ```

        `PartitionCount: 2`와 partition 0, 1을 확인해요. Broker가 하나뿐이므로 replication factor는 1이에요.
      </Step>

      <Step title="Key와 value를 구분해 네 record를 보내요">
        ```bash theme={null}
        sudo docker exec -it aha-kafka \
          /opt/kafka/bin/kafka-console-producer.sh \
          --bootstrap-server localhost:9092 \
          --topic order-sequence \
          --reader-property parse.key=true \
          --reader-property key.separator=:
        ```

        `>` prompt에서 아래 네 줄을 차례로 입력한 뒤 `Ctrl+C`를 눌러요.

        ```text theme={null}
        order-41:PAID
        order-45:PAID
        order-41:SHIPPING
        order-45:CANCELLED
        ```
      </Step>

      <Step title="Partition과 offset을 함께 출력해요">
        ```bash theme={null}
        sudo docker exec aha-kafka \
          /opt/kafka/bin/kafka-console-consumer.sh \
          --bootstrap-server localhost:9092 \
          --topic order-sequence \
          --from-beginning \
          --max-messages 4 \
          --formatter-property print.key=true \
          --formatter-property print.partition=true \
          --formatter-property print.offset=true \
          --formatter-property key.separator=' | '
        ```
      </Step>
    </Steps>
  </Tab>

  <Tab title="Ubuntu 직접 설치">
    Kafka를 압축 해제한 `kafka_2.13-4.3.1` directory에서 실행해요. 앞 글에서 broker를 종료하지 않고 바로 넘어왔다면 첫 단계의 `kafka-server-start.sh`는 건너뛰세요.

    <Steps>
      <Step title="기존 broker와 topic을 먼저 확인해요">
        ```bash theme={null}
        bin/kafka-server-start.sh -daemon config/server.properties

        bin/kafka-topics.sh \
          --bootstrap-server localhost:9092 \
          --describe \
          --topic orders.paid
        ```

        `orders.paid`의 partition 0이 보이면 앞 글의 broker에 연결된 거예요.
      </Step>

      <Step title="Partition이 두 개인 topic을 만들어요">
        ```bash theme={null}
        bin/kafka-topics.sh \
          --bootstrap-server localhost:9092 \
          --create \
          --topic order-sequence \
          --partitions 2 \
          --replication-factor 1

        bin/kafka-topics.sh \
          --bootstrap-server localhost:9092 \
          --describe \
          --topic order-sequence
        ```

        `PartitionCount: 2`와 partition 0, 1을 확인해요.
      </Step>

      <Step title="Key와 value를 구분해 네 record를 보내요">
        ```bash theme={null}
        bin/kafka-console-producer.sh \
          --bootstrap-server localhost:9092 \
          --topic order-sequence \
          --reader-property parse.key=true \
          --reader-property key.separator=:
        ```

        `>` prompt에서 같은 네 줄을 입력한 뒤 `Ctrl+C`를 눌러요.

        ```text theme={null}
        order-41:PAID
        order-45:PAID
        order-41:SHIPPING
        order-45:CANCELLED
        ```
      </Step>

      <Step title="Partition과 offset을 함께 출력해요">
        ```bash theme={null}
        bin/kafka-console-consumer.sh \
          --bootstrap-server localhost:9092 \
          --topic order-sequence \
          --from-beginning \
          --max-messages 4 \
          --formatter-property print.key=true \
          --formatter-property print.partition=true \
          --formatter-property print.offset=true \
          --formatter-property key.separator=' | '
        ```
      </Step>
    </Steps>
  </Tab>
</Tabs>

두 트랙에서 확인한 record 배치는 같았어요. Partition 사이의 출력 묶음 순서는 달라질 수 있지만, 각 partition 안의 offset 순서는 유지돼요.

```text theme={null}
Partition:1 | Offset:0 | order-45 | PAID
Partition:1 | Offset:1 | order-45 | CANCELLED
Partition:0 | Offset:0 | order-41 | PAID
Partition:0 | Offset:1 | order-41 | SHIPPING
Processed a total of 4 messages
```

`order-41`과 `order-45`가 record key예요. `PAID`, `SHIPPING`, `CANCELLED`는 value고요.

<Check title="Key와 순서 확인">
  `order-41`의 두 record가 같은 partition에서 offset 0, 1로 이어지고, `order-45`의 두 record도 다른 한 partition에서 offset 0, 1로 이어졌다면 key 기반 배치를 확인한 거예요.
</Check>

<Info title="다음 글에서도 이 상태를 사용해요">
  `order-sequence` topic과 네 record를 지우지 마세요. 다음 글에서 `shipping-service`와 `analytics-service` group이 같은 네 record를 서로 다른 위치에서 읽어요. 다시 시작할 복구 지점이 필요하면 broker를 정상 종료한 뒤 두 VM에 `topic-ordering-ready` snapshot을 남기세요.
</Info>

***

## Key는 같은 업무 흐름을 같은 줄로 모아요

기본 producer partitioning logic은 partition을 직접 지정하지 않았고 key가 있다면 key의 hash를 사용해 partition을 골라요. 같은 key를 같은 serializer와 같은 partition 수로 보내면 같은 partition으로 모을 수 있어요.

```mermaid theme={null}
flowchart TB
    A1[order-41 · PAID]
    B1[order-45 · PAID]
    A2[order-41 · SHIPPING]
    B2[order-45 · CANCELLED]

    subgraph P0[partition 0]
        direction LR
        P00[offset 0<br />order-41 · PAID]
        P01[offset 1<br />order-41 · SHIPPING]
        P00 --> P01
    end

    subgraph P1[partition 1]
        direction LR
        P10[offset 0<br />order-45 · PAID]
        P11[offset 1<br />order-45 · CANCELLED]
        P10 --> P11
    end

    A1 --> P00
    A2 --> P01
    B1 --> P10
    B2 --> P11
```

이 예제에서 `orderId`를 key로 고른 이유는 **같은 주문의 상태 변화 순서**가 중요하기 때문이에요. 모든 주문을 한 줄로 모으려는 게 아니에요.

Key를 고를 때는 “무엇끼리 같은 순서를 공유해야 하나요?”를 물어보세요.

| 업무 규칙                    | 생각해볼 key                            |
| ------------------------ | ----------------------------------- |
| 같은 주문의 상태 변화는 순서가 중요해요   | `orderId`                           |
| 같은 계좌의 입출금은 순서가 중요해요     | `accountId`                         |
| 같은 device의 측정값은 순서가 중요해요 | `deviceId`                          |
| Tenant별 부하를 분리해야 해요      | `tenantId`를 그대로 쓰기 전에 skew를 함께 검토해요 |

Key를 너무 넓게 잡으면 많은 record가 한 partition으로 몰려요. 너무 잘게 나누면 함께 처리해야 할 업무 흐름이 여러 partition으로 흩어질 수 있어요.

***

## 순서 보장은 partition 경계를 넘지 않아요

위 출력에서 partition 1의 record가 먼저 보였다고 해서 `order-45`의 결제가 `order-41`보다 먼저 일어났다고 단정할 수 없어요. Consumer가 여러 partition을 fetch하고 결과를 출력하는 시점은 topic 전체의 event time 순서를 만들지 않아요.

<Warning title="Topic 전체의 global ordering으로 읽으면 안 돼요">
  Kafka가 기본적으로 제공하는 log order는 **같은 partition 안의 offset 순서**예요. Partition 0의 offset 1과 partition 1의 offset 1 중 무엇이 전역적으로 먼저인지 offset만으로 비교할 수 없어요.
</Warning>

순서에는 또 다른 경계도 있어요.

* Producer가 record를 어떤 순서로 보냈는지
* Broker가 같은 partition에 어떤 offset으로 저장했는지
* Consumer가 어떤 순서로 `poll`했는지
* Business code가 병렬 작업을 어떤 순서로 끝냈는지
* Database나 외부 API side effect가 어떤 순서로 반영됐는지

Kafka log의 순서가 맞아도 consumer가 record 두 개를 별도 thread에서 병렬 처리하면 뒤 record의 side effect가 먼저 끝날 수 있어요. 업무 순서를 지키려면 consumer 내부의 concurrency와 실패 처리까지 같은 key 경계에 맞춰야 해요.

***

## Key가 없으면 같은 흐름이 한 partition에 남는다고 기대할 수 없어요

Kafka 4.3의 기본 producer logic에서 key와 명시적인 partition이 모두 없으면, producer는 batch를 모으는 동안 한 partition을 사용하다가 조건에 따라 다른 partition으로 바꿀 수 있어요. 이것을 “한 줄씩 정확히 round-robin한다”라고 이해하면 안 돼요.

Key가 없는 record도 partition 안에서는 offset 순서를 갖지만, 서로 관련된 record가 계속 같은 partition으로 간다는 업무 보장은 없어요.

<Tip title="순서가 필요한 업무 단위를 먼저 찾으세요">
  “Kafka가 순서를 보장하나요?”보다 “어떤 entity의 어떤 변화끼리 같은 partition에 있어야 하나요?”라고 물으면 key를 고르기 쉬워져요.
</Tip>

***

## Partition 수를 늘리면 같은 key의 번호도 바뀔 수 있어요

기본 key partitioning은 key hash와 현재 partition 수를 함께 사용해요. Partition 수가 2에서 4로 늘어나면 같은 key가 앞으로 다른 partition 번호로 갈 수 있어요.

이미 저장된 record가 새 partition으로 이동하는 것은 아니에요. 증설 전 record는 예전 partition에 남고, 증설 뒤 record는 새 계산 결과의 partition에 저장될 수 있어요. 그러면 같은 key의 전체 history가 둘 이상의 partition에 걸칠 수 있어요.

<Warning title="Partition 증설을 단순한 scale-out 버튼으로 보지 마세요">
  Key별 순서, stateful processing, consumer 병렬성, broker disk와 network, replication 비용을 함께 검토하세요. 특히 같은 key의 긴 history를 한 partition에서 이어 읽는 가정이 있다면 증설 전후의 배치 변화를 시험해야 해요.
</Warning>

Partition을 줄이는 작업도 일반적인 topic 변경 명령으로 지원되지 않아요. 새 topic으로 옮기는 migration을 설계해야 할 수 있으므로 처음 수를 정할 때는 현재 처리량만 보지 말고 성장과 운영 비용을 함께 봐야 해요.

***

## Partition 수는 consumer 병렬성의 상한도 만들어요

Partition 두 개를 같은 consumer group이 읽는다면, 동시에 소유할 수 있는 consumer는 최대 두 개예요.

```mermaid theme={null}
flowchart LR
    subgraph T[order-sequence]
        P0[partition 0]
        P1[partition 1]
    end

    subgraph G[shipping-service consumer group]
        C1[consumer A]
        C2[consumer B]
        C3[consumer C<br />할당받을 partition 없음]
    end

    P0 --> C1
    P1 --> C2
    C3 -. 대기 .-> G
```

Consumer를 세 개 띄워도 partition이 두 개라면 같은 group의 한 consumer는 쉬어요. 반대로 consumer 하나가 partition 두 개를 모두 맡을 수도 있어요.

이 관계 때문에 partition은 저장 구조이면서 병렬 처리 단위예요. 다음 글에서 consumer group이 partition을 어떻게 나눠 맡고, offset을 어디까지 읽었다는 위치로 사용하는지 이어갈게요.

***

## 자, 정리해볼까요?

<Callout title="오늘 우리가 배운 것" color="#86EFAC">
  * Topic은 하나 이상의 partition으로 나뉘고, offset은 partition마다 `0, 1, 2...`로 따로 증가해요.

  * 기본 producer는 key가 있으면 key hash를 사용해 partition을 골라요.

  * 같은 key를 같은 partition에 모으면 그 partition의 offset 순서로 업무 흐름을 읽을 수 있어요.

  * Kafka의 ordering 범위는 topic 전체가 아니라 같은 partition 안이에요.

  * Partition 수를 바꾸면 key 배치가 달라질 수 있고, partition 수는 같은 consumer group의 병렬성 상한을 만들어요.
</Callout>

<Columns cols={2}>
  <Card title="이전 글" icon="arrow-left" href="/messaging/kafka/run-local-kafka-and-send-first-record">
    로컬 broker에서 첫 record를 보내고 재시작 뒤에도 읽어봐요.
  </Card>

  <Card title="다음 글" icon="users" href="/messaging/kafka/consumer-group-offset-and-commit">
    Consumer group이 partition을 나눠 맡고 commit으로 다음 읽기 위치를 남기는 과정을 따라가요.
  </Card>
</Columns>
