- 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>
147 lines
12 KiB
Markdown
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:
|
|
- 원본 분석 절은 final/document.md#a04 §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 -->
|