--- kind: CONCEPT slug: adapter-outbound-cache-redis-c10 title: 뒤 단계일수록 비싸도록 검증 순서를 고정했다 topic: state-machines-and-ownership project: clean-architecture-backend-template status: 게시 전 sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 rootTreeNode: concept:adapter-outbound-cache-redis-c10 evidenceCapturedOn: 2026-09-01 assets: - key: adapter-outbound-cache-redis-c10 file: ../../../final/evidence/rendered/adapter-outbound-cache-redis-c10.svg - key: adapter-outbound-cache-redis-c10-diagram file: ../../../final/assets/diagrams/adapter-outbound-cache-redis-c10.svg evidence: - ../../../final/evidence/raw/adapter-outbound-cache-redis-c10.txt source: - 원본 분석 절은 final/document.md#a10#L560 이다. module: adapter-outbound-cache-redis --- # 뒤 단계일수록 비싸도록 검증 순서를 고정했다 `CommandPolicyGuard`의 javadoc이 순서와 이유를 적는다 — 각 단계가 다음 단계보다 싸므로 명백히 부적격한 명령은 인코딩도 전송도 하기 전에 거절된다. ## 본문 javadoc이 순서와 그 이유를 적는다 — "Validation order is fixed and **each step is cheaper than the one after it**, so an obviously inadmissible command is refused before anything is encoded or sent." ```text capability → risk/permit provenance → namespace → slot → request budget → connection lane → timeout/retry → invocation → reply budget → translation → telemetry ``` ## 고정된 검증 순서 :::evidence key="adapter-outbound-cache-redis-c10-diagram" alt="capability 와 위험 등급과 namespace 와 slot 과 요청 예산이 왼쪽에서 오른쪽으로 이어지는 구조" caption="고정된 검증 순서" zoom="false" ::: 각 단계가 구체적이다. `requireReachable`은 `BLOCKED`거나 `access == NONE`이면 거부하고 R3/R4를 애플리케이션 경로에서 배제한다. `requireCapability`는 명령의 최소 버전을 프로브된 서버 버전과 대조한다. `requireNamespace`는 모든 키의 네임스페이스를 확인하고 렌더까지 수행한다. `requireSameSlot`은 Cluster에서 두 개 이상 슬롯이면 `RedisCrossSlotException`을 **서버를 부르기 전에** 던진다. `effectiveTimeout`은 블로킹 명령이 유한한 server block을 선언하지 않으면 거부하고, 설정 상한을 넘으면 거부하며, 통과하면 `BLOCKING_MARGIN`(2초)을 더한다. ## CommandPolicyGuard 참조 위치 :::evidence key="adapter-outbound-cache-redis-c10" alt="코드베이스에서 CommandPolicyGuard 를 검색한 출력 26줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="CommandPolicyGuard 코드베이스 검색 — 26줄 · exit 0" zoom="true" ::: ## 죽은 중복을 지운 기록 `validateReply(...)`가 있었고 아무도 부르지 않았다. > "Two mechanisms for one rule, with the more visible one dead, is worse than one: a reader finds the guard's method, assumes replies are bounded during admission, and writes an operation that never bounds its own. **Admission cannot do this job anyway.** The guard runs before the command is sent, so the only reply size available to it is the estimate the request declared. The authority has to sit where the bytes actually arrive." ## 발화하지 못하던 조건을 떼어낸 기록 다중 키 permit 검사가 advanced permit 검사와 한 조건으로 접혀 있었고, "둘 다 없음"이 위에서 이미 던지므로 다중 키 절은 도달 불가였다 — "set algebra over any number of keys was admitted on an advanced permit alone." 지금은 `request.keys().size() > 1 && request.multiKeyPermit().isEmpty()`가 독립 조건이다. test `rejectsAMultiKeyCommandCarryingOnlyAnAdvancedPermit`과 `aSingleKeyAdvancedCommandStillNeedsNoMultiKeyPermit`가 양쪽을 고정한다.