--- kind: CASE slug: analysis-finding-a06-f015 title: MongoTtlPolicyValidator 를 부르는 프로덕션 코드가 없다 topic: multitenancy-isolation project: clean-architecture-backend-template status: 게시 전 sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 rootTreeNode: case:analysis-finding-a06-f015 evidenceCapturedOn: 2026-09-04 body: case-analysis-finding-a06-f015.body.md assets: - key: analysis-finding-a06-f015 file: ../../../final/evidence/rendered/analysis-finding-a06-f015.svg evidence: - ../../../final/evidence/raw/analysis-finding-a06-f015.txt source: - 원본 분석 절은 analysis/06-adapter-outbound-persistence-mongo.md §58 이다. --- # MongoTtlPolicyValidator 를 부르는 프로덕션 코드가 없다 `MongoIndexManifest:30` 의 `expireAfter` 에 걸리는 검사는 `:54` 의 음수 거절 하나다. `MongoTtlPolicyValidator` 는 오버로드 셋에 걸쳐 다섯 경우를 거절하는데, `schema/ttl` 밖 main 에서 `MongoTtlPolicy` 와 `MongoTtlIndexDescriptor` 와 `MongoExpirationAccessPolicy` 와 `MongoTtlPolicyValidator` 를 언급하는 줄이 0 이다. ## 관계 - **같은 개념의 두 어휘가 공존하면 하나를 죽은 것으로 표시한다** 이 사례가 그 규칙의 형태다. 두 표현 사이에 참조가 없어서 어느 쪽이 정본인지 코드가 말하지 않는다. - **diff 가 견주는 것은 열넷 중 keySignature 와 unique 둘이다** 그 기록의 감지 불가 목록에 `expireAfter` 가 들어 있다. 매니페스트로 선언한 TTL 은 검증도 받지 않고 드리프트로도 잡히지 않는다. - **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다** 그 규칙 2 대로 만들어지는 것과 불리는 것을 나눠 세면 둘 다 0 이다. `MongoIndexManifest` 를 밖에서 만드는 main 줄도, `MongoTtlPolicyValidator` 를 만드는 main 줄도 없다. ## 문제 매니페스트의 한 요소와 schema/ttl 패키지가 각각 TTL 을 표현한다. 둘이 서로를 아는지, 그리고 어느 쪽이 실제로 쓰이는지 확인했다. ## 결론 MongoIndexManifest:30 이 Duration expireAfter 를 선언한다. 그 필드에 걸리는 검사는 :54~:56 의 음수 거절 하나다. :72~:73 의 ttl() 이 Optional 을 돌려주고 :77~:78 의 isTtlIndex() 가 널 여부를 돌려준다. 값의 크기를 보는 코드는 없다. MongoTtlPolicyValidator 가 거절하는 경우는 다섯이고 세 오버로드에 나뉘어 있다. validate(MongoTtlPolicy) 가 :30·:39·:49 에서 정확한 만료 시각 주장과 질의 시점 검사 누락과 1 분 미만 보존 기간을, validate(MongoTtlIndexDescriptor):68 이 날짜가 아닌 만료 필드 타입을, validate(MongoExpirationAccessPolicy):86 이 읽기 필터 미적용을 거절한다. :10~:13 자바독에는 이 클래스가 막는 두 오용이 적혀 있다. 스윕 간격이 명세되지 않은 장치를 스케줄러로 쓰는 것과, 아직 지워지지 않은 문서가 읽히는데도 접근 제어로 기대는 것이다. 두 표현 사이에 참조가 없다. schema/ttl 밖 main 에서 이 네 타입을 언급하는 줄이 0 이고, 같은 검색이 schema/ttl 안에서는 22 줄을 찾는다. MongoIndexManifest 도 그 네 이름을 한 번도 쓰지 않는다. isTtlIndex() 와 ttl() 을 부르는 줄도 0 이다. 두 이름이 나오는 줄은 선언 둘뿐이다. 그래서 매니페스트 경로로 선언된 TTL 인덱스에는 그 다섯 중 어느 것도 걸리지 않는다. 안전 하한이 1 분으로 선언돼 있는데 매니페스트 쪽에는 그 값을 읽는 코드가 없어서 1 초짜리 보존 기간도 통과한다. 그리고 어느 쪽도 아직 배선되지 않았다. MongoIndexManifest.named(...) 나 new MongoIndexManifest 를 부르는 main 줄이 :226 의 자기 빌더뿐인데 시험에는 7 이 있고, 검증기를 만드는 main 줄은 0 이며 시험에 1 이 있다. ## 검증 환경 OpenJDK : 21.0.12 확인 방식 : 매니페스트의 TTL 요소와 그 검사와 접근자와 빌더 인용, 검증기 파일 전문 인용과 세 오버로드의 다섯 거절 확인, 정책 타입 인용, schema/ttl 밖 main 참조 계수와 안쪽 대조, 매니페스트가 그 네 타입을 언급하는 줄 계수, 두 접근자 호출 계수와 선언 포함 대조, 두 표현의 생성 계수와 시험 대조 소스 수정 : x ## 재현 조건 1. 매니페스트의 expireAfter 선언과 그 필드에 걸리는 검사와 접근자와 빌더 메서드를 인용한다. 2. MongoTtlPolicyValidator 를 파일 끝까지 싣고 세 오버로드가 각각 무엇을 거절하는지 본다. 3. MongoTtlPolicy 를 인용해 정책이 무엇을 요구하는지 보인다. 4. schema/ttl 밖 main 에서 네 타입을 언급하는 줄을 세고, 같은 검색을 schema/ttl 안에 걸어 대조한다. 5. 매니페스트가 그 네 타입을 언급하는 줄과 두 접근자를 부르는 줄을 세고, 선언까지 포함한 계수와 대조한다. 6. 두 표현을 만드는 main 줄을 각각 세고 시험 쪽 계수와 대조한다. ## 본문 이 범위 안에 TTL 을 표현하는 방법이 둘 있다. ## MongoIndexManifest 의 expireAfter :::evidence key="analysis-finding-a06-f015" alt="저장소 루트에서 돌린 정적 검색 출력 187줄. 먼저 MongoIndexManifest 28~32번이 실려 30번의 Duration expireAfter 선언과 그 앞뒤 필드가 보이고, 50~58번의 압축 생성자에서 54~56번이 expireAfter 가 널이 아니고 음수면 거절한다. 68~80번에는 72~73번의 ttl 접근자가 Optional.ofNullable 을 돌려주고 77~78번의 isTtlIndex 가 널이 아닌지를 돌려준다. 66~80번에 named 팩터리가 있고 186~194번의 빌더 메서드 expireAfter 는 널 검사만 한다. 그 아래 expireAfter 라는 이름이 그 파일에 나오는 줄이 8 개이고 그중 값을 거절하는 줄이 1 개라고 나온다. 이어서 MongoTtlPolicyValidator 7~94번 전문이 실린다. 7~14번 자바독은 TTL 인덱스가 오용되는 두 방식을 거절한다고 적는데, 첫째는 그것을 스케줄러로 다루는 것이며 TTL 모니터의 스윕 간격이 명세되지 않고 부하에 따라 달라져서 아홉 시에 지우라는 것은 약속하지 않는다는 것이고, 둘째는 접근 제어로 기대는 것이며 아직 존재하는 문서는 여전히 읽히므로 만료가 데이터를 가려야 한다면 읽기 쪽이 그것을 말해야 한다는 것이다. 17~21번이 MINIMUM_SAFE_RETENTION 을 1 분으로 두는데 그 아래로 줄이면 한 번의 스윕으로 큰 모집단이 만료돼 설정 변경이 계획되지 않은 대량 삭제가 된다고 적는다. 28~58번의 첫 validate 가 셋을 던지는데 30번이 정확한 업무 전이를 주장하는 정책을, 39번이 질의 시점 만료 검사를 선언하지 않은 정책을, 49번이 최소 보존 기간보다 짧은 것을 거절한다. 60~77번의 두 번째 오버로드는 TTL 인덱스 서술자를 받아 67번에서 정책 검증을 위임하고 68~76번에서 만료 필드가 BSON 날짜로 저장되지 않으면 MongoDB 가 날짜 필드만 만료시키고 나머지는 조용히 무시한다는 메시지로 거절하며, 79~93번의 세 번째 오버로드는 읽기 쪽 술어를 받아 86~92번에서 그것이 적용되지 않으면 만료된 문서가 TTL 모니터가 지울 때까지 계속 보인다는 메시지로 거절한다. 다음으로 MongoTtlPolicy 1~40번이 실린다. 9~12번 자바독은 TTL 모니터가 대략 1 분에 한 번 돌며 배치로 지우므로 만료된 문서가 명세되지 않은 간격 동안 계속 읽히고 부하가 높거나 바쁜 세컨더리에서는 더 길어지는데, 그것이 공간 회수에는 괜찮고 특정 시점에 보이지 않아야 하는 것에는 틀렸다고 적는다. 14~16번은 그래서 두 불린이 선언의 일부이며 physicalCleanupOnly 는 이 TTL 이 공간만 회수한다는 뜻이고 queryChecksLogicalExpiry 는 애플리케이션이 expiresAt 이 현재보다 큰지로 걸러 가시성이 모니터의 타이밍에 달리지 않게 한다는 뜻이라고 적는다. 18~22번이 field 와 retention 과 두 불린 넷을 선언한다. 이어서 schema/ttl 밖 main 에서 그 네 타입을 언급하는 줄이 0 개이고 schema/ttl 안에서는 22 개이며, MongoIndexManifest 가 그 네 타입을 언급하는 줄이 0 개이고, isTtlIndex 나 ttl 을 부르는 줄이 0 개인데 선언까지 포함하면 2 개다. 마지막으로 MongoIndexManifest.named 나 new MongoIndexManifest 를 부르는 main 줄이 1 개인데 그것이 226번의 자기 빌더이고 시험에서는 7 개이며, new MongoTtlPolicyValidator 를 부르는 main 줄이 0 개이고 시험에서는 1 개다." caption="매니페스트의 expireAfter 와 거기 걸리는 음수 검사와 두 접근자와 빌더 · 검증기 전문과 세 규칙과 최소 보존 기간 · 정책 타입이 두 불린으로 요구하는 것 · schema/ttl 밖 참조 0 과 안쪽 22 · 두 접근자 호출 0 과 선언 2 · 두 표현의 생성 계수 — 187줄 · exit 0" zoom="true" ::: `MongoIndexManifest` 는 TTL 을 요소 하나로 담는다. `:30` 의 `Duration expireAfter` 다. 그 필드에 걸리는 검사는 하나다. `:54`\~`:56` 이 널이 아니면서 음수인 경우를 `IllegalArgumentException` 으로 거절한다. `:72`\~`:73` 의 `ttl()` 이 `Optional.ofNullable(expireAfter)` 를 돌려주고, `:77`\~`:78` 의 `isTtlIndex()` 가 널이 아닌지를 돌려준다. `:189`\~`:191` 의 빌더 메서드는 널 검사만 한다. 값의 크기를 보지 않는다. ## MongoTtlPolicyValidator 가 거절하는 다섯 경우 `MongoTtlPolicyValidator` 는 `validate` 오버로드를 셋 갖고, 그 셋에 걸쳐 다섯 경우를 거절한다. `validate(MongoTtlPolicy):28`\~`:58` 이 그중 셋이다. `:30` 이 정확한 업무 전이를 주장하는 정책에 `MongoOperationRejectedException` 을 던진다. 메시지가 MongoDB 의 TTL 모니터는 명세되지 않은 간격으로 스윕하므로 정확한 전이에는 자기 스케줄러가 필요하고 TTL 인덱스는 물리적 정리로 남는다고 적는다. `:39` 가 질의 시점 만료 검사를 선언하지 않은 정책을 거절한다. 만료된 문서는 모니터가 지울 때까지 계속 읽히므로 읽기가 만료 필드를 현재와 견주어야 한다는 것이다. `:49` 가 `MINIMUM_SAFE_RETENTION` 보다 짧은 보존 기간을 거절한다. `:21` 이 그 값을 1 분으로 두고, `:18`\~`:19` 가 그 아래로 줄이면 한 번의 스윕으로 큰 모집단이 만료돼 설정 변경이 계획되지 않은 대량 삭제가 된다고 적는다. `:10`\~`:13` 자바독이 두 오용을 이름 짓는다. TTL 을 스케줄러로 다루는 것과 접근 제어로 기대는 것이다. 나머지 둘은 다른 오버로드에 있다. `validate(MongoTtlIndexDescriptor):65`\~`:77` 이 `:67` 에서 정책 검증을 위임한 뒤 `:68` 에서 만료 필드가 BSON 날짜가 아니면 거절하는데, MongoDB 가 날짜 필드만 만료시키고 나머지는 조용히 무시하기 때문이다. `validate(MongoExpirationAccessPolicy):84`\~`:93` 이 `:86` 에서 읽기가 논리적 만료를 걸지 않으면 거절한다. `MongoTtlPolicy:18`\~`:22` 가 그 검증을 받는 값이다. `field` 와 `retention` 말고 `physicalCleanupOnly` 와 `queryChecksLogicalExpiry` 두 불린이 선언의 일부다. `:9`\~`:12` 자바독에는 모니터가 대략 1 분에 한 번 배치로 지우므로 만료된 문서가 명세되지 않은 간격 동안 계속 읽히고 부하가 높거나 바쁜 세컨더리에서는 더 길어진다고 적혀 있다. ## 둘 사이에 참조가 없다 `schema/ttl` 밖의 main 에서 `MongoTtlPolicy` 나 `MongoTtlIndexDescriptor` 나 `MongoExpirationAccessPolicy` 나 `MongoTtlPolicyValidator` 를 언급하는 줄이 0 이다. 같은 검색을 `schema/ttl` 안에 걸면 22 줄이 나오므로, 이 0 은 검색이 대상을 못 찾아서 나온 값이 아니다. 매니페스트 파일 자체에도 그 네 이름이 한 번도 나오지 않는다. 반대 방향도 없다. `isTtlIndex()` 나 `ttl()` 을 부르는 줄이 0 이고, 두 이름이 나오는 줄은 `:77` 과 `:72` 의 선언뿐이다. ## 매니페스트 경로가 지나지 않는 다섯 검사 매니페스트로 선언한 TTL 인덱스는 그 다섯 중 어느 것도 지나지 않는다. `MINIMUM_SAFE_RETENTION` 이 1 분인데 `:189`\~`:191` 의 빌더는 널 검사만 하므로 1 초짜리 값도 그대로 들어간다. 실제로 넣어 보지는 않았다. `expireAfter` 는 앞의 기록에서 확인한 대로 인덱스 diff 가 비교하지 않는 아홉 요소 중 하나이므로, 그 값을 바꿔도 드리프트 보고서가 빈다. ## 둘 다 밖에서 만들어지지 않는다 `MongoIndexManifest.named(...)` 나 `new MongoIndexManifest` 를 부르는 main 줄은 1 인데 그것이 `:226` 의 자기 빌더다. 밖에서 만드는 코드는 없고, 같은 검색이 시험에서는 7 을 찾는다. `new MongoTtlPolicyValidator` 를 부르는 main 줄은 0 이고 시험에 1 이 있다. 지금은 두 표현 다 밖에서 만들어지지 않아 이 차이가 드러나지 않는다. 포크가 `MongoIndexManifest` 를 조립하면 `expireAfter` 는 `:54`\~`:56` 의 음수 거절만 지나고 검증기의 다섯 조건은 걸리지 않는다. ## 원문과 갈리는 자리 두 표현 사이에 참조가 없고 규칙을 가진 쪽이 쓰이지 않는다는 판정은 그대로다. 규칙의 목록이 갈린다. 원문이 든 셋은 최소 보존 기간과 만료 필드의 BSON 타입과 읽기의 만료 검사다. 실제 파일에는 그 셋 말고 `:30` 의 정확한 만료 시각 주장 거절이 더 있고, 원문이 든 셋 가운데 BSON 타입은 첫 오버로드가 아니라 `:68` 에 있다. 세 오버로드를 다 세면 다섯이다. 그리고 그 0 들이 검색 실패가 아니라는 대조를 더했다. 같은 검색이 `schema/ttl` 안에서 22 줄을 찾고, 두 접근자는 선언까지 포함하면 2 줄이 나오며, 매니페스트 생성 검색은 시험에서 7 을 찾는다. ## 확인하지 못한 것 빌더나 매니페스트로 1 초짜리 TTL 을 선언해 실제로 통과하고 만들어지는 것을 프로브로 재현하지 않았다. `MongoExpirationAccessPolicy` 자체는 열지 않았다. 확인한 것은 검증기가 그 타입에 대한 오버로드를 갖는다는 데까지다. 설계가 둘 중 어느 쪽을 정본으로 두었는지 설계 문서를 읽어 판단하지 않았다.