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,64 @@
---
kind: REFERENCE
slug: a-bean-is-not-composition-evidence
title: '@Bean이 있다는 것은 조립 증거가 아니다'
topic: assembly-ownership
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: reference:a-bean-is-not-composition-evidence
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
---
# @Bean이 있다는 것은 조립 증거가 아니다
## 목적
클래스에 붙은 애너테이션이나 팩토리의 존재를 그것이 실행 컨텍스트에 있다는 증거로 읽는 것을 막는다.
## 규칙
1. 애너테이션은 후보를 만들 뿐이다
Component 나 Repository 나 Bean 은 이 클래스가 빈이 될 수 있다는 뜻이지 빈이라는 뜻이 아니다. 스캔 범위 밖이거나 조건이 거짓이거나 소유자가 없으면 후보로 끝난다.
2. 이름은 아무것도 보장하지 않는다
이름이 AutoConfiguration 으로 끝나는 클래스가 자동설정 파일에 없으면 등록되지 않는다.
3. 타입이 하나뿐이어도 그것이 빈이라는 뜻은 아니다
구현이 하나뿐인 포트는 그 하나가 조립된다는 인상을 준다. 그 하나에 스테레오타입이 없고 생성하는 코드도 없으면 포트는 비어 있다.
4. 확인은 도달 경로로 한다
세 경로 중 어느 것이 이 클래스를 소유하는지 묻는다. 스캔이면 범위와 제외를, 자동설정이면 imports 파일과 조건을, 명시 조립이면 그 생성 지점을 확인한다.
5. main 참조 0 은 강한 신호다
프로덕션 소스에서 그 타입을 참조하는 파일이 자기 자신뿐이면, 테스트만 그것을 쓴다는 뜻이다.
## 적용 조건
능력이 활성인지 판정할 때
능력 리포트나 지원 매트릭스의 항목을 검증할 때
결함을 보고하기 전에 그 코드가 실제로 도는지 확인할 때
## 예외
명시적으로 애플리케이션이 제공하도록 설계된 포트는 플랫폼 쪽에 생산자가 없는 것이 정상이다. 그때 물을 것은 출하 애플리케이션이 그것을 제공하는가다.
## 예시
이름이 AutoConfiguration 인 세 클래스가 애너테이션도 Bean 도 imports 항목도 갖지 않은 채 능력 리포트에 Stable 로 올라 있었다.
스캔에서 제외된 다섯 패키지의 컴포넌트 여섯을 두 자동설정 어느 쪽도 만들지 않았다. 두 자동설정은 등록되어 실행되고 있었다.
JdbcOutboxRepository 는 2,276 줄이고 스프링 스테레오타입이 없으며 그것을 생성하는 main 코드가 없다.
## 관계
- **스캔에서 뺀 다섯 패키지의 컴포넌트 여섯을 두 자동설정 어느 쪽도 소유하지 않았다**
이 규칙이 필요한 대표 사례다.
- **outbox가 둘이고, 출하되는 것은 messaging 플랫폼 쪽이 아니다**
타입이 하나뿐인데도 조립되지 않는 사례다.
- **Spring 조립의 세 경로와 각각이 결정하는 것**
이 규칙의 확인 절차가 기대는 구조다.
@@ -0,0 +1,60 @@
---
kind: REFERENCE
slug: conditionalonbean-must-be-satisfiable
title: '@ConditionalOnBean은 조건이 만족될 수 있는지까지 확인해야 한다'
topic: assembly-ownership
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: reference:conditionalonbean-must-be-satisfiable
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
---
# @ConditionalOnBean은 조건이 만족될 수 있는지까지 확인해야 한다
## 목적
조건부 빈을 선언해 두고 그 조건을 만족시킬 수 있는 경로가 있는지 확인하지 않아, 능력 전체가 조용히 없는 상태를 막는다.
## 규칙
1. 조건의 뿌리를 끝까지 따라간다
사슬이면 뿌리 하나가 없을 때 전부 없다. 뿌리 타입의 빈을 만드는 곳이 프로덕션에 있는지 확인한다.
2. 테스트의 Bean 은 답이 아니다
조건을 만족시키는 유일한 곳이 테스트 설정이면 프로덕션에서는 만족되지 않는다.
3. 플랫폼이 제공하지 않겠다고 선언한 경우 질문을 바꾼다
애플리케이션이 제공해야 하는 계약이라면, 물을 것은 조건이 아니라 출하 애플리케이션이 그 계약을 이행하는가다.
4. 조건 불만족은 오류로 보고되지 않는다
Spring 은 조건부 빈이 조건을 만족하지 못하는 것을 정상 동작으로 본다. 로그에도 액추에이터에도 신호가 없다.
5. 꺼진 것과 조립될 수 없는 것을 구별할 방법을 남긴다
둘이 런타임에서 같아 보이면 운영자는 차이를 알 수 없다.
## 적용 조건
조건부 자동설정을 작성하거나 검토할 때
능력이 활성인지 판정할 때
조건 사슬이 두 단계 이상일 때
## 예외
의도적으로 애플리케이션이 채우도록 남긴 확장점은 조건이 프로덕션에서 거짓인 것이 정상이다. 그 경우 그 사실이 문서에 있어야 하고, 이 저장소처럼 출하 애플리케이션이 함께 있다면 그것이 채우는지 확인해야 한다.
## 예시
outbox 사슬 다섯 단계가 뿌리 두 타입의 빈에 걸려 있고, 그 두 타입을 만드는 프로덕션 코드가 없다.
OutboxEnvelopeFactory 를 Bean 으로 만드는 곳은 스타터의 테스트 하나뿐이다.
## 관계
- **outbox가 둘이고, 출하되는 것은 messaging 플랫폼 쪽이 아니다**
이 규칙을 적용해 원인이 갈린 사례다.
- **조건부 빈의 평가 시점 — 파싱 시점과 등록 시점**
조건이 거짓인 두 번째 이유를 다룬다.
@@ -0,0 +1,53 @@
---
kind: REFERENCE
slug: count-the-frameworks-own-autoconfigurations
title: 프레임워크가 기여하는 자동설정까지 세지 않으면 스위치가 아니다
topic: assembly-ownership
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: reference:count-the-frameworks-own-autoconfigurations
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
---
# 프레임워크가 기여하는 자동설정까지 세지 않으면 스위치가 아니다
## 목적
프로젝트 코드만 게이트하고 프레임워크가 기여하는 자동설정을 남겨 두어, 꺼진 능력이 여전히 자원을 잡는 것을 막는다.
## 규칙
1. 프로젝트 조건은 프레임워크 자동설정을 막지 못한다
후보 집합에 들어온 자동설정은 자기 조건으로 판단한다. 프로젝트의 마스터 스위치는 그 판단에 참여하지 않는다.
2. 후보 집합을 좁히는 필터가 따로 필요하다
AutoConfigurationImportFilter 는 어떤 프로젝트 조건보다 먼저 동작하므로 이 일을 할 수 있는 유일한 자리다.
3. 그 필터는 권한이 아니라 도구다
후보를 빼는 일과 능력이 켜졌는지 판정하는 일은 다르다. 판정 권한은 루트 하나가 갖는다.
4. 자원 필요 여부는 능력 질문으로 묻는다
커넥션 풀 같은 공유 자원은 한 능력의 사유물이 아니다. 그것을 필요로 하는 능력이 하나라도 활성인지를 묻는다.
## 적용 조건
프레임워크가 같은 기술에 대해 자기 자동설정을 갖는 모든 능력. 데이터소스, 메시징, 캐시가 대표적이다.
## 예외
프레임워크 자동설정이 이미 프로젝트 조건과 같은 속성을 보도록 설계되어 있으면 필터가 필요 없다.
## 예시
JPA 가 꺼진 배포에서 프레임워크의 데이터소스 자동설정을 후보에서 빼는 필터가 spring.factories 에 등록되어 있다.
풀이 필요한지는 JPA 가 켜졌는지가 아니라 커넥션을 필요로 하는 능력이 하나라도 활성인지로 묻는다. 이 저장소는 여섯 조건의 논리합으로 판정한다.
## 관계
- **풀이 필요한지는 JPA가 켜졌나가 아니라 커넥션이 필요한 capability가 있나로 묻는다**
이 규칙을 채택한 결정이다.
- **꺼짐은 조건의 반복이 아니라 구조여야 한다**
같은 목표의 짝 규칙이다.
@@ -0,0 +1,60 @@
---
kind: REFERENCE
slug: off-must-be-structural
title: '"꺼짐"은 조건의 반복이 아니라 구조여야 한다'
topic: assembly-ownership
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: reference:off-must-be-structural
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
---
# "꺼짐"은 조건의 반복이 아니라 구조여야 한다
## 목적
능력을 끄는 일을 빈마다 조건을 반복하는 방식으로 처리해서, 다음 달에 추가된 빈이 조건을 잊는 것을 막는다.
## 규칙
1. 루트 하나가 조건을 소유한다
그 루트가 자식 설정을 Import 하고 자식은 조건을 갖지 않는다. 자식에 빈이 추가되어도 자동으로 게이트된다.
2. 자식을 컴포넌트 스캔 밖에 둔다
스캔이 자식 설정을 독립적으로 발견하면 마스터 스위치와 무관하게 조립된다. 스캔 제외가 루트를 유일한 입구로 만든다.
3. 스캔에서 뺐으면 소유자를 반드시 지정한다
제외와 소유는 한 쌍이다. 한쪽만 하면 컴포넌트가 어디에도 없게 된다.
4. 스위치를 읽는 곳이 여럿이면 그중 하나만 권한을 갖는다
같은 속성을 읽는 주체가 여럿이면 각자가 다른 것이 켜졌다고 믿는 상태가 생긴다.
5. 프레임워크 자신의 자동설정도 후보에서 빼야 한다
프로젝트 조건은 그것을 막지 못한다. 후보 집합을 좁히는 필터가 따로 필요하다.
## 적용 조건
선택적 능력을 갖는 모든 모듈
능력이 여러 설정 클래스로 나뉘는 경우
## 예외
능력 안에서 다시 갈리는 하위 선택지는 자기 조건을 가질 수 있다. 그 조건은 마스터 스위치의 반복이 아니라 다른 질문이어야 한다.
## 예시
컴포지션 루트가 13개 패키지 접두사를 스캔에서 빼고 자동설정이 소유하게 한다. 클래스 목록이 아니라 접두사인 이유는 새 설정이 추가됐을 때 아무도 제외를 기억하지 못했다는 이유로 활성화되면 안 되기 때문이다.
mongo 리프에서는 세 주체가 같은 속성을 읽으며 각자 마스터처럼 행동했다. 지금은 루트 하나가 권한을 갖고, 자동설정 임포트 필터는 프레임워크 자신의 자동설정을 후보에서 빼는 일만 한다.
## 관계
- **마스터 스위치는 루트 하나가 소유하고 자식 설정은 조건을 갖지 않는다**
이 규칙을 채택한 결정이다.
- **넓은 스캔을 좁히자 여덟 컴포넌트에 아무것도 도달하지 않았다**
제외만 하고 소유를 지정하지 않았을 때의 결과다.
- **프레임워크가 기여하는 자동설정까지 세지 않으면 스위치가 아니다**
다섯 번째 규칙을 별도로 다룬다.
@@ -0,0 +1,55 @@
---
kind: REFERENCE
slug: read-the-assembling-side-first
title: 조립 결함을 판정하려면 조립하는 쪽을 먼저 읽어야 한다
topic: assembly-ownership
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: reference:read-the-assembling-side-first
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
---
# 조립 결함을 판정하려면 조립하는 쪽을 먼저 읽어야 한다
## 목적
조립되는 쪽만 읽고 결함을 판정해서, 원인을 잘못 지목하고 수정 방향을 반대로 잡는 것을 막는다.
## 규칙
1. 조립하는 쪽이 원인을 갖는다
빈이 없거나 값이 no-op 이거나 조건이 거짓인 이유는 대개 조립 지점에 있다. 조립되는 클래스만 읽으면 그 클래스가 옳게 보인다.
2. 타입이 요구하는 것과 조립이 넘기는 것을 비교한다
생성자가 무언가를 필수로 만들었더라도, 그것을 우회하는 오버로드가 있고 조립이 그쪽을 부르면 요구는 지켜지지 않는다.
3. 같은 일을 하는 다른 구현이 이미 배선되어 있는지 본다
조건이 만족되지 않는 이유가 그것을 대체하는 구현이 이미 있기 때문일 수 있다. 그때 문제는 조건이 아니라 정본이 정해지지 않은 중복이다.
4. 수정 방향은 원인 분류에서 갈린다
조건 결함으로 읽으면 조건을 만족시키는 수정이 되고, 중복으로 읽으면 어느 쪽이 정본인지 먼저 정하는 문제가 된다.
## 적용 조건
빈이 없다, 값이 기본값이다, 능력이 동작하지 않는다 계열의 모든 판정
플랫폼과 애플리케이션이 한 저장소에 함께 있는 경우 특히
## 예외
조립 지점이 저장소 밖에 있는 라이브러리라면 조립하는 쪽을 읽을 수 없다. 그때는 조립 계약을 문서로 확인하고, 판정에 그 한계를 적는다.
## 예시
messaging 스타터의 outbox 조건을 조건 결함으로 읽으면 app-bootstrap 에 빈을 등록하는 수정이 된다. 조립하는 쪽을 읽으면 application-core 쪽 outbox 가 이미 배선되어 돌고 있음이 보이고, 문제는 정본이 정해지지 않은 중복이 된다.
발행기만 읽으면 관측이 필수 인자로 보인다. 자동설정을 읽으면 인자 수가 여섯이다.
## 관계
- **outbox가 둘이고, 출하되는 것은 messaging 플랫폼 쪽이 아니다**
이 규칙을 적용해 원인 분류가 바뀐 사례다.
- **관측을 필수 생성자 인자로 만든 수정을 조립이 6인자 생성자로 되돌렸다**
타입과 조립이 어긋난 사례다.
@@ -0,0 +1,53 @@
---
kind: REFERENCE
slug: the-startup-validator-follows-the-autoconfiguration-root
title: 시작 검증기가 도는지는 그 능력에 자동설정 루트가 있는지와 일치한다
topic: assembly-ownership
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: reference:the-startup-validator-follows-the-autoconfiguration-root
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
---
# 시작 검증기가 도는지는 그 능력에 자동설정 루트가 있는지와 일치한다
## 목적
검증기 파일이 존재하고 그 테스트가 통과한다는 사실을 검증이 실행된다는 증거로 읽는 것을 막는다.
## 규칙
1. 검증기의 호출자를 센다
프로덕션 호출자가 0 이고 테스트 호출자만 있으면 그 검증은 실행 시점에 적용되지 않는다.
2. 자동설정 루트가 부르는지 확인한다
능력의 조립 지점이 검증기를 호출하지 않으면, 그 능력이 배포되기 시작해도 검증은 자동으로 시작되지 않는다.
3. 규칙 수와 실행 여부를 분리해서 본다
규칙이 많고 잘 테스트되어 있다는 것은 품질의 증거이지 실행의 증거가 아니다.
4. 시작 실패로 드러나야 할 것이 조용하면 검증기를 의심한다
설정 오류가 기동에서 잡히지 않고 런타임 증상으로만 나타나면, 그 검사가 배선되어 있는지 먼저 본다.
## 적용 조건
시작 검증기나 설정 검증기를 갖는 모든 능력
능력이 Stable 로 보고되는데 그 검증이 실제로 도는지 확인할 때
## 예외
의도적으로 라이브러리로만 제공되고 애플리케이션이 직접 호출하도록 설계된 검증기는 여기 해당하지 않는다. 그 경우 호출 방법이 문서에 있어야 한다.
## 예시
gRPC 플랫폼의 시작 검증기는 188줄에 13개 위반 규칙을 담고 있고, 호출자는 자기 테스트뿐이다. 같은 패키지의 자동설정은 106줄에 Bean 이 9개인데 검증기를 부르지 않는다.
## 관계
- **시작 검증기 13개 규칙이 유일한 조립 지점에서 호출되지 않는다**
이 규칙을 끌어낸 사례다.
- **Bean 애너테이션이 있다는 것은 조립 증거가 아니다**
같은 계열의 확인 규칙이다.