- 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
15 KiB
Markdown
147 lines
15 KiB
Markdown
---
|
|
kind: CASE
|
|
slug: analysis-finding-a06-f022
|
|
title: 허용 목록 검사를 부르는 프로덕션 코드가 없다
|
|
topic: multitenancy-isolation
|
|
project: clean-architecture-backend-template
|
|
status: 게시 전
|
|
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
|
rootTreeNode: case:analysis-finding-a06-f022
|
|
evidenceCapturedOn: 2026-09-04
|
|
body: case-analysis-finding-a06-f022.body.md
|
|
assets:
|
|
- key: analysis-finding-a06-f022
|
|
file: ../../../final/evidence/rendered/analysis-finding-a06-f022.svg
|
|
evidence:
|
|
- ../../../final/evidence/raw/analysis-finding-a06-f022.txt
|
|
source:
|
|
- 원본 분석 절은 final/document.md#a06 §77 이다.
|
|
---
|
|
|
|
# 허용 목록 검사를 부르는 프로덕션 코드가 없다
|
|
|
|
`MongoObservationConvention:14`~`:15` 가 이 허용 목록을 강제라고 적는다. 그런데 `:66` 의 `requireAllowed` 를 부르는 이 리프의 main 줄이 0 이고, 관측 클래스 넷은 `Tags.of(...)` 로 문자열을 직접 넣는다.
|
|
|
|
## 관계
|
|
|
|
- **카디널리티 경계를 타입으로 표현하기**
|
|
그 규칙이 요구하는 것을 이 허용 목록이 선언은 한다. 강제는 호출 지점이 그것을 지날 때만 생기는데 지나는 코드가 없다.
|
|
- **이름은 값이 아니라 registry key다**
|
|
태그 이름이 등록된 것이어야 한다는 규칙이다. 여기서는 `Tags.of` 의 첫 인자가 그냥 문자열이다.
|
|
- **단일 admission point는 우회 경로를 세어야 성립한다**
|
|
`requireAllowed` 를 지나는 main 경로가 0 이고 `Tags.of` 로 문자열을 직접 넣는 경로가 이 패키지에 아홉이다.
|
|
|
|
## 문제
|
|
|
|
허용 목록과 금지 목록이 한 파일에 있고, 자바독이 그것을 강제라고 부른다.
|
|
|
|
그 강제가 실제로 어디서 일어나는지 확인했다.
|
|
|
|
## 결론
|
|
|
|
MongoObservationConvention:9~:12 가 막으려는 두 가지를 적는다. 메트릭 카디널리티 폭증과, 메트릭 저장소가 프로덕션 데이터의 관리되지 않는 사본이 되는 것이다.
|
|
|
|
:14~:15 가 그 둘을 하나의 허용 목록으로 막는다고 적는다.
|
|
|
|
그 판단이 통하려면 :66 의 requireAllowed 를 호출 지점이 지나야 한다. 허용 목록은 여덟이고 금지 목록은 열하나다.
|
|
|
|
이 리프의 main 에서 그것을 부르는 줄이 0 이다. MongoObservationConvention 을 언급하는 main 파일은 자기 자신과 MongoDriverObservabilityConfiguration 둘뿐이다.
|
|
|
|
관측 클래스들은 Tags.of(...) 로 문자열을 직접 넣는다. 이 패키지에서 그 호출이 아홉 줄이고, MicrometerMongoOperationObserver 와 MongoCommandObservationListener 와 MongoPoolObservationListener 와 MongoSdamObservationListener 넷에 흩어져 있다.
|
|
|
|
지금 값들은 전부 허용 목록 안이다. 그것을 지키는 것은 코드가 아니라 검토와 MongoObservationConventionTest 다.
|
|
|
|
MongoDriverObservabilityConfiguration:26~:37 의 apply 가 리스너 셋을 만들어 붙이는 동안 규약을 지나지 않는다. 규약은 :40 의 convention() 안에만 있고 그것을 부르는 main 줄이 0 이다.
|
|
|
|
MongoObservationRedactor:36 의 describe 도 main 호출자가 없고, :45 의 isAlwaysRedacted 만 MongoCommandObservationListener:67 이 부른다. 프로덕션이 실제로 읽는 집합은 ALWAYS_REDACTED 하나이고, DESCRIBABLE_COMMANDS 는 어느 main 코드도 지나지 않는다.
|
|
|
|
## 검증 환경
|
|
|
|
OpenJDK : 21.0.12
|
|
확인 방식 : 규약의 클래스 자바독과 두 목록 인용, requireAllowed 본문 인용, 그 규약을 언급하는 main 파일 전수와 이 리프의 호출 계수와 같은 이름의 다른 메서드 대조, 그 둘째 파일의 조립 메서드 전문 인용과 standard()·convention() main 호출 계수, 이 패키지의 Tags.of 호출 전수와 저장소 전체 대조, 편집기의 세 갈래 인용과 두 메서드의 main 호출 계수와 시험 대조, 리스너가 쓰는 갈래 인용
|
|
소스 수정 : x
|
|
|
|
## 재현 조건
|
|
|
|
1. 규약의 클래스 자바독과 허용·금지 목록을 인용한다.
|
|
2. requireAllowed 본문을 인용한다.
|
|
3. 그 규약을 언급하는 main 파일을 전부 찾고, 이 리프에서 그 메서드를 부르는 줄을 센다.
|
|
4. 같은 이름의 메서드를 저장소 전체 main 에서 부르는 줄을 대조로 센다.
|
|
5. 그 둘째 파일의 apply 와 convention 을 전문으로 싣고 두 메서드의 main 호출을 센다.
|
|
6. 이 패키지에서 Tags.of 를 부르는 줄을 전부 나열하고 저장소 전체 계수와 대조한다.
|
|
7. 편집기의 세 갈래를 인용하고 두 메서드의 main 호출을 각각 센다.
|
|
8. 리스너가 그중 무엇을 쓰는지 인용한다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
`MongoObservationConvention` 은 MongoDB 텔레메트리가 실을 수 있는 태그를 목록으로 못박는다.
|
|
|
|
## 그 목록이 막으려는 두 가지
|
|
|
|
:::evidence key="analysis-finding-a06-f022" alt="저장소 루트에서 돌린 정적 검색 출력 205줄. 먼저 MongoObservationConvention 1~57번이 실린다. 6~16번 자바독은 이것이 MongoDB 텔레메트리가 실을 수 있는 유일한 태그들이라며 두 문제를 하나의 허용 목록으로 막는다고 적는다. 문서 아이디나 테넌트 아이디나 질의 파라미터 같은 높은 카디널리티 태그가 시계열을 늘려 메트릭 백엔드가 데이터를 버리거나 비용을 청구하게 만드는 것이 하나이고, 같은 값들이 메트릭 저장소를 프로덕션 데이터의 관리되지 않는 사본으로 바꿔 데이터베이스가 받는 보존과 접근 규칙 밖에 두는 것이 다른 하나다. 14~15번이 둘 다 같은 방식으로 막힌다며 이 목록에 없는 태그는 붙일 수 없으므로 실수가 호출 지점이 아니라 이 파일에서 일어나야 한다고 적는다. 19~28번의 허용 목록이 mongoProfile 과 databaseProfile 과 collectionProfile 과 operationName 과 operationType 과 result 와 failureCategory 와 consistencyProfile 여덟이고, 30~42번의 금지 목록 열하나가 documentId 와 rawTenantId 와 tenantId 와 dynamicCollectionName 과 queryParameter 와 query 와 fullBson 과 resumeToken 과 shardKeyValue 와 plaintextPII 와 credential 이다. 46~49번의 standard 가 인스턴스를 만들고 51~54번의 allowedTagNames 와 56~57번의 forbiddenTagNames 가 두 집합을 그대로 돌려준다. 이어서 58~76번이 실려 61~65번 자바독이 허용 목록에 없는 태그를 거절한다고 적고 66~75번의 requireAllowed 가 목록에 없으면 거절된 태그 이름과 허용 목록을 담은 IllegalArgumentException 을 던진다. 다음으로 MongoObservationConvention 을 언급하는 main 파일이 2 개라고 나오고 그 둘이 MongoDriverObservabilityConfiguration 과 MongoObservationConvention 자신이며, 이 리프 main 에서 requireAllowed 를 부르는 줄이 0 개다. requireAllowed 가 이 리프에 나오는 자리가 전부 나열되는데 main 은 MongoSearchQuery 48번과 ShardAwareQueryValidator 59번과 MongoAggregationProfile 86번과 PolicyAwareMongoAggregationExecutor 49번과 MongoObservationConvention 66번이고 앞의 넷은 이름이 비슷한 다른 메서드이며, test 쪽에 MongoSearchReadinessGateTest 52·54번과 ShardAwareQueryValidatorTest 57·60번과 PolicyAwareMongoAggregationExecutorTest 20·29·37·39·48·55·64·67번과 MongoObservationConventionTest 38·39번과 MongoSecurityIntegrationLaneTest 163·165·167번이 있다. 대조로 센 저장소 전체 main 의 같은 이름 호출은 3 개로 GraphQlRSocketAdmission 47번과 BlockingRedirectCoordinator 93번과 PublishPosterImageUseCase 106번이고, 같은 pathspec 으로 Tags.of 를 세면 9 개다. 이어서 MongoDriverObservabilityConfiguration 20~43번이 실린다. 25번 주석이 명령과 SDAM 과 풀 리스너를 클라이언트 설정 빌더에 더한다고 적고, 26~37번의 apply 가 28번에서 MongoCommandObservationListener 를, 29~31번에서 MongoSdamObservationListener 를, 32~35번에서 MongoPoolObservationListener 를 새로 만들어 붙인 뒤 빌더를 돌려준다. 39번 주석이 이 리스너들이 따르는 태그 규약이라고 적고 40~42번의 convention 이 MongoObservationConvention.standard 를 돌려준다. 그 아래 standard 를 부르는 main 줄이 1 개, convention 을 부르는 main 줄이 0 개다. 다음으로 이 패키지에서 Tags.of 를 부르는 줄이 9 개라고 나오고 그 아홉이 MicrometerMongoOperationObserver 46·72·77·83번과 MongoCommandObservationListener 63번과 MongoPoolObservationListener 40·60번과 MongoSdamObservationListener 46·61번이며, 저장소 전체에서는 30 개다. 마지막으로 MongoObservationRedactor 18~48번이 실린다. 18~20번의 DESCRIBABLE_COMMANDS 가 ping 과 hello 와 buildInfo 와 listCollections 와 listIndexes 와 collStats 여섯이고, 22~33번의 ALWAYS_REDACTED 가 authenticate 와 saslStart 와 saslContinue 와 getnonce 와 createUser 와 updateUser 와 copydbgetnonce 와 copydbsaslstart 와 copydb 아홉이다. 35~42번의 describe 가 항상 가림 목록에 있으면 redacted 를 돌려주고 아니면 서술 가능 목록에 있는지에 따라 이름 그대로거나 이름 뒤에 괄호 셋을 붙이며, 44~47번의 isAlwaysRedacted 는 항상 가림 목록 포함 여부만 돌려준다. 그 아래 redactor.describe 를 부르는 main 줄이 0 개, redactor.isAlwaysRedacted 가 1 개, 시험에서 describe 는 6 개라고 나오고, MongoCommandObservationListener 59~72번이 실려 63~69번이 Tags.of 로 mongoProfile 과 operationType 과 result 를 넣는데 67번이 isAlwaysRedacted 로 명령 이름을 가리거나 그대로 쓰는 것이 보인다." caption="규약이 적는 두 문제와 강제 문장 · 허용 여덟과 금지 열하나 · requireAllowed 본문 · 그 규약을 언급하는 main 파일 둘과 이 리프의 호출 0 · 리스너 셋을 조립하면서 규약을 지나지 않는 apply 와 convention 호출 0 · 관측 클래스 넷이 Tags.of 로 직접 넣는 아홉 줄 · 편집기의 세 갈래와 리스너가 쓰는 하나 — 205줄 · exit 0" zoom="true"
|
|
:::
|
|
|
|
`:9`\~`:12` 가 둘을 적는다. 문서 아이디나 테넌트 아이디나 질의 파라미터 같은 높은 카디널리티 태그는 시계열을 늘려 메트릭 백엔드가 데이터를 버리거나 비용을 청구하게 만든다. 그리고 같은 값들이 데이터베이스의 보존·접근 규칙 밖에 프로덕션 데이터 사본을 하나 더 만든다.
|
|
|
|
`:14`\~`:15` 가 둘 다 같은 방법으로 막힌다고 적는다. 목록에 없는 태그는 붙일 수 없으므로 실수가 호출 지점이 아니라 이 파일에서 일어나야 한다는 것이다.
|
|
|
|
`:19`\~`:28` 이 허용 여덟이고 `:30`\~`:42` 가 금지 열하나다. `shardKeyValue` 와 `plaintextPII` 와 `credential` 이 그 끝이다.
|
|
|
|
## 그 강제를 수행하는 메서드
|
|
|
|
`:66`\~`:75` 의 `requireAllowed` 가 허용 목록에 없으면 `IllegalArgumentException` 을 던진다. 메시지에 거절된 태그 이름과 허용 목록이 함께 들어간다.
|
|
|
|
`:62` 자바독이 그 역할을 한 줄로 적는다.
|
|
|
|
## requireAllowed 를 부르는 프로덕션 코드가 없다
|
|
|
|
`MongoObservationConvention` 을 언급하는 main 파일은 둘이다. 자기 자신과 `MongoDriverObservabilityConfiguration` 이다.
|
|
|
|
이 리프의 관측 패키지 main 에서 `requireAllowed` 를 부르는 줄이 0 이다.
|
|
|
|
그 둘째 파일이 규약으로 무엇을 하는지는 `MongoDriverObservabilityConfiguration:26`\~`:37` 에 있다. `apply` 가 `:28` 과 `:31` 과 `:35` 에서 명령·SDAM·풀 리스너 셋을 새로 만들어 클라이언트 설정 빌더에 붙이는데, 그 사이에 규약이 없다. 규약은 `:40`\~`:42` 의 `convention()` 이 `standard()` 를 돌려주는 별도 메서드로만 있고, 그것을 부르는 main 줄이 0 이다. `:39` 자바독은 그 메서드를 「이 리스너들이 따르는 태그 규약」이라고 부른다.
|
|
|
|
이 리프에 그 접두사가 나오는 main 자리 다섯 중 넷은 다른 타입의 것이다. `MongoSearchQuery:48` 의 `requireAllowedPaths` 와 `ShardAwareQueryValidator:59` 의 `requireAllowedRead` 는 이름부터 다르고, `MongoAggregationProfile:86` 과 그것을 부르는 `PolicyAwareMongoAggregationExecutor:49` 는 집계 단계를 받는 동명 메서드다.
|
|
|
|
부르는 것은 시험뿐이다. `MongoObservationConventionTest:38`·`:39` 와 `MongoSecurityIntegrationLaneTest:163`·`:165`·`:167` 이다.
|
|
|
|
## 관측 클래스들은 문자열을 직접 넣는다
|
|
|
|
이 패키지에서 `Tags.of(...)` 를 부르는 줄이 아홉이다. `MicrometerMongoOperationObserver:46`·`:72`·`:77`·`:83`, `MongoCommandObservationListener:63`, `MongoPoolObservationListener:40`·`:60`, `MongoSdamObservationListener:46`·`:61` 이다.
|
|
|
|
첫 인자가 그냥 문자열이다. `mongoProfile` 이나 `result` 나 `failureCategory` 처럼 허용 목록 안의 값이 들어 있지만, 그 사실을 코드가 확인하지 않는다.
|
|
|
|
지금 어긋남이 없는 것은 검토와 시험이 지킨 결과다.
|
|
|
|
## describe 를 지나지 않는 MongoCommandObservationListener
|
|
|
|
`MongoObservationRedactor:35`\~`:42` 의 `describe` 가 세 갈래를 나눈다. 항상 가리는 명령이면 `<redacted>`, 서술 가능 목록에 있으면 이름 그대로, 나머지는 이름 뒤에 괄호를 붙인다.
|
|
|
|
`:44`\~`:47` 의 `isAlwaysRedacted` 는 `ALWAYS_REDACTED` 포함 여부만 본다. `DESCRIBABLE_COMMANDS` 를 읽는 프로덕션 코드는 없다.
|
|
|
|
`redactor.describe` 를 부르는 main 줄이 0 이고 `redactor.isAlwaysRedacted` 는 1 이다. 그 하나가 `MongoCommandObservationListener:67` 인데, 삼항으로 `<redacted>` 아니면 명령 이름 그대로를 쓴다.
|
|
|
|
프로덕션이 읽는 집합은 `ALWAYS_REDACTED` 하나이고, 리스너가 만드는 렌더링은 `<redacted>` 와 원문 이름 둘이다. 시험에서는 `describe` 를 6 줄이 부른다.
|
|
|
|
## 원문과 갈리는 자리
|
|
|
|
원문은 이 허용 목록이 규약이지 강제가 아니며 `describe` 도 프로덕션 호출자가 0 이라고 적는다. 그대로 확인된다.
|
|
|
|
원문은 세 갈래 중 실제로 쓰이는 것이 하나라고 적는다. 세는 대상이 다르다 — 프로덕션이 읽는 집합으로 세면 `ALWAYS_REDACTED` 하나이고, 리스너가 실제로 만드는 태그 값으로 세면 `<redacted>` 와 원문 이름 둘이다.
|
|
|
|
여기에 더한 것은 그 0 이 검색 실패가 아니라는 대조다. 같은 이름의 메서드가 이 리프의 다른 타입에서 넷, 저장소 전체 main 에서 셋 불린다. `Tags.of` 도 이 패키지에서 아홉, 저장소 전체에서 서른이 나온다.
|
|
|
|
그리고 조립부가 그 관계를 어떻게 적는지다. `MongoDriverObservabilityConfiguration:39` 는 리스너들이 규약을 따른다고 적지만, 같은 파일의 `apply` 는 리스너 셋을 만들면서 규약을 지나지 않고 `convention()` 을 부르는 main 코드도 없다.
|
|
|
|
## 확인하지 못한 것
|
|
|
|
실제로 금지된 태그가 통과하는지 리스너를 만들어 돌려 보지 않았다.
|
|
|
|
`convention()` 이 나중에 붙일 강제 지점으로 남겨진 것인지, 이미 잊힌 것인지 커밋 이력으로 따지지 않았다.
|
|
|
|
`describe` 의 세 갈래 가운데 둘이 쓰이지 않는 것이 설계인지 누락인지 판단하지 않았다.
|
|
|
|
<!-- body:end -->
|