> ## 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.

# 로컬 Kafka에 첫 record를 남겨볼까요?

> Apache Kafka 4.3.1을 Docker 또는 직접 설치로 실행하고 topic 생성, produce, consume, 재시작 뒤 보존까지 확인해요.

> 결제 완료 record를 보냈는데, Kafka 안에서는 무엇이 생겼는지 직접 보고 싶어요.

[앞 글](/messaging/kafka/prepare-kafka-lab-on-ubuntu-vm)에서 `kafka-docker`와 `kafka-ubuntu` VM을 같은 profile로 준비했어요. 그보다 앞에서는 Kafka를 “메시지를 건네고 없애는 우체통”보다 “partition에 이어지는 기록”으로 바라봤고요.

이번에는 그 그림을 터미널에서 확인해볼게요. 로컬 broker 하나를 실행하고, `orders.paid` topic에 record를 남긴 뒤 다시 읽어요. Broker를 재시작해도 같은 record를 읽을 수 있는지까지 확인합니다.

<Note title="적용 버전">
  이 실습은 **Apache Kafka 4.3.1**을 기준으로 해요. Docker 트랙은 Docker Engine **29.5.3**과 공식 `apache/kafka:4.3.1` image를, 직접 설치 트랙은 Ubuntu **22.04 LTS aarch64**, OpenJDK **21.0.11**, `kafka_2.13-4.3.1.tgz`를 사용해요. 앞 글의 Ubuntu 26.04 LTS amd64 VM에서도 같은 Kafka 4.3.1과 Java 21 조합을 사용해요.
</Note>

***

## 시작하기 전에 환경을 확인해요

이번 실습은 학습용 단일 node예요. 한 process가 broker와 KRaft controller 역할을 함께 맡아요. 여러 broker에 복제하는 production cluster와는 장애 범위가 달라요.

<Tabs>
  <Tab title="Docker">
    | 항목         | 값                                              |
    | ---------- | ---------------------------------------------- |
    | VM         | `kafka-docker`                                 |
    | 운영체제       | Ubuntu 26.04 LTS                               |
    | 필수 도구      | Docker Engine                                  |
    | 사용하는 Kafka | `apache/kafka:4.3.1`                           |
    | Broker 주소  | VM 안의 `localhost:9092`                         |
    | Disk       | Image, container, 실습 log를 위해 **2 GB 이상 여유 권장** |
  </Tab>

  <Tab title="Ubuntu 직접 설치">
    | 항목         | 값                                               |
    | ---------- | ----------------------------------------------- |
    | VM         | `kafka-ubuntu`                                  |
    | 운영체제       | Ubuntu 26.04 LTS                                |
    | 필수 도구      | OpenJDK 21, `curl`, `tar`, `sha512sum`          |
    | 사용하는 Kafka | `kafka_2.13-4.3.1.tgz`                          |
    | Broker 주소  | VM 안의 `localhost:9092`                          |
    | Disk       | Archive, 압축 해제 파일, 실습 log를 위해 **2 GB 이상 여유 권장** |
  </Tab>
</Tabs>

2 GB는 Kafka의 production 용량 기준이 아니에요. 이번 실습에서 image 또는 binary와 작은 log를 담기 위한 여유예요. 실제 보관 용량은 record 크기, 유입량, retention, replication에 맞춰 별도로 계산해야 해요.

두 VM은 `tooling-ready` snapshot에서 시작해야 해요. 이전 실행에서 `aha-kafka` container, `orders.paid` topic, `$HOME/kafka-data`가 이미 생겼다면 snapshot을 복원하거나 기존 상태가 필요한지 먼저 판단하세요.

***

## 두 VM에서 같은 결과를 확인해요

작성할 때는 두 경로를 모두 검증해요. 학습할 때는 한 탭을 끝낸 뒤 다른 VM에서 같은 순서를 반복하면 됩니다. 둘 다 metadata, produce, consume, restart를 같은 fixture로 확인해요.

<Tabs>
  <Tab title="Docker">
    <Steps>
      <Step title="Port와 Docker 상태를 확인해요">
        먼저 Docker daemon이 응답하는지 확인해요.

        ```bash theme={null}
        sudo docker version
        ```

        `9092` port를 이미 사용하는 process가 있다면 종료하거나 다른 실습 환경을 골라야 해요.

        ```bash theme={null}
        lsof -nP -iTCP:9092 -sTCP:LISTEN
        ```

        아무 줄도 나오지 않으면 `9092`를 사용할 수 있어요.
      </Step>

      <Step title="공식 image로 broker를 실행해요">
        Version이 움직이지 않도록 `latest` 대신 `4.3.1` tag를 고정해요.

        ```bash theme={null}
        sudo docker pull apache/kafka:4.3.1

        sudo docker run -d \
          --name aha-kafka \
          -p 9092:9092 \
          apache/kafka:4.3.1
        ```

        `-p 9092:9092`는 host의 `localhost:9092`를 container의 listener에 연결해요. 이 단일 container 실습에서는 별도 Docker network가 필요하지 않아요.

        Broker log가 궁금하면 다음 명령으로 따라가요. 시작이 끝나면 `Ctrl+C`로 log 보기만 종료할 수 있어요.

        ```bash theme={null}
        sudo docker logs -f aha-kafka
        ```
      </Step>

      <Step title="Metadata 요청으로 broker 상태를 확인해요">
        “Container가 실행 중”과 “Kafka가 요청에 응답함”은 다른 상태예요. Topic 목록 요청이 성공하는지 확인해요.

        ```bash theme={null}
        sudo docker exec aha-kafka \
          /opt/kafka/bin/kafka-topics.sh \
          --bootstrap-server localhost:9092 \
          --list
        ```

        처음에는 topic 이름이 없어 빈 결과가 나올 수 있어요. 오류 없이 끝났다면 broker가 metadata 요청에 응답한 거예요.
      </Step>

      <Step title="Topic을 만들고 partition을 확인해요">
        첫 실습은 record의 위치를 한 줄로 보기 위해 partition 하나를 사용해요.

        ```bash theme={null}
        sudo docker exec aha-kafka \
          /opt/kafka/bin/kafka-topics.sh \
          --bootstrap-server localhost:9092 \
          --create \
          --topic orders.paid \
          --partitions 1 \
          --replication-factor 1

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

        설명에서 `PartitionCount: 1`과 `ReplicationFactor: 1`을 확인해요. 복제본 하나는 현재 broker 하나에만 있으므로 broker 자체가 사라지는 장애를 견디는 구성은 아니에요.
      </Step>

      <Step title="첫 record를 왕복해요">
        Console producer에서 입력한 한 줄이 record 하나가 돼요.

        ```bash theme={null}
        sudo docker exec -it aha-kafka \
          /opt/kafka/bin/kafka-console-producer.sh \
          --bootstrap-server localhost:9092 \
          --topic orders.paid
        ```

        `>` prompt가 보이면 아래 JSON 한 줄을 붙여 넣고 `Enter`, `Ctrl+C`를 차례로 눌러요.

        ```json theme={null}
        {"eventId":"evt-101","orderId":41,"status":"PAID"}
        ```

        이제 log의 처음부터 한 record만 읽어요.

        ```bash theme={null}
        sudo docker exec aha-kafka \
          /opt/kafka/bin/kafka-console-consumer.sh \
          --bootstrap-server localhost:9092 \
          --topic orders.paid \
          --from-beginning \
          --max-messages 1 \
          --formatter-property print.partition=true \
          --formatter-property print.offset=true
        ```

        JSON 앞에 `Partition:0`과 `Offset:0`이 보이면 record 하나가 partition 0의 첫 위치에 저장된 거예요.
      </Step>

      <Step title="Broker를 재시작하고 다시 읽어요">
        같은 container를 재시작한 뒤 metadata 요청과 consume을 다시 실행해요.

        ```bash theme={null}
        sudo docker restart aha-kafka

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

        sudo docker exec aha-kafka \
          /opt/kafka/bin/kafka-console-consumer.sh \
          --bootstrap-server localhost:9092 \
          --topic orders.paid \
          --from-beginning \
          --max-messages 1
        ```

        같은 JSON이 다시 보이면 broker process가 재시작되어도 log data를 다시 연 거예요.
      </Step>
    </Steps>
  </Tab>

  <Tab title="Ubuntu 직접 설치">
    <Steps>
      <Step title="Java와 Port를 확인해요">
        Kafka 4.3.1 quickstart는 Java 17 이상을 요구해요.

        ```bash theme={null}
        java -version
        lsof -nP -iTCP:9092 -sTCP:LISTEN
        ```

        Java major version이 17 이상이고, 두 번째 명령에서 listener가 나오지 않는지 확인해요.
      </Step>

      <Step title="Binary를 받고 SHA-512를 비교해요">
        Apache download server에서 binary와 checksum을 함께 받아요.

        ```bash theme={null}
        curl -fSLO https://downloads.apache.org/kafka/4.3.1/kafka_2.13-4.3.1.tgz
        curl -fSLO https://downloads.apache.org/kafka/4.3.1/kafka_2.13-4.3.1.tgz.sha512

        expected_sha512="$(sed 's/^[^:]*: //' kafka_2.13-4.3.1.tgz.sha512 | tr -d '[:space:]')"
        actual_sha512="$(sha512sum kafka_2.13-4.3.1.tgz | awk '{print toupper($1)}')"
        test "$actual_sha512" = "$expected_sha512" && echo "SHA-512 OK"
        ```

        `SHA-512 OK`가 나오기 전에는 archive를 실행하지 마세요. 더 강한 출처 확인이 필요하다면 Apache의 `KEYS`와 `.asc` signature를 사용해 서명도 검증할 수 있어요.

        ```bash theme={null}
        tar -xzf kafka_2.13-4.3.1.tgz
        cd kafka_2.13-4.3.1
        ```
      </Step>

      <Step title="KRaft storage를 format하고 broker를 실행해요">
        학습 data를 VM의 home directory 아래에 두고, 새 cluster ID로 standalone KRaft storage를 한 번 format해요. 이 글에서는 로그인한 일반 사용자가 broker process와 data를 소유합니다.

        ```bash theme={null}
        mkdir -p "$HOME/kafka-data"
        sed -i "s|^log.dirs=.*|log.dirs=$HOME/kafka-data|" config/server.properties

        KAFKA_CLUSTER_ID="$(bin/kafka-storage.sh random-uuid)"
        bin/kafka-storage.sh format \
          --standalone \
          -t "$KAFKA_CLUSTER_ID" \
          -c config/server.properties

        bin/kafka-server-start.sh -daemon config/server.properties
        ```

        Storage format은 빈 data directory를 초기화하는 단계예요. 이미 data가 있는 directory를 매번 다시 format하는 시작 명령으로 사용하면 안 돼요.
      </Step>

      <Step title="Metadata 요청으로 broker 상태를 확인해요">
        Topic 목록 요청이 성공하는지 확인해요.

        ```bash theme={null}
        bin/kafka-topics.sh \
          --bootstrap-server localhost:9092 \
          --list
        ```

        빈 결과라도 오류 없이 끝났다면 broker가 요청에 응답한 거예요. Broker log는 기본 설정의 `logs/`와 실행 terminal에서 확인할 수 있어요.
      </Step>

      <Step title="Topic을 만들고 partition을 확인해요">
        ```bash theme={null}
        bin/kafka-topics.sh \
          --bootstrap-server localhost:9092 \
          --create \
          --topic orders.paid \
          --partitions 1 \
          --replication-factor 1

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

        `PartitionCount: 1`과 `ReplicationFactor: 1`을 확인해요.
      </Step>

      <Step title="첫 record를 왕복해요">
        ```bash theme={null}
        bin/kafka-console-producer.sh \
          --bootstrap-server localhost:9092 \
          --topic orders.paid
        ```

        `>` prompt에서 아래 한 줄을 입력하고 `Enter`, `Ctrl+C`를 눌러요.

        ```json theme={null}
        {"eventId":"evt-101","orderId":41,"status":"PAID"}
        ```

        ```bash theme={null}
        bin/kafka-console-consumer.sh \
          --bootstrap-server localhost:9092 \
          --topic orders.paid \
          --from-beginning \
          --max-messages 1 \
          --formatter-property print.partition=true \
          --formatter-property print.offset=true
        ```

        JSON 앞에서 `Partition:0`과 `Offset:0`을 확인해요.
      </Step>

      <Step title="Broker를 재시작하고 다시 읽어요">
        정상 종료한 뒤 같은 `config/server.properties`와 data directory로 다시 시작해요.

        ```bash theme={null}
        bin/kafka-server-stop.sh
        bin/kafka-server-start.sh -daemon config/server.properties

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

        bin/kafka-console-consumer.sh \
          --bootstrap-server localhost:9092 \
          --topic orders.paid \
          --from-beginning \
          --max-messages 1
        ```

        같은 JSON이 보이면 재시작 뒤에도 log를 다시 읽은 거예요.
      </Step>
    </Steps>
  </Tab>
</Tabs>

<Check title="첫 왕복의 성공 기준">
  Topic 설명에서 partition 0이 보이고, producer가 보낸 JSON을 consumer가 `Partition:0`, `Offset:0`에서 읽었으며, broker 재시작 뒤에도 같은 JSON을 다시 읽었다면 실습이 끝났어요.
</Check>

***

## 방금 일어난 일을 그림으로 연결해요

```mermaid theme={null}
sequenceDiagram
    participant P as Console producer
    participant B as Kafka broker
    participant L as orders.paid-0 log
    participant C as Console consumer

    P->>B: 결제 완료 record produce
    B->>L: offset 0에 append
    B-->>P: 저장 결과 응답
    C->>B: partition 0을 처음부터 fetch
    B->>L: offset 0 읽기
    L-->>C: 결제 완료 record
```

Producer가 받은 응답은 broker가 record를 받아들였다는 뜻이에요. 배송 업무가 끝났다는 뜻은 아니에요. Consumer도 `poll`, 업무 처리, 읽기 위치 저장을 서로 다른 경계로 다뤄야 해요. 이번 console consumer는 그중 record를 가져오는 모습을 보여줬어요.

`--from-beginning`으로 새 consumer를 다시 실행했기 때문에 같은 record를 또 읽을 수 있었어요. Record는 누군가 한 번 출력했다고 바로 지워지지 않아요. Topic의 retention 범위 안에서 log에 남아요.

***

## Port, listener, network는 왜 자주 막힐까요?

`--bootstrap-server localhost:9092`는 첫 연결 주소예요. Client는 이 주소로 cluster metadata를 받은 뒤, broker가 알려준 **advertised listener** 주소로 실제 연결을 이어가요.

이번 단일 node 예제에서는 host와 container가 모두 `localhost:9092`를 사용할 수 있도록 공식 image의 기본값과 port mapping을 그대로 사용했어요. Client를 다른 container나 다른 computer로 옮기면 `localhost`가 가리키는 곳이 달라져요. 그때는 Docker network에서 해석 가능한 broker 이름과 host에서 접근 가능한 advertised listener를 분리해서 설계해야 해요.

<Warning title="이 예제를 production listener 설정으로 복사하지 마세요">
  이 실습은 인증과 암호화가 없는 local PLAINTEXT 연결이에요. Production에서는 network 경계, TLS, 인증, 여러 broker의 listener와 advertised listener를 함께 설계해야 해요.
</Warning>

***

## Data를 지우면 무엇이 사라질까요?

Docker 경로는 \*\*같은 container를 `docker restart`\*\*했을 때 container filesystem의 log를 다시 열어요. `docker rm`으로 container를 제거하면 그 실습 data도 함께 잃는다고 생각하는 편이 안전해요. 장기간 유지할 cluster라면 명시적인 volume과 `log.dirs` 설정, backup과 recovery 절차가 필요해요.

Ubuntu 직접 설치 경로는 `config/server.properties`의 `log.dirs`를 `$HOME/kafka-data`로 바꿨어요. VM을 재시작해도 home directory가 남아 있는 동안 record와 consumer offset을 다시 열 수 있어요. Production에서는 별도 service user와 용량·권한을 관리할 전용 filesystem을 설계해야 해요.

<Warning title="Cleanup은 record를 되돌릴 수 없게 지워요">
  아래 명령은 실습 container와 그 안의 Kafka data를 제거해요. `orders.paid` record가 더 필요하지 않을 때만 실행하세요.

  ```bash theme={null}
  sudo docker rm -f aha-kafka
  ```

  직접 설치의 data directory는 위치를 `config/server.properties`에서 확인하기 전까지 삭제하지 마세요. 경로를 확인한 뒤에도 필요한 record와 consumer offset을 backup했는지 먼저 판단해야 해요.
</Warning>

***

## 막히면 이 순서로 살펴봐요

<AccordionGroup>
  <Accordion title="9092 port를 이미 사용하고 있어요">
    `lsof -nP -iTCP:9092 -sTCP:LISTEN`으로 process를 확인해요. 기존 broker를 의도적으로 사용하려는 것이 아니라면 먼저 종료하고 다시 시작하세요. Docker port만 바꾸면 advertised listener와 client 주소도 함께 맞춰야 하므로 처음 실습에서는 빈 `9092`를 쓰는 편이 덜 헷갈려요.
  </Accordion>

  <Accordion title="Connection to node could not be established가 보여요">
    Container 상태만 보지 말고 `sudo docker logs aha-kafka` 또는 직접 설치의 broker log를 확인해요. 시작이 끝나기 전에 client 명령을 보냈거나, listener가 client가 접근할 수 없는 주소를 advertise했을 수 있어요.
  </Accordion>

  <Accordion title="Java version 오류가 나요">
    `java -version`과 `echo "$JAVA_HOME"`을 확인해요. Kafka 4.3.1 quickstart는 Java 17 이상을 요구해요. 여러 Java가 설치되어 있다면 shell이 실제로 어느 `java`를 실행하는지 `command -v java`로 확인하세요.
  </Accordion>

  <Accordion title="Permission denied 또는 No space left on device가 나요">
    직접 설치 directory와 `log.dirs`에 현재 사용자가 쓸 수 있는지, `df -h`로 filesystem 여유가 있는지 확인해요. Docker라면 `sudo docker system df`로 image와 volume 사용량을 함께 확인하세요. 공간을 확보하려고 volume이나 data directory부터 지우면 record도 함께 사라질 수 있어요.
  </Accordion>
</AccordionGroup>

***

## 다음 실습용 snapshot을 남겨요

두 broker를 정상 종료한 뒤 VM snapshot을 만들면 첫 record까지 확인한 상태로 돌아올 수 있어요.

<Tabs>
  <Tab title="Docker">
    `kafka-docker` VM에서 container를 멈추고 VM을 종료해요. `stop`은 container와 data를 지우지 않아요.

    ```bash theme={null}
    sudo docker stop aha-kafka
    sudo poweroff
    ```
  </Tab>

  <Tab title="Ubuntu 직접 설치">
    `kafka-ubuntu` VM에서 broker를 정상 종료하고 VM을 종료해요.

    ```bash theme={null}
    bin/kafka-server-stop.sh
    sudo poweroff
    ```
  </Tab>
</Tabs>

두 VM이 `shut off` 상태가 되면 host에서 `virsh`로 `working-first-record` snapshot을 만들어요.

```bash theme={null}
for VM_NAME in kafka-docker kafka-ubuntu; do
  while [ "$(LC_ALL=C virsh --connect qemu:///system domstate "$VM_NAME")" != "shut off" ]; do
    sleep 2
  done

  virsh --connect qemu:///system snapshot-create-as \
    --domain "$VM_NAME" \
    --name working-first-record \
    --description "첫 record 확인 완료"

  virsh --connect qemu:///system snapshot-list "$VM_NAME"
done
```

다음 글을 바로 이어갈 때는 `virsh --connect qemu:///system start VM_NAME`으로 VM을 다시 켜세요. Docker에서는 `sudo docker start aha-kafka`, Ubuntu 직접 설치에서는 Kafka directory에서 `bin/kafka-server-start.sh -daemon config/server.properties`를 실행합니다.

<Check title="복구 지점 확인">
  두 VM의 snapshot 목록에 `working-first-record`가 있고, broker를 다시 시작한 뒤 `orders.paid`의 `evt-101`을 읽을 수 있다면 다음 실습을 위한 상태가 준비됐어요.
</Check>

***

## 자, 정리해볼까요?

<Callout title="오늘 우리가 확인한 것" color="#86EFAC">
  * Docker와 Ubuntu 직접 설치 모두 KRaft 기반의 learning용 단일 node를 실행할 수 있어요.

  * Metadata 요청 성공, topic 생성, produce, consume은 서로 다른 확인 단계예요.

  * 첫 JSON record는 `orders.paid`의 partition 0, offset 0에 놓였어요.

  * Broker process를 재시작해도 같은 data directory가 남아 있으면 record를 다시 읽을 수 있어요.

  * Container나 data directory를 삭제하는 cleanup은 restart와 달리 record를 되돌릴 수 없게 지울 수 있어요.
</Callout>

<Columns cols={2}>
  <Card title="이전 글" icon="arrow-left" href="/messaging/kafka/prepare-kafka-lab-on-ubuntu-vm">
    두 Ubuntu VM과 `tooling-ready` snapshot을 준비해요.
  </Card>

  <Card title="다음 글" icon="split" href="/messaging/kafka/topic-partition-key-and-ordering">
    Key가 record를 어느 partition으로 보내고, 순서를 어디까지 지켜주는지 확인해요.
  </Card>
</Columns>
