# Batch·Transaction·Script·Function·Pub/Sub·Admin·Raw를 분리한 이유 > **Redis 코드 상세 시리즈 11/20** · [전체 지도](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-backend-policy-boundary.md) · 이전: [문자열 명령 대신 타입을 노출하는 RedisOperations 코드 지도](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-typed-operations.md) · 다음: [Timeout 뒤 쓰였는지 모를 때: Executor와 실행 확실성 모델](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-execution-failure-certainty.md) ## 이 글이 답하는 코드 질문 왜 advanced 기능을 `RedisOperations` 하나에 모두 넣지 않았으며, 각 surface는 어떤 connection·ACL·배포 계약을 가집니까? 이 분리는 기능 이름보다 failure mode와 ownership 차이에서 나옵니다. - batch는 pipeline 최적화이며 atomic하지 않습니다. - transaction은 한 connection의 `WATCH`/`MULTI`/`EXEC` 상태를 독점합니다. - script는 process에 등록한 source를 first use에 `SCRIPT LOAD`하고 `NOSCRIPT`에서 한 번 복구합니다. - function은 application이 load하지 않고 이미 배포된 library를 `FCALL`합니다. - Pub/Sub subscription은 long-lived connection lifecycle입니다. - admin은 read-only diagnostic account와 projection을 사용합니다. - raw는 catalog와 deployment approval이 모두 허용한 command만 실행합니다. 세부 class와 테스트는 있지만 이 surface들의 production bean 조립은 확인되지 않습니다. ## 먼저 보는 클래스·리소스 지도 | surface | 진입점 | connection·권한 | 핵심 결과 | |---|---|---|---| | Batch | [RedisBatchOperations](/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/RedisBatchOperations.java:10) | ordinary guarded calls, batch bounds | ordered per-item result | | Transaction | [RedisTransactionOperations](/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/programmability/RedisTransactionOperations.java:28) | exclusive `TRANSACTION` lane | executed/conflict, attempts | | Script | [LettuceRedisScriptOperations](/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/programmability/LettuceRedisScriptOperations.java:30) | scripting grant, guarded `EVALSHA` | decoded script reply | | Function | [LettuceRedisFunctionOperations](/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/programmability/LettuceRedisFunctionOperations.java:28) | capability-gated `FCALL/FCALL_RO` | decoded function reply | | Pub/Sub | [LettuceRedisPubSubOperations](/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/LettuceRedisPubSubOperations.java:23) | dedicated `PUBSUB` gateway | publish count/subscription | | Admin | [LettuceRedisAdminOperations](/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/admin/LettuceRedisAdminOperations.java:37) | own connection, admin-readonly account | bounded/redacted diagnostic | | Raw | [LettuceRedisRawGateway](/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/raw/LettuceRedisRawGateway.java:29) | raw account 의도, catalog+approval | caller decoder result | | Extensions | extension package의 `LettuceRedis*Operations` | probed module capability | JSON/TS/probabilistic/search | ## Connection lane은 API 모양과 함께 읽습니다 [RedisConnectionKind](/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/connection/RedisConnectionKind.java:21)은 `REGULAR`, `BLOCKING`, `TRANSACTION`, `SCRIPT`, `PUBSUB`, `ADMIN` 여섯 lane을 정의합니다. 다음 failure mode는 한 pool에 섞기 어렵습니다. - blocking command는 server block이 끝날 때까지 connection을 점유합니다. - transaction은 `MULTI` 이후 connection-local state를 가집니다. - subscribed connection은 ordinary command에 사용할 수 없습니다. - script와 admin은 application traffic과 다른 privilege가 필요합니다. - long-lived subscription close는 one-shot command reply와 lifecycle이 다릅니다. 다만 [RedisConnectionKind.forCommand](/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/connection/RedisConnectionKind.java:64)은 descriptor만으로 blocking/admin/regular을 정합니다. transaction, script, Pub/Sub의 실제 전용 connection 선택은 각 surface 조립이 맡아야 합니다. 이 조립은 production에서 확인되지 않습니다. ## Batch: pipeline이지 transaction이 아닙니다 [RedisBatchOperations](/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/RedisBatchOperations.java:3)은 세 가지를 명시합니다. - command는 독립적으로 성공하거나 실패할 수 있습니다. - 다른 client의 command가 사이에 실행될 수 있습니다. - write batch를 자동 retry하지 않습니다. `LettuceRedisBatchOperations`는 [BatchExecution](/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/LettuceRedisBatchOperations.java:15)에 실행을 위임합니다. 외부에서 구현한 `RedisBatch`는 받지 않고 SDK builder가 만든 batch인지 확인합니다. 호출 흐름은 다음과 같습니다. 1. builder가 item별 `CommandRequest`를 보존합니다. 2. batch 자체 command count와 request bytes를 선검사합니다. 3. 각 item을 guard에 미리 admission하면서 declared `expectedReplyBytes`를 합산하고 batch reply ceiling과 비교합니다. 4. 한 item이 거절되거나 declared 합계가 ceiling을 넘으면 어느 item도 보내지 않습니다. 5. dispatch는 in-flight bound와 batch/item timeout 중 짧은 값을 적용합니다. 6. 전송 뒤에는 item별 success/failure를 input order로 수집합니다. 7. decoded reply shape의 근사 누적값이 ceiling을 넘으면 그 지점의 item을 failure로 기록할 수 있습니다. [BatchExecution.measure](/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/BatchExecution.java:205)는 driver가 이미 decode한 결과를 셉니다. `byte[]`는 길이, `CharSequence`는 `length()`, collection과 map은 요소의 재귀 합계, unknown scalar는 1입니다. wire protocol의 byte 수를 계측하는 코드가 아니므로 이름이 `observedReplyBytes`여도 exact reply bytes로 읽으면 안 됩니다. 정상 결과에 partial failure flag가 있다는 사실은 atomicity가 없다는 API 신호입니다. ## Transaction: rollback이 아니라 optimistic concurrency입니다 [RedisTransactionOperations](/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/programmability/RedisTransactionOperations.java:6)은 Redis transaction이 rollback하지 않는다고 명시합니다. `EXEC` 안의 한 command가 runtime error여도 다른 queued command는 실행될 수 있습니다. `LettuceRedisTransactionOperations.watchAndExecute`의 흐름은 다음과 같습니다. ```mermaid sequenceDiagram participant A as Caller participant T as TransactionOperations participant Q as QueueingExecutor participant R as Redis gateway A->>T: watched keys, callback, options T->>T: Cluster same-slot 선검사 T->>Q: WATCH request admission/issue T->>R: MULTI T->>A: queue callback 실행 A->>Q: typed queued commands Q->>R: +QUEUED, reply는 아직 미확정 T->>R: EXEC alt executed R-->>T: replies T->>T: QueuedReply available 표시 else watched key changed R-->>T: null/conflict T->>T: attempt 상한까지 재시도 end ``` [runOnce](/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/programmability/LettuceRedisTransactionOperations.java:106)는 callback이나 guard가 실패해도 open window를 `DISCARD`하고, commit 뒤 watch가 남으면 `UNWATCH`합니다. Cluster에서는 watched key와 queued write key를 attempt 단위로 누적해 same-slot인지 확인합니다. command 하나씩 보면 합법이어도 transaction 전체가 cross-slot일 수 있기 때문입니다. `QueueingRedisCommandExecutor`는 `+QUEUED`에서 성공 observation을 기록하지 않습니다. reply stage가 `EXEC`에서 resolve될 때 성공/실패를 기록합니다. transaction queue의 TTL 계약도 ordinary value API와 같지 않습니다. transaction `set`은 expiration을 받지만 [Queue.increment](/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/programmability/LettuceRedisTransactionOperations.java:240)은 plain `INCRBY`를 enqueue합니다. absent key면 persistent counter가 만들어질 수 있습니다. 같은 queue의 hash/list/set/zset write도 expiration이나 persistent permit을 받지 않습니다. ## Script: 등록과 server load는 같은 시점이 아닙니다 [RedisScriptRegistry.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/programmability/RedisScriptRegistry.java:61)는 process 안에서 reviewed script identity와 source를 등록합니다. 같은 id에 다른 body를 재등록하면 실패합니다. 하지만 `register`는 Redis에 `SCRIPT LOAD`를 보내지 않습니다. server load는 [digest](/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/programmability/RedisScriptRegistry.java:87)이 처음 호출되어 cache miss가 났을 때 수행합니다. ```mermaid flowchart TD A[process setup: register script object] --> B[first execute] B --> C{digest cache hit인가} C -- 아니요 --> D[SCRIPT LOAD] D --> E[digest cache 저장] C -- 예 --> F[EVALSHA] E --> F F --> G{NOSCRIPT인가} G -- 아니요 --> H[result decode] G -- 예 --> I[digest forget] I --> J[SCRIPT LOAD 후 EVALSHA 한 번 재실행] ``` [LettuceRedisScriptOperations.execute](/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/programmability/LettuceRedisScriptOperations.java:61)는 key가 비어 있거나 `maxKeys`를 넘으면 거절합니다. key는 namespace와 same-slot 검사를 받으며 request/reply/timeout budget도 붙습니다. 다만 [EVALSHA 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/programmability/LettuceRedisScriptOperations.java:114)의 `expectedReplyBytes`는 0이고, decoder 호출 전 관측 reply 크기를 검사하지 않습니다. `maxReplyBytes`가 budget에 저장된다는 사실만 확인되며 실제 reply ceiling 집행은 빠져 있습니다. `NOSCRIPT`만 자동 복구합니다. server가 `EVALSHA` 실행 전에 script 부재를 답했으므로 reload와 1회 재호출이 ambiguous write retry는 아닙니다. 다른 failure는 자동 재호출하지 않습니다. `RedisScriptRegistry` class comment의 “registration is a deployment step”은 process registration을 뜻한다고 좁혀 읽어야 합니다. 실제 Redis `SCRIPT LOAD`는 first use입니다. ## Function: deployment-time library와 request-time call을 나눕니다 [RegisteredRedisFunction](/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/programmability/RegisteredRedisFunction.java:29)은 library, semantic version, function name, max keys, timeout, reply ceiling, read-only flag, decoder를 가집니다. application surface에는 `FUNCTION LOAD`가 없습니다. policy에서 `FUNCTION LOAD`는 admin-only이며, [LettuceRedisFunctionOperations](/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/programmability/LettuceRedisFunctionOperations.java:47)은 probed `FUNCTIONS` capability가 있을 때만 instance를 만듭니다. request-time에는 다음만 수행합니다. 1. key가 1개 이상이고 declared `maxKeys` 이내인지 확인합니다. 2. key와 arguments를 encode하고 request size를 계산합니다. 3. reply ceiling과 timeout으로 `OperationBudget`을 만듭니다. 4. read-only면 `FCALL_RO`, 아니면 `FCALL`을 선택합니다. 5. guard admission 후 function name으로 call합니다. [function 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/programmability/LettuceRedisFunctionOperations.java:103)도 `function.maxReplyBytes()`로 budget을 만들지만 `expectedReplyBytes`는 0입니다. [decoder 호출](/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/programmability/LettuceRedisFunctionOperations.java:106) 앞에 관측 reply budget 검사가 없습니다. script와 달리 function not found에서 library를 load하는 recovery가 없습니다. function library는 배포 pipeline이 먼저 설치해야 합니다. 현재 call path는 `RegisteredRedisFunction.library()`와 `version()`을 server request에 넣거나 server-side library metadata와 대조하지 않습니다. [실제 gateway 호출](/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/programmability/LettuceRedisFunctionOperations.java:106)은 `function.name()`만 전달합니다. record가 version을 보유한다는 것과 runtime deployment check가 구현됐다는 것은 다릅니다. ## Pub/Sub: publish와 subscription lifecycle이 다릅니다 [LettuceRedisPubSubOperations](/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/LettuceRedisPubSubOperations.java:16)은 publish는 guarded command로 보내지만 subscribe는 dedicated gateway로 시작해 caller가 닫아야 하는 `Subscription`을 반환합니다. channel subscription은 channel별 codec map을 만듭니다. 여러 channel을 구독하면서 첫 channel codec으로 모든 payload를 decode하지 않습니다. 요청하지 않은 channel message가 오면 codec을 추측하지 않고 실패합니다. pattern subscription은 concrete channel만 전달받으므로 어느 pattern codec인지 역산할 수 없습니다. 따라서 [singleCodec](/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/LettuceRedisPubSubOperations.java:101)이 모든 pattern의 codec id가 같은지 검사합니다. sharded Pub/Sub은 capability-gated 별도 surface입니다. ordinary Pub/Sub과 topology routing 의미가 같다고 합치지 않습니다. ## Admin: command allowlist가 아니라 projection까지 좁힙니다 [LettuceRedisAdminOperations.run](/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/admin/LettuceRedisAdminOperations.java:230)은 catalog policy가 `ADMIN_ONLY`이면서 read-only인지 다시 확인합니다. 노출 기능은 INFO, DBSIZE, MEMORY USAGE, bounded SLOWLOG, LATENCY LATEST, bounded CLIENT projection, CLUSTER INFO, fixed CONFIG GET projection, ACL DRYRUN입니다. CONFIG GET은 glob을 받지 않고 [DIAGNOSTIC_PARAMETERS](/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/admin/LettuceRedisAdminOperations.java:165)에 고정된 이름만 요청합니다. 응답에서도 allowlist를 다시 적용하고 secret-shaped parameter name의 value를 redact합니다. slow log에는 command family만 남기고 arguments를 버립니다. client projection에는 peer address와 connection name을 넣지 않습니다. admin run은 [OperationBudget을 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/admin/LettuceRedisAdminOperations.java:245) `expectedReplyBytes`를 0으로 두고 raw list를 그대로 반환합니다. projection별 count 상한은 있어도 실제 reply byte ceiling을 공통으로 집행하는 호출은 없습니다. ## Raw: 두 개의 독립된 승인이 필요합니다 [RawCommandApprovals](/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/raw/RawCommandApprovals.java:14)은 두 조건을 모두 요구합니다. 1. organization catalog가 command를 `RAW_ONLY`로 분류했습니다. 2. deployment가 concrete `ApprovedRawCommand`를 등록했습니다. approval은 policy id, command id, max arguments, request/reply ceiling, timeout, decoder를 고정합니다. token은 같은 registry가 같은 policy id에 대해 발급한 concrete instance여야 합니다. 여기서 reply ceiling을 고정한다는 말은 approval과 [OperationBudget](/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/raw/LettuceRedisRawGateway.java:93)이 그 숫자를 보유한다는 뜻입니다. raw request의 `expectedReplyBytes`는 0이고 caller decoder 앞에도 관측 reply 크기 검사가 없어, 실제 ceiling 집행까지 완성되지는 않았습니다. raw gateway는 argument에서 key를 추출해 bound namespace로 parse합니다. movable key command는 local parser가 정확히 위치를 결정할 수 있는 family만 허용합니다. 모르는 shape를 best guess하지 않습니다. `WAIT`는 catalog에 없으므로 raw approval 대상으로도 등록할 수 없습니다. `WAIT`를 raw escape hatch로 쓸 수 있다는 근거는 없습니다. ## Extension module: server capability가 bean 존재를 결정해야 합니다 JSON, Time Series, probabilistic structure, Search extension implementation은 각각 probed capability를 받는 `ifSupported` factory를 가집니다. - JSON path와 value ceiling을 검사합니다. - Time Series는 retention과 bounded range를 요구합니다. - probabilistic reserve는 error/capacity/compression 등의 bound를 요구합니다. - Search index name을 namespace에 묶고 page와 timeout을 요구합니다. extension 공통 runner는 [policy name이 있는 command에만 permit과 collection budget을 붙입니다](/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/extensions/ExtensionCommandRunner.java:85). null policy path는 permit과 budget이 모두 비어 있습니다. 예를 들어 [JSON.SET](/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/extensions/json/LettuceRedisJsonOperations.java:55)은 null을 넘기고, [bounded JSON.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/extensions/json/LettuceRedisJsonOperations.java:61)은 policy name을 넘깁니다. 두 분기 모두 `expectedReplyBytes`가 0이며 runner가 반환된 `List`를 그대로 넘기므로 관측 reply 검사가 없습니다. bounded read에 budget 객체가 있다는 사실도 reply byte ceiling 집행을 뜻하지 않고, null policy command에는 그 객체조차 없습니다. [RedisExtensionModulesContractTest](/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/RedisExtensionModulesContractTest.java:32)은 capability가 없으면 fixture에서 instance가 없음을 고정합니다. 이것은 production conditional bean이 실제로 조립됐다는 증거는 아닙니다. ## 테스트가 고정하는 계약 Batch 계약은 [batch ceiling 선검사](/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/RedisBatchOperationsContractTest.java:53), [refused item의 전체 batch 취소](/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/RedisBatchOperationsContractTest.java:78), [item permit·budget 보존](/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/RedisBatchOperationsContractTest.java:97), [foreign batch 거절](/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/RedisBatchOperationsContractTest.java:113), [decoded shape 근사 누적값의 ceiling 교차 처리](/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/RedisBatchOperationsContractTest.java:176)을 각각 고정합니다. 마지막 테스트는 ASCII string 사례에서 누적 failure가 나는 계약이며 exact wire-byte 계측을 증명하지 않습니다. Transaction 계약도 사례별로 나뉩니다. - [commit 전 queued command 미적용](/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/RedisTransactionContractTest.java:169) - [queued reply 조기 접근 금지](/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/RedisTransactionContractTest.java:201) - [watch conflict에서 실행하지 않음](/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/RedisTransactionContractTest.java:217) - [attempt ceiling 안의 conflict 재시도](/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/RedisTransactionContractTest.java:258) - [callback failure의 connection state cleanup](/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/RedisTransactionContractTest.java:283) - [queued command의 동일 admission 적용](/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/RedisTransactionContractTest.java:309) - [watch key와 queued write의 cross-slot 거절](/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/RedisTransactionSlotContractTest.java:121) - [여러 queued write 사이의 cross-slot 거절](/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/RedisTransactionSlotContractTest.java:139) - [co-located key commit](/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/RedisTransactionSlotContractTest.java:161) Script 계약은 [first use 실행과 load](/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/RedisScriptOperationsContractTest.java:34), [digest cache](/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/RedisScriptOperationsContractTest.java:45), [`NOSCRIPT` 1회 reload](/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/RedisScriptOperationsContractTest.java:57), [unregistered script 거절](/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/RedisScriptOperationsContractTest.java:71), [id/body identity 안정성](/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/RedisScriptOperationsContractTest.java:81)을 별도 테스트로 고정합니다. Function 계약은 [capability absence](/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/RedisFunctionOperationsContractTest.java:33), [deployed function call](/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/RedisFunctionOperationsContractTest.java:40), [key declaration·상한](/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/RedisFunctionOperationsContractTest.java:51), [semantic version identity](/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/RedisFunctionOperationsContractTest.java:66)을 각각 고정합니다. Pub/Sub은 [subscription close lifecycle](/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/RedisPubSubOperationsContractTest.java:32), [foreign namespace 거절](/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/RedisPubSubOperationsContractTest.java:50), [empty subscription 거절](/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/RedisPubSubOperationsContractTest.java:65), [channel별 codec](/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/RedisPubSubOperationsContractTest.java:72), [pattern mixed codec 거절](/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/RedisPubSubOperationsContractTest.java:100), [sharded capability gate](/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/RedisPubSubOperationsContractTest.java:157), [reactive cancellation cleanup](/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/RedisPubSubOperationsContractTest.java:188)을 서로 다른 테스트가 고정합니다. Admin은 [diagnostic parsing](/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/RedisAdminPlaneContractTest.java:35), [fixed·redacted config projection](/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/RedisAdminPlaneContractTest.java:45), [slow log argument 제거](/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/RedisAdminPlaneContractTest.java:63), [client identity 제거](/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/RedisAdminPlaneContractTest.java:73), [projection bound](/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/RedisAdminPlaneContractTest.java:113), [destructive command 차단](/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/RedisAdminPlaneContractTest.java:124), [`ADMIN_ONLY` read-only command 한정](/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/RedisAdminPlaneContractTest.java:149)을 개별 사례로 고정합니다. Raw는 [approved command 실행](/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/RedisRawGatewayContractTest.java:55), [`RAW_ONLY`만 approval 가능](/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/RedisRawGatewayContractTest.java:70), [movable key parser 필수](/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/RedisRawGatewayContractTest.java:82), [token provenance](/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/RedisRawGatewayContractTest.java:101), [registered approval과 token 일치](/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/RedisRawGatewayContractTest.java:119), [namespace parse-back](/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/RedisRawGatewayContractTest.java:140), [argument ceiling](/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/RedisRawGatewayContractTest.java:157)을 각각 고정합니다. 이번 문서 작업에서는 이 테스트를 실행하지 않았습니다. production source와 테스트를 정적으로 대조했습니다. ## 현재 구현 공백과 잘못 읽기 쉬운 지점 1. advanced surface implementation은 있지만 production Spring bean 조립은 확인되지 않습니다. 2. script의 process registration과 Redis server load 시점은 다릅니다. `SCRIPT LOAD`는 first use입니다. 3. function은 배포 시 load해야 하며 request-time load/recovery가 없습니다. 4. `RegisteredRedisFunction`의 library/version은 call path에서 server deployment와 대조되지 않습니다. Javadoc이 말하는 deployment check 구현도 찾지 못했습니다. 5. admin class는 `FUNCTION LOAD`를 public method로 노출하지 않습니다. function deployment는 이 application admin surface 밖의 작업입니다. 6. raw role credential은 settings에서 해석되지만 `RedisConnectionKind`에는 `RAW` lane이 없고 descriptor는 `RAW_GATEWAY`를 `REGULAR`로 매핑합니다. 실제 별도 raw account connection 조립은 확인되지 않습니다. 7. batch는 atomic하지 않고 transaction은 rollback하지 않습니다. 8. script, function, raw, admin에는 reply budget 값이 있지만 관측한 reply byte를 decoder 전에 검사하지 않습니다. extension은 policy name이 있을 때만 budget이 있고 null policy path에는 budget 자체가 없으며, 어느 쪽도 관측 reply를 검사하지 않습니다. 9. batch의 post-decode ceiling은 result shape의 근사 누적값에 적용됩니다. `CharSequence.length()`와 unknown scalar 1을 사용하므로 exact wire bytes가 아닙니다. 10. transaction `INCRBY`와 transaction collection write는 expiration이나 persistent permit 없이 absent key를 만들 수 있습니다. 11. extension fixture의 `ifSupported` 조립은 production conditional bean 증거가 아닙니다. 다음에 source를 열 때는 `RedisConnectionKind`, 각 public interface, implementation, contract test, 마지막으로 production auto-configuration 순으로 보면 됩니다. ## 시리즈의 관련 문서 관련 범위는 connection lifecycle, command admission, typed operations, execution failure certainty입니다. ## 시리즈에서 이어 읽기 - 이전 글: [문자열 명령 대신 타입을 노출하는 RedisOperations 코드 지도](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-typed-operations.md) - 다음 글: [Timeout 뒤 쓰였는지 모를 때: Executor와 실행 확실성 모델](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-execution-failure-certainty.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)