docs(keycloak-session-store): import the session-storage lab as a new project

The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.

Follows the import procedure in README.md.

  source/     the originating repository verbatim — 78 documents, 28 SVGs,
              8 manifests, plus .source-revision recording the commit
  final/      the SSOT
    document.md   729 lines written from the 29 experiment documents, not
                  concatenated: what was predicted, what was measured, and
                  where the measurement itself was wrong
    evidence/raw    125 outputs, flattened to <experiment>__<file> because
                    the originals collided (01-baseline.txt appeared three
                    times) and the audit only globs the top level
    evidence/meta   one per raw file; command and exitCode are null and the
                    README says why rather than inventing them
    evidence/browser  22 captures
    assets/       three diagrams through techviz
    .techviz/     their VizSpecs

A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.

Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.

verify-pipeline.py passes. audit-records.py reports no issues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-09-04 22:51:59 +09:00
co-authored by Claude Opus 5
parent 43bccd08a8
commit b2963105a8
5017 changed files with 372751 additions and 4943 deletions
@@ -0,0 +1,125 @@
---
kind: CASE
slug: a-build-gate-that-is-not-in-the-build
title: '"build gate"라 불리는 catalog drift 검사가 어디에서도 실행되지 않는다'
topic: redis-command-admission
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:a-build-gate-that-is-not-in-the-build
evidenceCapturedOn: 2026-09-02
assets:
- key: a-build-gate-that-is-not-in-the-build
file: ../../../final/evidence/rendered/a-build-gate-that-is-not-in-the-build.svg
evidence:
- ../../../final/evidence/raw/a-build-gate-that-is-not-in-the-build.txt
source:
- 원본 분석 절은 analysis/10 §47 · final/document.md#4-4 이다. 참조 수는 §8.1 의 원문 측정과 같다.
- Gradle 태스크와 CI 단계가 없다는 것은 이 기록에 붙은 자산에 있다. 토폴로지 레인의 두 설정은 analysis/10 §0 과 evidence/raw/158-cache-redis-module-inventory.txt 에, 레인의 결합 상태는 `cache-redis/build.gradle` 과 `.github/ci-gate-matrix.yml` 에 있다.
module: adapter-outbound-cache-redis
priority: P2
---
# "build gate"라 불리는 catalog drift 검사가 어디에서도 실행되지 않는다
명령 메타데이터 드리프트를 검사하는 코드가 있고 정책 파일 머리 주석이 그것을 빌드 게이트라고 부른다. 그 검사를 실행하는 Gradle 태스크도 CI 단계도 없다.
## 관계
- **산문이 선언한 게이트는 빌드에 있는 게이트가 아니다**
이 사례가 그 규칙을 만든 형태다.
- **두 파일이 같은 검증기를 "빌드를 실패시키는 것"이라 적고, 어떤 빌드도 그것을 부르지 않는다**
같은 형태가 gRPC 가족에서 나타난 사례다.
- **서버 메타데이터가 명령의 정의이고 정책 파일은 허용 범위다**
이 검사가 지키려는 관계다.
## 문제
명령 카탈로그의 정의는 서버 메타데이터에서 온다. 정책 파일은 그 위에서 허용 범위를 정한다.
두 쪽이 어긋날 수 있다. 서버 버전이 올라가면서 명령의 키 스펙이나 플래그가 바뀌면 정책 파일이 옛 정의 위에 서 있게 된다.
그 드리프트를 검사하는 코드가 있다. 정책 파일 머리 주석이 그것을 빌드 게이트라고 부른다.
## 결론
빌드에 없다.
그 검사를 실행하는 Gradle 태스크가 등록되어 있지 않고, CI 워크플로에도 그것을 부르는 단계가 없다. 그 코드는 누군가 수동으로 부를 때만 돌고, 부르는 절차는 문서에 없다.
이름은 게이트인데 빌드에서 그것을 부르는 곳이 없다.
## 검증 환경
OpenJDK : 21.0.12
Gradle : 9.0.0
확인 방식 : 검사 코드 확인과 태스크 및 CI 단계 이름 검색
소스 수정 : x
## 재현 조건
1. 명령 메타데이터 드리프트 검사 클래스를 확인한다.
2. 정책 파일 머리 주석에서 그것을 부르는 이름을 확인한다.
3. 그 검사를 실행하는 Gradle 태스크를 찾는다.
4. CI 워크플로에서 그 검사를 부르는 단계를 찾는다.
1~2 단계의 원문 근거는 final/evidence/raw/163-cache-redis-guard-connection-codec-probes.txt §8.1 이다. 3~4 단계의 출력은 이 기록에 붙은 자산에 있다.
## 본문
<!-- body:start -->
cache-redis 어댑터의 명령 정책 파일이 머리 주석에서 이 저장소에서 가장 강한 governance 주장을 한다 — 명령이 무엇인지는 서버 메타데이터가 정하고, 이 SDK 가 그것으로 무엇을 할지는 이 파일이 정하며, **"드리프트 게이트가 둘을 비교해 서버가 이 파일이 판정하지 않은 명령을 추가하면 빌드를 실패시킨다"**. `RedisCommandMetadataDiff` 의 javadoc 도 자신을 "The build gate" 라고 부른다.
비교 로직은 완성돼 있다. 테스트 여섯 개가 다섯 버킷을 덮는다.
## 이 이름을 참조하는 파일이 둘뿐이다
:::evidence key="a-build-gate-that-is-not-in-the-build" alt="코드베이스에서 RedisCommandMetadataDiff 를 검색한 출력 9줄. 이 이름을 참조하는 파일이 선언 자신과 자기 단위 테스트 둘뿐이고, 이 비교를 실행하는 Gradle 태스크와 CI 단계가 없다는 것이 그 출력에 그대로 보인다." caption="RedisCommandMetadataDiff 참조 · Gradle 태스크 · CI 단계 — 9줄 · exit 0" zoom="true"
:::
선언 자신과 자기 단위 테스트. 이 비교를 실행하는 Gradle 태스크가 없고, CI 워크플로에서 부르는 단계도 없다. 실제 서버 메타데이터를 이 함수에 넣는 코드가 저장소에 없다.
빌드를 깨는 게이트는 존재하지 않는다. 존재하는 것은 게이트가 쓸 비교 함수와 그 함수의 단위 테스트다.
## 다섯 버킷이 막기로 되어 있던 것
1. 아무도 분류하지 않은 새 명령
2. 서버에서 사라진 명령
3. 키 추출이 엉뚱한 인자를 가리키게 만드는 key spec 이동
4. 계정을 조용히 넓히는 ACL 카테고리 변경
5. 타입 있는 API 가 아직 노출하는 deprecation
여섯 번째 테스트는 버킷이 아니라 드리프트가 없을 때를 고정하는 음성 케이스다.
## 첫 버킷은 다른 장치가 대신 막는다
새 명령이 조용히 통과하지는 않는다. `RedisCommandCatalog.require` 가 분류되지 않은 명령을 fail-closed 로 거부하고, `theCatalogFailsClosedForAnUnclassifiedCommand` 가 그것을 고정한다. 그래서 이 결함의 데이터 위험이 즉각적이지 않다.
원본 분석이 위험으로 지목한 것은 3·4·5 번 셋이다. key spec 이 이동하면 이 SDK 의 네임스페이스·슬롯 검사가 잘못된 인자를 키로 본다. ACL 카테고리가 넓어지면 계정 분리 가정이 조용히 약해진다. deprecation 은 타입 있는 API 가 사라질 명령을 계속 노출하게 둔다.
2 번(서버에서 사라진 명령)은 어느 쪽으로도 논의되지 않았다. 카탈로그가 그것을 잡는다는 근거도, 위험 목록에 든다는 근거도 원본에 없다.
## 게이트라는 이름이 정책 파일의 주의 수준을 낮춘다
정책 파일을 읽는 사람이 그 주석을 보고 드리프트가 자동으로 잡힌다고 이해할 수 있다 — 이것은 원본 분석에 적힌 판정이 아니라 이 기록의 추론이다. 다만 주석의 문장이 조건 없는 단정이라는 것은 사실이고, 그 문장을 읽고 나서 정책 파일을 손볼 때 확인해야 할 것이 하나 줄어든다.
## 이 리프의 토폴로지 레인과 비교하면
같은 리프에 `redisTopologyTest` 레인이 있다. 그 레인은 `failOnNoDiscoveredTests = true``outputs.upToDateWhen { false }` 를 갖는다 — 발견 0 이 성공이 되지 않고, 이전 실행 결과를 다시 내놓지도 않는다.
다만 그 레인도 릴리스 게이트 Gradle 태스크에 묶여 있지는 않다. 기본 `test``excludeTags 'redis-topology'` 로 그것을 제외하고, 실행하는 것은 CI 워크플로 하나다. gate matrix 에서 그 항목은 `mechanism: workflow-job` 이고 `release_blocking: conditional` 이다 — 같은 매트릭스의 `httpclient-stable-contract``gradle-custom-task``release_blocking: true` 인 것과 다르다.
드리프트 검사에는 그 둘 중 아무것도 없다. 레인도 아니고 워크플로 항목도 아니다.
## 없는 것은 연결 한 줄이다
토폴로지 레인은 이미 실제 서버에 붙어 있고 비교 함수도 완성돼 있다. 그 레인에서 `COMMAND DOCS``COMMAND INFO` 를 읽어 `RedisCommandMetadataDiff.compare(...)` 를 돌리고, 결과가 비어 있지 않으면 실패시키면 된다.
## 확인하지 못한 것
지금 드리프트가 나 있는지는 직접 돌려 보지 않았다. 그 검사를 부르는 실행 경로가 없다는 것까지만 확인했다.
태스크와 CI 단계의 부재는 이름 기반 검색으로 판정했다. 리플렉션이나 서비스 로더처럼 이름이 문자열로만 등장하는 호출 형태는 배제하지 못했다.
<!-- body:end -->
@@ -0,0 +1,136 @@
---
kind: CASE
slug: a-startup-probe-that-never-runs
title: startup probe가 production에서 한 번도 실행되지 않는다
topic: redis-command-admission
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:a-startup-probe-that-never-runs
evidenceCapturedOn: 2026-09-02
assets:
- key: a-startup-probe-that-never-runs
file: ../../../final/evidence/rendered/a-startup-probe-that-never-runs.svg
evidence:
- ../../../final/evidence/raw/a-startup-probe-that-never-runs.txt
source:
- 분석 문서는 cache-redis 어댑터 편 §6 이다. 그 절이 탐침이 확인하는 넷을 나열하고, 넷째의 javadoc 을 인용하고, 두 탐침의 프로덕션 참조가 javadoc 링크 하나뿐이라는 것과 자동설정이 탐침을 부르지 않는다는 것을 기록한다. 판정은 P2 이고, 수정은 자동설정에 탐침을 실행하는 빈 하나를 더하는 것이다.
- 같은 사고의 더 상세한 기록은 저장소 문서 쪽에 있다. 지원 매트릭스가 승격과 강등 시각을 초 단위로 적고, 운영 문서와 런북이 가드를 적용한 뒤 같은 승격에서 무엇이 달라졌는지 적는다.
---
# startup probe가 production에서 한 번도 실행되지 않는다
기동 시 서버 능력을 확인하는 탐침이 있다. 그것을 만들거나 부르는 프로덕션 코드가 없어 한 번도 실행되지 않는다.
## 관계
- **@Bean이 있다는 것은 조립 증거가 아니다**
이 사례가 그 규칙의 형태다.
- **시작 검증기가 도는지는 그 능력에 자동설정 루트가 있는지와 일치한다**
시작 검증기가 도는지를 자동설정 루트로 확인하는 절차다.
- **하위 시스템 전체가 미배선인데 그것을 켜는 플래그는 시작 검사를 수행한다**
플래그와 실행의 어긋남이 반대 방향으로 나타난 사례다.
## 문제
기동 탐침은 서버가 실제로 무엇인지 묻는다. 그것이 확인하는 것은 넷이다. 서버 버전, 클러스터와 데이터베이스 번호, 명시적으로 켠 능력의 실재, 그리고 복제 배포의 쓰기 내구성이다.
넷째의 javadoc 이 이 저장소의 실측 사고 기록을 숫자째로 담고 있다.
## 결론
그 기동 실패는 일어나지 않는다.
두 탐침 클래스를 언급하는 프로덕션 코드는 다른 설정 클래스의 javadoc 링크 하나뿐이고, 확인 메서드를 부르는 프로덕션 코드는 0 이다. 자동설정이 만드는 빈 일곱에 탐침은 없다.
로직은 완성되어 있고 테스트 열여덟 케이스가 네 규칙을 고정한다. 없는 것은 호출 지점 하나다.
## 검증 환경
OpenJDK : 21.0.12
확인 방식 : 정적 도달성 확인
소스 수정 : x
## 재현 조건
1. 기동 탐침의 클래스 javadoc 과 확인 메서드 본문을 읽고, 무엇을 몇 가지 확인하는지 센다.
2. 넷째 항목의 javadoc 에 적힌 사고 기록과 결론, 그리고 검사가 두 겹인 이유를 읽는다.
3. 두 탐침 클래스를 언급하는 프로덕션 코드를 세고 그것이 무엇인지 확인한다.
4. 확인 메서드를 부르는 프로덕션 코드를 센다.
5. 자동설정이 만드는 빈을 나열하고 그중 탐침이 있는지 센다.
6. 두 테스트 파일의 케이스 수를 센다.
## 본문
<!-- body:start -->
기동 탐침은 서버가 실제로 무엇인지 한 번, 기동 시에 묻는다.
이 탐침이 무엇을 막는지부터 본다. 그것이 실행되지 않는다는 판정이 무엇을 잃는 것인지가 거기서 정해진다.
## 확인하는 것은 넷이다
:::evidence key="a-startup-probe-that-never-runs" alt="코드베이스에서 기동 탐침이 자기를 소개하는 문단과 확인 메서드 본문이 부르는 두 검사, 넷째 항목의 javadoc 이 담은 실측과 결론과 검사가 두 겹인 이유, 두 탐침을 언급하는 프로덕션 코드 하나와 그것이 javadoc 링크라는 것과 확인 메서드를 부르는 코드 수, 자동설정이 만드는 빈 일곱과 그중 탐침의 수, 그리고 로직을 고정하는 테스트 케이스 수를 뽑은 출력 70줄. 탐침을 부르는 프로덕션 코드가 0 이고 자동설정의 빈 일곱에 탐침이 없다는 것이 그 출력에 보인다." caption="탐침의 소개 문단과 두 검사 · 넷째의 실측과 두 겹 검사 · 언급 1(javadoc 링크) · 호출 0 · 빈 일곱에 탐침 없음 · 테스트 18케이스 — 70줄" zoom="true"
:::
확인 메서드가 두 호출로 넷을 덮는다. 앞의 호출이 서버 버전과 배포 모드와 데이터베이스 번호와 명령 목록으로 능력을 판정하고, 뒤의 호출이 복제 배포의 쓰기 내구성을 요구한다.
클래스 javadoc 이 왜 서버에 묻는지 적는다. 설정은 배포가 무엇을 의도하는지 말하고 서버만이 무엇이 참인지 말한다는 것, 관리형 Redis 가 광고하는 버전이 모듈의 존재를 뜻하지 않는다는 것, 그리고 복제 배포의 쓰기 내구성은 어떤 클라이언트도 보상할 수 없는 서버 설정이라는 것이다.
같은 문단이 시점의 값도 적는다. 빠진 능력을 첫 요청에서 발견하면 장애이고, 여기서 발견하면 실패한 배포다.
## 넷째 항목의 javadoc 은 실측을 숫자째로 담는다
그 javadoc 은 이것이 SDK 가 보상할 수 없는 유일한 서버 설정이라고 적는다.
승격으로 밀려난 주 노드는 그 사실을 즉시 알지 못한다. 아는 순간까지 쓰기에 계속 성공을 답하고, 새 주 노드에서 재동기화할 때 그 쓰기들이 버려진다.
센티널 레인이 그것을 쟀다. 11초 동안 2,086건이 승인된 뒤 폐기됐고, 같은 실행에서 호출자에게 실패로 보인 명령은 통틀어 하나뿐이었다.
그 아래 문장이 왜 아무도 못 보는지 적는다. 서버가 답했으므로 드라이버도, 이 SDK 도, 호출자도 전부 성공으로 기록한다. 더할 지표도, 재시도할 실패도, 그것을 서술할 확신 값도 없다.
## 그래서 경고가 아니라 기동 실패다
복제 최소 개수와 상한이 걸린 최대 지연이 그 상황을 호출자가 대응할 수 있는 거절로 바꾼다. 같은 승격이 그때는 2,086건 대신 한 건을 잃었다.
그래서 그 설정 없는 복제 배포는 경고가 아니라 기동 실패라고 적는다. 보증을 무의미하게 만드는 설정은 조용히 성능을 낮추는 대신 컨텍스트를 멈춘다는 것이 이 SDK 의 원칙이라는 것이다.
면제는 가능하다. 배포가 정말로 쓰기 하나를 잃어도 괜찮을 수 있고 이 SDK 가 서버를 소유하지 않기 때문이다. 다만 면제에는 명시적 설정이 필요해서, 그 거래가 사고 중에 발견되는 대신 기록으로 남는다.
## 그 검사 자체도 한 번 고쳐졌다
같은 javadoc 이 검사가 두 겹인 이유를 적는다.
복제 개수만으로는 몇 개가 연결되어 있어야 하는지만 정해진다. 얼마나 뒤처져도 되는지는 최대 지연이 정하고, Redis 는 그 값 0 을 지연 요구 없음으로 다룬다.
그래서 복제 둘을 요구하면서 지연 상한을 0 으로 둔 배포는, 임의로 뒤처진 복제 둘이 붙어 있기만 하면 쓰기를 받는다. 개수 요구가 없애려던 바로 그 노출이 그대로 남는다.
개수만 검사하던 동안 그 설정이 통과했다. 그러면서 실패 메시지는 운영자에게 지연 상한을 설정하라고 말하고 있었다.
## 그 기동 실패는 일어나지 않는다
두 탐침 클래스를 언급하는 프로덕션 코드가 하나뿐이다. 그것도 다른 설정 클래스의 javadoc 안에 있는 링크다.
확인 메서드를 부르는 프로덕션 코드는 0 이다.
자동설정은 빈 일곱을 만든다. 설정과 그 검증, 자격증명, 런타임 클라이언트와 소유자, 그리고 헬스 지표 둘이다. 그중 탐침은 0 이다.
## 없는 것은 호출 지점 하나다
로직은 완성되어 있다. 두 테스트 파일의 열여덟 케이스가 네 규칙을 고정한다.
탐침이 연결을 들고 있지 않은 것은 설계된 것이다. 서버 사실을 인자로 받아서, 같은 로직을 단위 테스트와 실제 레인이 함께 쓰고 어느 계정이 물을지를 호출자가 정한다.
그 사실 중 둘을 읽는 명령이 관리 평면이라 애플리케이션 계정은 거부된다. 그래서 조립은 관리 계정이 설정된 배포에서만 완전하다.
## 지금 남은 것은 장애 쪽이다
빠진 능력을 첫 요청에서 발견하면 장애이고 여기서 발견하면 실패한 배포다. 조립이 없으므로 남은 것은 앞쪽이다.
복제 내구성은 그마저도 아니다. 서버가 성공을 답하므로 장애로도 나타나지 않는다.
## 확인하지 못한 것
애플리케이션을 부팅해 탐침이 실행되지 않는 것을 관측하지 않았다. 정적으로 도달성만 확인했고, 위 실측값은 이 저장소의 센티널 레인이 잰 것을 javadoc 에서 인용한 것이지 이 기록을 위해 다시 잰 것이 아니다.
<!-- body:end -->
@@ -0,0 +1,104 @@
---
kind: CASE
slug: five-adapters-bypass-the-single-admission-point
title: 의미 어댑터 다섯이 gateway를 직접 불러 admission 아홉 단계를 건너뛴다
topic: redis-command-admission
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:five-adapters-bypass-the-single-admission-point
evidenceCapturedOn: 2026-09-01
body: case-five-adapters-bypass-the-single-admission-point.body.md
assets:
- key: five-adapters-bypass-the-single-admission-point
file: ../../../final/evidence/rendered/five-adapters-bypass-the-single-admission-point.svg
evidence:
- ../../../final/evidence/raw/five-adapters-bypass-the-single-admission-point.txt
source:
- 원본 분석 절은 final/document.md#4-4 · analysis/10 §64 이다.
---
# 의미 어댑터 다섯이 gateway를 직접 불러 admission 아홉 단계를 건너뛴다
명령 정책 가드는 자기를 모든 명령이 지나는 단일 승인 지점이라고 적는다. 캐시와 멱등성과 리스와 레이트리밋과 실시간 어댑터 다섯은 그 가드도 실행기도 타입 API 도 참조하지 않고 게이트웨이를 직접 부른다.
## 관계
- **명령 카탈로그와 admission 아홉 단계**
우회되는 대상이다.
- **단일 admission point는 우회 경로를 세어야 성립한다**
이 사례에서 끌어낸 규칙이다.
- **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다**
같은 계열의 확인 규칙이다.
## 문제
가드의 클래스 javadoc 은 자기를 모든 명령이 지나는 단일 승인 지점이라고 선언한다.
그 선언이 참인지는 우회 경로를 세어야 알 수 있다.
## 결론
다섯 어댑터가 우회한다.
가드나 실행기나 명령 요청 타입을 참조하는 파일을 다섯 어댑터 패키지에서 찾으면 일치가 없다.
타입 지정 연산 API 를 참조하는 파일도 일치가 없다.
대신 게이트웨이를 직접 부른다. 호출 수는 이렇다.
캐시 리전 어댑터 6
멱등성 저장소 어댑터 6
분산 리스 어댑터 4
엣지 레이트리밋 어댑터 1
커넥션 레지스트리 어댑터 13
합계 30 이다.
임포트 목록도 같은 그림을 보여 준다. 다섯 어댑터가 SDK 에서 가져오는 것은 네임스페이스와 커넥션 소유자와 리스와 커넥션 종류와 게이트웨이 계열이다. 가드나 실행기는 없다.
즉 아홉 단계는 우회하지 않은 호출에만 적용되는 규칙이다. 능력 확인도 허가 출처 확인도 네임스페이스 검사도 예산도 이 다섯 경로에는 걸리지 않는다.
가드의 javadoc 이 틀린 것은 아니다. 그 문장은 가드를 지나는 명령에 대해 참이다. 틀린 것은 그 문장을 읽고 모든 명령이 지난다고 이해하는 것이다.
## 검증 환경
OpenJDK : 21.0.12
확인 방식 : 다섯 어댑터 패키지에 대한 참조 검색과 호출 계수
소스 수정 : x
## 재현 조건
원문은 final/evidence/raw/165-cache-redis-semantic-adapters-probes.txt 8.1 절에 있다.
1. 다섯 어댑터 패키지에서 가드와 실행기와 명령 요청 타입을 참조하는 파일을 찾는다. 일치 0 이다.
2. 같은 패키지에서 타입 지정 연산 API 를 참조하는 파일을 찾는다. 일치 0 이다.
3. 같은 패키지에서 게이트웨이 호출 수를 센다.
4. 다섯 어댑터가 SDK 에서 가져오는 임포트를 집계한다.
## 본문
<!-- body:start -->
두 javadoc이 "The single admission point every command passes through"와 "everything routed through it has already passed `CommandPolicyGuard`"를 주장한다.
## CommandPolicyGuard 참조 위치
:::evidence key="five-adapters-bypass-the-single-admission-point" alt="코드베이스에서 CommandPolicyGuard 를 검색한 출력 26줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="CommandPolicyGuard 코드베이스 검색 — 26줄 · exit 0" zoom="true"
:::
## 다섯 어댑터가 gateway를 직접 부른다
`RedisRuntimeOwner``RedisLease``lease.gateway()`를 30곳에서 부른다. `SyncRedisCommandExecutor`·`ReactiveRedisCommandExecutor`·`CommandPolicyGuard`·`CommandRequest`를 참조하는 파일이 0이고 타입 있는 API 참조도 0이다.
## 아홉 단계 중 남는 것 하나
네임스페이스만 `CapabilityKeyspace`가 같은 `RedisNamespace`에서 키를 조립해 구성으로 유지된다.
## 확인하지 못한 것
우회 경로로 실제로 거부되어야 할 명령이 통과하는 것을 재현하지 않았다. 이 기록은 참조 계수에 근거한다.
실제 Redis에 붙여 우회 경로의 명령이 정책 없이 나가는지 관측하지 않았다
<!-- body:end -->
@@ -0,0 +1,89 @@
---
kind: CASE
slug: five-copies-of-noscript-recovery
title: NOSCRIPT 복구가 다섯 벌이고 넷은 스크립트 레지스트리를 지나지 않는다
topic: redis-command-admission
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:five-copies-of-noscript-recovery
evidenceCapturedOn: 2026-09-01
assets:
- key: five-copies-of-noscript-recovery
file: ../../../final/evidence/rendered/five-copies-of-noscript-recovery.svg
evidence:
- ../../../final/evidence/raw/five-copies-of-noscript-recovery.txt
source:
- 원본 분석 절은 final/document.md#4-4 · analysis/10 §56 이다.
---
# NOSCRIPT 복구가 다섯 벌이고 넷은 스크립트 레지스트리를 지나지 않는다
서버가 스크립트를 모른다고 답할 때의 복구 경로가 다섯 곳에 있다. 그중 넷은 스크립트 레지스트리를 거치지 않고 자기 자리에서 다시 적재한다.
## 관계
- **같은 개념의 두 어휘가 공존하면 하나를 죽은 것으로 표시한다**
같은 계열의 중복 문제다.
- **단일 admission point는 우회 경로를 세어야 성립한다**
같은 리프에서 반복되는 형태다.
- **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다**
다섯 벌 중 어느 것이 도는지 확인해야 하는 이유다.
## 문제
Redis 는 스크립트를 해시로 실행한다. 서버가 그 해시를 모르면 특정 오류로 답하고, 클라이언트는 스크립트를 다시 적재한 뒤 재시도해야 한다.
이 복구는 흔한 경로다. 서버 재시작이나 스크립트 캐시 정리 후에 반드시 일어난다.
## 결론
복구 경로가 다섯 벌이다.
그중 하나만 스크립트 레지스트리를 지난다. 나머지 넷은 자기 자리에서 스크립트를 다시 적재한다.
이 상태의 비용은 셋이다.
레지스트리가 어떤 스크립트가 적재되어 있는지에 대한 정본이 아니게 된다. 넷이 그것을 우회해 적재하기 때문이다.
스크립트 내용이 바뀔 때 다섯 곳을 함께 고쳐야 한다. 하나를 놓치면 그 경로만 옛 스크립트를 다시 적재한다.
복구 정책을 바꾸기 어렵다. 재시도 횟수나 백오프를 조정하려면 다섯 곳을 봐야 한다.
증상이 드러나지 않는 것이 이 계열의 특징이다. 다섯 벌이 각각 동작하므로 기능은 정상이다. 비용은 전부 다음에 고치는 사람에게 간다.
## 검증 환경
OpenJDK : 21.0.12
Redis 클라이언트 : Lettuce
확인 방식 : 복구 경로 전수 확인
소스 수정 : x
## 재현 조건
1. 스크립트 없음 오류를 처리하는 코드를 리프 전체에서 찾는다.
2. 각각이 스크립트를 어떻게 다시 적재하는지 확인한다.
3. 스크립트 레지스트리를 지나는 것과 그렇지 않은 것을 구분한다.
## 본문
<!-- body:start -->
Redis가 `NOSCRIPT`를 반환했을 때의 복구(스크립트 재등록 후 재시도)가 다섯 벌로 구현돼 있고 그중 넷이 스크립트 레지스트리를 지나지 않는다.
## NOSCRIPT 복구가 구현된 다섯 곳
:::evidence key="five-copies-of-noscript-recovery" alt="분석 문서 final/document.md 에서 이 기록의 근거 절을 그대로 잘라낸 18줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md 발췌 — 18줄" zoom="true"
:::
## 레지스트리를 지나는 한 벌만 대응을 유지한다
레지스트리가 스크립트 SHA와 소스의 대응을 유지하므로, 나머지 넷은 재등록하는 스크립트가 레지스트리가 아는 것과 같다는 보장이 없다.
## 확인하지 못한 것
서버 재시작 후 다섯 경로가 각각 어떻게 동작하는지 실행으로 확인하지 않았다. 이 기록은 코드 경로 전수 확인에 근거한다.
실제 SCRIPT FLUSH 이후 다섯 경로의 동작 차이를 관측하지 않았다
<!-- body:end -->