218 lines
21 KiB
Markdown
218 lines
21 KiB
Markdown
# Redis 값의 스키마를 코드로 고정하기: Registry·Envelope·Version
|
|
|
|
> **Redis 코드 상세 시리즈 09/20** · [전체 지도](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-backend-policy-boundary.md) · 이전: [Raw key와 영구 쓰기를 막는 코드: Namespace·Hash Slot·TTL](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-keyspace-expiration.md) · 다음: [문자열 명령 대신 타입을 노출하는 RedisOperations 코드 지도](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-typed-operations.md)
|
|
|
|
## 이 글이 답하는 코드 질문
|
|
|
|
Redis에 저장한 object byte가 어느 schema와 version인지 어떻게 판별하며, 배포가 읽지 못하는 값은 cache miss가 아니라 어떤 실패가 됩니까?
|
|
|
|
코드는 payload와 framing의 책임을 나눕니다.
|
|
|
|
- `RedisPayloadCodec<T>`는 schema id, write version, readable versions, payload encode/decode를 소유합니다.
|
|
- `VersionedJsonCodec<T>`는 timestamp가 포함된 envelope와 byte ceiling을 소유합니다.
|
|
- `RedisCodecRegistry`는 deployment가 승인한 schema와 Java type의 닫힌 집합을 소유합니다.
|
|
|
|
다른 schema, 읽을 수 없는 version, 깨진 framing은 `RedisSerializationException`입니다. ordinary miss로 바꾸지 않습니다.
|
|
|
|
## 먼저 보는 클래스·리소스 지도
|
|
|
|
| 클래스·리소스 | 입력 | 출력 | 다음 호출 |
|
|
|---|---|---|---|
|
|
| [RedisPayloadCodec](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/codec/RedisPayloadCodec.java:14) | domain object 또는 payload bytes/version | payload bytes 또는 object | `VersionedJsonCodec` |
|
|
| [RedisEnvelope](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/codec/RedisEnvelope.java:17) | schema, version, createdAt, payload | immutable envelope | framing |
|
|
| [JsonEnvelopeFraming](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/JsonEnvelopeFraming.java:28) | envelope 또는 stored bytes | canonical JSON bytes 또는 envelope | `VersionedJsonCodec` |
|
|
| [VersionedJsonCodec](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/VersionedJsonCodec.java:23) | typed value/stored bytes | versioned bytes/typed value | typed operation |
|
|
| [RedisCodecRegistry](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/RedisCodecRegistry.java:19) | payload codec와 value type | schema별 `RedisCodec<V>` | typed key factory |
|
|
| [golden order-summary-v1](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/resources/redis-sdk/golden/order-summary-v1.json:1) | 고정 timestamp와 payload | byte compatibility 기준 | codec contract test |
|
|
|
|
## 객체 생성 시점: registry를 닫습니다
|
|
|
|
[RedisCodecRegistry.builder](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/RedisCodecRegistry.java:40)는 세 값을 받습니다.
|
|
|
|
- `maxValueBytes`
|
|
- envelope에 기록할 `Clock`
|
|
- decode failure metadata에 기록할 `RedisDeploymentMode`
|
|
|
|
builder의 [register](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/RedisCodecRegistry.java:181)는 `RedisPayloadCodec<V>`와 `Class<V>`를 함께 받습니다. 내부에서 `VersionedJsonCodec`을 만들고 schema id를 key로 저장합니다.
|
|
|
|
동일 schema를 두 번 등록하면 실패합니다. class name을 보고 codec을 반사적으로 만들거나 stored bytes의 schema를 보고 미등록 decoder를 동적으로 로드하는 path는 없습니다.
|
|
|
|
registry에는 object envelope 외에도 네 built-in codec이 있습니다.
|
|
|
|
- UTF-8 string
|
|
- native long counter
|
|
- native double counter
|
|
- opaque byte array
|
|
|
|
이 built-in codec은 `forSchema` map과 별도로 singleton을 반환합니다.
|
|
|
|
## 생성자 단계의 불변식
|
|
|
|
`VersionedJsonCodec` 생성자는 다음을 확인합니다.
|
|
|
|
1. payload codec, clock, deployment mode가 null이 아닙니다.
|
|
2. maximum encoded bytes가 양수입니다.
|
|
3. payload codec이 자신이 쓰는 `writeVersion()`을 읽을 수 있습니다.
|
|
|
|
세 번째 규칙은 배포가 쓴 직후 자기 값을 못 읽는 설정을 시작 전에 막습니다. [constructor 검사](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/VersionedJsonCodec.java:38)에 있습니다.
|
|
|
|
`RedisEnvelope`도 schema가 1..128자의 제한된 alphabet인지, version이 양수인지 검사합니다. [schema pattern](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/codec/RedisEnvelope.java:19)은 letter, digit, `.`, `_`, `-`만 허용합니다. quote나 control character가 framing 구조를 바꾸지 못하게 합니다.
|
|
|
|
payload byte array는 constructor와 accessor에서 defensive copy됩니다. array를 record component로 두지 않고 value equality를 직접 구현했습니다.
|
|
|
|
## Encode 호출 순서
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant O as Typed operation
|
|
participant V as VersionedJsonCodec
|
|
participant P as RedisPayloadCodec
|
|
participant F as JsonEnvelopeFraming
|
|
participant R as Redis
|
|
O->>V: encode(value)
|
|
V->>P: encodePayload(value)
|
|
P-->>V: payload bytes
|
|
V->>V: schema·writeVersion·clock.instant로 envelope 생성
|
|
V->>F: write(envelope)
|
|
F-->>V: canonical UTF-8 JSON
|
|
V->>V: encoded byte ceiling 검사
|
|
V-->>O: bytes
|
|
O->>R: admission 후 write
|
|
```
|
|
|
|
[encode](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/VersionedJsonCodec.java:62)는 Redis 호출 전 byte 길이를 검사합니다. 초과하면 `RedisSerializationException`이며 bytes는 server로 가지 않습니다.
|
|
|
|
codec id는 `json:<schema>:v<writeVersion>`입니다. 예를 들면 `json:order-summary:v1`입니다.
|
|
|
|
## Canonical envelope bytes
|
|
|
|
[JsonEnvelopeFraming.write](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/JsonEnvelopeFraming.java:39)는 field를 다음 순서로 씁니다.
|
|
|
|
```json
|
|
{"schema":"order-summary","version":1,"createdAt":"2026-08-07T00:00:00Z","payload":"<base64>"}
|
|
```
|
|
|
|
payload는 Base64입니다. JSON serializer 설정이나 reflection에 byte 결과가 좌우되지 않습니다. 같은 envelope를 주면 writer는 같은 UTF-8 byte를 만듭니다. timestamp가 envelope의 일부이므로 실제 encode 호출의 clock instant가 다르면 전체 byte도 달라집니다.
|
|
|
|
golden-byte test는 fixed clock을 사용해 이 변수를 고정합니다.
|
|
|
|
## Decode 호출 순서
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
A[stored bytes] --> B{byte ceiling 이내인가}
|
|
B -- 아니요 --> X[RedisSerializationException]
|
|
B -- 예 --> C[framing read]
|
|
C --> D{exact four field set이고 값 변환이 가능한가}
|
|
D -- 아니요 --> X
|
|
D -- 예 --> E{schema가 codec schema와 같은가}
|
|
E -- 아니요 --> X
|
|
E -- 예 --> F{payloadCodec.canRead version인가}
|
|
F -- 아니요 --> X
|
|
F -- 예 --> G[decodePayload payload, version]
|
|
```
|
|
|
|
[decode](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/VersionedJsonCodec.java:79)는 먼저 stored byte 길이를 검사합니다. 그다음 framing을 읽고 schema와 readable version을 확인합니다. 마지막에만 payload decoder를 호출합니다.
|
|
|
|
이 순서는 잘못된 schema의 payload를 우연히 같은 Java shape로 decode하는 것을 막습니다. future version도 caller가 `canRead`에서 명시하지 않으면 hard failure입니다.
|
|
|
|
## Framing parser가 실제로 검사하는 범위
|
|
|
|
[JsonEnvelopeFraming.read](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/JsonEnvelopeFraming.java:49)는 범용 JSON parser가 아니라 hand-written framing parser입니다. 다음 입력은 거절합니다.
|
|
|
|
- null 또는 empty bytes
|
|
- JSON object brace가 없는 문자열
|
|
- 네 field 중 일부가 없거나 extra field가 있는 object
|
|
- 중복 field
|
|
- integer가 아닌 version
|
|
- `Instant`로 읽히지 않는 timestamp
|
|
- Base64가 아닌 payload
|
|
- envelope constructor 규칙을 어긴 schema/version
|
|
- field name의 quote, colon, escape 구조가 parser 문법과 맞지 않는 framing
|
|
|
|
그러나 이 목록을 strict 또는 canonical JSON validation으로 읽으면 안 됩니다. [fields parser](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/JsonEnvelopeFraming.java:88)는 quoted value면 quote를 벗기고, 아니면 다음 comma까지의 text를 그대로 가져옵니다. 이후 `version`은 `Integer.parseInt`, `createdAt`은 `Instant.parse`, `payload`는 Base64 decode가 성공하는지만 봅니다. 그래서 `"version":"1"`처럼 JSON type이 writer와 달라도 통과하며 schema·timestamp·payload의 unquoted text도 변환 가능하면 통과할 수 있습니다. 마지막 field 뒤 trailing comma도 현재 loop가 허용합니다.
|
|
|
|
source comment의 “reordered fields를 거절한다”는 설명도 실제 코드와 일치하지 않습니다. parser는 `LinkedHashMap`에 읽지만 [key set equality](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/JsonEnvelopeFraming.java:57)만 비교합니다. 같은 네 field를 재배열한 object는 통과합니다. writer가 canonical bytes를 만든다는 사실, reader가 exact field set과 변환 가능성을 확인한다는 사실, reader가 canonical JSON까지 강제한다는 주장은 서로 다릅니다.
|
|
|
|
## Schema evolution을 적용하는 순서
|
|
|
|
`RedisPayloadCodec`은 stored version을 `decodePayload`에 넘깁니다. 따라서 호환 변경은 다음 배포 순서를 취할 수 있습니다.
|
|
|
|
1. reader가 old version과 next version을 모두 `canRead`하도록 배포합니다.
|
|
2. 실제 decode가 version별 payload를 처리하도록 합니다.
|
|
3. `writeVersion`을 next version으로 올린 writer를 배포합니다.
|
|
4. old data의 TTL·migration 조건을 확인한 뒤 old reader 제거를 검토합니다.
|
|
|
|
이 순서는 API가 허용하는 패턴이지 자동 migration 구현이 있다는 뜻은 아닙니다. registry나 codec에는 stored data backfill, read-repair, dual-write, version usage metric이 없습니다.
|
|
|
|
## 정상·실패 분기와 failure metadata
|
|
|
|
### 정상
|
|
|
|
- registered schema와 요청한 Java type이 일치합니다.
|
|
- envelope schema가 payload codec schema와 같습니다.
|
|
- stored version을 `canRead`가 허용합니다.
|
|
- framing과 payload decode가 성공합니다.
|
|
|
|
### lookup 실패
|
|
|
|
[forSchema](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/RedisCodecRegistry.java:97)는 미등록 schema와 잘못된 requested `Class<V>`를 `IllegalArgumentException`으로 거절합니다. generic cast가 나중의 `ClassCastException`으로 밀리지 않습니다.
|
|
|
|
등록 단계도 같은 schema id의 두 구현을 허용하지 않습니다. [putIfAbsent 검사](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/RedisCodecRegistry.java:181)는 두 번째 등록이 같은 payload codec인지 비교해 합치지 않고 즉시 실패합니다. 따라서 schema id 하나가 배포 안에서 어느 decoder를 뜻하는지 모호해지지 않습니다. 다만 서로 다른 배포가 같은 schema id를 다른 의미로 등록하는 문제까지 중앙에서 탐지하는 registry는 아닙니다. 그 호환성은 golden byte와 교차 version test로 관리해야 합니다.
|
|
|
|
### serialization 실패
|
|
|
|
다른 schema, unreadable version, oversized bytes, framing 오류는 모두 `RedisSerializationException`입니다. 다만 metadata의 deployment mode 경로는 같지 않습니다. `VersionedJsonCodec`이 직접 만드는 size/schema/version failure는 [failure factory](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/VersionedJsonCodec.java:97)를 거쳐 bound deployment mode를 넣습니다.
|
|
|
|
반면 [decode의 framing 호출](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/VersionedJsonCodec.java:85)은 `JsonEnvelopeFraming.read`가 던진 failure를 다시 감싸지 않습니다. framing 쪽 [serializationFailure](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/JsonEnvelopeFraming.java:193)는 deployment mode를 `STANDALONE`으로 고정합니다. Cluster에 bound된 codec이라도 malformed framing이면 metadata가 현재 `STANDALONE`을 보고합니다.
|
|
|
|
stored data corruption은 retryable도 ambiguous도 아닙니다. 같은 byte를 다시 decode해도 성공할 근거가 없으므로 read라는 이유만으로 retryable로 표시하지 않습니다.
|
|
|
|
## 테스트가 고정하는 계약
|
|
|
|
registry 테스트는 [declared type lookup](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/RedisCodecRegistryTest.java:39), [wrong type의 lookup-time 거절](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/RedisCodecRegistryTest.java:48), [unregistered schema 거절](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/RedisCodecRegistryTest.java:58)을 각각 고정합니다.
|
|
|
|
versioned codec 테스트도 계약별 시작 행이 다릅니다.
|
|
|
|
- [v1 golden payload read](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/VersionedJsonCodecTest.java:78)
|
|
- [fixed clock writer와 golden byte 일치](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/VersionedJsonCodecTest.java:85)
|
|
- [다른 schema 거절](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/VersionedJsonCodecTest.java:93)
|
|
- [future version의 silent decode 방지](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/VersionedJsonCodecTest.java:108)
|
|
- [empty·non-JSON·missing field·bad version·extra field 거절](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/VersionedJsonCodecTest.java:120)
|
|
- [encode size 선검사](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/VersionedJsonCodecTest.java:138)
|
|
- [stable codec id](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/VersionedJsonCodecTest.java:149)
|
|
- [foreign-schema failure의 non-retryable·non-ambiguous·bound Cluster metadata](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/VersionedJsonCodecTest.java:154)
|
|
- [schema id의 control character와 quote 거절](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/VersionedJsonCodecTest.java:181)
|
|
- [framing failure의 non-retryable 속성](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/VersionedJsonCodecTest.java:200)
|
|
|
|
reordered field, 잘못된 JSON value type, trailing comma 거절 test는 없습니다. framing failure 테스트는 retryable만 검사하고 deployment mode는 검사하지 않습니다. 이번 문서 작업에서 테스트를 실행하지 않았고 production source와 test를 정적으로 대조했습니다.
|
|
|
|
## Golden byte가 의미하는 범위
|
|
|
|
golden file은 envelope framing, field spelling/order, timestamp rendering, Base64 payload를 한 사례로 고정합니다. payload codec의 모든 version 호환성을 자동으로 증명하지는 않습니다.
|
|
|
|
payload representation을 바꾸려면 새 golden fixture와 old-version read test가 필요합니다. 기존 golden file을 새 writer output으로 덮어쓰는 것만으로는 backward compatibility를 증명할 수 없습니다.
|
|
|
|
## 현재 구현 공백과 잘못 읽기 쉬운 지점
|
|
|
|
1. `RedisCodecRegistry`, payload codec 등록, typed key 조립의 production bean은 확인되지 않습니다.
|
|
2. aggregate `RedisOperations` production facade도 확인되지 않으므로 registry가 application path에 실제 연결됐다고 단정할 수 없습니다.
|
|
3. framing writer는 field order와 JSON value type을 고정하지만 reader는 reordered field, 변환 가능한 잘못된 JSON value type, trailing comma를 허용합니다. strict/canonical JSON parser가 아니며 class comment와 구현도 drift했습니다.
|
|
4. framing parser failure metadata는 bound deployment mode 대신 `STANDALONE`을 hard-code합니다. 현재 테스트는 corrupt framing의 topology를 고정하지 않습니다.
|
|
5. 자동 migration, read-repair, dual-write, stored version inventory는 없습니다.
|
|
6. `createdAt`은 compatibility framing의 일부지만 expiry나 freshness를 자동 판단하지 않습니다.
|
|
7. built-in string/number/bytes codec은 versioned object envelope와 다른 wire format입니다.
|
|
|
|
다음에 source를 열 때는 `RedisPayloadCodec`, `RedisEnvelope`, framing, `VersionedJsonCodec`, registry, golden test 순으로 보면 됩니다.
|
|
|
|
## 시리즈의 관련 문서
|
|
|
|
관련 범위는 keyspace, typed operations, execution failure certainty입니다.
|
|
|
|
## 시리즈에서 이어 읽기
|
|
|
|
- 이전 글: [Raw key와 영구 쓰기를 막는 코드: Namespace·Hash Slot·TTL](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-keyspace-expiration.md)
|
|
- 다음 글: [문자열 명령 대신 타입을 노출하는 RedisOperations 코드 지도](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-typed-operations.md)
|
|
- 전체 흐름: [Redis를 범용 클라이언트가 아니라 정책 경계로 다루기](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-backend-policy-boundary.md)
|
|
- 운영 흐름: [Redis를 켠다는 말의 운영적 의미: 단일 활성화 스위치에서 Sentinel 쓰기 손실 검증까지](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-platform-sre-operations.md)
|