refactor: 문서 개선 중

This commit is contained in:
donghyeon-ka
2026-09-21 14:30:55 +09:00
parent c93cdea150
commit 805a18f486
1497 changed files with 525837 additions and 59152 deletions
+235
View File
@@ -0,0 +1,235 @@
# 문자열 명령 대신 타입을 노출하는 RedisOperations 코드 지도
> **Redis 코드 상세 시리즈 10/20** · [전체 지도](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-backend-policy-boundary.md) · 이전: [Redis 값의 스키마를 코드로 고정하기: Registry·Envelope·Version](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-codec-schema-evolution.md) · 다음: [Batch·Transaction·Script·Function·Pub/Sub·Admin·Raw를 분리한 이유](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-advanced-surfaces.md)
## 이 글이 답하는 코드 질문
`GET`, `HSET`, `ZRANGE` 같은 command string 대신 애플리케이션이 무엇을 호출하며, sync와 reactive API가 같은 정책을 적용한다는 근거는 어디에 있습니까?
public aggregate interface는 `RedisOperations``ReactiveRedisOperations`입니다. 둘 다 12개 accessor를 노출합니다. 각 operation은 typed key와 value codec을 받고, 공통 request builder가 `CommandRequest`를 만든 뒤 sync 또는 reactive executor로 보냅니다.
다만 이 aggregate interface를 구현한 production class와 Spring bean은 확인되지 않습니다. 세부 operation 구현과 contract 테스트가 존재한다는 사실과 application이 aggregate facade를 주입받을 수 있다는 사실을 구분해야 합니다.
## 먼저 보는 클래스·리소스 지도
| 클래스 | 입력 | 출력 | 다음 호출 |
|---|---|---|---|
| [RedisOperations](/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/RedisOperations.java:24) | 없음, accessor 호출 | sync operation group | 각 `LettuceRedis*Operations` |
| [ReactiveRedisOperations](/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/ReactiveRedisOperations.java:23) | 없음, accessor 호출 | reactive operation group | 각 `LettuceReactiveRedis*Operations` |
| typed key interfaces | `QualifiedRedisKey`와 codec | `ValueKey`, `HashKey` 등 | request builder |
| operation interface | typed key, value, option, permit, budget | domain-shaped result | Lettuce implementation |
| package-private request builder | operation arguments | `CommandRequest<R>` | executor |
| sync executor | deferred request | value/collection | gateway |
| reactive executor | deferred request | `Mono`/`Flux` | gateway |
대표 호출을 볼 때는 [LettuceRedisValueOperations](/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/operations/LettuceRedisValueOperations.java:23)과 [ValueOperationRequests](/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/operations/ValueOperationRequests.java:26)을 함께 읽으면 구조가 드러납니다.
## Aggregate가 노출하는 12개 그룹
sync와 reactive aggregate accessor는 다음과 같습니다.
| accessor | sync surface | 대표 자료형·명령군 |
|---|---|---|
| `values()` | `RedisValueOperations` | value/string, GET·SET·counter |
| `hashes()` | `RedisHashOperations` | hash field/value |
| `lists()` | `RedisListOperations` | ordered list |
| `sets()` | `RedisSetOperations` | unordered set·algebra |
| `sortedSets()` | `RedisSortedSetOperations` | score/rank/range |
| `bitmaps()` | `RedisBitmapOperations` | bit offset·BITOP |
| `bitFields()` | `RedisBitFieldOperations` | typed bitfield subcommand |
| `hyperLogLogs()` | `RedisHyperLogLogOperations` | PFADD·PFCOUNT·PFMERGE |
| `geo()` | `RedisGeoOperations` | point·distance·bounded search |
| `streams()` | `RedisStreamOperations` | append·range·group·pending |
| `keys()` | `RedisKeyOperations` | exists·delete·expiry·scan·rename |
| `batches()` | `RedisBatchOperations` | bounded pipelined batch |
[aggregate accessor 선언](/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/RedisOperations.java:26)은 blocking list/stream, transaction, Pub/Sub, admin, raw, script/function을 포함하지 않습니다. 이 surface들은 connection ownership이나 ACL이 달라 별도 API로 남습니다.
## Key type이 data structure와 codec을 고정합니다
operation은 `String key``byte[] value`를 받지 않습니다. 예를 들어 `ValueKey<V>`는 qualified key와 `RedisCodec<V>`를 묶고, `HashKey<F,V>`는 field codec과 value codec을 함께 가집니다.
이 형태가 고정하는 계약은 다음과 같습니다.
- namespace 없는 raw key가 typed operation signature에 들어오지 않습니다.
- 같은 key를 hash API와 list API에 우연히 넘길 수 없습니다.
- encode/decode codec이 call마다 따로 선택되지 않습니다.
- `Optional<V>`, `ExpirationResult`, `ScanPage<T>` 같은 결과가 Redis reply sentinel을 감춥니다.
server에 이미 다른 data type으로 저장된 key라면 compile-time type만으로 막을 수 없습니다. 이 경우 driver의 `WRONGTYPE`을 exception translator가 `RedisDataTypeMismatchException`으로 바꿉니다.
## Sync value read의 호출 순서
```mermaid
sequenceDiagram
participant A as Application
participant V as LettuceRedisValueOperations
participant B as ValueOperationRequests
participant C as RedisOperationContext
participant E as SyncRedisCommandExecutor
participant G as RedisCommandGateway
A->>V: get(ValueKey<V>)
V->>B: get(key)
B->>C: renderKey(key.key)
B->>B: GET CommandRequest 구성
V->>E: execute(request)
E->>E: guard.validate
E->>G: deferred get(bytes)
G-->>B: stored bytes/null
B->>C: value codec으로 decode
C-->>A: Optional<V>
```
[ValueOperationRequests.get](/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/operations/ValueOperationRequests.java:43)은 key를 render하고 `GET` command id, request byte 수, deferred gateway call을 한 객체에 넣습니다. reply가 오면 key에 묶인 codec으로 decode합니다.
이 기본 GET에는 `OperationBudget`이 없고 `expectedReplyBytes`도 0입니다. [공통 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/operations/ValueOperationRequests.java:295)는 codec만 호출하므로 관측한 reply byte ceiling을 집행하지 않습니다. MGET의 [decodeAll](/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/operations/ValueOperationRequests.java:299)이 budget을 검사하는 것과 다른 경로입니다.
`LettuceRedisValueOperations.get`은 request builder와 executor를 연결할 뿐 command 정책을 다시 구현하지 않습니다.
## Reactive path가 공유하는 부분과 다른 부분
reactive value implementation도 같은 `ValueOperationRequests`를 사용합니다. 따라서 command 선택, key rendering, permit, budget, encoding 분기가 sync와 reactive에서 따로 복제되지 않습니다.
다른 것은 executor와 반환 shape입니다.
- sync는 `CompletionStage`를 deadline까지 기다리고 값을 반환합니다.
- reactive는 `Mono.defer` 안에서 admission을 실행하고 `Mono.fromCompletionStage``CompletionStage``Mono`로 변환합니다.
- `Optional<T>` sync 결과는 reactive에서 empty `Mono<T>`가 됩니다.
- `List<T>`/`Set<T>` sync 결과는 `Flux<T>`가 됩니다.
- primitive는 boxed `Mono`가 됩니다.
[ApiParityInspector의 규칙](/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/api/ApiParityInspector.java:15)은 method name과 generic parameter를 비교하고 예상 reactive return shape를 계산합니다.
Pub/Sub은 mechanical parity 대상에서 의도적으로 빠집니다. sync는 handler와 closeable subscription을 반환하고 reactive는 publisher cancellation을 lifecycle로 사용하기 때문입니다.
## Group별로 봐야 하는 정책 지점
### Value
[RedisValueOperations](/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/operations/RedisValueOperations.java:9)은 ordinary `SET` 계열을 expiration이 필수인 public method로 표현합니다. `setIfAbsent``setIfPresent`는 각각 `SET NX``SET XX`, `getAndSet``SET GET`, `getAndExpire``GETEX` 옵션으로 내려갑니다. deprecated command 이름인 `SETNX``GETSET` 자체는 [policy에서 `BLOCKED`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/resources/redis-sdk/redis-command-policy.yml:90)이며 이 API가 전송하지 않습니다.
multi-get과 range/append는 permit·budget을 요구합니다. 다만 APPEND와 SETRANGE의 method에는 expiration이나 `PersistentKeyPermit`이 없습니다. [append request](/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/operations/ValueOperationRequests.java:182)와 [setRange request](/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/operations/ValueOperationRequests.java:226)는 absent key를 만들 수 있는데도 TTL 경계를 호출하지 않습니다.
### Hash
[RedisHashOperations](/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/operations/RedisHashOperations.java:10)은 field와 value codec을 분리합니다. full collection read나 scan은 bound를 가진 API로 표현됩니다. hash write에는 expiration 인자가 없다는 현재 공백이 있습니다.
### List
[RedisListOperations](/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/operations/RedisListOperations.java:10)은 side를 enum으로 표현하고 count/range를 bound합니다. blocking pop/move는 aggregate 밖의 blocking surface입니다.
### Set과 sorted set
set algebra의 multi-key 비용은 permit과 budget으로 드러납니다. sorted set은 `ScoreRange`, `RankRange`, `LexRange`, page/bound 자료형으로 overload ambiguity를 줄입니다.
### Bitmap과 bitfield
bitmap은 bit offset과 multi-key bit operation을 구분합니다. bitfield는 raw subcommand string 대신 `BitFieldSubcommand`, overflow enum, typed result를 사용합니다.
### HLL과 Geo
HyperLogLog merge는 multi-key permit 대상입니다. Geo search는 center/radius/unit/page를 자료형으로 묶고 reply 수를 제한합니다.
### Stream
stream은 `StreamId`, `StreamRange`, `StreamReadOffset`, `StreamGroup`, `StreamConsumer`, pending/claim result를 사용합니다. blocking read와 version-gated deletion은 기본 aggregate와 분리됩니다.
### Key
key group은 expiry, TTL, scan, delete/unlink, rename을 담당합니다. scan은 전 keyspace materialization 대신 cursor page를 반환합니다.
### Batch
batch는 aggregate에 있지만 atomic transaction이 아닙니다. per-command outcome과 partial failure를 반환하는 latency optimization입니다.
## Request builder가 공유하는 guardrail
각 family의 package-private `*OperationRequests`는 다음 일을 맡습니다.
1. null과 local option을 검사합니다.
2. key를 render합니다.
3. value/member/field를 codec으로 encode합니다.
4. request byte와 expected reply byte를 계산합니다.
5. 필요한 permit과 `OperationBudget`을 붙입니다.
6. gateway call을 supplier로 지연합니다.
7. reply를 typed result로 decode합니다. 관측 reply budget 검사는 builder가 `requireReplyWithinBudget`을 호출한 MGET, bounded range, collection page 등 일부 경로에만 있습니다.
예를 들어 [multiGet](/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/operations/ValueOperationRequests.java:54)은 empty key list를 거절하고, 모든 rendered key byte를 합산하고, collection budget과 multi-key permit을 `MGET` request에 넣습니다.
## 정상·실패 분기
### 정상
- absent GET/hash field/list pop은 `Optional.empty` 등 typed absence로 돌아옵니다.
- conditional write는 boolean 또는 typed outcome으로 조건 불충족을 표현합니다.
- cursor operation은 elements와 next cursor/complete state를 반환합니다.
- sync와 reactive는 같은 request builder를 거쳐 같은 command·permit·budget을 적용합니다.
### 전송 전 거절
- malformed key와 foreign namespace
- forged/missing permit
- empty 또는 configured maximum을 넘긴 collection
- request/reply estimate가 budget을 넘긴 경우
- Cluster cross-slot
- server version에 없는 version-gated command
- codec encode size 초과
### server reply 실패
- `WRONGTYPE`: `RedisDataTypeMismatchException`
- ACL 오류: `RedisAccessDeniedException`
- redirection/partition: `RedisRedirectionException`
- busy/loading: typed busy failure
- timeout/connection loss: read/write와 ambiguity에 따라 분기
### decode 실패
schema, version, framing이 맞지 않으면 cache miss로 바뀌지 않고 `RedisSerializationException`입니다.
## 테스트가 고정하는 계약
[PAIRS 선언](/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/api/ApiParityTest.java:51)은 15개 sync/reactive surface pair를 열거합니다. 기본 12개 외에 blocking list, blocking stream, hash field expiration도 pair 대상이며, [전체 pair parity 테스트](/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/api/ApiParityTest.java:81)가 각 pair의 method shape를 비교합니다.
[aggregate accessor 테스트](/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/api/ApiParityTest.java:95)는 accessor가 정확히 `batches`, `bitFields`, `bitmaps`, `geo`, `hashes`, `hyperLogLogs`, `keys`, `lists`, `sets`, `sortedSets`, `streams`, `values`인지 고정합니다. [publisher 반환 테스트](/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/api/ApiParityTest.java:117)는 모든 reactive method의 return type을 별도로 검사합니다.
family별 contract 테스트도 있습니다.
- [RedisValueOperationsContractTest](/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/operations/RedisValueOperationsContractTest.java:1)
- [RedisHashOperationsContractTest](/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/operations/RedisHashOperationsContractTest.java:1)
- [RedisListOperationsContractTest](/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/operations/RedisListOperationsContractTest.java:1)
- [RedisSetOperationsContractTest](/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/operations/RedisSetOperationsContractTest.java:1)
- [RedisSortedSetOperationsContractTest](/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/operations/RedisSortedSetOperationsContractTest.java:1)
- [RedisBitmapGeoOperationsContractTest](/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/operations/RedisBitmapGeoOperationsContractTest.java:1)
- [RedisStreamOperationsContractTest](/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/operations/RedisStreamOperationsContractTest.java:1)
- [RedisKeyOperationsContractTest](/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/operations/RedisKeyOperationsContractTest.java:1)
이 테스트는 in-memory gateway와 contract fixture를 많이 사용합니다. 일부 live test가 별도 존재하지만 이번 문서 작업에서는 어떤 테스트도 실행하지 않았습니다.
## `WAIT`와 typed surface
`RedisOperations`와 세부 typed interface에는 `WAIT` method가 없습니다. command policy에도 `WAIT`가 없습니다. durability 문맥에서 `WAIT`를 언급한 기존 문서를 typed API 지원 증거로 읽으면 안 됩니다. 현재는 catalog default-deny입니다.
## 현재 구현 공백과 잘못 읽기 쉬운 지점
1. `RedisOperations``ReactiveRedisOperations` 구현 class를 production source에서 찾지 못했습니다.
2. 두 aggregate type의 Spring bean도 확인되지 않습니다.
3. guard, executor, translator의 production DI가 확인되지 않으므로 세부 Lettuce operation을 application에 연결하는 bridge가 미조립입니다.
4. tests의 `RedisOperationsFixture`는 production composition 증거가 아닙니다.
5. sync/reactive parity는 signature와 return shape를 고정하지만 실서버에서 두 path의 모든 동작이 같다는 증명은 아닙니다.
6. expiration 의무는 ordinary value `SET` 계열과 nontransactional increment에는 적용되지만 모든 write에 완결되지 않았습니다. APPEND, SETRANGE, transaction의 INCRBY, transaction collection write, hash/list/set/zset write는 absent key를 만들 수 있어도 expiration이나 persistent permit을 받지 않습니다.
7. budget 객체와 관측 reply ceiling은 같은 뜻이 아닙니다. 기본 GET과 advanced script/function/raw/admin/extension path에는 관측 reply 크기를 검사하는 호출이 없습니다.
8. version-gated extension은 aggregate accessor에 자동으로 들어오지 않습니다.
다음에 source를 열 때는 aggregate interface, 한 family interface, sync/reactive implementation, 공통 request builder, context, executor, family contract test 순으로 보면 됩니다.
## 시리즈의 관련 문서
관련 범위는 command admission, keyspace·expiration, codec, advanced surfaces, execution failure certainty입니다.
## 시리즈에서 이어 읽기
- 이전 글: [Redis 값의 스키마를 코드로 고정하기: Registry·Envelope·Version](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-codec-schema-evolution.md)
- 다음 글: [Batch·Transaction·Script·Function·Pub/Sub·Admin·Raw를 분리한 이유](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-advanced-surfaces.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)