Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a-catalog-nine-entries-short.md
T
DongHyeonkaandClaude Opus 5 b2963105a8 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>
2026-09-04 22:51:59 +09:00

8.0 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 a-catalog-nine-entries-short 패키지 카탈로그가 트리보다 아홉 개 적었고, 선언된 간선의 DAG 검사도 없었다 what-a-gate-does-not-prove clean-architecture-backend-template 게시 전 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 case:a-catalog-nine-entries-short 2026-09-02
key file
a-catalog-nine-entries-short ../../../final/evidence/rendered/a-catalog-nine-entries-short.svg
../../../final/evidence/raw/a-catalog-nine-entries-short.txt
원본 분석 절은 analysis/05 §115 이고, 리프가 단일 Gradle 모듈이 된 배경은 §1 에 있다. 카탈로그 크기와 사이클은 `JpaModuleBoundaryTest` 의 javadoc 과 DAG 테스트 단언 메시지가 사후 기록으로 소유하며, 값 쪽 잔여 설정의 근거는 evidence/raw/110-governance-doc-count-drift.txt §I 이다.

패키지 카탈로그가 트리보다 아홉 개 적었고, 선언된 간선의 DAG 검사도 없었다

모듈 경계를 강제하는 테스트가 두 가지를 놓치고 있었다. 패키지 카탈로그가 열세 개를 담은 채 트리에는 스물두 개가 있어서 아홉 개가 아무 규칙의 지배도 받지 않았고, 그 안의 새 패키지나 새 간선은 빠뜨림으로 초록불이었다. 그리고 선언된 간선이 DAG 를 이루는지 보는 검사가 없어서 transactionpostgresql 사이의 두 간선이 그대로 있었다.

관계

  • 빠뜨림이 통과가 되는 게이트는 게이트가 아니다 이 사례가 그 규칙을 만든 형태다.
  • @Bean이 있다는 것은 조립 증거가 아니다 빈 선언만으로 조립을 단정하지 말라는 확인 규칙이다.

문제

설계상 여러 모듈인 이 리프가 한 리프 안의 패키지 묶음이 된 것은 저장소의 fail-closed 레지스트리가 배치보다 앞서기 때문이다.

패키지는 그 자체로 아무것도 강제하지 않는다. 그래서 테스트가 모듈 의존 맵을 패키지 규칙으로 강제한다. 테스트가 빠지면 모듈 맵은 강제력을 잃는다. 경계를 넘는 임포트가 아무 저항 없이 컴파일된다.

그리고 이 검사는 레지스트리 게이트가 대신해 주지 않는다. 레지스트리 게이트는 등록된 리프 사이의 간선을 다루는데, 여기서 단언하는 모든 간선은 한 리프 안에 있어 그 게이트에 보이지 않는다.

결론

두 결함이고 원인이 다르다.

카탈로그가 트리보다 아홉 개 적었다. 지배받지 않던 아홉 개는 audit · config · failure · fileserver · h2 · idempotency · lock · notification · outbox 다. 그 안에서 새로 생긴 패키지와 간선은 허가를 받은 것이 아니라 아무도 묻지 않아서 통과했다.

사이클은 그것과 무관하다. 빠진 목록에 transaction 과 postgresql 이 들어 있지 않다. 둘 다 카탈로그 안에 있었고 간선도 선언돼 있었는데, 선언된 간선들이 순환을 이루는지 보는 검사가 없었다.

수정도 둘이다. 카탈로그를 디스크의 실제 트리와 정확히 같은지 양방향으로 대조하게 만들었고, 선언된 간선이 DAG 인지 보는 테스트를 따로 두었다. 소스 루트를 찾지 못했을 때 스캔이 빈 집합으로 통과해 버리는 것을 막는 방어도 함께 들어갔다.

검증 환경

OpenJDK : 21.0.12 Gradle : 9.0.0 확인 방식 : 테스트 파일의 javadoc 과 단언 메시지 확인, 카탈로그 키와 빠진 아홉 개 대조 소스 수정 : x

재현 조건

  1. JpaModuleBoundaryTest 의 PACKAGE_CATALOG 위 javadoc 을 읽는다. 이전 카탈로그 크기와 트리 크기와 빠진 아홉 개가 이름으로 적혀 있다.
  2. 그 아홉 개에 transaction 과 postgresql 이 없다는 것과, 둘이 PACKAGE_CATALOG 의 키라는 것을 확인한다.
  3. theDeclaredEdgesFormADag 의 단언 메시지를 읽는다. 두 간선이 모두 존재했다는 것이 거기 적혀 있다.
  4. 카탈로그가 존재하는 패키지를 정확히 이름 대는지 검사하는 theCatalogNamesExactlyThePackagesThatExist 를 확인한다.
  5. 소스 루트를 찾지 못할 때 스캔으로 통과하지 않도록 막는 방어를 확인한다.

본문

이 리프는 설계상 여러 모듈인데 저장소의 fail-closed 레지스트리가 그 배치보다 우선해서, 모듈이 한 리프 안의 패키지가 되었다. 패키지는 그 자체로 아무것도 강제하지 않으므로 경계 테스트가 모듈 의존 맵을 패키지 규칙으로 대신 강제한다.

그 테스트가 두 가지를 놓치고 있었고, 둘은 서로 다른 결함이다.

카탈로그가 트리보다 아홉 개 적었다

:::evidence key="a-catalog-nine-entries-short" alt="코드베이스에서 JpaModuleBoundaryTest 의 두 구간을 잘라낸 출력 30줄. 이전 카탈로그가 열세 개이고 트리가 스물두 개였다는 javadoc 과, 사이클을 기록한 DAG 테스트의 단언 메시지가 그 출력에 그대로 보인다." caption="JpaModuleBoundaryTest — PACKAGE_CATALOG javadoc · DAG 테스트 — 30줄 · exit 0" zoom="true" :::

카탈로그는 닫힌 집합이어야 의미가 있는데, 열세 개를 담은 채 트리에는 스물두 개가 있었다. javadoc 이 빠진 아홉 개를 이름으로 적는다.

  • audit
  • config
  • failure
  • fileserver
  • h2
  • idempotency
  • lock
  • notification
  • outbox

이 아홉 개 안의 새 패키지나 새 간선은 규칙이 거부한 것이 아니라 규칙이 아무 말도 하지 않아서 초록불이었다. javadoc 의 표현이 그것이다 — "green by omission".

사이클은 다른 이유로 통과했다

transactionpostgresql 은 빠진 아홉 개에 없다. 둘 다 카탈로그 안에 있었고 각자의 간선도 선언돼 있었다. 그런데 선언된 간선들이 순환을 이루는지 보는 검사가 없었다.

DAG 테스트의 단언 메시지가 그 상태를 기록한다 — 두 간선이 모두 존재했고, 그래서 어느 쪽도 먼저 풀지 않고는 추출하거나 교체할 수 없었다.

앞의 것은 규칙의 적용 범위가 좁았던 것이고, 뒤의 것은 선언된 범위 안에서 검사 항목이 하나 없었던 것이다.

레지스트리 게이트가 대신해 주지 않는다

저장소에는 리프 사이의 간선을 보는 fail-closed 레지스트리 게이트가 따로 있다. 여기서 단언하는 간선은 전부 한 리프 안에 있어서 그 게이트에 보이지 않는다. 이 테스트가 없으면 모듈 맵은 제약이 아니라 그림이고, 그것을 넘는 첫 임포트가 그냥 컴파일된다.

지금은 네 검사가 서로 다른 실패를 막는다

theCatalogNamesExactlyThePackagesThatExist 가 카탈로그와 디스크를 양방향으로 대조하고, everyObservedEdgeIsDeclared 가 관측된 간선이 선언된 것인지 보고, theDeclaredEdgesFormADag 가 선언된 간선이 순환하는지 본다.

네 번째는 importActuallyLoadedTheProductionClasses 다. ArchUnit 의 noClasses() 규칙이 아무것도 매칭하지 않아 공허하게 통과하는 실패 모드를 막는다 — 규칙 모음에서 가장 자주 조용히 무너지는 지점이다.

값 쪽은 아직 키 쪽만큼 검증되지 않는다

postgresql 의 허용 대상에 "inbox" 가 들어 있는데 그 이름의 top-level 패키지는 존재하지 않는다. 실제 inbox 는 postgresql 하위 패키지라서 최상위 이름을 돌려주는 함수가 언제나 postgresql 을 준다.

theCatalogNamesExactlyThePackagesThatExist 는 키만 대조하므로 이런 값은 잡히지 않는다. 방향은 안전한 쪽이다 — 존재하지 않는 이름은 규칙을 더 엄격하게 만들 뿐 느슨하게 만들지 않는다. 그래서 결함이 아니라 잔여 설정이다.

확인하지 못한 것

그 패키지들 안에 규칙 위반 간선이 실제로 있었는지까지는 보지 않았다. 이 기록이 든 결함은 규칙이 없다는 것이지 특정 위반이 아니다.