--- 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 주장과 로거의 실패 기록 본문을 나란히 놓는다. ## 본문 `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 의 나머지 문장까지 소스와 맞춰 보지는 않았다.