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>
7.9 KiB
kind, slug, title, topic, project, status, sourceRevision, rootTreeNode, evidenceCapturedOn, assets, evidence, source
| kind | slug | title | topic | project | status | sourceRevision | rootTreeNode | evidenceCapturedOn | assets | evidence | source | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CASE | a05-f019-ssot | 정본이 어디인지 주석에 적어 두고 정의는 다시 타이핑한다 | declaration-and-document-drift | clean-architecture-backend-template | 게시 전 | 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 | case:a05-f019-ssot | 2026-09-02 |
|
|
|
정본이 어디인지 주석에 적어 두고 정의는 다시 타이핑한다
리프가 내보내는 패키지 목록을 선언하고, 컴포지션 루트의 소비자 규칙이 같은 목록을 자기 안에 다시 적는다. 사본의 javadoc 은 정본이 리프 쪽이라고 이름으로 적지만, 그 이름을 코드로 읽는 곳은 없다. 두 목록은 이미 두 항목 다르다.
관계
- 문서의 수치는 세지 말고 파생하거나 게이트로 붙든다 문서의 수치를 세는 대신 파생하거나 게이트로 붙들라는 규칙이다.
- 빠뜨림이 통과가 되는 게이트는 게이트가 아니다 한쪽에 항목을 더해도 다른 쪽이 조용한 형태가 같다.
- 같은 개념의 두 어휘가 공존하면 하나를 죽은 것으로 표시한다 같은 값을 두 곳이 각자 적고 있어 어느 쪽이 정본인지 정해야 한다.
문제
이 리프에는 jar 하나에 공개 구현 타입이 많이 들어 있다. 그래서 자바 접근 제어와 아키텍처 노출 목록을 각각 따로 둔다.
내보낼 패키지 목록을 두 곳이 각각 들고 있다.
결론
리프의 경계 시험 쪽에는 열 개가 선언돼 있다. springdata 와 querydsl 은 거기 없다.
컴포지션 루트의 아키텍처 시험이 같은 열 개를 자기 안에 다시 적고, 벤더 진입점 둘을 더 넣는다. postgresql 과 h2 다. 이유는 그 자리 주석에 적혀 있다. 벤더 설정이 코어 JPA 설정을 임포트하는 방향이라 컴포지션 루트가 대신 막을 단일 내부 진입점이 없고, 방향을 뒤집으면 패키지 순환이 생겼다는 것이다.
사본의 javadoc 에는 이 목록이 리프의 export 허용 목록을 옮겨 적은 것이고 정의는 리프의 경계 시험에 있다고 적혀 있다.
그 문장이 컴포지션 루트에서 그 클래스를 언급하는 유일한 줄이다. 선언 파일 밖에서 그 목록 상수를 참조하는 자바 코드는 저장소 전체에 0 이다. 어느 쪽이 정본인지는 산문이 말하고, 값은 사람이 옮겨 적는다.
리프 목록은 바깥 소비자를 검사하지도 않는다. 그 목록을 쓰는 시험 둘은 목록에 적힌 패키지가 실제로 있는지, 그 패키지가 카탈로그의 거버넌스 대상인지를 본다. 임포트 관계는 컴포지션 루트 쪽 규칙이 따로 본다.
그래서 두 시험 모두 통과한다. 통과는 각자의 규칙을 만족한다는 뜻이고, 두 목록이 같다는 뜻은 아니다. 지금 이미 두 항목 다르다.
권고는 등록부를 한 곳에 두고 두 검사가 같은 데이터를 보게 하라는 것이다.
검증 환경
OpenJDK : 21.0.12 확인 방식 : 두 목록의 항목 추출과 집합 대조, 정본 참조 계수, 리프 시험의 검사 범위 확인 소스 수정 : x 실행 : 없음. 정적 검색과 집합 연산이다.
재현 조건
- 리프 경계 시험의 export 목록을 읽는다.
- 컴포지션 루트의 소비자 규칙 안에 있는 같은 이름의 목록을 읽는다.
- 두 집합을 뽑아 차집합을 구한다.
- 사본의 javadoc 이 정본을 어떻게 지목하는지 읽는다.
- 컴포지션 루트에서 그 정본 클래스를 언급하는 줄과, 선언 파일 밖에서 그 상수를 참조하는 코드를 각각 센다.
- 리프 목록을 쓰는 시험 둘이 무엇을 확인하는지 읽는다.
본문
이 리프는 하나의 jar 안에 공개 구현 타입이 많다. 그래서 자바의 public 과 아키텍처가 내보내는 패키지를 따로 관리한다.
내보내는 패키지 목록이 두 곳에 있다.
두 목록과 그 차이
:::evidence key="a05-f019-ssot" alt="리프 경계 시험이 선언하는 export 패키지 목록, 컴포지션 루트의 소비자 규칙 안에 다시 적힌 같은 목록과 거기 더해진 벤더 진입점 둘과 그 이유 주석, 두 집합의 크기와 차집합, 사본의 javadoc 이 정본을 지목하는 줄과 그 정본 클래스를 언급하는 줄 수와 선언 파일 밖의 상수 참조 수, 그리고 리프 목록을 쓰는 두 시험이 무엇을 보는지를 출력한 터미널 기록." caption="리프 10개와 루트 12개, 차이는 postgresql 과 h2 · 사본 javadoc 이 정본을 어디라고 적는지 · 그 클래스 언급 1줄은 그 주석뿐 · 상수 참조 0 · 리프 시험 둘은 목록의 자기 정합만 확인 — 65줄 · exit 0" zoom="true" :::
리프의 경계 시험이 열 개를 선언한다.
private static final Set<String> EXPORTED_PACKAGES =
Set.of(
"api", "notification.configuration", "transaction", "security",
"observation", "migration", "hibernate", "fileserver", "failure", "config");
컴포지션 루트의 아키텍처 시험은 같은 열 개에 둘을 더 적는다. 그 자리의 주석이 이유를 적는다.
The two vendor entry points. A vendor configuration imports the core JPA config rather
than the reverse, so there is no single internal entry the composition root could gate
instead — inverting the import to make one produced a package cycle.
집합으로 빼면 차이가 정확히 둘이다.
리프 10개 / 루트 12개
루트에만 있는 것: ['h2', 'postgresql']
리프에만 있는 것: []
사본이 정본을 지목하는 방식
The list is the leaf's export allowlist, mirrored here because this is the consumer side
of the same boundary. JpaModuleBoundaryTest owns the definition.
그 문장이 컴포지션 루트에서 리프 경계 시험을 언급하는 유일한 줄이다. 선언 파일 밖에서 EXPORTED_PACKAGES 를 참조하는 자바 코드는 저장소 전체에 0 이다.
정본을 지목하는 것은 산문이고, 값은 손으로 옮겨져 있다.
리프 목록은 바깥 소비자를 보지 않는다
그 목록을 쓰는 시험은 둘이다. 목록에 적힌 패키지가 소스 트리에 실제로 있는지, 그리고 그 패키지가 카탈로그의 거버넌스 대상인지를 본다.
누가 무엇을 임포트하는지는 컴포지션 루트의 규칙이 따로 본다. split 은 실수가 아니라 역할 분리의 결과이고, 그래서 어느 쪽도 상대를 검사할 이유가 없다.
두 시험이 각각 무엇을 보는가
두 시험의 입력이 겹치지 않는다. 리프 시험은 리프의 소스 트리와 자기 카탈로그만, 루트 시험은 루트의 임포트 그래프와 자기 목록만 읽는다.
한쪽 목록이 늘어도 다른 쪽 단언의 입력은 그대로다. 실패할 근거가 없다.
고칠 방향
내보내는 패키지 등록부를 한 곳으로 옮기고, 리프의 패키지 그래프 검사와 소비자 규칙이 같은 데이터를 읽게 한다. 분석 문서의 권고가 그것이다.
확인하지 못한 것
한쪽 목록에 항목을 더해 다른 쪽이 조용한지 실행으로 확인하지 않았다. 두 선언을 대조하고 참조를 센 것까지가 확인 범위다.