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,110 @@
---
kind: CASE
slug: analysis-finding-a18-f001
title: 출하되는 web 어댑터의 스위치가 활성화 모델 밖에 있다
topic: composition-root-and-bootstrap
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:analysis-finding-a18-f001
evidenceCapturedOn: 2026-09-01
assets:
- key: analysis-finding-a18-f001
file: ../../../final/evidence/rendered/analysis-finding-a18-f001.svg
evidence:
- ../../../final/evidence/raw/analysis-finding-a18-f001.txt
source:
- 원본 분석 절은 analysis/18-app-bootstrap.md#L214 이다.
---
# 출하되는 web 어댑터의 스위치가 활성화 모델 밖에 있다
이 어댑터는 두 런타임의 구성원이라 빌드 전용 예외가 아니다. 그런데 네 스위치가 조건 안의 문자열 리터럴로만 존재해 마스터 스위치 자바독이 경계하는 상태다. 다만 이 어댑터는 성질이 달라 그 모델에 그대로 넣을 수 없다.
## 관계
- **이 leaf의 리액티브 절반 29개 파일은 어떤 출하 배포에서도 활성화될 수 없다**
같은 어댑터의 조립 미해결 사례다.
- **능력 프로퍼티 이름을 만드는 코드와 실제 게이트가 다른 접두사를 쓴다**
같은 계열의 스위치 이름 사례다.
- **무엇을 스위치로 부를지 정하기 전에는 활성화 모델에 넣을 수 없다**
이 사례가 그 규칙의 형태다.
## 문제
이 조립 루트는 선택 어댑터의 활성화를 명시적 스위치 모델로 다룬다.
전부 기본 꺼짐이고, 환경 키 파일에 행이 있고, 삼자 일치 테스트가 강제한다.
웹 어댑터가 그 모델 안에 있는지 확인했다.
## 결론
밖에 있다.
이 어댑터는 두 런타임의 구성원이므로 빌드 전용 예외에 해당하지 않는다.
그런데 그 네 스위치가 조건 안의 문자열 리터럴로만 존재한다.
마스터 스위치의 자바독이 경계하는 상태다. 조건 곳곳에 문자열 리터럴로 흩어지면 이름 변경이 조용한 활성화 변경이 된다는 것이다.
다만 이 어댑터는 다른 넷과 성질이 다르다.
두 전송 스위치가 없으면 참으로 처리되도록 되어 있어 기본이 켜짐이다. 그러므로 선택 어댑터가 아니다.
마스터 스위치가 규정하는 명시적 스위치 모델에 그대로 넣을 수 없다.
그리고 그 어댑터의 조립 자체가 미해결이다. 훑기에서 다섯 패키지를 빼고 넘겨받는 자동 설정을 만들지 않은 상태다.
기록하는 것은 순서다.
이 어댑터의 스위치를 활성화 모델에 넣는 것은 어떤 자동 설정이 무엇을 소유하는가를 먼저 정한 뒤에 할 수 있는 일이다.
지금은 무엇을 스위치로 부를지가 결정되지 않았다.
판정은 P3 다.
## 검증 환경
Spring Boot : 4.0.8
확인 방식 : 스위치 선언 위치 확인과 활성화 모델 대조
소스 수정 : x
## 재현 조건
원문은 final/evidence/raw/188 계열에 있다.
1. 활성화 모델의 규칙 셋을 확인한다.
2. 이 어댑터의 런타임 구성원 목록을 확인한다.
3. 네 스위치가 어디에 선언되어 있는지 확인한다.
4. 두 전송 스위치의 기본값을 확인한다.
5. 마스터 스위치 자바독의 경계 문장을 읽는다.
## 본문
<!-- body:start -->
`adapter-inbound-web`은 두 런타임 멤버이므로 build-only 예외에 해당하지 않는다(§4.1).
## MasterSwitch 참조 위치
:::evidence key="analysis-finding-a18-f001" alt="코드베이스에서 MasterSwitch 를 검색한 출력 17줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MasterSwitch 코드베이스 검색 — 17줄 · exit 0" zoom="true"
:::
## 네 스위치가 문자열 리터럴로만 존재한다
`backend.web.mvc.enabled` · `backend.web.webflux.enabled`(둘 다 `matchIfMissing=true`, 기본 켜짐) · `backend.web.budgets.enabled` · `app.web-platform.durable-operations.enabled``MasterSwitch`에도 `env-keys.yaml` 341개 키에도 없다. `MasterSwitch`의 javadoc이 경계하는 상태다 — "Spread across conditions as string literals, a rename becomes a silent activation change."
## 다만 web은 다른 넷과 성질이 다르다
MVC/WebFlux 스위치는 `matchIfMissing = true`로 기본 켜짐이므로 "옵션 어댑터"가 아니고, `MasterSwitch`가 규정하는 explicit switch 모델(전부 기본 꺼짐, `env-keys.yaml`에 행이 있고 삼자 일치 테스트가 강제)에 그대로 넣을 수 없다. 그리고 모듈 14 §8.1이 확인했듯 web의 조립 자체가 미해결이다.
## 기록하는 것은 순서다
web의 스위치를 활성화 모델에 넣는 것은 모듈 14 §8.1(어떤 자동설정이 무엇을 소유하는가)을 먼저 정한 뒤에 할 수 있는 일이다. 지금은 "무엇을 스위치로 부를지"가 결정되지 않았다. P3.
## 확인하지 못한 것
스위치 이름을 바꿔 조용한 활성화 변경이 일어나는지 재현하지 않았다.
<!-- body:end -->
@@ -0,0 +1,124 @@
---
kind: CASE
slug: analysis-finding-a18-f002
title: 실패는 환경 원인이며, 그 테스트의 도구 가드가 불완전하다
topic: composition-root-and-bootstrap
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:analysis-finding-a18-f002
evidenceCapturedOn: 2026-09-01
body: case-analysis-finding-a18-f002.body.md
assets:
- key: analysis-finding-a18-f002
file: ../../../final/evidence/rendered/analysis-finding-a18-f002.svg
evidence:
- ../../../final/evidence/raw/analysis-finding-a18-f002.txt
source:
- 원본 분석 절은 analysis/18-app-bootstrap.md#L549 이다.
---
# 실패는 환경 원인이며, 그 테스트의 도구 가드가 불완전하다
이 묶음의 유일한 실패는 저장소 결함이 아니다. 분석 환경에 도구 하나가 없고 위임된 스크립트가 그것을 요구하며 정직하게 실패한다. 기록하는 것은 같은 테스트가 첫째 도구는 건너뛰고 둘째 도구는 실패로 다룬다는 비대칭이다.
## 관계
- **아무것도 발견하지 못한 레인은 성공이 아니라 실패여야 한다**
이 저장소가 가진 반대 방향 원칙이다.
- **초록일 수 없는 게이트는 사람들이 건너뛰는 법을 배우게 만든다**
같은 계열의 규칙이다.
- **한 test 안에서 도구별로 갈리지 않는 편이 낫다**
권고의 형태다.
## 문제
부트스트랩 묶음을 돌렸다.
천열다섯 테스트가 통과하고 하나가 실패하고 넷이 건너뛰어졌다.
그 하나를 확인했다.
## 결론
저장소 결함이 아니다.
실패 메시지가 원인을 그대로 적는다. 위임된 스크립트가 도구 하나를 요구하고 없어서 종료 코드 칠십팔로 끝났다는 것이다.
분석 환경에 그 도구가 없다.
기록하는 것은 가드의 비대칭이다.
테스트는 컨테이너 도구 부재를 가정으로 처리해 건너뛴다.
같은 스크립트가 요구하는 다른 도구의 부재는 실패로 나타난다.
도구가 없는 기계에서 이 테스트는 계약 위반처럼 읽히는 실패를 낸다.
메시지가 원인을 드러내므로 오해가 오래가지는 않는다. 다만 이미 건너뛰기를 선택한 테스트가 두 번째 도구에 대해서만 다르게 행동한다.
이 저장소는 반대 방향의 원칙도 갖고 있다.
역방향 프록시 레인이 건너뛰기를 거부하며 그 이유를 적는다. 컨테이너 실행기가 없을 때 조용히 통과하는 레인은 그 실행기가 마지막으로 망가진 이래로 아무것도 인증하지 않은 레인이라는 것이다.
두 원칙 중 어느 쪽을 택하든 한 테스트 안에서 도구별로 갈리지는 않는 편이 낫다.
판정은 P3 다.
## 검증 환경
Gradle : 9.0.0
확인 방식 : 묶음 실행과 실패 메시지 확인, 도구 존재 확인
소스 수정 : x
## 재현 조건
원문은 final/evidence/raw/188 계열에 있다.
1. 부트스트랩 묶음을 실행한다.
2. 실패한 테스트와 메시지를 확인한다.
3. 위임된 스크립트가 요구하는 도구를 확인한다.
4. 그 도구가 환경에 있는지 확인한다.
5. 테스트의 가정 처리 대상을 확인한다.
## 본문
<!-- body:start -->
테스트 실패의 형태는 다음이다.
```
org.opentest4j.AssertionFailedError: [verify-compose-profile-contracts.sh said:
jq is required
]
expected: 0 but was: 78
at ComposeMergeCharacterizationTest.everyLaneMatchesItsContract(ComposeMergeCharacterizationTest.java:62)
$ which jq -> NO_JQ
```
## 테스트 실패의 형태
:::evidence key="analysis-finding-a18-f002" alt="분석 문서 analysis/18-app-bootstrap.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/18-app-bootstrap.md 발췌 — 15줄" zoom="true"
:::
## 기록하는 것은 가드의 비대칭이다
```java
@Test
void everyLaneMatchesItsContract() {
Assumptions.assumeTrue(dockerComposeIsAvailable(), "docker compose is not on this machine");
ProcessResult result = run(List.of("./scripts/verify-compose-profile-contracts.sh"));
assertThat(result.exitCode()).isZero();
}
```
docker compose는 가정으로 확인하고 `jq`는 확인하지 않는다. P3.
## 확인하지 못한 것
없는 도구를 설치하지 않았다. 분석 환경을 바꾸지 않는다는 원칙 때문이다. 계약 일치 자체는 독립 구현으로 따로 확인했다.
저장소 결함이 아니다. 분석 컨테이너에 jq가 없고, 위임된 스크립트가 그것을 요구하며 exit 78로 정직하게 실패한다.
<!-- body:end -->