Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a04-f005.md
T
DongHyeonkaandClaude Fable 5.1 b25357c48a docs(clean-architecture-backend-template): fold analysis into final and re-select one topic
- analysis/·source-index·state.json 을 final/document.md 제2부·제3부로 접었다. SSOT 는 하나다
- 파일럿 — commit-ambiguity-as-a-result 를 새 기준으로 재선별. 후보 14 → 글감 5
  (PROMOTE 5 · MERGE_INTO 3 · KEEP_IN_SSOT 4 · 보류 2). 기록 5건을 다시 썼고 그림 1개를
  techviz 로 만들었다
- 재선별이 잡은 것: 제1부 §6.2·§11.1 이 자기 §13.2 와 어긋나 있었다(레인을 안 돌렸다 vs
  돌렸다) — 정정. 이미 답이 나와 있던 Question 을 HEAD 재실행 질문으로 다시 세웠다.
  Concept 이 인용한 코드가 SSOT 에 없어 뺐다
- candidateScope·sourceRepository 기록. 나머지 43개 주제는 재선별 대기(PENDING 905)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 12:39:20 +09:00

12 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-a04-f005 README 의 세 문장 중 둘은 쓰일 때부터 틀렸고 하나만 나중에 어긋났다 multitenancy-isolation clean-architecture-backend-template 게시 전 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 case:analysis-finding-a04-f005 2026-09-04 case-analysis-finding-a04-f005.body.md
key file
analysis-finding-a04-f005 ../../../final/evidence/rendered/analysis-finding-a04-f005.svg
../../../final/evidence/raw/analysis-finding-a04-f005.txt
원본 분석 절은 final/document.md#a04 §6 이다.

README 의 세 문장 중 둘은 쓰일 때부터 틀렸고 하나만 나중에 어긋났다

support/README.mdsupport/CLAUDE.md821fe00c 에서 나란히 추가됐다. 그래서 README 가 적은 "아직 별도 CLAUDE.md 를 두지 않았다" 는 쓰인 날부터 거짓이었고, "의존 정책의 SSOT 는 src/build.gradleallowedProjectDependencies" 도 그때 이미 파생 코드를 가리켰다. 실제로 시간이 지나 어긋난 것은 셋 중 하나뿐이다.

관계

  • 문서의 수치는 세지 말고 파생하거나 게이트로 붙든다 그 규칙은 문서의 서술을 소스에서 파생하거나 검사로 붙들라고 한다. 이 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 주장과 로거의 실패 기록 본문을 나란히 놓는다.

본문

src/adapter/outbound/support/README.md 는 이 모듈의 존재 이유와 의존 정책을 설명한다. 새 어댑터를 붙이는 사람이 먼저 여는 문서다.

README 가 적은 네 문장

:::evidence key="analysis-finding-a04-f005" alt="저장소 루트에서 돌린 정적 검색 출력 96줄. 먼저 support/README.md 711번 줄과 2025번 줄이 실린다. 앞엣것은 허용·금지 의존 정책의 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 4152번 줄이 이 모듈의 allowed_dependencies 셋과 runtime_memberships 를 담는다. 다음으로 이 디렉터리의 마크다운 파일 둘과 CLAUDE.md 110번 줄이 실리는데 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 2425번 줄의 PII 주장과 FailOpenDependencyLogger 3648번 줄이 나란히 실리는데, 그 logFailure 가 47번 줄에서 cause.getMessage() 를 로그 형식 문자열에 넣는다." caption="README 의 두 대목과 그 파일의 단일 커밋 이력 · 두 파일이 같은 커밋에서 태어난 기록과 초기 레지스트리 위치 · 지금의 파생 자리 셋과 레지스트리의 실제 항목 · CLAUDE.md 가 못 박은 SSOT · 없는 로거의 계수와 자기시험과 대조와 그 삭제 이력 · README 의 PII 주장과 로거가 실제로 넣는 값 — 96줄 · exit 0" zoom="true" :::

:7~:9 한 문장에 두 가지가 들어 있다. 허용·금지 의존 정책의 SSOT 가 src/build.gradleallowedProjectDependencies['adapter:outbound:support'] 항목이고, 이 모듈에는 아직 별도 CLAUDE.md 가 없다는 것이다.

:23~:25 에 둘이 더 있다. 이 로거가 httpclient 모듈의 OutboundHttpDependencyLogger 와 구분된다는 것, 그리고 메서드 시그니처가 본문·수신자·페이로드를 받지 않아 PII 가 로그에 닿지 않는다는 것이다.

이 파일에는 821fe00c 이후 커밋이 없다.

두 파일은 같은 커밋에서 태어났다

CLAUDE.mdREADME.md821fe00c 에서 나란히 A 로 추가된다. CLAUDE.mdb3add016 에서 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.gradleallowedProjectDependencies 가 나오는 줄은 셋이고 :1419registry.modules 에서 만들어 내는 자리다. 나머지 둘은 그 맵을 읽는다.

값을 담는 것은 modules.json:41~:52 다. 이 모듈의 allowed_dependencies 셋과 runtime_memberships 가 거기 있다.

같은 디렉터리의 CLAUDE.md:9Registry SSOT: src/config/architecture/modules.json 이라고 적는다. README 가 없다고 한 그 파일이 정본 위치를 못 박고 있다.

하나만 시간이 지나 어긋났다

OutboundHttpDependencyLogger821fe00c 에서 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~:48logFailureThrowable cause 를 받는다. :41 의 형식 문자열이 error="{}: {}" 를 담고 :46~:47 이 그 자리에 cause.getClass().getSimpleName()cause.getMessage() 를 넣는다.

시그니처가 payload 를 받지 않는다는 것은 맞다. 예외 메시지를 통해 닿지 않는다는 것은 그 문장이 보장하지 못한다.

원문과 갈리는 자리

원문은 정본 위치와 안내 문서 존재 여부와 HTTP 로거 존재 여부 셋이 어긋난다고 적었다. 셋 다 지금 소스와 다르다.

갈리는 것은 그 셋을 드리프트로 묶은 부분이다. 커밋 이력을 보면 앞의 둘은 문서가 낡은 것이 아니라 작성 시점부터 사실과 달랐다.

원문이 적지 않은 것은 넷째다. 같은 README 가 PII 가 로그에 닿지 않는다고 적는데 그 로거는 예외 메시지를 형식 문자열에 넣는다.

확인하지 못한 것

이 서술을 근거로 잘못된 의존이 들어간 적이 있는지 커밋을 뒤지지 않았다.

5f10b791 이 그 로거를 지운 이유를 커밋 메시지 밖에서 확인하지 않았다.

여기서 다룬 넷 밖의 서술은 소스와 대조하지 않았다.

5f10b791 이 그 로거를 지운 이유를 커밋 메시지 밖에서 확인하지 않았다.

README 의 나머지 문장까지 소스와 맞춰 보지는 않았다.