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

# 기록은 언제까지 다시 읽을 수 있을까요?

> Retention과 segment로 읽을 수 있는 log 범위가 줄어드는 과정을 확인하고, 남은 offset에서만 replay할 수 있는 이유를 실습해요.

> Offset을 처음으로 되돌렸는데, 지난 주문은 왜 보이지 않을까요?

[앞 글](https://blog.nvim.me/messaging/kafka/consumer-group-offset-and-commit.md)에서는 consumer group의 읽기 위치를 옮겨 같은 record를 다시 읽었어요. 그런데 책갈피를 앞쪽으로 옮겨도, 이미 버린 책장은 돌아오지 않죠.

Kafka에서도 **읽기 위치와 기록의 수명은 서로 다른 규칙**이에요. 이번에는 아무도 읽지 않은 record가 retention으로 사라지는 것을 확인해요. 이어 새 record를 남기고, 실제로 남아 있는 범위에서만 replay할 수 있다는 경계까지 봅니다.

## 시작 상태를 맞춰요

[첫 record 실습](https://blog.nvim.me/messaging/kafka/run-local-kafka-and-send-first-record.md)의 Kafka 4.3.1 broker가 실행 중이어야 해요. Docker라면 `aha-kafka`, 직접 설치라면 같은 data directory로 실행한 broker에 접속합니다. Client 주소는 선택한 환경 안의 `localhost:9092`예요.

앞 글의 `orders.paid`, `order-sequence`, `shipping-service`, `analytics-service`는 그대로 둬요. 이번에는 **아직 없는** `order-retention` topic과 `retention-reader` group만 사용합니다. 외부 database나 배송 API는 호출하지 않아요.

### 검증 환경

| 항목 | Docker | Ubuntu 직접 설치 |
| - | - | - |
| Kafka | 4.3.1 | 4.3.1 |
| 배포물 | `apache/kafka:4.3.1` | 공식 `kafka_2.13-4.3.1.tgz`, SHA-512 확인 |
| 운영체제·architecture | Host Ubuntu 22.04.5 LTS, aarch64 | Ubuntu 22.04.5 LTS, aarch64 |
| Java | Image 안의 Temurin 21.0.11 | Temurin 21.0.11 |
| Docker Engine | 29.5.3 | 사용하지 않음 |
| 검증일 | 2026-10-08 | 2026-10-08 |

검증에 사용한 서버는 4 vCPU, RAM 27 GiB이며 시작할 때 disk 여유는 약 32 GiB였어요. 같은 서버에서 container와 직접 설치 broker의 network·data directory를 분리했어요. 이 자원은 실제 검증 환경이며 Kafka의 최소 사양이 아니에요. 독자는 두 탭 중 하나를 고르면 됩니다.

## 책갈피와 책장 정리를 나눠 생각해요

Consumer group의 committed offset은 책갈피에 가까워요. 반면 retention은 오래된 책장을 정리하는 규칙에 가까워요. 책갈피가 꽂혀 있다는 이유로 정리가 멈추지는 않아요.

이 비유의 한계도 있어요. Kafka가 record 한 개를 책 한 장처럼 따로 골라 지우는 것은 아니에요. `cleanup.policy=delete`에서는 partition log를 나눈 **segment 단위**로 오래된 기록을 제거해요.

```mermaid theme={null}
flowchart LR
    S0["segment A<br/>offset 0, 1"]
    S2["segment B<br/>offset 2"]
    S3["새 segment<br/>다음 offset 3"]
    G["retention-reader<br/>committed offset 0"]
    S0 --> S2 --> S3
    G -. "읽기 위치" .-> S0
```

* `segment.bytes`와 `segment.ms`는 새 segment로 넘어가는 크기·시간 조건이에요.
* `retention.ms`는 시간 보존 조건이에요. Segment 안의 가장 큰 timestamp를 기준으로 만료 여부를 판단해요.
* `retention.bytes`는 **partition별** 크기 보존 조건이에요. 시간 조건과 함께 설정하면 어느 한쪽으로도 오래된 segment가 삭제될 수 있어요.
* Consumer가 읽었는지, 업무를 끝냈는지, commit했는지는 이 삭제의 승인 조건이 아니에요.

이 글은 local log와 `cleanup.policy=delete`에 한정해요. Key별 상태를 남기는 compaction이나 tiered storage의 보관 규칙까지 같은 그림으로 일반화하면 안 됩니다.

### Retention 1초는 1초 뒤 삭제 완료가 아니에요

이번에는 `retention.ms=1000`을 잠깐 적용해요. 그래도 record별 1초 TTL처럼 동작하지 않아요. Segment의 timestamp, roll 조건, broker의 주기적인 삭제 검사에 영향을 받아요.

Kafka 4.3.1의 `log.retention.check.interval.ms` 기본값은 `300000`, 즉 5분이에요. 이 실습도 그 값을 사용해요. `segment.ms=1000`을 설정한 뒤 새 record를 append해서 roll을 관찰하고, retention 적용 뒤에는 실제 earliest offset의 변화를 기다립니다.

> [!NOTE]
>
> Kafka 4.3.1은 마지막 비어 있지 않은 segment까지 만료되면 새 빈 segment를 만든 뒤 기존 segment들을 지울 수 있어요. “Active segment의 record는 항상 남는다”고 가정하지 마세요. 이번 실습에서는 기존 세 record가 모두 사라진 상태까지 확인해요.

## 보관 범위가 줄어드는 것을 직접 확인해요

두 탭의 topic·key·offset·group은 같아요. Bash 함수 `kafka`는 CLI 경로만 줄여 쓰는 도우미예요. 데이터와 설정은 해당 설치 경로의 broker에만 적용합니다.

> [tab] Docker
>
> Docker CLI는 container 안의 `/opt/kafka/bin`에 있어요. 아래 함수를 정의한 **같은 Bash 터미널**에서 이 탭을 끝까지 진행하세요.
>
> ```bash theme={null}
> sudo docker exec aha-kafka /opt/kafka/bin/kafka-topics.sh \
>   --bootstrap-server localhost:9092 --list
>
> kafka() {
>   sudo docker exec -i aha-kafka "/opt/kafka/bin/$1.sh" "${@:2}"
> }
> ```
>
> **1. 폐기용 topic을 만들고 설정을 확인해요**
>
> 시간·크기 보존을 먼저 꺼서 관찰 도중 record가 사라지지 않게 해요. 이름이 이미 있으면 중단하고 새 실습용 이름을 모든 명령에 일관되게 사용하세요.
>
> ```bash theme={null}
> kafka kafka-topics --bootstrap-server localhost:9092 \
>   --create --topic order-retention --partitions 1 --replication-factor 1 \
>   --config cleanup.policy=delete \
>   --config retention.ms=-1 --config retention.bytes=-1 \
>   --config segment.ms=1000 --config segment.jitter.ms=0 \
>   --config message.timestamp.type=LogAppendTime
>
> kafka kafka-topics --bootstrap-server localhost:9092 \
>   --describe --topic order-retention
> ```
>
> `PartitionCount: 1`, `ReplicationFactor: 1`과 지정한 설정을 확인해요. `LogAppendTime`은 broker가 기록한 시간을 써서 producer 시계 차이를 줄이기 위한 실습 설정이에요.
>
> **2. Record를 나눠 보내 segment를 바꿔요**
>
> ```bash theme={null}
> printf '%s\n' 'order-41:PAID' 'order-45:PAID' |
>   kafka kafka-console-producer --bootstrap-server localhost:9092 \
>     --topic order-retention \
>     --reader-property parse.key=true --reader-property key.separator=:
>
> sleep 2
>
> printf '%s\n' 'order-41:SHIPPING' |
>   kafka kafka-console-producer --bootstrap-server localhost:9092 \
>     --topic order-retention \
>     --reader-property parse.key=true --reader-property key.separator=:
> ```
>
> 처음 두 record는 offset 0·1, 나중 record는 offset 2에 놓여요. `segment.ms=1000`을 넘긴 뒤 새 append를 보내 roll 조건을 검사하게 합니다.
>
> **3. Segment 파일을 확인해요**
>
> ```bash theme={null}
> sudo docker exec aha-kafka sh -c \
>   'find /tmp/kraft-combined-logs/order-retention-0 -maxdepth 1 -type f | sort'
> ```
>
> `00000000000000000000.log`와 `00000000000000000002.log`를 확인해요. 파일명은 segment가 시작하는 base offset이에요. `.index`, `.timeindex`는 탐색을 돕는 파일이며 record 본문 파일과 구분합니다. 공식 image의 기본 log 경로를 사용했어요. 경로를 바꿨다면 실제 `log.dirs`에서 확인하세요.
>
> **4. 삭제 전 record와 범위를 읽어요**
>
> ```bash theme={null}
> kafka kafka-console-consumer --bootstrap-server localhost:9092 \
>   --topic order-retention --partition 0 --offset earliest \
>   --max-messages 3 --timeout-ms 30000 \
>   --formatter-property print.partition=true \
>   --formatter-property print.offset=true \
>   --formatter-property print.timestamp=true \
>   --formatter-property print.key=true
>
> kafka kafka-get-offsets --bootstrap-server localhost:9092 \
>   --topic order-retention --time earliest
> kafka kafka-get-offsets --bootstrap-server localhost:9092 \
>   --topic order-retention --time latest
> ```
>
> 세 record의 key·value와 offset 0·1·2를 확인해요. Earliest 결과는 `order-retention:0:0`, latest는 `order-retention:0:3`이에요. Latest 3은 다음에 쓸 위치이며, 아직 offset 3의 record는 없어요.
>
> **5. 읽지 않은 group의 위치를 남겨요**
>
> > [!WARNING]
> >
> > 아래 명령은 실습용 `retention-reader`의 committed offset을 설정해요. 이 이름은 아직 없어야 하며, 같은 이름의 consumer를 실행하지 마세요. 운영 group에 적용하면 업무를 건너뛰거나 반복할 수 있어요.
>
> ```bash theme={null}
> kafka kafka-consumer-groups --bootstrap-server localhost:9092 \
>   --group retention-reader --topic order-retention:0 \
>   --reset-offsets --to-offset 0 --dry-run
>
> kafka kafka-consumer-groups --bootstrap-server localhost:9092 \
>   --group retention-reader --topic order-retention:0 \
>   --reset-offsets --to-offset 0 --execute
>
> kafka kafka-consumer-groups --bootstrap-server localhost:9092 \
>   --group retention-reader --describe
> ```
>
> Dry run의 `NEW-OFFSET=0`을 확인한 뒤 실행하세요. `CURRENT-OFFSET=0`, `LOG-END-OFFSET=3`, `LAG=3`이면 아직 읽지 않은 위치를 만들었어요.
>
> **6. 짧은 retention을 적용하고 삭제를 관찰해요**
>
> > [!WARNING]
> >
> > 이 변경은 `order-retention`의 기존 세 record를 모두 삭제할 수 있어요. Group이 아직 읽지 않았어도 삭제됩니다. 앞 글의 `orders.paid`와 `order-sequence`에는 적용하지 마세요.
>
> ```bash theme={null}
> kafka kafka-configs --bootstrap-server localhost:9092 \
>   --entity-type topics --entity-name order-retention \
>   --alter --add-config retention.ms=1000
>
> deadline=$((SECONDS + 420))
> earliest=''
> while [ "$SECONDS" -lt "$deadline" ]; do
>   earliest=$(kafka kafka-get-offsets --bootstrap-server localhost:9092 \
>     --topic order-retention --time earliest) || break
>   printf '%s\n' "$earliest"
>   if [ "$earliest" = 'order-retention:0:3' ]; then break; fi
>   sleep 10
> done
>
> kafka kafka-configs --bootstrap-server localhost:9092 \
>   --entity-type topics --entity-name order-retention \
>   --alter --add-config retention.ms=-1
>
> test "$earliest" = 'order-retention:0:3' && echo '기존 record 만료 확인'
>
> kafka kafka-get-offsets --bootstrap-server localhost:9092 \
>   --topic order-retention --time latest
> kafka kafka-consumer-groups --bootstrap-server localhost:9092 \
>   --group retention-reader --describe
> ```
>
> `기존 record 만료 확인`이 출력되고 latest도 3인지 확인하세요. Group의 committed offset은 여전히 0이에요. 읽을 수 있는 시작 위치만 3으로 이동했어요.
>
> 420초는 실습을 멈출 기준이지 Kafka의 삭제 보장 시간이 아니에요. 확인 문구가 없으면 다음 단계로 넘어가지 말고 broker 로그와 실제 설정을 확인하세요. 위 명령은 성공·시간 초과 모두 짧은 retention을 해제합니다. 중간에 `Ctrl+C`로 끊었다면 `retention.ms=-1` 변경을 직접 실행하세요.
>
> **7. 새 record를 쓰고 범위 밖 위치를 확인해요**
>
> ```bash theme={null}
> printf '%s\n' 'order-45:RECHECKED' |
>   kafka kafka-console-producer --bootstrap-server localhost:9092 \
>     --topic order-retention \
>     --reader-property parse.key=true --reader-property key.separator=:
>
> kafka kafka-get-offsets --bootstrap-server localhost:9092 \
>   --topic order-retention --time earliest
> kafka kafka-get-offsets --bootstrap-server localhost:9092 \
>   --topic order-retention --time latest
>
> kafka kafka-console-consumer --bootstrap-server localhost:9092 \
>   --topic order-retention --group retention-reader \
>   --consumer-property auto.offset.reset=none \
>   --consumer-property enable.auto.commit=false \
>   --max-messages 1 --timeout-ms 15000
> ```
>
> Earliest는 3, latest는 4예요. Topic을 다시 만들지 않았으므로 새 record는 offset 0이 아닌 **3**에 들어갑니다.
>
> Consumer는 아직 committed offset 0에서 시작하려 하므로 `OffsetOutOfRangeException`으로 끝나요. 이번 단계에서는 의도한 오류예요. `auto.offset.reset=none`으로 자동 이동을 막았고, auto commit도 꺼서 실패 관찰 중 위치를 바꾸지 않아요.
>
> **8. 남아 있는 offset 3으로 이동해 replay해요**
>
> > [!WARNING]
> >
> > Consumer가 종료된 뒤 실행하세요. `--describe --state`에서 `Empty`와 member 수 0을 확인해요. Dry run의 결과를 읽기 전에 `--execute`를 실행하지 마세요.
>
> ```bash theme={null}
> kafka kafka-consumer-groups --bootstrap-server localhost:9092 \
>   --group retention-reader --describe --state
>
> kafka kafka-consumer-groups --bootstrap-server localhost:9092 \
>   --group retention-reader --topic order-retention:0 \
>   --reset-offsets --to-offset 3 --dry-run
>
> kafka kafka-consumer-groups --bootstrap-server localhost:9092 \
>   --group retention-reader --topic order-retention:0 \
>   --reset-offsets --to-offset 3 --execute
>
> kafka kafka-console-consumer --bootstrap-server localhost:9092 \
>   --topic order-retention --group retention-reader \
>   --consumer-property auto.offset.reset=none \
>   --max-messages 1 --timeout-ms 15000 \
>   --formatter-property print.partition=true \
>   --formatter-property print.offset=true \
>   --formatter-property print.timestamp=true \
>   --formatter-property print.key=true
>
> kafka kafka-consumer-groups --bootstrap-server localhost:9092 \
>   --group retention-reader --describe
> ```
>
> Dry run의 `NEW-OFFSET=3`을 확인하고 실행하면 `order-45:RECHECKED`만 읽어요. 종료 뒤 `CURRENT-OFFSET=4`, `LOG-END-OFFSET=4`, `LAG=0`인지 확인하세요. 삭제된 offset 0·1·2가 복원된 것은 아니에요.

> [tab] Ubuntu 직접 설치
>
> 압축을 해제한 `kafka_2.13-4.3.1` directory에서 실행해요. CLI는 그 안의 `bin/`에 있어요. 아래 함수를 정의한 **같은 Bash 터미널**에서 이 탭을 끝까지 진행하세요.
>
> ```bash theme={null}
> bin/kafka-topics.sh --bootstrap-server localhost:9092 --list
>
> kafka() {
>   "bin/$1.sh" "${@:2}"
> }
> ```
>
> **1. 폐기용 topic을 만들고 설정을 확인해요**
>
> 시간·크기 보존을 먼저 꺼서 관찰 도중 record가 사라지지 않게 해요. 이름이 이미 있으면 중단하고 새 실습용 이름을 모든 명령에 일관되게 사용하세요.
>
> ```bash theme={null}
> kafka kafka-topics --bootstrap-server localhost:9092 \
>   --create --topic order-retention --partitions 1 --replication-factor 1 \
>   --config cleanup.policy=delete \
>   --config retention.ms=-1 --config retention.bytes=-1 \
>   --config segment.ms=1000 --config segment.jitter.ms=0 \
>   --config message.timestamp.type=LogAppendTime
>
> kafka kafka-topics --bootstrap-server localhost:9092 \
>   --describe --topic order-retention
> ```
>
> `PartitionCount: 1`, `ReplicationFactor: 1`과 지정한 설정을 확인해요. `LogAppendTime`은 broker가 기록한 시간을 써서 producer 시계 차이를 줄이기 위한 실습 설정이에요.
>
> **2. Record를 나눠 보내 segment를 바꿔요**
>
> ```bash theme={null}
> printf '%s\n' 'order-41:PAID' 'order-45:PAID' |
>   kafka kafka-console-producer --bootstrap-server localhost:9092 \
>     --topic order-retention \
>     --reader-property parse.key=true --reader-property key.separator=:
>
> sleep 2
>
> printf '%s\n' 'order-41:SHIPPING' |
>   kafka kafka-console-producer --bootstrap-server localhost:9092 \
>     --topic order-retention \
>     --reader-property parse.key=true --reader-property key.separator=:
> ```
>
> 처음 두 record는 offset 0·1, 나중 record는 offset 2에 놓여요. `segment.ms=1000`을 넘긴 뒤 새 append를 보내 roll 조건을 검사하게 합니다.
>
> **3. Segment 파일을 확인해요**
>
> ```bash theme={null}
> find "$HOME/kafka-data/order-retention-0" -maxdepth 1 -type f | sort
> ```
>
> `00000000000000000000.log`와 `00000000000000000002.log`를 확인해요. 파일명은 segment가 시작하는 base offset이에요. `.index`, `.timeindex`는 탐색을 돕는 파일이며 record 본문 파일과 구분합니다. 앞 설치 글의 `log.dirs=$HOME/kafka-data`를 사용해요. 경로를 바꿨다면 실제 `log.dirs`에서 확인하세요.
>
> **4. 삭제 전 record와 범위를 읽어요**
>
> ```bash theme={null}
> kafka kafka-console-consumer --bootstrap-server localhost:9092 \
>   --topic order-retention --partition 0 --offset earliest \
>   --max-messages 3 --timeout-ms 30000 \
>   --formatter-property print.partition=true \
>   --formatter-property print.offset=true \
>   --formatter-property print.timestamp=true \
>   --formatter-property print.key=true
>
> kafka kafka-get-offsets --bootstrap-server localhost:9092 \
>   --topic order-retention --time earliest
> kafka kafka-get-offsets --bootstrap-server localhost:9092 \
>   --topic order-retention --time latest
> ```
>
> 세 record의 key·value와 offset 0·1·2를 확인해요. Earliest 결과는 `order-retention:0:0`, latest는 `order-retention:0:3`이에요. Latest 3은 다음에 쓸 위치이며, 아직 offset 3의 record는 없어요.
>
> **5. 읽지 않은 group의 위치를 남겨요**
>
> > [!WARNING]
> >
> > 아래 명령은 실습용 `retention-reader`의 committed offset을 설정해요. 이 이름은 아직 없어야 하며, 같은 이름의 consumer를 실행하지 마세요. 운영 group에 적용하면 업무를 건너뛰거나 반복할 수 있어요.
>
> ```bash theme={null}
> kafka kafka-consumer-groups --bootstrap-server localhost:9092 \
>   --group retention-reader --topic order-retention:0 \
>   --reset-offsets --to-offset 0 --dry-run
>
> kafka kafka-consumer-groups --bootstrap-server localhost:9092 \
>   --group retention-reader --topic order-retention:0 \
>   --reset-offsets --to-offset 0 --execute
>
> kafka kafka-consumer-groups --bootstrap-server localhost:9092 \
>   --group retention-reader --describe
> ```
>
> Dry run의 `NEW-OFFSET=0`을 확인한 뒤 실행하세요. `CURRENT-OFFSET=0`, `LOG-END-OFFSET=3`, `LAG=3`이면 아직 읽지 않은 위치를 만들었어요.
>
> **6. 짧은 retention을 적용하고 삭제를 관찰해요**
>
> > [!WARNING]
> >
> > 이 변경은 `order-retention`의 기존 세 record를 모두 삭제할 수 있어요. Group이 아직 읽지 않았어도 삭제됩니다. 앞 글의 `orders.paid`와 `order-sequence`에는 적용하지 마세요.
>
> ```bash theme={null}
> kafka kafka-configs --bootstrap-server localhost:9092 \
>   --entity-type topics --entity-name order-retention \
>   --alter --add-config retention.ms=1000
>
> deadline=$((SECONDS + 420))
> earliest=''
> while [ "$SECONDS" -lt "$deadline" ]; do
>   earliest=$(kafka kafka-get-offsets --bootstrap-server localhost:9092 \
>     --topic order-retention --time earliest) || break
>   printf '%s\n' "$earliest"
>   if [ "$earliest" = 'order-retention:0:3' ]; then break; fi
>   sleep 10
> done
>
> kafka kafka-configs --bootstrap-server localhost:9092 \
>   --entity-type topics --entity-name order-retention \
>   --alter --add-config retention.ms=-1
>
> test "$earliest" = 'order-retention:0:3' && echo '기존 record 만료 확인'
>
> kafka kafka-get-offsets --bootstrap-server localhost:9092 \
>   --topic order-retention --time latest
> kafka kafka-consumer-groups --bootstrap-server localhost:9092 \
>   --group retention-reader --describe
> ```
>
> `기존 record 만료 확인`이 출력되고 latest도 3인지 확인하세요. Group의 committed offset은 여전히 0이에요. 읽을 수 있는 시작 위치만 3으로 이동했어요.
>
> 420초는 실습을 멈출 기준이지 Kafka의 삭제 보장 시간이 아니에요. 확인 문구가 없으면 다음 단계로 넘어가지 말고 broker 로그와 실제 설정을 확인하세요. 위 명령은 성공·시간 초과 모두 짧은 retention을 해제합니다. 중간에 `Ctrl+C`로 끊었다면 `retention.ms=-1` 변경을 직접 실행하세요.
>
> **7. 새 record를 쓰고 범위 밖 위치를 확인해요**
>
> ```bash theme={null}
> printf '%s\n' 'order-45:RECHECKED' |
>   kafka kafka-console-producer --bootstrap-server localhost:9092 \
>     --topic order-retention \
>     --reader-property parse.key=true --reader-property key.separator=:
>
> kafka kafka-get-offsets --bootstrap-server localhost:9092 \
>   --topic order-retention --time earliest
> kafka kafka-get-offsets --bootstrap-server localhost:9092 \
>   --topic order-retention --time latest
>
> kafka kafka-console-consumer --bootstrap-server localhost:9092 \
>   --topic order-retention --group retention-reader \
>   --consumer-property auto.offset.reset=none \
>   --consumer-property enable.auto.commit=false \
>   --max-messages 1 --timeout-ms 15000
> ```
>
> Earliest는 3, latest는 4예요. Topic을 다시 만들지 않았으므로 새 record는 offset 0이 아닌 **3**에 들어갑니다.
>
> Consumer는 아직 committed offset 0에서 시작하려 하므로 `OffsetOutOfRangeException`으로 끝나요. 이번 단계에서는 의도한 오류예요. `auto.offset.reset=none`으로 자동 이동을 막았고, auto commit도 꺼서 실패 관찰 중 위치를 바꾸지 않아요.
>
> **8. 남아 있는 offset 3으로 이동해 replay해요**
>
> > [!WARNING]
> >
> > Consumer가 종료된 뒤 실행하세요. `--describe --state`에서 `Empty`와 member 수 0을 확인해요. Dry run의 결과를 읽기 전에 `--execute`를 실행하지 마세요.
>
> ```bash theme={null}
> kafka kafka-consumer-groups --bootstrap-server localhost:9092 \
>   --group retention-reader --describe --state
>
> kafka kafka-consumer-groups --bootstrap-server localhost:9092 \
>   --group retention-reader --topic order-retention:0 \
>   --reset-offsets --to-offset 3 --dry-run
>
> kafka kafka-consumer-groups --bootstrap-server localhost:9092 \
>   --group retention-reader --topic order-retention:0 \
>   --reset-offsets --to-offset 3 --execute
>
> kafka kafka-console-consumer --bootstrap-server localhost:9092 \
>   --topic order-retention --group retention-reader \
>   --consumer-property auto.offset.reset=none \
>   --max-messages 1 --timeout-ms 15000 \
>   --formatter-property print.partition=true \
>   --formatter-property print.offset=true \
>   --formatter-property print.timestamp=true \
>   --formatter-property print.key=true
>
> kafka kafka-consumer-groups --bootstrap-server localhost:9092 \
>   --group retention-reader --describe
> ```
>
> Dry run의 `NEW-OFFSET=3`을 확인하고 실행하면 `order-45:RECHECKED`만 읽어요. 종료 뒤 `CURRENT-OFFSET=4`, `LOG-END-OFFSET=4`, `LAG=0`인지 확인하세요. 삭제된 offset 0·1·2가 복원된 것은 아니에요.

## 결과를 세 위치로 읽어요

두 트랙에서 확인한 위치 변화는 다음과 같았어요. 이 표의 latest는 transaction을 쓰지 않는 이번 실습의 log end offset이에요.

| 시점 | Earliest | Latest | Group committed offset | 읽을 수 있는 record |
| - | -: | -: | -: | - |
| 삭제 전, group 위치 설정 뒤 | 0 | 3 | 0 | 0, 1, 2 |
| 기존 record 만료 뒤 | 3 | 3 | 0 | 없음 |
| 짧은 retention 해제 뒤 새 record 추가 | 3 | 4 | 0 | 3 |
| Offset 3으로 reset하고 consume 종료 | 3 | 4 | 4 | 3 |

삭제 직후에도 group의 lag는 `3 - 0 = 3`으로 보일 수 있어요. 하지만 그 세 record는 이미 읽을 수 없어요. **Lag라는 위치 차이만으로 replay할 수 있는 record 수를 판단하면 안 되는 이유**예요.

파일을 다시 보면 `.deleted`가 붙은 파일이 잠시 남을 수도 있어요. 읽기 범위에서 제거하는 것과 disk에서 파일을 없애는 것은 별도 단계예요. `file.delete.delay.ms` 기본값은 60000ms지만, 이 값을 “record를 그동안 더 읽을 수 있는 시간”으로 해석하면 안 됩니다.

### Auto offset reset은 복구가 아니라 새 시작 위치 선택이에요

Committed offset이 없거나 현재 보관 범위를 벗어나면 consumer의 `auto.offset.reset` 정책이 적용돼요.

| 정책 | 이 실습에서 committed offset 0이 범위 밖일 때 |
| - | - |
| `earliest` | 현재 남아 있는 시작 위치 3으로 이동해요 |
| `latest` | 끝 위치 4로 이동해 이미 있는 offset 3도 건너뛸 수 있어요 |
| `none` | 자동으로 위치를 선택하지 않고 오류를 알려줘요 |

Kafka 4.3.1에서 기본값은 `latest`예요. 이 글은 위 세 정책만 비교하며, 실습에서는 `none`을 명시했어요. `--from-beginning`도 “topic을 만든 순간부터 복원”이 아니라 현재 남은 earliest에서 시작하도록 하는 선택이에요. 유효한 committed offset이 있으면 그 위치가 우선합니다.

CLI에 삭제된 offset 0을 reset 목표로 넣는 것도 복구 방법이 아니에요. Kafka 4.3.1의 reset 도구는 보관 범위보다 작은 요청을 earliest로 보정할 수 있으므로, \*\*요청 숫자보다 dry run의 실제 `NEW-OFFSET`\*\*을 읽어야 해요.

## 복구 상태를 정리하고 실습 data를 지워요

정상적으로 끝났다면 `order-retention`은 `retention.ms=-1`, `retention.bytes=-1`이고 offset 3의 새 record만 남아 있어요. Consumer는 종료됐고 `retention-reader`의 committed offset은 4예요. 이전 주문 record를 되살린 상태가 아니라, 남은 기록에서 다시 읽을 수 있게 된 상태입니다.

> [!WARNING]
>
> 아래 명령은 `order-retention`의 새 record와 `retention-reader`의 위치를 삭제해요. Consumer를 모두 종료한 뒤 실행하세요. 앞 글의 topic·group과 broker의 data directory는 지우지 않아요. Retention을 무제한으로 둔 실습 topic에 계속 데이터를 보내지 않도록 정리합니다.

> [tab] Docker
>
> ```bash theme={null}
> sudo docker exec aha-kafka /opt/kafka/bin/kafka-consumer-groups.sh \
>   --bootstrap-server localhost:9092 --delete --group retention-reader
> sudo docker exec aha-kafka /opt/kafka/bin/kafka-topics.sh \
>   --bootstrap-server localhost:9092 --delete --topic order-retention
> sudo docker exec aha-kafka /opt/kafka/bin/kafka-topics.sh \
>   --bootstrap-server localhost:9092 --list
> unset -f kafka
> ```

> [tab] Ubuntu 직접 설치
>
> ```bash theme={null}
> bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
>   --delete --group retention-reader
> bin/kafka-topics.sh --bootstrap-server localhost:9092 \
>   --delete --topic order-retention
> bin/kafka-topics.sh --bootstrap-server localhost:9092 --list
> unset -f kafka
> ```

Topic 삭제도 비동기 작업이에요. 목록에서 `order-retention`이 사라지고, `orders.paid`와 `order-sequence`가 남아 있는지 확인하세요.

> [!TIP]
>
> Docker와 Ubuntu 직접 설치 모두 segment base offset 0·2, 삭제 전 범위 0~~3, 만료 뒤 빈 범위 3~~3을 확인했어요. 읽지 않은 group의 committed offset 0은 그대로 남았고, 범위 밖 오류가 발생했어요. 새 record를 offset 3으로 쓴 뒤 dry run과 reset을 거쳐 그 record만 다시 읽었습니다.

## 자, 정리해볼까요?

**오늘 우리가 확인한 것**

* Group의 책갈피와 topic의 보관 정책은 따로 움직여요. 아직 읽지 않은 record도 삭제될 수 있어요.

* 시간 보존은 record별 정각 삭제가 아니라 segment와 주기적인 검사에 의해 동작해요.

* Earliest와 latest가 같으면 남은 record가 없는 상태예요. 다음 append가 offset 0부터 다시 시작하는 것은 아니에요.

* Replay는 남아 있는 기록을 다시 읽는 동작이에요. Offset reset은 삭제한 데이터를 복원하지 않아요.

* 복구 작업 전에는 committed offset뿐 아니라 partition의 실제 보관 범위와 side effect를 함께 확인해야 해요.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.