Files
document-haness/.run/redis/redis-command-policy-admission.md
T

237 lines
26 KiB
Markdown

# YAML 한 줄이 Redis 명령을 거절하기까지: Policy Loader·Catalog·Guard
> **Redis 코드 상세 시리즈 07/20** · [전체 지도](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-backend-policy-boundary.md) · 이전: [Redis 연결을 여섯 lane으로 나눈 이유: Pool과 RuntimeOwner 생명주기](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-connection-lanes-lifecycle.md) · 다음: [Raw key와 영구 쓰기를 막는 코드: Namespace·Hash Slot·TTL](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-keyspace-expiration.md)
## 이 글이 답하는 코드 질문
Redis 명령 하나가 애플리케이션 코드에서 Lettuce 호출로 넘어가기 전에 무엇을 검사합니까?
이 질문은 다음 세 경계를 나눠 읽어야 답할 수 있습니다.
- YAML은 조직이 명령을 어떻게 분류했는지 기록합니다.
- catalog는 분류되지 않은 명령을 기본 거절합니다.
- guard는 서버 능력, permit, namespace, slot, budget, timeout을 순서대로 검사합니다.
기준은 source HEAD `3b5aee50e33c44c02d08c94bb39ad34814482010`, 2026-08-13입니다.
정적 조사 결과 정책 파일에는 명령과 subcommand를 합쳐 314개 항목이 있습니다. `WAIT` 항목은 없습니다. 따라서 현재 `WAIT`는 허용 명령이 아니라 catalog 조회에서 거절되는 default-deny 대상입니다.
## 먼저 보는 클래스·리소스 지도
| 진입점 | 입력 | 출력 | 다음 호출 |
|---|---|---|---|
| [redis-command-policy.yml](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/resources/redis-sdk/redis-command-policy.yml:1) | 명령별 scalar 필드 | 조직 정책 314개 | `RedisCommandPolicyLoader` |
| [RedisCommandPolicyLoader.loadDefault](/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/command/RedisCommandPolicyLoader.java:59) | classpath YAML | `Map<CommandId, RedisCommandPolicy>` | `RedisCommandCatalog` |
| [RedisCommandCatalog.require](/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/command/RedisCommandCatalog.java:56) | `CommandId` | 분류된 policy | `CommandPolicyGuard` |
| [CommandRequest](/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/command/CommandRequest.java:34) | key, 크기, permit, budget, block, 지연된 invocation | 실행 전 요청 | executor |
| [CommandPolicyGuard.validate](/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/command/CommandPolicyGuard.java:89) | `CommandRequest` | `CommandAdmission` | sync/reactive/queueing executor |
| [ConfiguredRedisPermitVerifier](/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/config/ConfiguredRedisPermitVerifier.java:22) | permit와 요구 policy | 통과 또는 거절 | guard/context |
| [RedisCommandDescriptor](/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/command/RedisCommandDescriptor.java:12) | policy에서 파생된 값 | 실행 불변식 | lane·translator |
`CommandRequest.invocation`은 이미 시작한 future가 아니라 `Supplier<CompletionStage<R>>`입니다. [invocation field 선언](/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/command/CommandRequest.java:34) 덕분에 guard가 끝나기 전에는 driver call이 시작되지 않습니다.
## 객체 생성 시점과 request-time을 구분합니다
### 객체 생성 시점
의도된 조립 순서는 다음과 같습니다.
1. `RedisCommandPolicyLoader``/redis-sdk/redis-command-policy.yml`을 읽습니다.
2. loader가 각 block을 `RedisCommandPolicy`로 바꿉니다.
3. `RedisCommandCatalog`가 immutable map을 소유합니다.
4. deployment 설정으로 `ConfiguredRedisPolicyAuthority`와 verifier를 만듭니다.
5. probed `RedisCapabilities`, `RedisNamespace`, renderer, slot calculator로 guard를 만듭니다.
6. guard와 translator를 sync/reactive/queueing executor에 주입합니다.
그러나 이 순서가 production Spring bean으로 완성됐다고 볼 근거는 없습니다. [RedisSdkAutoConfiguration](/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/config/RedisSdkAutoConfiguration.java:226)은 runtime client와 owner를 만들지만 catalog, authority, verifier, guard, executor bean은 만들지 않습니다.
### request-time
```mermaid
sequenceDiagram
participant O as Typed/Advanced operation
participant R as CommandRequest
participant G as CommandPolicyGuard
participant C as RedisCommandCatalog
participant E as Executor
participant L as Lettuce gateway
O->>R: key·size·optional permit/budget·invocation 구성
E->>G: validate(request)
G->>C: require(commandId)
C-->>G: policy 또는 default-deny
G->>G: reachability→capability→permit→namespace→slot→budget(if present)→timeout
G-->>E: CommandAdmission
E->>L: invocation.get()
```
여기서 관측한 reply 크기와 예외 번역은 `validate` 안에 있지 않습니다. guard의 주석은 전체 pipeline을 요약하지만, 실제 `validate`는 admission까지 담당합니다. [requireRequestBudget](/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/command/CommandPolicyGuard.java:207)이 비교하는 값은 request byte와 request builder가 미리 선언한 `expectedReplyBytes`입니다. typed decoder path에서 서버가 돌려준 byte·element 수를 `OperationBudget`과 비교하려면 해당 decoder가 [RedisOperationContext.requireReplyWithinBudget](/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/RedisOperationContext.java:360)을 명시적으로 호출해야 합니다.
그 호출은 MGET, bounded range, collection page 같은 일부 typed decoder에는 있지만 모든 경로에 있지는 않습니다. 기본 [GET 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:43)은 `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)에도 관측 reply 검사가 없습니다. script, function, raw, admin은 budget을 request에 붙이지만 결과 decoder 앞에서 관측 크기를 검사하지 않습니다. extension은 더 나뉩니다. [policy name이 있으면 collection budget을 붙이고 null이면 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), 어느 분기도 관측 reply 크기를 검사하지 않습니다.
batch는 이 typed helper를 쓰지 않는 별도 경로입니다. [preflight](/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:90)에서는 item의 declared `expectedReplyBytes` 합계를 검사하고, 응답 뒤에는 [decoded result shape의 근사치를 누적](/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:180)합니다. 이 값은 exact wire bytes가 아닙니다. 따라서 admission을 통과했다는 사실만으로 실제 reply byte ceiling까지 집행됐다고 말할 수 없습니다.
## YAML parser가 fail-closed인 방식
loader는 범용 YAML parser를 사용하지 않습니다. 허용하는 문법은 `commands:` root 하나, 명령 block, scalar field뿐입니다.
[readBlocks](/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/command/RedisCommandPolicyLoader.java:84)는 다음 입력을 거절합니다.
- tab이 들어간 문서
- 두 번째 root 또는 `commands:`가 아닌 root
- root보다 먼저 나온 command block
- 0·2·4칸 외 indentation
- 중복 command
- 알 수 없는 field
- 값이 비어 있는 field
- 중복 field
허용 field 집합은 [FIELDS](/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/command/RedisCommandPolicyLoader.java:39)에 고정되어 있습니다. `risk``support`는 필수입니다. boolean은 정확히 `true` 또는 `false`여야 합니다.
### 기본값도 정책입니다
[toPolicy](/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/command/RedisCommandPolicyLoader.java:149)는 생략한 값을 다음처럼 채웁니다.
- `minimum-version`: `7.2`
- `read-only`: `false`
- `blocking`: `false`
- `retry-safe`: `read-only`
- `may-be-ambiguous`: `!read-only`
- `key-spec`: `1 1 1`
- `access`: support class에서 파생
- `timeout-profile`: risk와 blocking에서 파생
`key-spec``none`, `movable`, 또는 `<first> <last> <step>`만 읽습니다. [keySpec 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/command/RedisCommandPolicyLoader.java:201)가 다른 표기를 거절합니다.
## R1~R4와 support class는 다른 축입니다
[RedisRiskLevel](/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/command/RedisRiskLevel.java:4)은 비용과 위험을 분류합니다.
| risk | 코드상 의미 |
|---|---|
| `R1` | bounded single-key ordinary command |
| `R2` | O(N), 큰 reply, blocking, multi-key, 큰 payload 등; permit와 budget 필요 |
| `R3` | server·client·ACL·topology 작업; application path 거절 |
| `R4` | destructive; SDK 전체 차단 |
[CommandSupport](/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/command/CommandSupport.java:4)은 어떤 surface로 노출하는지 정합니다.
- `TYPED`
- `ADVANCED_TYPED`
- `RAW_ONLY`
- `ADMIN_ONLY`
- `VERSION_GATED`
- `BLOCKED`
두 축의 조합은 자유롭지 않습니다. [RedisCommandDescriptor 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/api/command/RedisCommandDescriptor.java:25)는 `R4``BLOCKED`가 아니거나 `R3``ADMIN_ONLY`/`BLOCKED`가 아니면 실패합니다. ambiguous write를 retry-safe로 표시하는 조합도 거절합니다.
## Guard의 실제 검사 순서
[validate](/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/command/CommandPolicyGuard.java:89)의 순서는 다음과 같습니다.
1. catalog에서 policy를 찾습니다.
2. `BLOCKED`, `NONE`, application에서 도달할 수 없는 risk를 거절합니다.
3. probed server version이 `minimumVersion`을 만족하는지 봅니다.
4. R2이면 permit와 budget을 요구합니다.
5. 모든 key가 process namespace에 속하는지 확인하고 render합니다.
6. key의 slot을 계산하고 Cluster에서 여러 slot이면 거절합니다.
7. request byte와 예상 reply byte가 budget 이내인지 확인합니다.
8. effective timeout을 계산합니다.
9. descriptor로 connection lane을 정해 `CommandAdmission`을 반환합니다.
이 순서에서 실패하면 invocation supplier는 평가되지 않습니다. 즉 namespace 위반이나 budget 초과는 Redis 서버 오류가 아니라 전송 전 SDK 거절입니다.
## Permit은 marker interface가 아닙니다
R2 요청에 `AdvancedOperationPermit` 구현체를 넣었다고 통과하지 않습니다. [ConfiguredRedisPermitVerifier.check](/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/config/ConfiguredRedisPermitVerifier.java:71)는 네 가지를 확인합니다.
1. concrete granted type인가
2. 현재 authority의 issuer id인가
3. HMAC signature가 맞는가
4. command가 요구한 policy name과 같은가
여러 key를 건드리는 R2 요청은 advanced permit과 별개로 multi-key permit이 필요합니다. [별도 검사](/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/command/CommandPolicyGuard.java:145)는 한 permit이 비싼 연산 승인과 fan-out 승인을 동시에 뜻하지 않게 합니다.
## 정상·거절·timeout 분기
### 정상 분기
`GET`처럼 catalog의 R1/TYPED 명령은 server version과 namespace를 통과하면 기본 `FAST` timeout 500ms와 `REGULAR` lane을 받습니다.
R2 명령은 올바른 policy로 발급된 permit, 필요한 multi-key permit, 양수 budget을 갖춰야 admission을 받습니다. non-blocking 명령과 server block을 선언하지 않은 optional-blocking 명령에서는 budget timeout이 policy 기본 timeout을 덮습니다.
### 거절 분기
- catalog에 없는 명령: `RedisCommandRejectedException`, not sent
- `BLOCKED`/R4: SDK 전체 거절
- server version 미달: `RedisCapabilityUnavailableException`
- permit 없음·위조·다른 policy: `RedisCommandRejectedException`
- namespace 이탈: `RedisCommandRejectedException`
- Cluster cross-slot: `RedisCrossSlotException`
- request/예상 reply budget 초과: `RedisCommandRejectedException`
- bounded block 누락·0·음수·상한 초과: `RedisCommandRejectedException`
blocking 명령이 bounded server block을 선언한 경우에는 budget timeout을 쓰지 않습니다. [effectiveTimeout](/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/command/CommandPolicyGuard.java:231)은 block 상한을 검사한 뒤 `serverBlock + BLOCKING_MARGIN(2초)`를 반환합니다. `BLPOP`처럼 block이 필수인 명령은 선언이 없으면 거절하고, `XREAD`처럼 optional인 명령은 block을 생략했을 때만 budget 또는 profile timeout으로 돌아갑니다.
### `WAIT`는 현재 사용할 수 없습니다
정책 YAML의 314개 block을 정적으로 세었지만 `WAIT` block은 찾지 못했습니다. catalog는 unknown command에 permissive fallback을 두지 않습니다. [default-deny require](/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/command/RedisCommandCatalog.java:56) 때문에 `WAIT`를 typed, raw, semantic surface에서 실행할 수 있다고 읽으면 안 됩니다.
기존 [operations.md](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docs/redis/operations.md:62)의 `WAIT` 설명은 실행 가능한 현행 surface의 근거가 아닙니다.
## 테스트가 고정하는 계약
policy loader 테스트는 계약마다 시작점을 나눠 읽을 수 있습니다.
- [`GET`의 R1과 `KEYS`의 `BLOCKED/NONE`](/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/command/RedisCommandPolicyLoaderTest.java:21)
- [access·timeout·retry·ambiguity 파생](/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/command/RedisCommandPolicyLoaderTest.java:52)
- [version-gated minimum version 보존](/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/command/RedisCommandPolicyLoaderTest.java:80)
- [모든 R4 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/command/RedisCommandPolicyLoaderTest.java:94)
- [advanced command의 required policy name](/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/command/RedisCommandPolicyLoaderTest.java:113)
- [unknown field·enum·duplicate·tab·잘못된 root 거절](/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/command/RedisCommandPolicyLoaderTest.java:124)
- [unclassified command default-deny](/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/command/RedisCommandPolicyLoaderTest.java:153)
- [deprecated command name 차단](/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/command/RedisCommandPolicyLoaderTest.java:163)
- [arbitrary `EVAL` 차단과 registered `EVALSHA` 분리](/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/command/RedisCommandPolicyLoaderTest.java:187)
guard 테스트도 한 링크에 여러 사례를 묶지 않습니다.
- [ordinary command의 lane과 timeout](/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/command/CommandPolicyGuardTest.java:47)
- [R2 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/command/CommandPolicyGuardTest.java:56)
- [caller 구현 permit 거절](/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/command/CommandPolicyGuardTest.java:63)
- [advanced permit만 있는 multi-key 요청 거절](/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/command/CommandPolicyGuardTest.java:75)
- [두 permit을 가진 multi-key advanced 요청 허용](/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/command/CommandPolicyGuardTest.java:94)
- [다른 policy용 permit 거절](/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/command/CommandPolicyGuardTest.java:135)
- [blocked 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/command/CommandPolicyGuardTest.java:149)
- [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/command/CommandPolicyGuardTest.java:158)
- [Cluster 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/command/CommandPolicyGuardTest.java:168)
- [standalone의 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/command/CommandPolicyGuardTest.java:194)
- [request 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/command/CommandPolicyGuardTest.java:207)
- [blocking server block 상한과 2초 margin](/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/command/CommandPolicyGuardTest.java:233)
- [server version 미달 거절](/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/command/CommandPolicyGuardTest.java:256)
permit provenance는 [authority가 발급한 permit 허용](/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/config/RedisPermitProvenanceTest.java:24), [caller 구현체 거절](/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/config/RedisPermitProvenanceTest.java:38), [다른 policy permit 거절](/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/config/RedisPermitProvenanceTest.java:53), [다른 authority permit 거절](/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/config/RedisPermitProvenanceTest.java:62)로 각각 고정됩니다.
이 테스트들은 이번 문서 작업에서 실행하지 않았습니다. source를 정적으로 조사했습니다. 공유 검증 기록에 따르면 기본 module test는 이전 root 세션에서 성공했지만, 이것을 이번 실행 결과로 표현하지 않습니다.
## 현재 구현 공백과 잘못 읽기 쉬운 지점
1. 314개 policy와 guard 구현은 존재하지만 production bean 조립은 확인되지 않습니다.
2. aggregate facade와 executor까지 조립되지 않았으므로 “애플리케이션의 모든 Redis 명령이 현재 이 guard를 지난다”고 단정할 수 없습니다.
3. admission guard는 request 크기와 예상 reply 크기만 budget과 비교합니다. typed decoder의 actual-size 집행은 일부 경로에만 있습니다. 기본 GET, script, function, raw, admin에는 그 호출이 없고, extension은 policy name이 있을 때만 budget을 갖지만 어느 분기도 관측 reply를 검사하지 않습니다. batch는 decoded shape를 별도로 근사 측정하므로 exact wire-byte 집행이 아닙니다.
4. permit은 Redis ACL을 넓히지 않습니다. process 내부 provenance 증명이며 실제 보안 경계는 계정 ACL입니다.
5. `WAIT`는 policy에 없으므로 현재 default-deny입니다.
6. server metadata drift gate용 코드와 테스트가 있어도 이 조사에서는 real-server metadata 비교를 실행하지 않았습니다.
다음에 source를 열 때는 policy YAML, loader, catalog, guard, `CommandRequest`, 각 executor 순으로 보면 됩니다.
## 시리즈의 관련 문서
관련 범위는 keyspace·expiration, typed operations, advanced surfaces, execution failure certainty입니다.
## 시리즈에서 이어 읽기
- 이전 글: [Redis 연결을 여섯 lane으로 나눈 이유: Pool과 RuntimeOwner 생명주기](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-connection-lanes-lifecycle.md)
- 다음 글: [Raw key와 영구 쓰기를 막는 코드: Namespace·Hash Slot·TTL](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-keyspace-expiration.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)