# 문자열 명령 대신 타입을 노출하는 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` | 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`는 qualified key와 `RedisCodec`를 묶고, `HashKey`는 field codec과 value codec을 함께 가집니다. 이 형태가 고정하는 계약은 다음과 같습니다. - namespace 없는 raw key가 typed operation signature에 들어오지 않습니다. - 같은 key를 hash API와 list API에 우연히 넘길 수 없습니다. - encode/decode codec이 call마다 따로 선택되지 않습니다. - `Optional`, `ExpirationResult`, `ScanPage` 같은 결과가 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->>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 ``` [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` sync 결과는 reactive에서 empty `Mono`가 됩니다. - `List`/`Set` sync 결과는 `Flux`가 됩니다. - 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)