--- kind: CASE slug: analysis-finding-a06-f010 title: 집계 실행기가 컬렉션 이름을 문자열로 따로 받는다 topic: multitenancy-isolation project: clean-architecture-backend-template status: 게시 전 sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 rootTreeNode: case:analysis-finding-a06-f010 evidenceCapturedOn: 2026-09-04 body: case-analysis-finding-a06-f010.body.md assets: - key: analysis-finding-a06-f010 file: ../../../final/evidence/rendered/analysis-finding-a06-f010.svg evidence: - ../../../final/evidence/raw/analysis-finding-a06-f010.txt source: - 원본 분석 절은 final/document.md#a06 §42 이다. --- # 집계 실행기가 컬렉션 이름을 문자열로 따로 받는다 `MongoCollectionProfileRegistry` 의 클래스 자바독은 이 레지스트리를 지나지 않고 컬렉션 이름이 드라이버까지 가는 경로는 없다고 적는다. 그런 경로가 둘 있다. `PolicyAwareMongoAggregationExecutor:63` 과 `MongoReactiveCursorPublisher:37` 인데, 둘 다 널 검사 말고는 어떤 검사도 거치지 않고 그 문자열을 드라이버 연산에 넘긴다. ## 관계 - **단일 admission point는 우회 경로를 세어야 성립한다** 이 사례가 그 규칙의 형태다. 레지스트리가 유일한 승인 지점이라는 문장은 그것을 지나지 않는 서명이 하나도 없어야 성립하는데, 여기서는 둘이 있다. - **path·identifier는 등록하고 value는 바인딩한다** 컬렉션 이름은 등록 대상이고 값이 아니다. 두 서명은 컬렉션 이름을 `String` 값으로 받는다. - **데드라인을 서버로 보내는 접근자를 부르는 프로덕션 코드가 없다** 그 기록이 연산 타임아웃이 서버까지 닿는 유일한 경로로 든 것이 이 집계 실행기다. 같은 `MongoOperationContext` 에서 `timeout()` 은 읽어 `maxTime` 에 반영하면서 `collectionProfile()` 은 한 번도 읽지 않는다. ## 문제 컬렉션 프로파일 레지스트리의 자바독이 이 리프의 강한 주장 하나를 편다. 동적 컬렉션 이름 금지를 이 간접이 강제 가능하게 만든다는 주장이다. 그 주장을 깨는 서명이 있는지 확인했다. ## 결론 MongoCollectionProfileRegistry:12~:15 가 근거를 적는다. 물리 이름은 기동 시점에 고정되고, 그 대응을 아는 코드가 이 클래스뿐이라는 것이다. MongoOperationContext 도 같은 것을 요구한다. 호출자가 넘겨야 하는 다섯 값 가운데 하나가 :18 의 CollectionProfileName collectionProfile 이다. collections.require(...) 를 부르는 main 줄은 셋이다. DefaultMongoImperativeExecutor:81 과 DefaultReactiveMongoExecutor:130 과 SpringReactiveChangeStreamSource:47 이다. PolicyAwareMongoAggregationExecutor 의 execute 가 다르다. :63 이 컨텍스트와 별개로 컬렉션 이름을 String 인자로 받고, :66 의 널 검사 말고는 어떤 검사도 거치지 않고 :76 의 operations.aggregate(...) 로 그대로 간다. 같은 모양이 하나 더 있다. MongoReactiveCursorPublisher:37 도 String collection 을 받고 :44 의 널 검사만 거쳐 :64 의 operations.find(...) 에 넘긴다. 그쪽은 컨텍스트를 아예 읽지 않는다 — 받아서 널인지만 본다. 그 파일은 MongoCollectionProfileRegistry 를 한 줄도 언급하지 않고 context.collectionProfile() 도 부르지 않는다. import 열둘 가운데 레지스트리가 없다. 집계 실행기는 같은 컨텍스트에서 timeout() 은 읽는다. :72 가 budgetFor(context, budget) 을 부르고 그 메서드 :92 가 context.timeout() 을 밀리초로 바꿔 서버 데드라인을 좁힌다. collectionProfile() 만 읽지 않는다. 둘 다 지금 조립되지 않는다. new PolicyAwareMongoAggregationExecutor 와 new MongoReactiveCursorPublisher 를 부르는 main 줄이 각각 0 이다. 대조로 센 new MongoBudgetEnforcer 는 main 에 한 줄 있다. 이름이 걸린 시험조차 집계 실행기를 인스턴스로 만들지 않는다. 포크가 둘 중 하나를 배선하면 그 강제가 사라진다. ## 검증 환경 OpenJDK : 21.0.12 확인 방식 : 레지스트리 자바독과 require 인용, 컨텍스트 타입의 자바독과 필드 인용, require 를 부르는 main 자리 전수, 집계 실행기의 서명과 본문 인용, 그 파일의 레지스트리 언급 계수와 collectionProfile() 계수와 import 전수, 그 실행기의 등장 자리 전수와 생성 계수와 대조 계수 소스 수정 : x ## 재현 조건 1. 레지스트리의 클래스 자바독과 require 를 인용한다. 2. MongoOperationContext 의 자바독과 필드를 인용한다. 3. collections.require(...) 를 부르는 main 자리를 전부 찾는다. 4. 집계 실행기의 execute 서명과 드라이버 호출까지의 본문을 인용한다. 5. 그 파일이 레지스트리를 언급하는 줄과 collectionProfile() 을 부르는 줄을 세고 import 를 전부 나열한다. 6. 그 실행기의 이름이 나오는 자리를 소스 세트와 함께 전부 나열하고, 생성 줄을 세고 대조 이름으로 같은 검색을 건다. ## 본문 `MongoCollectionProfileRegistry` 는 등록된 컬렉션 프로파일을 물리 이름으로 바꾼다. 클래스 자바독이 그 간접이 무엇을 보장하는지 적는다. ## 레지스트리 자바독이 적은 보장 :::evidence key="analysis-finding-a06-f010" alt="저장소 루트에서 돌린 정적 검색 출력 242줄. 먼저 MongoCollectionProfileRegistry 9~45번이 실린다. 9~16번 자바독은 이것이 등록된 컬렉션 프로파일을 물리 컬렉션 이름으로 대응시키며, 이 간접이 동적 컬렉션 이름 금지를 강제 가능하게 만드는 것이고, 애플리케이션 코드는 프로파일을 이름 짓고 물리 이름을 아는 것은 이 레지스트리뿐이며 기동 시점에 고정되고, 따라서 요청 값에서 조립된 컬렉션 이름은 드라이버에 도달할 수 없는데 문자열에서 컬렉션으로 가는 경로 중 여기를 지나지 않는 것이 없기 때문이라고 적는다. 35~40번의 require 가 등록되지 않은 프로파일이면 MongoOperationRejectedException 을 던진다. 이어서 DefaultMongoImperativeExecutor 80~84번이 실려 81번이 collections.require 로 컨텍스트의 컬렉션 프로파일을 물리 이름으로 바꾸는 것이 보인다. 그 아래 require 를 부르는 main 줄이 셋 나오는데 SpringReactiveChangeStreamSource 47번과 DefaultMongoImperativeExecutor 81번과 DefaultReactiveMongoExecutor 130번이다. 다음으로 MongoOperationContext 1~40번이 실린다. 8~13번 자바독은 이것이 모든 플랫폼 연산이 요구하는 불변 실행 컨텍스트이고 실행 경로에 닿는 유일한 길로 일부러 만들어졌으며 호출자에게 연산 이름과 데이터베이스 프로파일과 컬렉션 프로파일과 요청하는 일관성 보장과 양수 타임아웃을 이름 짓게 만들고, 여기에는 문서도 테넌트도 사용자도 식별하는 것이 없어서 컨텍스트 전체를 편집 단계 없이 텔레메트리에 붙일 수 있다고 적는다. 15~20번이 다섯 필드를 선언하는데 그중 하나가 CollectionProfileName collectionProfile 이다. 이어서 PolicyAwareMongoAggregationExecutor 36~76번이 실린다. 59~64번의 execute 서명이 MongoOperationContext 와 MongoAggregationProfile 과 MongoAggregationPlan 과 String collection 과 Class 출력 타입 다섯을 받고, 65~67번이 널 검사를 하며 68번이 계획을 검증하고, 70~72번이 예산을 받아 좁히며, 74~75번이 Aggregation 을 만들고 76번이 operations.aggregate 에 aggregation 과 collection 과 outputType 을 넘긴다. 그 String 인자는 어떤 검사도 거치지 않고 드라이버로 간다. 다음으로 이 파일이 MongoCollectionProfileRegistry 를 언급하는 줄이 0 개이고 context.collectionProfile 을 부르는 줄도 0 개라고 나오며, import 열둘이 전부 나열되는데 MongoOperationContext 와 MongoOperationRejectedException 과 예산 관련 셋과 자바 표준 셋과 스프링 데이터 넷이고 레지스트리가 없다. 마지막으로 PolicyAwareMongoAggregationExecutor 이름이 나오는 자리가 소스 세트와 함께 나오는데 main 은 24번의 클래스 선언과 32번의 생성자 둘뿐이고 나머지 하나는 PolicyAwareMongoAggregationExecutorTest 14번이며, main 에서 그것을 만드는 줄이 0 개이고 대조로 센 new MongoBudgetEnforcer 는 1 개다." caption="레지스트리가 펴는 주장과 require · 그 주장을 지키는 세 자리 · 컨텍스트가 이미 들고 있는 컬렉션 프로파일 · 집계 실행기의 서명과 드라이버 호출 · 그 파일의 레지스트리 언급 0 과 import 전수 · main 참조 둘과 생성 0 — 242줄 · exit 0" zoom="true" ::: `:12`\~`:13` 이 근거를 적는다. 애플리케이션 코드는 프로파일을 이름 짓고, 물리 이름을 아는 것은 이 레지스트리뿐이며, 그 대응은 기동 시점에 고정된다. `:14`\~`:15` 가 결론이다. 요청에서 온 값으로 컬렉션 이름을 만들어도 드라이버까지 갈 수 없는데, 문자열에서 컬렉션으로 가는 경로 중 이 레지스트리를 지나지 않는 것이 없기 때문이다. `:35`\~`:40` 의 `require` 가 등록되지 않은 프로파일을 `MongoOperationRejectedException` 으로 거절한다. ## 컨텍스트가 이미 들고 있는 CollectionProfileName `MongoOperationContext:10`\~`:13` 은 이 타입이 실행 경로에 닿는 유일한 길로 일부러 만들어졌다고 적는다. 호출자가 연산 이름과 두 프로파일과 일관성 보장과 양수 타임아웃을 넘겨야 한다. `:18` 이 `CollectionProfileName collectionProfile` 이다. 문자열이 아니라 프로파일 이름 타입이다. ## collections.require(...) 를 부르는 세 자리 그 관문을 지나는 main 줄은 셋이다. `DefaultMongoImperativeExecutor:81` 과 `DefaultReactiveMongoExecutor:130` 이 컨텍스트의 프로파일을 물리 이름으로 바꾸고, `SpringReactiveChangeStreamSource:47` 이 구독의 프로파일로 같은 일을 한다. 셋 다 컨텍스트나 구독에서 프로파일을 꺼내 레지스트리에 넣는다. 문자열을 받지 않는다. ## 집계 실행기의 서명 `PolicyAwareMongoAggregationExecutor:59`\~`:64` 의 인자 목록에는 `MongoOperationContext` 와 `MongoAggregationProfile` 과 `MongoAggregationPlan` 과 출력 타입 말고 `String collection` 이 하나 더 있다. `:66` 이 그 문자열에 널 검사만 한다. `:68` 의 `validate(plan, profile)` 은 단계 수와 허용 목록과 `lookup` 대상 컬렉션을 보고 이 인자는 보지 않는다. `:76` 의 `operations.aggregate(aggregation, collection, outputType)` 이 그것을 드라이버에 그대로 넘긴다. 생성자 `:32`\~`:33` 이 받는 것도 `MongoOperations` 와 예산 레지스트리와 예산 강제기 셋이다. 레지스트리가 없다. ## 같은 모양이 리액티브 쪽에도 있다 `MongoReactiveCursorPublisher:32`\~`:39` 의 `stream` 도 `MongoOperationContext` 를 받으면서 `:37` 에서 `String collection` 을 따로 받는다. `:44` 가 널 검사를 하고, `:55`\~`:59` 가 질의를 복사해 배치 크기와 데드라인과 결과 상한을 걸고, `:64` 의 `operations.find(bounded, documentType, collection)` 이 그 문자열을 넘긴다. 그 파일에서 `context` 가 나오는 줄은 `:33` 의 인자와 `:40` 의 널 검사 둘뿐이다. 컬렉션 프로파일을 한 번도 읽지 않는다. 레지스트리를 언급하는 줄도 0 이다. 컨텍스트와 `String collection` 을 함께 받는 main 파일은 셋인데, `MongoAtomicOperationsTemplate` 은 공개 메서드가 컨텍스트만 받고 `String collection` 이 `:104`·`:123` 의 private 헬퍼 인자라 이 모양이 아니다. ## 두 파일에는 레지스트리 import 도 collectionProfile() 호출도 없다 집계 실행기에서 `MongoCollectionProfileRegistry` 를 언급하는 줄이 0 이고 `context.collectionProfile()` 을 부르는 줄도 0 이다. 같은 두 검색을 `DefaultMongoImperativeExecutor` 에 걸면 2 와 1 이 나오므로, 두 0 은 검색이 도는 상태에서 나온 값이다. `import` 열둘이 전부 실려 있다. `MongoOperationContext` 와 `MongoOperationRejectedException` 과 예산 관련 셋과 자바 표준 셋과 스프링 데이터 넷이다. 레지스트리가 없다. 컨텍스트를 아예 안 쓰는 것은 아니다. `:72` 가 `budgetFor(context, budget)` 을 부르고 그 private 메서드 `:89`\~`:98` 의 `:92` 가 `context.timeout()` 을 밀리초로 바꿔 등록된 예산과의 최솟값을 만든다. `:91` 주석이 컨텍스트의 타임아웃은 `maxTimeMS` 를 좁힐 수는 있어도 늘릴 수는 없다고 적는다. 같은 컨텍스트에서 타임아웃은 꺼내고 컬렉션 프로파일은 꺼내지 않는다. ## 지금은 조립되지 않는다 이 클래스 이름이 main 에 나오는 줄은 `:24` 의 클래스 선언과 `:32` 의 생성자 둘뿐이다. 나머지 하나는 `PolicyAwareMongoAggregationExecutorTest:14` 다. `new PolicyAwareMongoAggregationExecutor` 를 부르는 main 줄이 0 이고 `new MongoReactiveCursorPublisher` 도 0 이다. 같은 검색을 `new MongoBudgetEnforcer` 로 걸면 1 이 나오므로, 이 리프에도 자동 설정이 만드는 빈은 있다. 이름이 걸린 `PolicyAwareMongoAggregationExecutorTest` 조차 그 실행기를 만드는 줄이 0 이다. 그래서 레지스트리 자바독의 문장은 지금 성립한다. 포크가 둘 중 하나를 조립하고 요청 값으로 컬렉션 이름을 만들면 성립하지 않게 된다. ## 원문이 든 서명 말고 하나가 더 있다 원문은 집계 실행기의 서명을 그 경로로 든다. `MongoReactiveCursorPublisher:37` 도 같은 모양인데, 그쪽은 컨텍스트를 받아 널 검사만 하고 프로파일을 한 번도 읽지 않는다. 고치는 비용도 원문에 없다. 컨텍스트가 이미 `CollectionProfileName` 을 들고 있으므로, `String` 인자를 지우고 `context.collectionProfile()` 을 쓰는 것은 새 인자를 만드는 일이 아니다. ## 확인하지 못한 것 이 실행기를 조립한 예시가 저장소에 없어 실제 사용 모양을 보지 못했다. 그 인자를 프로파일 타입으로 바꿀 때 집계 계획 쪽이 무엇을 더 요구하게 되는지 따져 보지 않았다. 리액티브 쪽 집계 경로에 같은 서명이 있는지 따로 훑지 않았다.