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>
9.6 KiB
kind, slug, title, topic, project, status, sourceRevision, rootTreeNode, evidenceCapturedOn, body, assets, evidence, source
| kind | slug | title | topic | project | status | sourceRevision | rootTreeNode | evidenceCapturedOn | body | assets | evidence | source | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CASE | analysis-finding-a05-f010 | 정본이라던 코디네이터는 그 포트를 구현하지 않고 다른 클래스가 구현한다 | multitenancy-isolation | clean-architecture-backend-template | 게시 전 | 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 | case:analysis-finding-a05-f010 | 2026-09-04 | case-analysis-finding-a05-f010.body.md |
|
|
|
정본이라던 코디네이터는 그 포트를 구현하지 않고 다른 클래스가 구현한다
JpaTransactionAutoConfiguration:37 주석이 정본 경계로 지목한 메서드는 PolicyTransactionPort 의 것인데, 같은 문장이 아래에 만들어지는 재시도 코디네이터로 이어진다. 그 포트를 실제로 구현하는 것은 어댑터 쪽 SpringTransactionPort 이고 코디네이터는 자기 실행기와 짝을 이룬다.
관계
- 중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다 이 사례가 속한 구조다. 두 스택이 다 조립되고 어느 쪽이 정본인지가 갈린다.
- 재시도 코디네이터는 빈이지만 그것을 어디에도 적용하지 않는다 같은 코디네이터의 배선 공백을 다룬 기록이다.
- 같은 개념의 두 어휘가 공존하면 하나를 죽은 것으로 표시한다 이 사례가 어기는 규칙이다. 둘 다 살아 있고 어느 쪽도 죽은 것으로 표시되지 않았다.
문제
정본 트랜잭션 경계가 어디인지는 이 리프의 구조를 읽는 출발점이다.
자동설정 주석이 그 경계를 지목한다. 그 지목이 실제 구현 관계와 맞는지 확인했다.
결론
그 포트를 구현한다고 선언한 파일은 하나뿐이다. 없는 이름으로 같은 검색식을 걸어도 0 이 나오므로, 상위 인터페이스로 같은 검색을 걸어 21 을 받아 검색이 도는 것을 확인했다.
그 클래스가 자기 자바독에서 설명하는 것은 모드별 템플릿과 격리 수준 고정이다. 재시도라는 낱말이 거기 없다.
자동설정이 만드는 빈은 여섯이다 — CommitFailureClassifier, SpringJpaTransactionExecutor 둘, FullTransactionRetryCoordinator 둘, RetrySleeper. 그 목록에 포트 구현이 없다.
즉 주석이 한 문장 안에서 두 스택을 잇고 있다. 앞부분이 지목한 포트는 어댑터 쪽 클래스가 구현하고, 뒷부분이 설명하는 코디네이터는 자동설정 쪽 실행기와 짝을 이룬다.
네 이름의 main 참조 파일 수가 모두 4 개다. 어느 쪽도 죽어 있지 않다.
저장소 자신도 이것을 문제로 적어 두었다. docs/reviews/2026-08-14-jpa-module-code-review.md:119 가 트랜잭션 권위와 애플리케이션 계약이 두 벌이라는 것을 P1 로 매기고 PolicyTransactionPort 단일 facade 로 통합하자고 적는다.
동작이 틀리는 것은 아니다. 두 스택이 각각 돈다. 비용은 다음에 고치는 사람이 어느 쪽을 고쳐야 하는지 주석만으로는 알 수 없다는 데 있다.
검증 환경
OpenJDK : 21.0.12 확인 방식 : 자동설정 주석 인용, implements PolicyTransactionPort 검색을 없는 이름 자기시험과 대조 이름과 함께 계수, 구현체의 선언과 자바독 인용, 자동설정이 만드는 빈 목록 전수, 네 이름의 main 참조 파일 수 계수와 그중 하나의 파일 목록, 저장소 문서가 같은 것을 어떻게 적었는지 전수 검색 소스 수정 : x
재현 조건
- 자동설정에서 정본 경계를 지목하는 주석을 인용한다.
- 그 포트를 구현한다고 선언한 클래스를 찾고, 없는 이름과 대조 이름으로 같은 검색을 함께 건다.
- 구현체의 선언과 클래스 자바독을 인용한다.
- 자동설정이 만드는 빈을 전부 나열한다.
- 두 스택의 이름 넷을 main 참조 파일 수로 센다.
- 저장소 문서에서 그 포트 이름이 나오는 자리를 전부 찾는다.
본문
JpaTransactionAutoConfiguration 은 JPA 트랜잭션 스택을 조립한다. 그 파일의 주석이 정본 경계가 어디인지 적어 둔다.
주석이 지목한 경계와 실제 구현 관계
:::evidence key="analysis-finding-a05-f010" alt="저장소 루트에서 돌린 정적 검색 출력 84줄. 먼저 JpaTransactionAutoConfiguration 2855번 줄이 실리는데 37번 줄 주석이 PolicyTransactionPort.inTransaction 을 정본 경계로 지목하고 아래의 재시도 코디네이터를 함께 설명한다. 이어서 PolicyTransactionPort 를 구현한다고 선언한 클래스가 SpringTransactionPort 31번 줄 하나로 나오고, 없는 이름으로 건 자기시험이 0 개, 대조로 센 implements TransactionPort 가 21 개다. 그 구현체의 선언과 자바독이 2440번 줄로 실리는데 모드별로 미리 만든 트랜잭션 템플릿을 쓰고 모두 READ_COMMITTED 로 고정한다고 적는다. 그 아래에 자동설정이 만드는 빈들이 나오는데 CommitFailureClassifier 와 SpringJpaTransactionExecutor 둘과 FullTransactionRetryCoordinator 둘과 RetrySleeper 이고, 기본 최대 재시도 경과 시간이 30 초로 선언돼 있다. 두 스택을 참조하는 main 파일 수는 PolicyTransactionPort 와 FullTransactionRetryCoordinator 와 SpringJpaTransactionExecutor 와 SpringTransactionPort 가 각각 4 개이고, PolicyTransactionPort 를 참조하는 네 파일이 이름으로 나열된다. 마지막으로 문서 쪽 서술이 나오는데 코드 리뷰 문서가 두 벌의 트랜잭션 계약이 공존한다는 것을 P1 로 적고 PolicyTransactionPort 를 단일 facade 로 통합하자고 적는다." caption="자동설정 주석이 지목한 정본 경계 · 그 포트를 구현한 클래스 하나와 자기시험과 대조 · 구현체의 선언과 자바독 · 자동설정이 만드는 빈 목록 · 두 스택의 main 참조 수 · 코드 리뷰 문서가 같은 것을 P1 로 적은 자리 — 84줄 · exit 0" zoom="true"
:::
:37 주석은 PolicyTransactionPort.inTransaction(TransactionRequest, Supplier) 를 지목하고, 같은 문장이 아래의 재시도 코디네이터를 함께 설명한다.
그 포트를 구현한 클래스는 하나다
implements PolicyTransactionPort 를 가진 파일은 SpringTransactionPort:31 하나다. 없는 이름으로 같은 검색식을 걸면 0 이 나오므로, 그 1 이 검색 운은 아닌지 확인하려고 implements TransactionPort 를 세면 21 이 나온다.
SpringTransactionPort 는 adapter/outbound/persistence-jpa 에 있고 @Component 다. 자바독 :26~:28 은 모드별로 미리 만든 TransactionTemplate 을 쓰고 모두 READ_COMMITTED 로 고정한다고 적는다. 미리 만드는 이유는 가변 템플릿 경합 때문이라고 README 를 가리킨다.
재시도는 그 자바독 어디에도 없다.
자동설정이 만드는 것은 다른 스택이다
빈 목록은 여섯이다. :61 의 CommitFailureClassifier, :66 과 :82 의 SpringJpaTransactionExecutor, :99 와 :107 의 FullTransactionRetryCoordinator, :114 의 RetrySleeper 다.
포트 구현을 만드는 빈이 없다. SpringTransactionPort 는 @Component 로 스캔되어 들어온다.
그래서 :37 주석은 한 문장 안에서 두 계통을 잇는다. 앞이 가리키는 포트는 어댑터 쪽이 구현하고, 뒤가 설명하는 코디네이터는 같은 파일이 만드는 실행기와 짝이다.
두 스택 다 살아 있다
PolicyTransactionPort 와 FullTransactionRetryCoordinator 와 SpringJpaTransactionExecutor 와 SpringTransactionPort 의 main 참조 파일 수가 모두 4 개다.
PolicyTransactionPort 를 참조하는 넷은 그 인터페이스 자신과 SpringPolicyTransactionPort 와 SpringTransactionPort 와 JpaTransactionAutoConfiguration 이다.
어느 쪽에도 폐기 표시가 없다.
저장소 자신이 이미 적어 둔 것
docs/reviews/2026-08-14-jpa-module-code-review.md:31 은 애플리케이션이 이미 쓰는 포트와 새 JPA 실행기·AOP 계약이 두 벌로 존재한다고 적는다.
:119 는 그것을 JPA-007 로 번호 매겨 P1 · High 로 두고 PolicyTransactionPort 단일 facade 로 통합하자고 적는다. :427 은 템플릿의 정본 경계를 그 포트의 inTransaction 으로 삼자고 적는다.
즉 이 상태는 발견되지 않은 것이 아니라 결정이 내려지지 않은 것이다.
원문과 갈리는 자리
원문은 문서와 소스 주석이 코디네이터가 포트를 구현한다고 설명하는데 실제 구현체는 다른 클래스라고 적었다. 실제 주석은 그보다 조금 다르다. :37 은 코디네이터가 그 포트를 구현한다고 적지 않고, 정본 경계로 포트를 지목한 뒤 아래 코디네이터를 이어서 설명한다. 두 계통이 한 문장에 붙어 있어서 읽는 사람이 그렇게 이해하게 된다.
원문이 적지 않은 것은 저장소가 이미 이 중복을 JPA-007 로 기록하고 통합 방향까지 정해 두었다는 것이다.
확인하지 못한 것
실제 요청이 두 계통 중 어느 쪽을 더 자주 지나는지 세지 않았다.
그 문장이 쓰였을 때는 맞았는지 커밋을 되짚지 않았다.
JPA-007 이 어느 쪽으로 결정됐는지 추적하지 않았다.
JPA-007 이 어느 쪽으로 결정됐는지 추적하지 않았다.