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,85 @@
---
kind: CASE
slug: reject-new-admission-that-rejects-nothing
title: 새 승인을 거절한다는 메서드가 단계만 기록하고 아무것도 거절하지 않는다
topic: drain-and-shutdown-ordering
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:reject-new-admission-that-rejects-nothing
evidenceCapturedOn: 2026-09-01
assets:
- key: reject-new-admission-that-rejects-nothing
file: ../../../final/evidence/rendered/reject-new-admission-that-rejects-nothing.svg
evidence:
- ../../../final/evidence/raw/reject-new-admission-that-rejects-nothing.txt
source:
- 원본 분석 절은 analysis/grpc/grpc-admin.md §17.1 이다.
---
# 새 승인을 거절한다는 메서드가 단계만 기록하고 아무것도 거절하지 않는다
배수 조정자의 새 승인 거절 메서드가 현재 단계를 기록하는 것이 전부다. 승인을 실제로 판정하는 제어기는 다른 리프에 있고 이 단계를 읽지 않는다.
## 관계
- **8단계 종료 순서 계약과 실제 종료 경로**
이 사례가 속한 구조다.
- **배수를 시작한 뒤에도 한 서비스가 다시 SERVING 이 될 수 있다**
같은 리프에서 배수를 무력하게 만드는 다른 절반이다.
- **검증기는 발행이 아니라 주입이 강제다**
같은 계열의 규칙이다.
## 문제
배수의 첫 단계는 새 요청을 받지 않는 것이다. 그래야 남은 작업이 줄어들고 드레인이 끝난다.
배수 조정자가 그 단계를 메서드로 갖는다. 이름이 새 승인을 거절한다고 말한다.
## 결론
본문이 현재 단계를 기록하는 것이 전부다.
승인을 실제로 판정하는 것은 다른 리프의 승인 제어기이고, 그 제어기는 이 조정자의 단계를 읽지 않는다. 두 리프 사이에 참조가 없다.
그래서 배수를 시작해도 새 요청은 계속 승인된다. 드레인이 기다리는 작업 수가 줄지 않고, 드레인 마감이 지나면 아직 처리 중인 요청을 두고 종료한다.
같은 리프의 헬스 레지스트리가 배수 중에 서비스를 다시 서빙으로 돌릴 수 있다는 것과 겹치면 결론이 하나로 모인다. 배수라는 절차가 상태 기록으로만 존재하고 트래픽에 대해서는 아무 효과가 없다.
## 검증 환경
OpenJDK : 21.0.12
Gradle : 9.0.0
확인 방식 : 메서드 본문 확인과 두 리프 사이의 타입 참조 검색
소스 수정 : x
## 재현 조건
1. 배수 조정자의 새 승인 거절 메서드 본문을 읽는다.
2. 그 메서드가 바꾸는 상태를 읽는 코드를 저장소에서 찾는다.
3. 승인 제어기가 배수 단계를 참조하는지 확인한다.
## 본문
<!-- body:start -->
배수의 첫 단계는 새 요청을 받지 않는 것이다. `rejectNewAdmission()` 은 이름이 그것을 말하고, 본문은 현재 단계를 기록하는 것이 전부다.
## 메서드 본문이 하는 일
:::evidence key="reject-new-admission-that-rejects-nothing" alt="분석 문서 analysis/grpc/grpc-admin.md 에서 이 기록의 근거 절을 그대로 잘라낸 18줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-admin.md 발췌 — 18줄" zoom="true"
:::
## 승인을 판정하는 곳이 이 단계를 읽지 않는다
승인 제어기는 다른 리프에 있고 이 조정자의 단계를 읽지 않는다. 그래서 배수를 시작해도 새 요청은 계속 승인된다.
## 헬스 레지스트리 쪽과 겹치면
배수 중에 서비스를 다시 SERVING 으로 돌릴 수 있다는 것과 합쳐져, 배수라는 절차 전체가 상태 기록으로만 존재하고 트래픽에 대해서는 아무 효과가 없다.
## 확인하지 못한 것
배수 중 승인 시도를 실행으로 재현하지 않았다. 두 리프 사이에 참조가 없다는 것으로 판정했다.
<!-- body:end -->
@@ -0,0 +1,89 @@
---
kind: CASE
slug: the-last-step-of-secret-erasure-is-not-wired
title: 비밀을 힙에서 지우는 마지막 단계가 종료 경로에 연결되지 않았다
topic: drain-and-shutdown-ordering
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:the-last-step-of-secret-erasure-is-not-wired
evidenceCapturedOn: 2026-09-01
assets:
- key: the-last-step-of-secret-erasure-is-not-wired
file: ../../../final/evidence/rendered/the-last-step-of-secret-erasure-is-not-wired.svg
evidence:
- ../../../final/evidence/raw/the-last-step-of-secret-erasure-is-not-wired.txt
source:
- 원본 분석 절은 analysis/messaging/messaging-security.md §17 이다.
---
# 비밀을 힙에서 지우는 마지막 단계가 종료 경로에 연결되지 않았다
자격증명 레지스트리의 전체 소거 메서드가 종료용이라고 javadoc 에 적혀 있고 호출자가 저장소에 없다. 리프 전체가 비밀을 힙에 남기지 않기 위해 설계됐다.
## 관계
- **8단계 종료 순서 계약과 실제 종료 경로**
이 사례가 속한 구조다.
- **순서 계약을 구현하는 것이 없고, 그것을 검증한다는 테스트는 enum 선언 순서만 본다**
같은 계약이 구현되지 않았다는 사실의 구체적 결과 하나다.
- **검증기는 발행이 아니라 주입이 강제다**
같은 계열의 규칙이다.
## 문제
이 리프는 자격증명을 다루는 방식 전체가 하나의 목적을 향한다. 비밀을 문자열이 아니라 문자 배열로 들고, 쓰고 나면 그 배열을 지우고, 회전할 때 옛 값을 즉시 소거한다.
그 규율의 마지막 단계가 종료다. 프로세스가 끝날 때 아직 들고 있는 자격증명을 전부 지우는 메서드가 있다.
## 결론
그 메서드를 부르는 곳이 없다.
javadoc 은 용도를 명확히 적는다.
> Clears every held credential, for shutdown
호출자를 저장소 전역에서 검색하면 선언과 테스트만 나온다.
프로세스가 끝나면 힙도 사라지므로 사소해 보인다. 그러나 이 통제가 겨냥하는 상황이 정확히 그 전제가 성립하지 않는 경우다. 종료가 느려서 힙이 오래 남거나, 종료 중에 덤프가 뜨거나, 컨테이너가 정지 상태로 유지되는 경우다.
8단계 종료 계약에 자격증명 소거에 해당하는 단계가 있고 그 단계를 수행하는 코드가 없다. 그러므로 이 사례는 계약이 구현되지 않았다는 사실의 구체적 결과 하나다.
## 검증 환경
OpenJDK : 21.0.12
Gradle : 9.0.0
확인 방식 : 전체 소거 메서드의 호출자 전수 검색
소스 수정 : x
## 재현 조건
1. 자격증명 레지스트리에서 전체 소거 메서드와 그 javadoc 을 확인한다.
2. 그 메서드 이름으로 저장소를 검색해 호출자를 센다.
3. 종료 수명주기 클래스가 그것을 부르는지 확인한다.
## 본문
<!-- body:start -->
이 리프 전체가 "비밀이 힙에 남지 않게 한다" 를 목적으로 설계됐다 — 자격증명을 `char[]` 로 들고, 사용 후 `clear()` 하고, 회전 시 즉시 소거한다.
## 리프가 설계된 목적
:::evidence key="the-last-step-of-secret-erasure-is-not-wired" alt="분석 문서 analysis/messaging/messaging-security.md 에서 이 기록의 근거 절을 그대로 잘라낸 18줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/messaging/messaging-security.md 발췌 — 18줄" zoom="true"
:::
## 마지막 단계에 호출자가 없다
`clearAll()` 의 javadoc 이 "Clears every held credential, for shutdown" 이라고 적고, 호출자가 저장소에 없다.
## 사소해 보이지만 겨냥한 상황이 그것이다
프로세스가 끝나면 힙도 사라지지만, 종료가 느리거나 힙 덤프가 뜨는 경우가 정확히 이 통제가 겨냥하는 상황이다. 8단계 종료 계약에 자격증명 소거 단계가 있고 그 단계를 수행하는 코드가 없다는 점에서, 이 사례는 계약이 구현되지 않았다는 사실의 구체적 결과 하나다.
## 확인하지 못한 것
힙 덤프로 잔존을 확인하지 않았다. 호출자 부재로 판정했다.
<!-- body:end -->
@@ -0,0 +1,95 @@
---
kind: CONCEPT
slug: the-eight-phase-shutdown-contract
title: 8단계 종료 순서 계약과 실제 종료 경로
topic: drain-and-shutdown-ordering
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: concept:the-eight-phase-shutdown-contract
evidenceCapturedOn: 2026-09-01
assets:
- key: the-eight-phase-shutdown-contract
file: ../../../final/evidence/rendered/the-eight-phase-shutdown-contract.svg
- key: the-eight-phase-shutdown-contract-diagram
file: ../../../final/assets/diagrams/the-eight-phase-shutdown-contract.svg
evidence:
- ../../../final/evidence/raw/the-eight-phase-shutdown-contract.txt
source:
- 원본 분석 절은 analysis/messaging/messaging-transport-spi.md §17 이다.
---
# 8단계 종료 순서 계약과 실제 종료 경로
전송 SPI 가 종료를 8단계로 선언하고 그 순서를 계약이라고 못 박는다. 실제 종료는 스타터의 다른 클래스가 하고 8단계 중 셋만 명시적으로 수행한다.
## 관계
- **순서 계약을 구현하는 것이 없고, 그것을 검증한다는 테스트는 enum 선언 순서만 본다**
이 계약이 코드에 닿지 않는다는 사례다.
- **새 승인을 거절한다는 메서드가 단계만 기록하고 아무것도 거절하지 않는다**
단계 하나가 이름만 남은 사례다.
- **비밀을 힙에서 지우는 마지막 단계가 종료 경로에 연결되지 않았다**
단계 하나가 통째로 빠진 사례다.
## 본문
<!-- body:start -->
`MessagingLifecycle` 이 종료를 8단계로 선언하고 javadoc 이 그 순서를 계약이라고 못 박는다 — "The order in ShutdownPhase is the contract, not an implementation detail. Each adapter implements the phases; none of them chooses the order."
## 순서가 계약인 이유
각 단계가 앞 단계의 결과 위에 서기 때문이다. 새 승인을 멈추기 전에 드레인하면 드레인이 끝나지 않고, 핸들러가 끝나기 전에 플러시하면 그 핸들러가 만들 정착을 잃는다.
## MessagingLifecycle 참조 위치
:::evidence key="the-eight-phase-shutdown-contract" alt="코드베이스에서 MessagingLifecycle 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingLifecycle 코드베이스 검색 — 2줄 · exit 0" zoom="true"
:::
## 선언과 실제의 거리
:::evidence key="the-eight-phase-shutdown-contract-diagram" alt="선언된 단계에 승인 정지와 드레인과 플러시 및 나머지가 놓이고 실제 수행에서 마지막 칸만 빗금이다" caption="선언과 실제의 거리" zoom="false"
:::
실제 종료는 스타터의 `MessagingShutdownLifecycle` 이 하고 8단계 중 셋만 명시적으로 수행한다 — 승인 정지 · 새 핸들러 정지 · 드레인. 나머지는 Spring 의 `getPhase()` 정수와 빈 소멸 순서에 위임되거나 명시 단계가 없다.
## 순서를 결정하는 것
`ShutdownPhase` 가 아니다.
:::note
실제 종료 시퀀스를 부팅해 관측하지 않았다. 두 클래스의 코드로 판정했다
:::
## 왜 순서가 계약인가
각 단계가 앞 단계의 결과 위에 선다.
새 승인을 멈추기 전에 드레인을 시작하면 드레인이 끝나지 않는다. 계속 들어오는 요청이 남은 작업 수를 다시 늘린다.
핸들러가 끝나기 전에 플러시하면 그 핸들러가 만들 정착을 잃는다. 아직 만들어지지 않은 것은 플러시할 수 없다.
인터페이스 javadoc 이 그 성질을 직접 적는다.
> The order in ShutdownPhase is the contract, not an implementation detail. Each adapter implements the phases; none of them chooses the order.
어댑터가 각 단계를 구현하고, 순서는 어느 어댑터도 고르지 않는다는 뜻이다.
## 실제로 무엇이 도는가
종료를 수행하는 것은 스타터의 종료 수명주기 클래스다. 그 클래스가 명시적으로 하는 것은 셋이다. 승인 정지, 새 핸들러 정지, 드레인.
나머지는 Spring 의 단계 정수와 빈 소멸 순서에 위임되거나, 대응하는 명시 단계가 없다.
즉 순서를 결정하는 것은 선언된 단계 열거형이 아니라 프레임워크의 소멸 순서다.
## 두 이름의 거리
선언된 계약과 실행되는 절차가 이름을 공유하지 않는다. 계약 쪽은 여덟 단계를 갖고 구현체가 없으며, 실행 쪽은 세 동작을 갖고 그 동작을 단계 이름으로 부르지 않는다.
그래서 계약을 읽은 사람과 실행을 읽은 사람이 같은 시스템에 대해 다른 그림을 갖는다.
<!-- body:end -->
@@ -0,0 +1,55 @@
---
kind: REFERENCE
slug: a-declaration-order-test-is-a-gate-only-if-something-reads-that-order
title: 선언 순서를 단언하는 테스트는 그 순서를 읽는 코드가 있을 때만 게이트다
topic: drain-and-shutdown-ordering
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: reference:a-declaration-order-test-is-a-gate-only-if-something-reads-that-order
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
---
# 선언 순서를 단언하는 테스트는 그 순서를 읽는 코드가 있을 때만 게이트다
## 목적
열거형이나 목록의 선언 순서를 단언하는 테스트가 무엇을 지키는지 정확히 판정한다.
## 규칙
1. 이 열거형의 값 목록이나 서수를 읽는 production 코드를 찾는다
없으면 그 테스트가 지키는 것은 타이핑 순서다.
2. 테스트 이름과 설명 메시지가 무엇을 주장하는지 읽는다
시스템 동작을 서술하는데 단언이 선언 순서면 그 둘이 어긋나 있다.
3. 순서를 실제로 결정하는 것이 무엇인지 찾는다
프레임워크의 소멸 순서나 다른 클래스의 절차일 수 있다. 그것이 진짜 계약이다.
4. 순서 자체가 문서인 경우를 구분한다
그때는 테스트 이름이 문서와의 일치를 말해야 하고 시스템 동작을 주장하면 안 된다.
## 적용 조건
순서가 의미를 갖는 모든 열거형과 목록. 종료 단계, 필터 체인, 실패 번역 사슬, 마이그레이션 순서.
## 예외
선언 순서가 곧 실행 순서인 구조가 있다. 그 구조를 만드는 코드가 명시적으로 값 목록을 순회하면 이 규칙은 만족된다.
## 예시
종료 단계 열거형을 순서대로 단언하는 다섯 테스트가 있다. 그 열거형의 외부 소비자가 없고, 실제 종료 순서는 스타터의 다른 클래스와 프레임워크 소멸 순서가 정한다.
테스트를 통과시키려면 상수를 그 순서대로 적으면 되고, 시스템 동작은 그것과 무관하게 유지된다.
## 관계
- **순서 계약을 구현하는 것이 없고, 그것을 검증한다는 테스트는 enum 선언 순서만 본다**
이 규칙을 만든 사례다.
- **빠뜨림이 통과가 되는 게이트는 게이트가 아니다**
같은 계열의 규칙이다.
- **이름이 검사한다고 말하는 것을 본문이 검사하지 않는 테스트 다섯**
이름과 본문이 갈리는 다른 형태들이다.