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,50 @@
---
kind: REFERENCE
slug: messaging-claim-check-f02
title: 같은 튜닝 값이 두 계층에 있으면 어느 쪽이 이기는지 정한다
topic: state-ownership-and-concurrency
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: reference:messaging-claim-check-f02
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
source:
- analysis/messaging/messaging-claim-check.md#L516
---
# 같은 튜닝 값이 두 계층에 있으면 어느 쪽이 이기는지 정한다
## 관계
- **배포 아티팩트가 싣지만 아무도 부르지 않고, 다른 곳의 에러 메시지가 이 경로를 권한다**
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
- **예외 승격이 에러 코드 문자열 접미사에 의존한다**
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
## 목적
두 값이 나란히 선언돼 있고 어느 쪽도 다른 쪽을 읽지 않으면, 실행되는 순간 한쪽만 살아남고 다른 쪽은 선언으로만 남는다. 목적지별로 다르게 두려던 설계가 전역 값 하나에 덮이는 것이 그 형태다.
## 규칙
1. 같은 의미의 튜닝 값이 두 계층에 있는지 먼저 센다
messaging-policy 의 PayloadPolicy.claimCheckThresholdBytes 는 목적지별이고 DestinationProfileValidator:49 가 검사한다. messaging-claim-check 의 ClaimCheckPolicy.thresholdBytes 는 전역이다.
2. 두 값을 대조하는 코드가 있는지 확인한다
대조가 없으면 둘은 같은 이름을 가진 서로 다른 설정이다.
3. 우선순위를 코드로 표현한다
좁은 쪽이 넓은 쪽을 읽거나, 넓은 쪽에서 그 필드를 없앤다. 문서로만 정한 우선순위는 강제되지 않는다.
## 적용 조건
같은 값이 정책 계층과 구현 계층에 각각 선언되는 자리. 문턱·상한·타임아웃처럼 목적지별로 달라질 수 있는 값이 특히 그렇다.
## 예외
SSOT 가 이 규칙의 반례를 적지 않았다. 두 값을 의도적으로 다르게 두는 설계가 있다면 그 이유가 어느 한쪽 javadoc 에 있어야 하는데, 지금은 없다.
## 예시
두 필드의 선언 위치와 DestinationProfileValidator:49 의 검사 대상, 그리고 두 값을 잇는 코드가 없다는 것. 원문 근거는 evidence/raw/290 §B 이다.
@@ -0,0 +1,52 @@
---
kind: REFERENCE
slug: messaging-security-f08
title: 가변 필드로 상태 전이를 표현하면 가시성을 함께 정한다
topic: state-ownership-and-concurrency
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: reference:messaging-security-f08
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
source:
- analysis/messaging/messaging-security.md#L697
---
# 가변 필드로 상태 전이를 표현하면 가시성을 함께 정한다
## 관계
- **종료 시 자격증명 소거가 호출되지 않는다**
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
- **같은 TLS posture를 두 클래스가 다른 엄격도로 검사한다**
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
- **권한 거부가 `AUTHORIZATION`이 아니라 `CONFIGURATION`으로 기록된다**
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
## 목적
정상 경로에서는 ConcurrentHashMap 의 compute 가 happens-before 를 준다. clearAll() 경로에는 그 보장이 없어서, 다른 스레드가 소거된 배열의 옛 참조를 읽을 수 있다. 방향은 안전한 쪽이다 — 비밀 유출이 아니라 0 으로 채워진 값을 읽는다.
## 규칙
1. 상태 전이를 표현하는 필드의 선언을 확인한다
private char[] material 이 volatile 이 아니고 clear() 가 그것을 교체한다.
2. 그 필드를 읽는 경로가 전부 같은 동기화 안에 있는지 본다
clearAll() 은 락 없이 순회한다. 그 경로만 보장 밖이다.
3. 가시성을 필드나 접근 경로 중 한쪽에서 정한다
material 을 volatile 로 하거나 clearAll() 을 compute 기반으로 바꾼다.
## 적용 조건
소거·회전·상태 전이를 필드 교체로 표현하고, 그 필드를 여러 스레드가 읽는 모든 자리.
## 예외
모든 읽기와 쓰기가 같은 compute 안에서 일어나면 별도 가시성 선언이 필요 없다. 이 클래스는 그 조건을 한 경로에서만 만족한다.
## 예시
CredentialRuntime.java:29,129-132 와 CredentialRuntimeRegistry.java:129-132. 확인 방법은 필드 선언을 보는 것이다.
@@ -0,0 +1,43 @@
---
kind: REFERENCE
slug: messaging-spring-cloud-stream-bridge-f04
title: 함께 읽히는 두 맵은 한 값으로 묶는다
topic: state-ownership-and-concurrency
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: reference:messaging-spring-cloud-stream-bridge-f04
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
source:
- analysis/messaging/messaging-spring-cloud-stream-bridge.md#L579
---
# 함께 읽히는 두 맵은 한 값으로 묶는다
## 목적
두 put 사이에 dispatch 가 들어오면 destination == null 이 되어 NO_BRIDGED_HANDLER 가 난다. 방향은 안전하다 — 잘못된 목적지로 전달하지는 않는다. 틀리는 것은 에러 코드다. 핸들러가 없다고 말하는데 실제로는 핸들러가 있고 목적지가 아직 없다.
## 규칙
1. 한 논리 등록이 몇 번의 put 으로 나뉘는지 센다
SpringCloudStreamConsumerBridge.register 가 handlers.put(...) 후 destinations.put(...) 을 한다. 같은 형태가 publisher 의 두 맵에도 있고 그쪽은 키가 각각 독립이다.
2. 그 사이에 읽는 경로가 있는지 본다
dispatch 가 그 창에 들어온다.
3. 두 값을 한 record 로 묶어 한 번에 넣는다
창 자체가 사라진다.
## 적용 조건
한 등록·한 전이가 두 개 이상의 맵 갱신으로 표현되고, 그 맵들을 함께 읽는 경로가 있는 자리.
## 예외
두 맵의 키가 독립이고 읽는 쪽이 둘을 함께 보지 않으면 대상이 아니다. publisher 쪽이 그 경우에 가깝다.
## 예시
SpringCloudStreamConsumerBridge.java:38-39 의 두 put. 확인 방법은 그 사이의 창을 보는 것이다.
@@ -0,0 +1,43 @@
---
kind: REFERENCE
slug: messaging-spring-cloud-stream-bridge-f06
title: 에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다
topic: state-ownership-and-concurrency
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: reference:messaging-spring-cloud-stream-bridge-f06
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
source:
- analysis/messaging/messaging-spring-cloud-stream-bridge.md#L597
---
# 에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다
## 목적
STREAM_BRIDGE_DISABLED 메시지가 지시하는 프로퍼티를 읽는 코드가 없다. 이 저장소에서 같은 형태가 세 번째다 — messaging-kafka-share-experimental 과 messaging-claim-check 가 앞선 둘이다.
## 규칙
1. 메시지에 등장하는 키를 검색한다
git grep -n 'spring-cloud-stream=true' -- src 가 이 leaf 의 문자열 하나만 돌려준다.
2. 같은 형태가 가족 안에 몇 번 있는지 센다
세 leaf 가 같은 방식으로 실행 불가능한 지시를 남겼다. 개별 실수가 아니라 형태다.
3. 바인딩을 만들거나 메시지에서 키를 뺀다
지시는 실행 가능할 때만 지시다.
## 적용 조건
실패 메시지가 복구 방법을 프로퍼티 키로 제시하는 모든 자리.
## 예외
SSOT 가 이 규칙의 반례를 적지 않았다. 배선 계획이 확정된 키를 미리 안내하는 경우라면 그 사실이 메시지 안에 있어야 한다.
## 예시
backend.messaging.bridge.spring-cloud-stream=true 가 STREAM_BRIDGE_DISABLED 메시지에만 존재한다는 것.