Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a04-f005.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

147 lines
12 KiB
Markdown

---
kind: CASE
slug: analysis-finding-a04-f005
title: README 의 세 문장 중 둘은 쓰일 때부터 틀렸고 하나만 나중에 어긋났다
topic: multitenancy-isolation
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:analysis-finding-a04-f005
evidenceCapturedOn: 2026-09-04
body: case-analysis-finding-a04-f005.body.md
assets:
- key: analysis-finding-a04-f005
file: ../../../final/evidence/rendered/analysis-finding-a04-f005.svg
evidence:
- ../../../final/evidence/raw/analysis-finding-a04-f005.txt
source:
- 원본 분석 절은 analysis/04-adapter-outbound-support.md §6 이다.
---
# README 의 세 문장 중 둘은 쓰일 때부터 틀렸고 하나만 나중에 어긋났다
`support/README.md``support/CLAUDE.md``821fe00c` 에서 나란히 추가됐다. 그래서 README 가 적은 "아직 별도 CLAUDE.md 를 두지 않았다" 는 쓰인 날부터 거짓이었고, "의존 정책의 SSOT 는 `src/build.gradle``allowedProjectDependencies`" 도 그때 이미 파생 코드를 가리켰다. 실제로 시간이 지나 어긋난 것은 셋 중 하나뿐이다.
## 관계
- **문서의 수치는 세지 말고 파생하거나 게이트로 붙든다**
그 규칙은 문서의 서술을 소스에서 파생하거나 검사로 붙들라고 한다. 이 README 의 문장들은 손으로 적혀 있고 소스와 대조하는 검사가 없다.
- **과대 진술 문서를 과소보다 먼저 고친다**
그 규칙은 문서가 실제보다 많이 약속하는지 적게 약속하는지부터 가르라고 한다. 이 셋의 방향이 서로 다르다.
- **다섯 문서가 "exactly 19 leaf"라고 적고 레지스트리는 62다**
그 기록에서도 문서가 레지스트리를 따라가지 못했다. 다만 그쪽은 시간이 지나 벌어진 차이이고 여기는 절반이 작성 시점의 오기다.
## 문제
이 README 는 이 모듈이 왜 갈라져 나왔고 어떤 의존이 허용되는지를 적어 둔다. 다른 어댑터가 이 모듈에 기댈 때 참고하는 자리다.
세 문장이 지금 소스와 맞지 않는다. 각각이 언제부터 틀렸는지 커밋으로 되짚었다.
## 결론
두 마크다운 파일의 추가 커밋이 같다. 이후 규범 문서만 한 번 더 수정됐고 README 에는 그 뒤 커밋이 없다. 규범 문서가 없다고 적은 문장은 나중에 낡은 것이 아니라 처음부터 사실과 달랐다.
의존 정책 쪽도 마찬가지다. 그 커밋의 빌드 스크립트는 이미 저장소 바깥 파일에서 맵을 만들어 내고 있었고, 지금의 레지스트리 파일은 나흘 뒤에야 들어왔다. allowedProjectDependencies 가 SSOT 였던 적이 없고, 옮겨 간 것은 레지스트리 파일의 위치다.
지금도 그 이름은 빌드 스크립트에 세 줄 남아 있지만 셋 다 레지스트리에서 파생하거나 그 결과를 읽는 자리다. 값을 담는 것은 modules.json:44~:48 이고, 같은 디렉터리의 CLAUDE.md:9 가 그것을 Registry SSOT 로 못 박는다.
셋 중 진짜 드리프트는 하나다. OutboundHttpDependencyLogger 는 821fe00c 에 main·test 두 파일로 추가됐다가 5f10b791 에서 함께 삭제됐다. README 가 쓰였을 때는 맞는 문장이었다.
그 모듈에는 지금 로거 클래스도, slf4j 를 끌어오는 파일도, ERROR 를 남기는 호출도 없다. 대조 기준 하나가 사라진 것이 아니라 대조할 로깅 자체가 남아 있지 않다.
같은 자리에 넷째가 있다. 이 문단은 PII 가 로그에 닿을 수 없다고 단언하는데, FailOpenDependencyLogger:47 이 예외 메시지를 형식 문자열에 그대로 채워 넣는다.
## 검증 환경
확인 방식 : README 의 두 대목과 그 파일의 커밋 이력 인용, 같은 디렉터리 두 마크다운 파일의 추가·수정 이력을 나란히 뽑기, README 가 쓰인 커밋의 build.gradle 에서 레지스트리 파일 경로 확인과 modules.json 의 추가 커밋 확인, 지금의 파생 자리 전수와 레지스트리 항목 인용, CLAUDE.md 머리 열 줄 인용, 없는 로거 이름 계수를 자기시험·대조와 함께 확인하고 그 이름의 추가·삭제 이력 추적, httpclient main 의 로거 파일·slf4j import·error 호출 계수, README 의 PII 주장과 로거 본문 대조
소스 수정 : x
## 재현 조건
1. README 에서 정본 위치와 규범 문서 존재 여부와 대조 로거 이름과 PII 주장을 적은 대목을 인용한다.
2. 그 파일과 같은 디렉터리 규범 문서의 추가·수정 이력을 나란히 뽑는다.
3. README 가 쓰인 커밋의 빌드 스크립트에서 레지스트리 파일 경로를 확인하고, 지금 레지스트리 파일이 언제 들어왔는지 본다.
4. 지금 그 이름이 나오는 자리를 전부 세고 값을 담는 파일과 규범 문서의 SSOT 선언을 인용한다.
5. 대조 로거 이름을 자기시험과 대조 이름과 함께 세고, 그 이름의 추가·삭제 커밋을 뽑는다.
6. 그 모듈의 로거 파일 수와 slf4j import 수와 error 호출 수를 센다.
7. README 의 PII 주장과 로거의 실패 기록 본문을 나란히 놓는다.
## 본문
<!-- body:start -->
`src/adapter/outbound/support/README.md` 는 이 모듈의 존재 이유와 의존 정책을 설명한다. 새 어댑터를 붙이는 사람이 먼저 여는 문서다.
## README 가 적은 네 문장
:::evidence key="analysis-finding-a04-f005" alt="저장소 루트에서 돌린 정적 검색 출력 96줄. 먼저 support/README.md 7~11번 줄과 20~25번 줄이 실린다. 앞엣것은 허용·금지 의존 정책의 SSOT 가 src/build.gradle 의 allowedProjectDependencies 항목이며 이 모듈은 아직 별도 CLAUDE.md 를 두지 않았다고 적는다. 뒤엣것은 이 로거가 WARN 을 쓰는 이유를 적으면서 httpclient 모듈의 OutboundHttpDependencyLogger 와 구분되고 시그니처가 본문·수신자·페이로드를 받지 않아 PII 가 로그에 닿지 않는다고 적는다. 그 README 의 커밋 이력은 821fe00c 한 줄뿐이다. 이어서 두 파일이 언제 태어났는지가 나온다. CLAUDE.md 와 README.md 가 821fe00c 에서 나란히 A 로 추가되고 CLAUDE.md 만 b3add016 에서 M 으로 수정된다. README 가 쓰인 커밋의 build.gradle 558번 줄은 레지스트리 파일을 저장소 바깥의 .harness/project/modules.yaml 로 가리키고, src/config/architecture/modules.json 은 나흘 뒤 b3add016 에서 처음 들어온다. 그 아래에 지금의 src/build.gradle 에서 allowedProjectDependencies 가 나오는 세 줄이 전부 실리는데 1419번이 registry.modules 에서 만들어 내는 자리이고, modules.json 41~52번 줄이 이 모듈의 allowed_dependencies 셋과 runtime_memberships 를 담는다. 다음으로 이 디렉터리의 마크다운 파일 둘과 CLAUDE.md 1~10번 줄이 실리는데 9번 줄이 Registry SSOT 를 src/config/architecture/modules.json 으로 못 박는다. 그 파일의 이력도 함께 나온다. 세 번째로 OutboundHttpDependencyLogger 를 가진 파일이 0 개이고, httpclient main 에 이름에 Logger 가 든 파일도 0 개, org.slf4j 를 import 하는 파일도 0 개, log.error 호출도 0 줄이다. 없는 이름 자기시험은 0 개, 대조 FailOpenDependencyLogger 는 12 개다. 그 이름의 이력은 821fe00c 에서 main 과 test 두 파일이 A 로 추가되고 5f10b791 에서 둘 다 D 로 삭제된 것이다. 마지막으로 README 24~25번 줄의 PII 주장과 FailOpenDependencyLogger 36~48번 줄이 나란히 실리는데, 그 logFailure 가 47번 줄에서 cause.getMessage() 를 로그 형식 문자열에 넣는다." caption="README 의 두 대목과 그 파일의 단일 커밋 이력 · 두 파일이 같은 커밋에서 태어난 기록과 초기 레지스트리 위치 · 지금의 파생 자리 셋과 레지스트리의 실제 항목 · CLAUDE.md 가 못 박은 SSOT · 없는 로거의 계수와 자기시험과 대조와 그 삭제 이력 · README 의 PII 주장과 로거가 실제로 넣는 값 — 96줄 · exit 0" zoom="true"
:::
`:7`\~`:9` 한 문장에 두 가지가 들어 있다. 허용·금지 의존 정책의 SSOT 가 `src/build.gradle``allowedProjectDependencies['adapter:outbound:support']` 항목이고, 이 모듈에는 아직 별도 `CLAUDE.md` 가 없다는 것이다.
`:23`\~`:25` 에 둘이 더 있다. 이 로거가 `httpclient` 모듈의 `OutboundHttpDependencyLogger` 와 구분된다는 것, 그리고 메서드 시그니처가 본문·수신자·페이로드를 받지 않아 PII 가 로그에 닿지 않는다는 것이다.
이 파일에는 `821fe00c` 이후 커밋이 없다.
## 두 파일은 같은 커밋에서 태어났다
`CLAUDE.md``README.md``821fe00c` 에서 나란히 `A` 로 추가된다. `CLAUDE.md``b3add016` 에서 `M` 으로 한 번 더 수정됐다.
그러므로 "이 모듈은 아직 별도 CLAUDE.md 를 두지 않았다" 는 문장은 갱신을 놓친 것이 아니다. 그것을 적은 커밋이 같은 파일을 함께 넣었다.
## SSOT 는 그 자리에 있었던 적이 없다
`821fe00c` 시점의 `build.gradle:558` 은 레지스트리 파일을 `.harness/project/modules.yaml` 로 가리킨다. 저장소 바깥 경로다. `:577` 이 그 레지스트리에서 `allowedProjectDependencies` 를 만들어 낸다.
`src/config/architecture/modules.json` 은 나흘 뒤 `b3add016` 에 처음 들어온다.
즉 README 가 쓰인 날에도 `src/build.gradle` 의 그 이름은 파생물이었다. 이후에 바뀐 것은 레지스트리 파일이 저장소 밖에서 안으로 들어온 것이고, 정본의 성격이 옮겨 간 것이 아니다.
## 지금의 파생 관계
`src/build.gradle``allowedProjectDependencies` 가 나오는 줄은 셋이고 `:1419``registry.modules` 에서 만들어 내는 자리다. 나머지 둘은 그 맵을 읽는다.
값을 담는 것은 `modules.json:41`\~`:52` 다. 이 모듈의 `allowed_dependencies` 셋과 `runtime_memberships` 가 거기 있다.
같은 디렉터리의 `CLAUDE.md:9``Registry SSOT: src/config/architecture/modules.json` 이라고 적는다. README 가 없다고 한 그 파일이 정본 위치를 못 박고 있다.
## 하나만 시간이 지나 어긋났다
`OutboundHttpDependencyLogger``821fe00c` 에서 main 과 test 두 파일로 추가됐다. `5f10b791` 에서 둘 다 `D` 로 삭제됐다.
README 가 쓰인 날에는 실재하는 클래스였고 그 뒤에 사라졌다. 셋 중 이것만이 문서가 소스를 따라가지 못한 경우다.
지금 그 이름을 가진 파일은 0 개다. 없는 이름으로 같은 검색을 걸어도 0 이 나오므로, 실재하는 `FailOpenDependencyLogger` 로 같은 검색을 걸어 12 를 받았다.
`httpclient` main 에는 이름에 `Logger` 가 든 파일이 0 개이고, `org.slf4j` 를 import 하는 파일이 0 개이며, `log.error` 호출이 0 줄이다. 대조 대상만 사라진 것이 아니라 그 모듈의 로깅 자체가 남아 있지 않다.
## 넷째 문장
`:24`\~`:25` 는 메서드 시그니처가 본문·수신자·페이로드를 받지 않아 PII 가 로그에 닿지 않는다고 적는다.
`FailOpenDependencyLogger:37`\~`:48``logFailure``Throwable cause` 를 받는다. `:41` 의 형식 문자열이 `error="{}: {}"` 를 담고 `:46`\~`:47` 이 그 자리에 `cause.getClass().getSimpleName()``cause.getMessage()` 를 넣는다.
시그니처가 payload 를 받지 않는다는 것은 맞다. 예외 메시지를 통해 닿지 않는다는 것은 그 문장이 보장하지 못한다.
## 원문과 갈리는 자리
원문은 정본 위치와 안내 문서 존재 여부와 HTTP 로거 존재 여부 셋이 어긋난다고 적었다. 셋 다 지금 소스와 다르다.
갈리는 것은 그 셋을 드리프트로 묶은 부분이다. 커밋 이력을 보면 앞의 둘은 문서가 낡은 것이 아니라 작성 시점부터 사실과 달랐다.
원문이 적지 않은 것은 넷째다. 같은 README 가 PII 가 로그에 닿지 않는다고 적는데 그 로거는 예외 메시지를 형식 문자열에 넣는다.
## 확인하지 못한 것
이 서술을 근거로 잘못된 의존이 들어간 적이 있는지 커밋을 뒤지지 않았다.
`5f10b791` 이 그 로거를 지운 이유를 커밋 메시지 밖에서 확인하지 않았다.
여기서 다룬 넷 밖의 서술은 소스와 대조하지 않았다.
`5f10b791` 이 그 로거를 지운 이유를 커밋 메시지 밖에서 확인하지 않았다.
README 의 나머지 문장까지 소스와 맞춰 보지는 않았다.
<!-- body:end -->