The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1773 lines
188 KiB
Markdown
1773 lines
188 KiB
Markdown
# adapter-outbound-persistence-mongo 상세 분석
|
||
|
||
|
||
## SSOT identity — 2026-08-31 재검증
|
||
|
||
- registered leaf id: `adapter-outbound-persistence-mongo`
|
||
- canonical state `analysisFile`: `analysis/06-adapter-outbound-persistence-mongo.md` (이 문서) — 이 leaf의 단일 SSOT
|
||
- source path: `src/adapter/outbound/persistence-mongo` · Gradle `:adapter:outbound:persistence-mongo`
|
||
- registry `allowed_dependencies`: **`[]`**
|
||
- registry `runtime_memberships`: `["app-bootstrap"]`
|
||
- coverage ledger: `FULL_READ` **497** / `STRUCTURAL_ONLY` **0** / `EXCLUDED` **0** / `UNCLASSIFIED` **0**
|
||
- 최초 분석 revision `a24ece9c` → 재검증 revision `21234e38` · 이 리프의 변경 파일 **0**
|
||
- 재검증 증거: `EVD-333`(소스 드리프트 0), `EVD-334`(lane 재실행)
|
||
|
||
> 재검증이 확인한 것은 대상이 움직이지 않았다는 사실이지, 아래 서술이 옳다는 보증이 아니다.
|
||
> 이번 사이클에서 코드에 대고 다시 확인한 항목은 이 문서의 검증 절과 위 증거가 가리키는 범위다.
|
||
|
||
---
|
||
> 상태: COMPLETE
|
||
> 기준 revision: `a24ece9cf797f7ea647e33bf846b115208ed1ba5`
|
||
> 분석 범위: `src/adapter/outbound/persistence-mongo`
|
||
> Gradle path: `:adapter:outbound:persistence-mongo`
|
||
|
||
## 0. 왜 내부 sub-scope로 나누는가
|
||
|
||
이 leaf도 persistence-jpa와 같은 이유로 한 번에 훑지 않는다. tracked file은 **497개**, production Java만 351개(약 22,927 LOC)이고, 설계 원본은 이것을 19개 Stable + 12개 Advanced Gradle module로 모델링한다. 이 저장소의 fail-closed registry가 그 배치를 대체하므로 module 경계는 `dev.caskeleton.adapter.outbound.mongo` 아래 package가 되고, package graph 자체가 내부 module graph 역할을 한다. 따라서 파일이 정확히 하나의 내부 bounded sub-scope에 귀속되도록 ledger를 먼저 고정한다.
|
||
|
||
### 전체 denominator
|
||
|
||
- tracked leaf files: **497**
|
||
- leaf top-level: `CLAUDE.md`, `README.md`, `build.gradle`, `gradle.lockfile` (4)
|
||
- `src/main`: 353 files / 351 Java / 2 resources / 약 22,927 LOC
|
||
- `src/test`: 104 files / 약 12,380 LOC
|
||
- `src/testkit`: 35 files / 약 3,036 LOC
|
||
- `src/mongoPerformanceTest`: 1 file / 194 LOC
|
||
- public top-level type: **346** (committed baseline `docs/architecture/mongo-api-surface.txt`가 스스로 `# types: 346`을 적고, 비주석 항목도 346개)
|
||
|
||
근거: `evidence/raw/121-persistence-mongo-module-inventory.txt`.
|
||
|
||
### 내부 bounded sub-scope ledger
|
||
|
||
| # | sub-scope | main | test | testkit | 기타 | denominator | status |
|
||
|---:|---|---:|---:|---:|---:|---:|---|
|
||
| 1 | governance / build / root boundary / autoconfigure | 15 | 12 | – | 4 | **31** | **COMPLETE** |
|
||
| 2 | `api/**` — framework-free core contract | 61 | 9 | – | – | 70 | **COMPLETE** |
|
||
| 3 | `mapping` + `nativecap` + `geo` | 23 | 4 | – | – | 27 | **COMPLETE** |
|
||
| 4 | `imperative` + `reactive` 실행 경로 | 47 | 14 | – | – | 61 | **COMPLETE** |
|
||
| 5 | `query` + `aggregation` | 22 | 7 | – | – | 29 | **COMPLETE** |
|
||
| 6 | `transaction` (+ `retry`, `session`) | 20 | 7 | – | – | 27 | **COMPLETE** |
|
||
| 7 | `schema` + `migration` | 49 | 9 | – | – | 58 | **COMPLETE** |
|
||
| 8 | `changestream` | 21 | 5 | – | – | 26 | **COMPLETE** |
|
||
| 9 | `security` + `failure` + `observation` + `client` | 30 | 14 | – | – | 44 | **COMPLETE** |
|
||
| 10 | `advanced/**` | 65 | 10 | – | – | 75 | **COMPLETE** |
|
||
| 11 | testkit + architecture/rs/release/compat test + performance lane | – | 13 | 35 | 1 | 49 | **COMPLETE** |
|
||
| | **TOTAL** | **353** | **104** | **35** | **5** | **497** | **11 / 11** |
|
||
|
||
sub-scope 1의 main 15는 root package Java 4 + `autoconfigure/**` 9 + resources 2다. 합계는 497로 leaf tracked file 전체와 일치하며, 모든 파일이 정확히 하나의 sub-scope에 귀속된다.
|
||
|
||
이 ledger는 module completion 전까지 모든 tracked file의 최종 disposition(`FULL_READ` / `STRUCTURAL_ONLY` / `EXCLUDED`)을 추적하기 위한 내부 작업 단위다. module-level `state.json`은 11개가 모두 닫힐 때만 COMPLETE로 전환한다.
|
||
|
||
## 1. 모듈 구조의 1차 관찰
|
||
|
||
이 leaf는 **opt-in**이라는 한 가지 성질을 축으로 설계돼 있고, 그 성질이 나머지 모든 구조를 결정한다.
|
||
|
||
- `allowed_dependencies`가 `[]`다. project dependency가 하나도 없고, 외부 의존은 Spring Boot의 Mongo starter(sync/reactive), autoconfigure, Micrometer, SLF4J뿐이다. `verifyCleanArchitectureDependencies`는 "실제 edge ⊆ 허용 edge"만 보므로 쓰이지 않는 허용은 영원히 통과한다 — 그래서 반대 방향을 보는 `MongoRegistryPermissionParityTest`가 따로 있다.
|
||
- `runtime_memberships`는 `["app-bootstrap"]`이고, composition root가 실제로 이 leaf를 `implementation`으로 싣는다(reactive starter와 reactivestreams driver는 exclude). 즉 이 module은 **jar에 들어 있고 property가 스위치**다. CLAUDE.md/README가 이 선택을 명시적으로 방어한다 — "빠져 있는 모듈은 꺼진 모듈과 같은 계약이 아니다. 부재는 배포 시점에 되돌릴 수 없고, gating 결함을 전부 가린다."
|
||
- JPA adapter와의 책임 분리가 선언돼 있다. idempotency / outbox / distributed lock은 Mongo에 재구현하지 않고 JPA에 남긴다.
|
||
- production에 가짜 도메인(`Example*`)을 두지 않는다. 이 leaf가 제공하는 것은 client·template·**정책 표면**이고, document/repository/mapper와 port 구현은 fork가 추가한다. 이 선택은 뒤에서 반복적으로 나타난다 — 여러 계약이 "정책과 value object는 있으나 실행체는 fork가 공급한다"는 형태다.
|
||
|
||
`docs/mongodb/repository-adaptation.md`가 설계의 module 배치를 이 leaf의 package로 매핑한 기록이고, package 간 방향은 `MongoModuleBoundaryTest`가 닫힌 edge matrix로 강제한다. 이 문서는 각 sub-scope를 닫아가며 그 주장들과 실제 source/build/test/runtime evidence를 대조한다.
|
||
|
||
---
|
||
|
||
## 2. Sub-scope 01 범위와 denominator
|
||
|
||
> 내부 상태: COMPLETE — **31 / 31 FULL_READ**
|
||
> 범위: leaf 최상위 4 + production root package 4 + `autoconfigure/**` 9 + auto-configuration 등록 resource 2 + 해당 test 12
|
||
> 역할: "이 애플리케이션이 MongoDB와 말하는가"를 결정하는 층 전체
|
||
|
||
| 구분 | 파일 | 라인 |
|
||
|---|---|---:|
|
||
| governance | `CLAUDE.md` | 167 |
|
||
| rationale | `README.md` | 147 |
|
||
| build | `build.gradle` | 283 |
|
||
| build | `gradle.lockfile` | 192 |
|
||
| production root | `MongoRootAutoConfiguration.java` | 37 |
|
||
| production root | `MongoPersistenceConfig.java` | 27 |
|
||
| production root | `MongoPersistenceSettings.java` | 38 |
|
||
| production root | `MongoOptInAutoConfigurationImportFilter.java` | 59 |
|
||
| production | `autoconfigure/**` 9개 | 1,382 |
|
||
| resource | `META-INF/spring.factories` | 2 |
|
||
| resource | `META-INF/spring/…AutoConfiguration.imports` | 1 |
|
||
| test | root package 2 (`MongoNamespaceContractTest`, `MongoPersistenceConfigTest`) | 202 |
|
||
| test | `autoconfigure/**` 10개 | 1,156 |
|
||
|
||
manifest: `evidence/raw/122-mongo-governance-optin-manifest.txt`.
|
||
|
||
## 3. opt-in은 네 겹이고, 각 겹이 서로 다른 실패를 막는다
|
||
|
||
| 겹 | 무엇 | 왜 그 층이어야 하는가 |
|
||
|---|---|---|
|
||
| Boot import filter | `MongoOptInAutoConfigurationImportFilter` (`spring.factories` 등록) | Mongo starter는 classpath만으로 auto-configuration 후보를 등록한다. project condition은 후보 선정 **뒤에** 평가되므로, 후보 단계에서 9개 Boot Mongo auto-configuration을 빼지 않으면 평범한 `@EnableAutoConfiguration` 앱이 client와 template을 만든다 |
|
||
| auto-configuration entry | `MongoRootAutoConfiguration` (`AutoConfiguration.imports` 등록) | 마스터 하나. 예전에는 filter·component-scan된 config·platform auto-config 셋이 각자 같은 property를 읽는 마스터였고, 서로가 꺼져 있다고 믿는 것을 조립할 수 있었다 |
|
||
| infrastructure | `MongoPersistenceConfig` | `@ImportAutoConfiguration`은 **명시적** import라 `spring.autoconfigure.exclude`의 영향을 받지 않는다. 켠 프로필에서만 Mongo client/template을 다시 들여온다 |
|
||
| platform | `MongoPlatformAutoConfiguration`, `MongoDriverObservabilityAutoConfiguration` | 정책 bean. 후자는 `MeterRegistry`가 있을 때만 driver listener를 붙인다 — publish할 곳 없는 listener는 모든 command에 비용만 얹는다 |
|
||
|
||
네 겹 모두 `ca-skeleton.persistence-mongo.enabled=true`라는 같은 조건을 읽는다(`evidence/raw/123-...` §8.2). 이것은 중복이 아니라 계층별 차단이다: filter는 Boot의 후보군, 나머지 셋은 자기 bean 그래프를 담당한다. `MongoPersistenceConfigTest`가 실제 `@EnableAutoConfiguration` context로 default/false에서 `MongoClient`·`MongoTemplate` 부재를, `enabled=true` + mock client에서 `MongoTemplate` 단일 bean을 확인한다.
|
||
|
||
`MongoPlatformAutoConfiguration`(443줄)은 이 leaf에서 가장 밀도가 높은 파일이고, 거의 모든 `@Bean`의 javadoc이 **과거에 "shipped했지만 아무 configuration도 만들지 않던" 경로**를 기록한다 — atomic/bulk template, reactive 실행 경로 일체, change-stream source와 consumer, startup validator, client generation registry, health indicator. 이 leaf는 그 미연결들을 한 번 훑어 고친 이력을 갖고 있고, 그 사실이 이 sub-scope의 판단 기준을 바꾼다: 남아 있는 미연결은 "아직 안 한 것"이 아니라 "훑고도 남은 것"이다.
|
||
|
||
startup 검증 쪽 설계도 눈여겨볼 만하다. `mongoPlatformStartupCheck`는 `MongoTopologyProbe` bean이 있을 때만 돌지만, 그 조건이 곧 탈출구가 되는 것을 막기 위해 `mongoTopologyProbeRequirement`가 **probe 조건 없이** 등록되어 "platform profile이 있는데 probe가 없으면" 실패시킨다. javadoc이 그 이유를 한 줄로 적는다 — "a requirement that only applies when the thing it requires is present is not a requirement".
|
||
|
||
## 4. Confirmed P2 — README가 제시하는 활성화 recipe를 그대로 따르면 애플리케이션이 시작되지 않는다
|
||
|
||
leaf README §활성화가 제시하는 전체 recipe는 두 줄이다.
|
||
|
||
```properties
|
||
ca-skeleton.persistence-mongo.enabled=true
|
||
spring.data.mongodb.uri=mongodb://localhost:27017/portfolio
|
||
```
|
||
|
||
이 두 줄에는 서로 독립적인 문제가 둘 있다.
|
||
|
||
**(1) 필수 property가 빠져 있다.** composition root의 `CapabilityDependencyValidator`는 Mongo가 켜져 있고 `ca-skeleton.persistence-mongo.active-profile`이 blank이면 violation을 만들고, `CapabilityDependencyStartupCheck`가 context refresh에서 그 violation으로 startup을 중단시킨다. 이 key는 `app-bootstrap/src/main/resources/application.yml:370`이 `${APP_PERSISTENCE_MONGO_ACTIVE_PROFILE:}`로 노출하고 `.env.local.example`과 `docs/registries/env-keys.yaml`도 required로 기록한다. 그런데 leaf에서 `active-profile`을 언급하는 파일은 **0개**다(`123-...` §8.3, exit=1). CLAUDE.md도 README도 이 key를 적지 않는다.
|
||
|
||
`MongoPersistenceSettings`가 이 key를 bind하지 않는 것 자체는 일관적이다 — 그 클래스는 "모듈의 opt-in 스위치만 소유한다". 문제는 key가 이 module의 property namespace(`ca-skeleton.persistence-mongo.*`) 안에 있으면서 소유·문서화가 전부 leaf 밖에 있고, leaf의 활성화 문서가 그것을 모른다는 점이다.
|
||
|
||
**(2) 폐기된 namespace를 지시한다.** §5에서 따로 다룬다.
|
||
|
||
**판정: P2 confirmed.** leaf의 활성화 문서를 그대로 따른 배포는 뜨지 않으며, 실패 메시지는 leaf 문서 어디에도 없는 property를 지목한다. 근거는 `evidence/raw/125-...` §D이고, 규칙이 실제로 강제된다는 사실은 `app-bootstrap`의 기존 `CapabilityDependencyValidatorTest`를 원본 상태로 재실행해 확인했다(`126-...`, BUILD SUCCESSFUL). 수정은 README/CLAUDE.md의 recipe에 `active-profile`을 추가하고 유효한 값의 출처(= `ca-skeleton.persistence-mongo.platform.profiles`의 key)를 함께 적는 것이다.
|
||
|
||
## 5. Confirmed P3 — 폐기된 namespace guard의 탐색 domain이 operator가 읽는 두 문서를 덮지 않는다
|
||
|
||
`MongoNamespaceContractTest`(MNG-INT-002)는 정확히 이 문제를 위해 존재하고, javadoc이 막으려는 defect를 이렇게 정의한다.
|
||
|
||
> A sentence recording that the old namespace is deprecated is the opposite of the defect — **the defect was a document telling an operator to use it.**
|
||
|
||
그 guard의 탐색 domain은 다음과 같다(`125-...` §C).
|
||
|
||
- `adapter/outbound/persistence-mongo`와 `app-bootstrap` 아래
|
||
- 경로에 `/src/main/`을 포함하는 파일만
|
||
- `.java`는 **주석을 제거한 뒤**, `.yml`/`.properties`는 통째로
|
||
|
||
따라서 다음 세 곳은 domain 밖이고, 셋 다 `spring.data.mongodb.`를 담고 있다.
|
||
|
||
| 위치 | 내용 |
|
||
|---|---|
|
||
| `README.md:37` | 붙여넣기용 예제 `spring.data.mongodb.uri=mongodb://localhost:27017/portfolio` |
|
||
| `README.md:53`, `CLAUDE.md:25` | "URI/database/credential은 표준 `spring.data.mongodb.*` 설정을 사용한다" |
|
||
| `src/test/.../MongoPersistenceConfigTest.java:20`, `:64` | 이 leaf 자신의 opt-in 대표 test가 `spring.data.mongodb.database=portfolio`를 사용 |
|
||
|
||
`src/main` 쪽은 깨끗하다 — 유일한 매치는 `MongoPersistenceSettings`의 javadoc이고, 그것은 "예전에 이 javadoc이 폐기 키를 가리켰다"는 기록이라 guard가 주석을 제거하는 이유 그대로다.
|
||
|
||
**판정: P3 confirmed.** guard가 막겠다고 명시한 형태(문서가 operator에게 폐기 키를 쓰라고 말하는 것)가 guard의 사각지대에서 그대로 살아 있고, 그중 하나는 복사해 쓰라고 제시된 예제다. 런타임은 영향받지 않는다 — Compose lane은 `SPRING_MONGODB_URI`를 공급하고, 폐기는 제거가 아니다. 수정은 두 문서의 키를 `spring.mongodb.*`로 바꾸고, guard의 domain에 leaf의 `*.md`를 추가하는 것이다(추가하면 위 세 곳이 즉시 red가 되므로 함께 고쳐야 한다).
|
||
|
||
## 6. Confirmed P3 — `change-streams=true`는 거부되지 않고 조용히 버려지며, 그 결과 startup validator의 한 분기가 production에서 도달 불가다
|
||
|
||
`MongoPlatformSettings`의 compact constructor는 세 입력을 서로 다르게 처리한다.
|
||
|
||
```java
|
||
profiles = profiles == null ? Map.of() : Map.copyOf(profiles); // 흡수
|
||
changeStreams = false; // 무조건 덮어씀
|
||
if (requiredSecondaries < 0) { throw MongoOperationRejectedException.of(...); } // 거부
|
||
```
|
||
|
||
`changeStreams` 자리의 주석은 이렇게 말한다 — "Accepting the flag and ignoring it would leave an operator believing it took effect, so **the value is refused rather than stored**: zero beans, zero threads, and a `true` that cannot be honoured never becomes one that looks honoured."
|
||
|
||
실제 동작은 refuse가 아니라 silent discard다. 임시 probe(`evidence/raw/124-...`, `124a-...`)로 세 입력을 실제 binding에 통과시켰다.
|
||
|
||
```text
|
||
changeStreams.contextFailed=false
|
||
changeStreams.boundValue=false
|
||
transactions.contextFailed=false
|
||
transactions.boundValue=true
|
||
negativeSecondaries.contextFailed=true
|
||
negativeSecondaries.failureType=dev.caskeleton.adapter.outbound.mongo.api.error.MongoOperationRejectedException
|
||
```
|
||
|
||
즉 같은 생성자 안에서 `required-secondaries=-1`은 예외로 거부되고, 형제 flag `transactions=true`는 그대로 보존되며, `change-streams=true`만 예외 없이 `false`가 된다. operator는 자기가 켠 것이 꺼졌다는 신호를 받지 못한다 — 주석이 막겠다고 한 바로 그 상태다.
|
||
|
||
파생 결과가 하나 더 있다. `MongoStartupValidator`는 `changeStreamsEnabled`가 참일 때 topology capability를 검사하는 분기를 갖는데(`MongoStartupValidator.java:104`), production 생성 지점은 `MongoPlatformAutoConfiguration.java:354` 하나뿐이고 거기서 넘기는 값은 `properties.changeStreams()`다. 그 값은 위에서 항상 `false`이므로 **이 분기는 shipped composition에서 도달할 수 없다**. 도달하는 유일한 경로는 validator를 직접 생성하는 `MongoStartupValidatorTest.java:143`이다. 근거: `123-...` §8.2b, §8.2c.
|
||
|
||
**판정: P3 confirmed.** 현재 잘못된 동작을 만들지는 않는다 — change stream 실행체는 애초에 shipped되지 않는다고 CLAUDE.md가 명시한다. 문제는 (a) 문서가 refuse라고 말하는 것이 discard이고, (b) 그 결과 capability 검사 한 갈래가 test에서만 살아 있다는 점이다. 수정은 두 방향 중 하나다 — 값을 정말로 거부하거나(`requiredSecondaries`와 같은 형태), 아니면 flag를 record component에서 제거해 존재하지 않는 스위치로 만드는 것.
|
||
|
||
## 7. Negative-space probes — governance / opt-in scope
|
||
|
||
근거: `evidence/raw/123-mongo-optin-reachability-and-siblings.txt`.
|
||
|
||
### 7.1 Public surface reachability
|
||
|
||
이 sub-scope의 production public type 13개 중 leaf 밖에서 참조되는 것은 둘뿐이다.
|
||
|
||
| type | leaf 밖 참조 |
|
||
|---|---|
|
||
| `MongoPlatformHealthIndicator` | `app-bootstrap`의 `MongoPlatformHealthConfig`, `MongoPlatformHealthContributor` (+ 해당 test) |
|
||
| `MongoRootAutoConfiguration` | `app-bootstrap`의 `ShippedRuntimeFacadePresenceTest` |
|
||
| 나머지 11개 | 0 |
|
||
|
||
zero-reference를 dead로 읽어서는 안 되는 경우가 여기 있다. `MongoRootAutoConfiguration`은 `META-INF/spring/…AutoConfiguration.imports`가, `MongoOptInAutoConfigurationImportFilter`는 `META-INF/spring.factories`가 이름으로 등록한다 — 두 resource 모두 이 sub-scope가 소유하며 manifest에 포함돼 있다. `MongoPersistenceConfig`/`MongoPlatformAutoConfiguration`/`MongoDriverObservabilityAutoConfiguration`은 root의 `@Import`로 도달하고, settings 세 종류는 `@EnableConfigurationProperties` 인자로 도달한다. 즉 이 sub-scope의 도달성은 Java import graph가 아니라 등록 metadata와 annotation 인자에 있으며, 정적 참조 검색만으로는 판단할 수 없다.
|
||
|
||
### 7.2 Conditional sibling comparison
|
||
|
||
같은 master switch를 읽는 production 지점은 6곳이다 — root, persistence config, platform auto-config, driver observability auto-config, mapping configuration, advanced configuration. 앞의 넷은 §3의 계층별 차단이고, `MongoMappingConfiguration`과 `MongoAdvancedConfiguration`은 각각 sub-scope 3·10 소유이므로 그쪽에서 다시 본다. 이 sub-scope 범위에서는 조건 비대칭이 발견되지 않았다: 네 configuration이 모두 같은 prefix/name/havingValue를 쓴다.
|
||
|
||
property record 쪽에서는 비대칭이 하나 있고 §6에서 다뤘다.
|
||
|
||
### 7.3 Duplicate-mechanism sweep
|
||
|
||
`ca-skeleton.persistence-mongo.*` namespace를 소유하는 주체가 셋이다.
|
||
|
||
| key | 소유자 | 위치 |
|
||
|---|---|---|
|
||
| `.enabled` | `MongoPersistenceSettings` | leaf root |
|
||
| `.platform.*` | `MongoPlatformSettings` | leaf `autoconfigure` |
|
||
| `.advanced.*` | `MongoAdvancedSettings` / `MongoAdvancedCapabilityFlags` | leaf `advanced` (sub-scope 10) |
|
||
| `.active-profile` | **없음** — `application.yml`이 노출하고 `CapabilityDependencyValidator`가 요구 | `app-bootstrap` |
|
||
|
||
경쟁 구현은 없다. 다만 마지막 행이 §4의 결함이다 — 한 namespace의 네 번째 key만 소유자가 leaf 밖에 있고 leaf 문서가 그것을 모른다.
|
||
|
||
### 7.4 Documentation / measured-count drift
|
||
|
||
§8에서 따로 다룬다.
|
||
|
||
## 8. Confirmed documentation / measured-count drift
|
||
|
||
근거: `evidence/raw/125-mongo-governance-doc-count-drift.txt`, `126-mongo-hermetic-lane-original-verification.txt`.
|
||
|
||
| 항목 | 문서가 말하는 값 | 측정값 | 위치 |
|
||
|---|---|---|---|
|
||
| public top-level type / production 파일 | "311 of this leaf's 313 production files" | **346 / 351** | `build.gradle:260` |
|
||
| hermetic contract test | "382 hermetic contract tests" | **526** (83 classes) | `build.gradle:87` |
|
||
| registered leaf | 19 | **44** | `MongoModuleBoundaryTest.java:16`, `docs/mongodb/repository-adaptation.md:18`, `docs/adr/ADR-MONGO-001:61` |
|
||
|
||
앞의 두 건은 같은 파일 안에서 서로를 반박한다 — `build.gradle`은 311/313을 적으면서 그 아래 `apiSurface` 블록으로 `docs/architecture/mongo-api-surface.txt`를 baseline으로 지정하고, 그 baseline은 스스로 `# types: 346`을 적는다. `verifyMongoApiSurface`는 baseline과 실제 surface를 비교하므로 **green이면서 동시에** 주석의 숫자가 틀릴 수 있고, 실제로 그렇다(`126-...`: `verifyMongoApiSurface: OK — the committed public API surface is unchanged.`).
|
||
|
||
contract test 수도 마찬가지다. 주석의 382는 두 lane이 겹쳐 돌던 시점의 값이고, 원본 상태에서 lane을 재실행한 측정값은 526이다. lane 분리 자체는 유효하다 — `verifyMongoTestLaneDisjointness`가 두 lane의 JUnit XML을 비교해 overlap 0을 확인하고 통과한다.
|
||
|
||
19-leaf claim은 persistence-jpa scope에서 확인한 것과 같은 사각지대다. `verifyDocumentedLeafCount`의 탐색 domain은 `CLAUDE.md`와 (root를 뺀) `build.gradle` 두 파일명뿐이라 `*.java`와 `docs/**`를 보지 않는다. 이 leaf 쪽 생존 지점 3곳이 그 domain 밖이다.
|
||
|
||
**drift가 아닌 것도 기록한다.** README §의존성 경계는 "`MongoModuleBoundaryTest`(ArchUnit) 10개 규칙"이라고 쓰고 8개를 열거한다. 실제 파일의 `@Test`는 13개이며, 그중 10개가 방향 규칙(core-api framework 무의존, core-api ↛ 다른 platform package, Stable starter ↛ Advanced, Stable ↛ Advanced, imperative ↛ reactive, aggregation→query, production ↛ testkit, schema ↛ 실행 경로, observability→core-api only, migration ↛ engine adapter)이고 나머지 3개는 구조 검사(edge matrix가 디스크의 package 집합과 정확히 일치, 관측된 모든 edge가 선언된 것, 선언된 edge가 DAG)다. README의 "10개 규칙"은 방향 규칙 개수로 정확하다.
|
||
|
||
## 9. Sub-scope 01 findings backlog
|
||
|
||
| 우선순위 | finding | reachability |
|
||
|---|---|---|
|
||
| **P2** | leaf README의 활성화 recipe에 필수 `ca-skeleton.persistence-mongo.active-profile`이 빠져 있어, 그대로 따르면 `CapabilityDependencyStartupCheck`가 startup을 거부한다. 이 key를 언급하는 leaf 파일은 0개 | **문서를 따른 모든 신규 활성화** |
|
||
| **P3** | `MongoNamespaceContractTest`의 domain(`src/main/**`의 java/yml/properties)이 leaf `CLAUDE.md`·`README.md`와 `src/test`를 덮지 않아, guard가 정의한 defect(문서가 operator에게 폐기 키를 지시)가 붙여넣기용 예제로 생존 | 문서 3곳 + 자기 leaf test 2곳; 런타임 영향 없음 |
|
||
| **P3** | `MongoPlatformSettings`가 `change-streams=true`를 예외 없이 `false`로 덮어쓰면서 주석은 "refused"라고 서술. 형제 입력 `required-secondaries=-1`은 예외로 거부되고 `transactions=true`는 보존됨 | 모든 platform 설정 binding |
|
||
| **P3** | 위의 결과로 `MongoStartupValidator`의 change-stream capability 분기가 production 생성 경로에서 도달 불가(production 생성 지점 1곳이 항상 `false`를 넘김) | test에서만 도달 |
|
||
| **P3** | `build.gradle` 주석의 측정치 2건 drift — "311 of 313 production files"(실측 346/351), "382 hermetic contract tests"(실측 526) | 주석; gate는 green |
|
||
| **P3** | 19-leaf claim 3곳(`MongoModuleBoundaryTest`, `docs/mongodb/repository-adaptation.md`, `ADR-MONGO-001`)이 registry 44와 불일치하며 `verifyDocumentedLeafCount`의 domain 밖 | 문서/주석 |
|
||
|
||
## 10. Fresh verification evidence — sub-scope 01
|
||
|
||
- `evidence/raw/126-mongo-hermetic-lane-original-verification.txt` — 원본 소스, `--rerun-tasks`, git clean before/after
|
||
- `:adapter:outbound:persistence-mongo:test` — 14 classes / **72 tests** / 0 skipped / 0 failures
|
||
- `:adapter:outbound:persistence-mongo:mongoStableContractTest` — 83 classes / **526 tests** / 0 skipped / 0 failures
|
||
- `verifyMongoTestLaneDisjointness`, `verifyMongoReleaseContractLanes`, `verifyMongoApiSurface` 모두 통과(`verifyMongoApiSurface: OK — the committed public API surface is unchanged.`), 17 actionable tasks executed
|
||
- `:app-bootstrap:test --tests '*CapabilityDependencyValidatorTest*'` — BUILD SUCCESSFUL (§4의 활성화 규칙이 실제로 강제됨을 확인)
|
||
- `evidence/raw/124-...` / `124a-...` — platform settings binding probe 3 case, 임시 test는 실행 후 삭제하고 `git status --short` clean 확인
|
||
|
||
## 11. Sub-scope 01 완료 조건
|
||
|
||
- denominator 31 / 31 FULL_READ (`122-...`)
|
||
- opt-in 네 겹의 계층별 역할과 등록 metadata 도달성 확인(`123-...` §8.1)
|
||
- conditional sibling(같은 master switch를 읽는 6개 production 지점, property record 3종)과 duplicate mechanism(`ca-skeleton.persistence-mongo.*` namespace 소유자 4주체) 비교 수행
|
||
- documentation/count drift 재측정(`125-...`)과 gate 실행 결과 대조(`126-...`)
|
||
- 실행 probe 1건(`124-...`)으로 P3 확정, 원본 복구 후 git clean
|
||
- original source hermetic lane 2종 + governance gate 3종 + 활성화 규칙 test 재실행 green
|
||
|
||
## 12. 다음 sub-scope로 넘긴 것
|
||
|
||
- `api/**` 61개 production type의 framework-free 계약과 `MongoModuleBoundaryTest`의 edge matrix 전수 대조 → sub-scope 2
|
||
- `MongoPlatformAutoConfiguration`이 등록하는 각 bean의 **구현** 정확성(consistency binder, imperative/reactive executor, atomic/bulk policy, budget enforcer, failure translator) → sub-scope 4·5·9
|
||
- change stream source/consumer 배선과 `changeStreams` flag의 관계 → sub-scope 8
|
||
- `MongoProfileProperties.validate()`가 강제하는 production 계약(TLS·인증·Stable API·topology·타임아웃)의 실제 검증 범위와 `security` package의 credential resolver → sub-scope 9
|
||
- Advanced capability gate(`@MongoAdvancedEntryPoint`, `MongoAdvancedRules`)와 flag binding → sub-scope 10
|
||
- testkit 35개와 6개 Docker lane, release contract manifest → sub-scope 11
|
||
|
||
---
|
||
|
||
## 13. Sub-scope 02 범위와 denominator
|
||
|
||
> 내부 상태: COMPLETE — **70 / 70 FULL_READ**
|
||
> 범위: `src/main/java/**/api/**` 61개(2,687 LOC) + 전용 test 9개
|
||
> 역할: Spring·driver·BSON·Reactor 없이 platform의 의미론을 고정하는 core contract
|
||
|
||
| sub-package | production | dedicated test | 역할 |
|
||
|---|---:|---:|---|
|
||
| `api` root | 7 | 2 | operation identity, 실행 context, profile 이름 |
|
||
| `api.error` | 25 | 1 | 실행 결과·실패 분류·retry scope·예외 계층 |
|
||
| `api.mapping` | 9 | 1 | BSON 표현 manifest |
|
||
| `api.profile` | 5 | 1 | client plane, topology, Stable API 선언 |
|
||
| `api.capability` | 5 | 2 | capability 보고 vocabulary |
|
||
| `api.consistency` | 4 | 1 | consistency profile registry |
|
||
| `api.schema` | 3 | 1 | document schema version 정책 |
|
||
| `api.observation` | 3 | 0 | 관측 seam(no-op 포함) |
|
||
| **합계** | **61** | **9** | **70** |
|
||
|
||
manifest: `evidence/raw/127-mongo-api-scope-manifest.txt`.
|
||
|
||
committed public API surface 346개 중 `...mongo.api.`로 시작하는 것은 **59개**다(61에서 `package-info.java`와 package-private `NoOpMongoOperationObserver`를 뺀 수). 즉 이 leaf가 공개하는 타입의 **17%만이 의도된 외부 계약**이고 나머지 287개는 build.gradle과 CLAUDE.md가 스스로 "implementation that has not been moved under an internal root yet"라고 부르는 것들이다. 이 숫자는 두 문서의 서술과 일치하며, `internal` root 이전이 끝났을 때 표면이 실제로 줄었는지 판정할 기준점이 된다.
|
||
|
||
## 14. framework-free 규칙은 ArchUnit과 별개로도 성립한다
|
||
|
||
`MongoModuleBoundaryTest.coreApiIsFreeOfSpringDriverBsonAndReactor()`가 이 규칙을 강제하지만, rule이 vacuous하게 통과하는 경우를 배제하기 위해 소스 자체를 직접 훑었다.
|
||
|
||
```text
|
||
$ git grep -n 'import org\.springframework\|import com\.mongodb\|import org\.bson\|import reactor\.' -- '…/mongo/api'
|
||
exit=1
|
||
```
|
||
|
||
61개 파일 전체에서 매치 0이다(`128-...` §8.1b). `api.observation`이 이 규칙의 비용을 가장 잘 보여 준다 — `MongoOperationObserver`는 core에 선언되고 Micrometer 구현은 경계 밖 `observation` package에 있으며, 그래서 실행 경로가 관측성 module에 의존하지 않고도 관측할 수 있다. `NoOpMongoOperationObserver`는 nullable 필드 대신 null object여서 "관측성 꺼짐" 경로가 켜짐 경로와 다른 코드로 갈라지지 않는다.
|
||
|
||
`api/**`를 leaf 밖에서 참조하는 파일은 **0개**다(§8.1). 이것을 dead로 읽어서는 안 된다 — 이 leaf는 의도적으로 가짜 도메인을 두지 않고, README가 "실제 프로젝트가 자신의 document/repository/mapper와 port 구현을 추가한다"고 선언한다. 즉 `api`는 저장소 안에 소비자가 없는 것이 **설계된 상태**다. 한계는 그대로 남는다: 정적 검색은 이 저장소 밖 adopter를 증명하지도 반증하지도 않는다.
|
||
|
||
## 15. 이 sub-scope의 중심 설계 — 두 개의 모호한 결과를 무너뜨리지 않는 것
|
||
|
||
CLAUDE.md가 platform invariant로 못박은 문장이 여기 구현돼 있다 — "`MongoExecutionOutcome`'s two ambiguous values must not be collapsed into success or failure."
|
||
|
||
`MongoExecutionOutcome`은 boolean이 아니라 7값 enum이고, `isAmbiguous()`(`WRITE_RESULT_UNKNOWN`, `TRANSACTION_COMMIT_UNKNOWN`)와 `forbidsBlindReplay()`(여기에 `PARTIAL_BULK_WRITE` 추가)를 구분한다. `READ_CONFIRMED`가 별도 값으로 존재하는 이유도 주석에 있다 — 두 executor가 성공한 `FIND`를 `WRITE_CONFIRMED`로 기록해 모든 read가 확인된 write처럼 보였던 과거 결함이다.
|
||
|
||
그리고 이 의미론이 무너지지 않게 하는 방어가 **예외 타입 두 개의 생성자**에 있다.
|
||
|
||
- `MongoTransactionCommitUnknownException`은 context가 commit-unknown·ambiguous·non-retryable이 아니면 `IllegalArgumentException`으로 거부한다.
|
||
- `MongoTransactionTransientException`은 반대로 context가 commit-unknown이거나 ambiguous이면 거부한다.
|
||
|
||
두 javadoc이 막으려는 과거 상태를 그대로 기록한다 — session factory가 `commitUnknown` context를 먼저 만든 뒤 classifier가 고른 예외로 감싸는 바람에 "body를 재실행하라"는 예외가 "unknown commit, not retryable, ambiguous"라는 context를 들고 다녔다. 지금은 factory와 생성자 검사가 그 조합을 불가능하게 만든다.
|
||
|
||
production 경로도 일관적이다. `DefaultMongoFailureTranslator`는 `MongoFailureClassification`(category+outcome+retryScope 삼중항)을 먼저 만들고 `retryable`은 `classification.bodyReplayAllowed()`, `ambiguous`는 `classification.ambiguous()`에서 **파생**한다. 즉 두 boolean이 scope와 어긋날 여지가 production 경로에는 없다.
|
||
|
||
## 16. Confirmed P2 — schema version 실패는 두 경로 중 어느 쪽도 온전하지 않다
|
||
|
||
`MongoFailureCategory`에는 이 실패를 위한 전용 값 `SCHEMA_VERSION_UNSUPPORTED`("The stored document's schema version is outside the supported range")가 있고, 전용 예외 `MongoDataSchemaUnsupportedException`이 `documentVersion` / `minimumSupported` / `currentVersion` 세 정수를 공개 accessor로 노출한다. production 생성 지점은 정확히 둘이고, 각각 반쪽만 맞다.
|
||
|
||
| 생성 지점 | category | 세 버전 값 |
|
||
|---|---|---|
|
||
| `MongoSchemaVersionPolicy:85` (버전을 실제로 아는 유일한 곳) | `MongoFailureContext.rejected(...)` → **`OPERATION_REJECTED`** / outcome `NOT_SENT` | 실제 값 |
|
||
| `DefaultMongoFailureTranslator:111` (전용 category를 붙이는 유일한 곳) | **`SCHEMA_VERSION_UNSUPPORTED`** | **`-1, -1, -1`** |
|
||
|
||
`MongoFailureCategory`의 클래스 javadoc은 category가 "the value that appears in metrics and dashboards"라고 명시한다. 따라서 실제로 발생하는 schema-version 실패는 대시보드에서 `OPERATION_REJECTED`(= 로컬 guardrail 거절) bin에 들어가고, `SCHEMA_VERSION_UNSUPPORTED` bin은 세 버전이 `-1`인 실패만 받는다. 두 신호 모두 운영자가 필요로 하는 답을 주지 못한다 — 앞은 "어떤 종류의 실패인가"를, 뒤는 "어떤 버전이 문제인가"를 잃는다.
|
||
|
||
근거: `evidence/raw/128-...` §8.2c. 수정은 작다 — `MongoSchemaVersionPolicy.unsupported(...)`가 `rejected(...)` 대신 category `SCHEMA_VERSION_UNSUPPORTED`를 가진 context를 만들면 되고, 그러면 translator 쪽 `-1` 경로는 도달 불가 분기로 정리할 수 있다. regression은 정책이 던진 예외의 `category()`가 `SCHEMA_VERSION_UNSUPPORTED`인지 보는 한 줄이다.
|
||
|
||
같은 형태가 하나 더 있다. `DefaultMongoFailureTranslator:106`은 `MongoDocumentTooLargeException`을 `-1L, -1L`로 만든다. `estimatedBytes()`/`budgetBytes()`의 javadoc은 "Estimated serialized size. A size, not content: safe to log."라고만 적고 값이 없을 수 있다는 말을 하지 않는다. driver가 보고한 실패에서는 그 두 수를 알 수 없으므로 sentinel 자체는 불가피하지만, 계약에 그 사실이 없다. P3.
|
||
|
||
## 17. Confirmed P3 — 예외 계층의 "cause를 붙이지 않는다" 규칙에 문서화되지 않은 예외가 하나 있다
|
||
|
||
`MongoPersistenceException`의 javadoc은 두 번째 규칙을 절대적으로 서술한다.
|
||
|
||
> Second, **no constructor accepts a {@link Throwable} cause**: attaching the driver exception would re-expose everything the failure context deliberately dropped, through `getCause()` and through every stack trace printer.
|
||
|
||
하위 타입 20개 중 하나가 이 규칙을 벗어난다. `MongoTimeoutException`은 2-arg 생성자에서 `initCause(cause)`를 호출한다(`MongoTimeoutException.java:27`).
|
||
|
||
실제 유출 표면은 좁다. 그 생성자의 유일한 호출처는 `DefaultReactiveMongoExecutor:152`이고, 넘기는 값은 **Reactor 자신의** `java.util.concurrent.TimeoutException`이다 — driver 예외가 아니며 document·query·credential을 담지 않는다. 그리고 그렇게 감싸는 이유가 주석에 있다: 이전에는 raw `TimeoutException`이 그대로 새어 나가 operation도 outcome도 관측도 없이 호출자에게 도달했다.
|
||
|
||
문제는 계약 쪽이다. 규칙이 "어떤 생성자도 cause를 받지 않는다"로 쓰여 있으면 adopter는 `MongoPersistenceException`을 cause chain까지 통째로 로깅해도 안전하다고 읽는다. 그 판단의 근거가 되는 문장이 한 타입에 대해 거짓이고, 그 사실은 어디에도 적혀 있지 않다.
|
||
|
||
이 규칙을 검사하는 유일한 test는 `MongoFailureContextTest.exceptionsDoNotExposeADriverCause()`인데, 대상이 `MongoTransactionCommitUnknownException` — cause를 받는 생성자가 **없는** 타입이다. 즉 규칙은 그것을 깨지 않는 타입에 대해서만 단언되고, 유일하게 깨는 타입은 검사 밖이다. 근거: `128-...` §8.2b.
|
||
|
||
수정은 둘 중 하나다 — root javadoc을 "driver 예외를 cause로 붙이지 않는다"로 좁히고 `MongoTimeoutException`의 예외를 명시하거나, cause를 붙이지 않고 Reactor timeout의 정보를 failure context에 흡수시키는 것. 어느 쪽이든 test는 "모든 `MongoPersistenceException` 하위 타입에 대해 cause가 driver/BSON 타입이 아니다"로 넓혀야 규칙과 검사가 같은 것을 말한다.
|
||
|
||
## 18. Negative-space probes — api scope
|
||
|
||
근거: `evidence/raw/128-mongo-api-negative-space-probes.txt`.
|
||
|
||
### 18.1 Public surface reachability
|
||
|
||
`api/**` 참조는 leaf 밖에서 0이고(§14), 그것이 설계된 상태다. 대신 이 sub-scope에서 실제로 의미 있는 도달성 질문은 **api 타입을 소비하는 leaf 내부 경로가 존재하는가**였고, 확인한 것들은 다음과 같다: `MongoServerVersion` → `schema/validation/MongoValidatorApplyPolicy:54`(유일한 production 소비자), `MongoRetryScope` → `failure/MongoFailureClassification` + 두 transaction session factory + `transaction/retry/MongoRetryDecision`, `MongoFailureContext` factory 5종 → schema policy / type mapper / reactive executor / 두 session factory / retry coordinator. zero-consumer인 api 타입은 발견되지 않았다.
|
||
|
||
### 18.2 Invariant sibling comparison
|
||
|
||
같은 성격의 타입들이 불변식을 얼마나 강제하는지 비교했다.
|
||
|
||
| 타입 | 거부하는 것 | 거부하지 않는 것 |
|
||
|---|---|---|
|
||
| `MongoTransactionCommitUnknownException` | commit-unknown이 아닌 context | — |
|
||
| `MongoTransactionTransientException` | ambiguous하거나 commit-unknown인 context | — |
|
||
| `MongoFailureClassification` | `COMMIT_ONLY` + non-commit-unknown outcome | 그 외 조합 |
|
||
| `MongoFailureContext` | null, attempt<1, 음수 elapsed | **outcome ↔ ambiguous 정합** |
|
||
| `MongoConsistencyDescriptor` | causal session + non-majority concern | **secondaryPreferred + majority write** |
|
||
| `MongoProfileProperties`(sub-scope 1) | production TLS/인증/topology/타임아웃 | — |
|
||
|
||
두 개의 빈칸이 이 sub-scope의 P3다.
|
||
|
||
**(a) `MongoFailureContext`** — `outcome=WRITE_RESULT_UNKNOWN, ambiguous=false` 같은 조합을 canonical constructor가 막지 않는다. `MongoExecutionOutcome.isAmbiguous()`가 이미 있으므로 한 줄이면 강제된다. 다만 실제 위험은 제한적이다: production 경로는 classification에서 파생하고(§15), 가장 위험한 두 쌍은 예외 타입이 생성 시점에 거부한다. 남는 노출은 `api`가 외부 표면이라 adopter가 record를 직접 만들 수 있다는 점이다.
|
||
|
||
**(b) `MongoConsistencyDescriptor`** — `MongoConsistencyProfile`의 javadoc은 "A caller that picks `majority` write concern and `secondaryPreferred` reads **has not chosen durability, it has chosen a bug**"라고 그 조합을 명시적으로 bug라 부른다. 그런데 record의 compact constructor는 causal-session 규칙 두 개만 검사한다. `MongoConsistencyRegistry.of(...)`는 public이고 javadoc이 "used by tests and by profile overrides"라고 적으므로, 그 조합을 담은 descriptor를 등록하는 경로가 타입 수준에서 열려 있다. `standard()`가 만드는 6개 profile은 모두 정합적이므로 현재 결함은 아니다.
|
||
|
||
### 18.3 Duplicate-mechanism sweep
|
||
|
||
**(a) 두 profile-name record가 검증 코드까지 동일하다.** `DatabaseProfileName`과 `CollectionProfileName`을 이름만 치환해 diff하면 남는 차이는 javadoc 문장뿐이고, `FORMAT`(`[a-z][a-z0-9-]{2,63}`)·`UUID_LIKE`·생성자 검사·`toString`이 모두 같다. 같은 규칙이 두 벌 유지되므로 한쪽만 강화하면 조용히 갈라진다. P3/기록.
|
||
|
||
**(b) retry 의미론이 두 표현으로 존재한다.** `MongoRetryScope`의 javadoc은 "Encoding that as a scope rather than a `retryable` boolean is what stops the two from collapsing into one flag at the call site"라고 쓰는데, 같은 package의 `MongoFailureContext`는 정확히 `boolean retryable`을 필드로 갖는다. 다만 §15에서 확인했듯 production 경로에서 그 boolean은 scope에서 파생되고, 삼중항을 들고 다니는 타입(`MongoFailureClassification`)은 `api`가 아니라 `failure` package에 있다. 즉 이것은 결함이 아니라 **경계 배치의 결과**다 — framework-free core는 boolean만 들고, scope를 읽는 코드는 경계 밖에 있다. 기록만 한다.
|
||
|
||
**(c) 자리표시자 profile 이름이 실제 이름의 값 공간을 공유한다.** `MongoOperationScope.UNSPECIFIED = "unspecified"`는 `DatabaseProfileName`의 `FORMAT`을 통과하는 평범한 값이라, `unspecified`라는 이름으로 실제 profile을 등록하면 `isProfileResolved()`가 그것을 미해결로 판정한다. 현재 그런 profile은 없다. P3/기록.
|
||
|
||
### 18.4 Documentation / measured-count drift
|
||
|
||
이 sub-scope 범위에서 새로 확인된 drift는 없다. api 표면 기여 59/346은 §13에서 실측했고, build.gradle 주석의 311/313 drift는 sub-scope 01(§8)에서 이미 확정했다.
|
||
|
||
## 19. Sub-scope 02 findings backlog
|
||
|
||
| 우선순위 | finding | reachability |
|
||
|---|---|---|
|
||
| **P2** | schema version 실패의 두 생성 경로가 각각 반쪽만 맞다 — 버전을 아는 경로는 category `OPERATION_REJECTED`, 전용 category를 붙이는 경로는 버전 `-1,-1,-1` | production 두 경로 모두; 대시보드 bin과 공개 accessor 값 |
|
||
| **P3** | 예외 계층의 "no constructor accepts a Throwable cause" 규칙을 `MongoTimeoutException`의 2-arg 생성자가 `initCause`로 벗어나며, 규칙을 검사하는 유일한 test는 cause 생성자가 없는 타입을 본다 | 유일 호출처의 cause는 Reactor `TimeoutException`이라 실제 payload 없음 |
|
||
| **P3** | `MongoDocumentTooLargeException`이 translator 경로에서 `-1L, -1L`로 생성되며 accessor 계약이 값 부재를 말하지 않음 | driver 보고 실패 전체 |
|
||
| **P3** | `MongoFailureContext`의 canonical constructor가 outcome ↔ ambiguous 정합을 강제하지 않음 | production은 classification에서 파생해 일관; 노출은 외부 adopter의 직접 생성 |
|
||
| **P3** | `MongoConsistencyDescriptor`가 자기 enum javadoc이 "bug"라 부른 `secondaryPreferred` + `majority` write 조합을 거부하지 않음 | `MongoConsistencyRegistry.of(...)`는 public; `standard()`의 6개는 정합 |
|
||
| **P3/기록** | `DatabaseProfileName`/`CollectionProfileName`의 검증 코드가 javadoc을 빼면 동일 | 한쪽만 강화하면 갈라짐 |
|
||
| **P3/기록** | `MongoOperationScope.UNSPECIFIED` 자리표시자가 정상 profile 이름 값 공간과 겹침 | 현재 충돌하는 profile 없음 |
|
||
|
||
## 20. Sub-scope 02 완료 조건
|
||
|
||
- denominator 70 / 70 FULL_READ (`127-...`)
|
||
- framework-free 규칙을 ArchUnit과 독립적으로 소스 전수 검색으로 재확인(매치 0)
|
||
- public surface reachability(외부 0 — 설계된 상태이자 한계), invariant sibling 6종 비교, duplicate mechanism 3종, count 기여 59/346 측정
|
||
- 두 확정 finding(§16 P2, §17 P3)은 생성 지점·호출처·test 커버리지를 모두 지목해 근거화(`128-...`)
|
||
- 이 sub-scope는 소스를 수정하지 않았고 별도 실행 probe도 필요하지 않았다 — 모든 판정이 정적으로 결정 가능하며, hermetic lane 재실행 결과는 sub-scope 01의 `126-...`이 이미 담고 있다
|
||
|
||
## 21. 다음 sub-scope로 넘긴 것
|
||
|
||
- `MongoConsistencyBinder` / `ReactiveMongoConsistencyBinder`가 descriptor를 실제 driver 설정으로 번역하는 방식과 `MongoTemplateSupportContract` → sub-scope 4
|
||
- `failure` package의 classifier·translator·extractor 전체(§15에서 cross-scope 근거로만 읽었다) → sub-scope 9
|
||
- `MongoValidatorApplyPolicy`가 `MongoServerVersion`을 쓰는 방식과 schema/index manifest → sub-scope 7
|
||
- `mapping/type/PolicyAwareMongoTypeMapper`가 `MongoTypeRepresentationManifest`를 강제하는 실제 경로 → sub-scope 3
|
||
|
||
---
|
||
|
||
## 22. Sub-scope 03 범위와 denominator
|
||
|
||
> 내부 상태: COMPLETE — **27 / 27 FULL_READ**
|
||
> 범위: `mapping/**` 13 + `nativecap/**` 5 + `geo/**` 5 (production 23, 1,502 LOC) + 전용 test 4
|
||
> 역할: api가 고정한 BSON 표현 manifest를 Spring Data 변환기에 실제로 강제하고, D3 native capability와 geospatial 경계를 정의한다
|
||
|
||
manifest와 probe: `evidence/raw/130-mongo-mapping-nativecap-geo-manifest-and-probes.txt`.
|
||
|
||
세 package의 배선 상태가 서로 다르다. 이것이 이 sub-scope를 읽는 축이다.
|
||
|
||
| package | production 배선 |
|
||
|---|---|
|
||
| `mapping` | `MongoPlatformAutoConfiguration:48`이 `@Import(MongoMappingConfiguration.class)` — **platform이 켜지면 항상 조립된다** |
|
||
| `geo` | 자기 package 밖 production 참조 **0** — bean도 소비자도 없다 |
|
||
| `nativecap` | 자기 package 밖 production 참조 **0** — bean도 소비자도 없다 |
|
||
|
||
## 23. Confirmed P1 — shipped default 조합이 첫 write에서 예외를 던진다
|
||
|
||
세 사실이 겹친다.
|
||
|
||
1. `MongoMappingConfiguration.mongoTypeMetadataRegistry()`가 **비어 있는** `MongoTypeMetadataRegistry.empty()`를 기본 bean으로 등록한다. javadoc: "An empty registry so a deployment with no long-lived collection still starts."
|
||
2. `MongoTypeMetadataConfigurer.afterPropertiesSet()`가 `PolicyAwareMongoTypeMapper`를 **모든** `MappingMongoConverter`에 무조건 설치한다(`converters.forEach(converter -> converter.setTypeMapper(typeMapper))`).
|
||
3. `PolicyAwareMongoTypeMapper.writeType(...)`은 등록되지 않은 타입에 대해 **`IllegalStateException`을 던진다** — "no type metadata policy is registered for …; a stored document's type metadata outlives the class, so the policy is a decision to record rather than to default".
|
||
|
||
즉 module을 켜기만 하고 type metadata를 등록하지 않은 배포는 **시작은 하고 첫 write에서 실패한다.**
|
||
|
||
### 실행 probe
|
||
|
||
`evidence/raw/129-mongo-empty-type-registry-write-probe.txt` / `129a-...java`. 실제 `MappingMongoConverter`에 shipped default 조합(빈 registry + policy-aware mapper)을 설치하고 평범한 document를 썼다.
|
||
|
||
```text
|
||
emptyRegistry.rootWrite=IllegalStateException: no type metadata policy is registered for …$ProbeDocument; …
|
||
emptyRegistry.nestedWrite=IllegalStateException: no type metadata policy is registered for …$ProbeDocument; …
|
||
springDefault.rootWrite=written keys=[_id, value, _class]
|
||
```
|
||
|
||
같은 converter에 Spring 기본 type mapper를 두면 같은 write가 성공한다. 즉 실패는 문서·엔티티 형태가 아니라 이 leaf가 설치한 mapper에서 온다.
|
||
|
||
### 같은 컴포넌트가 같은 질문에 세 가지로 답한다
|
||
|
||
probe는 그 불일치도 함께 측정했다.
|
||
|
||
```text
|
||
emptyRegistry.policyFor=CLASS_METADATA_ALLOWED
|
||
emptyRegistry.writeTypeRestrictions={"_class": {"$in": ["…$ProbeDocument"]}}
|
||
emptyRegistry.writeType=IllegalStateException
|
||
```
|
||
|
||
| 물음 | 답 | 근거 |
|
||
|---|---|---|
|
||
| 미등록 타입의 정책은? | `CLASS_METADATA_ALLOWED` | `MongoTypeMetadataRegistry.policyFor` (javadoc: "unregistered types keep Spring Data's default") |
|
||
| 미등록 타입으로 type-restricted **query**를 만들면? | Java class name을 `_class` predicate에 씀 | `PolicyAwareMongoTypeMapper:134` `orElse(CLASS_METADATA_ALLOWED)` |
|
||
| 미등록 타입을 **write**하면? | 예외 | 같은 클래스 `:75` `orElseThrow(...)` |
|
||
|
||
읽기 경로와 쓰기 경로가 같은 정책 질문에 정반대로 답하고, 그중 어느 쪽도 registry가 스스로 문서화한 기본값과 일치하지 않는다.
|
||
|
||
### 왜 지금까지 드러나지 않았나
|
||
|
||
이 leaf는 가짜 도메인을 두지 않으므로 저장소 안에 document type이 하나도 없고, 따라서 이 경로를 밟는 저장소 내부 코드가 없다. 그리고 `PolicyAwareMongoTypeMapperTest`는 mapper를 항상 **채워진** registry(`fromAnnotations(List.of(LongLivedOrder, ShortLivedAudit))`)로 만든다 — shipped default인 빈 registry로 `writeType`을 부르는 test는 없다.
|
||
|
||
**판정: P1 conditional-production.** 저장소 안에서는 재현되지 않지만, README가 서술한 정상 사용법(`enabled=true` + fork가 자기 document를 추가)을 그대로 따르면 첫 write에서 반드시 발생한다. 수정 방향은 둘 중 하나이고 어느 쪽이든 세 답을 하나로 만들어야 한다 — `writeType`도 `policyFor`처럼 `CLASS_METADATA_ALLOWED`로 떨어뜨리거나(레거시 허용), 기본 bean을 "미등록이면 실패"가 아니라 "등록을 요구하는 명시적 opt-in"으로 바꾸거나. regression은 빈 registry로 `MappingMongoConverter.write(...)`를 부르는 한 줄이면 된다.
|
||
|
||
## 24. mapping의 나머지는 manifest를 실제로 강제한다
|
||
|
||
P1과 별개로, 이 package의 나머지는 api manifest를 말이 아니라 코드로 만든다.
|
||
|
||
- `MongoCustomConversionsFactory.converters(...)`가 변환기를 **명시적 List 순서로** 조립한다. 이유가 주석에 있다 — Spring의 conversion service는 첫 매칭 변환기를 쓰므로 `Set`이나 classpath 스캔에서 조립하면 JVM 실행마다 다른 변환기가 선택될 수 있다. `fingerprint(manifest)`가 manifest fingerprint에 변환기 클래스 이름을 이어 붙여 golden BSON snapshot이 비교할 identity를 만든다.
|
||
- 같은 factory가 `requireEveryAxisImplemented(...)`로 `LOCAL_DATE_TIME_WITH_REGISTERED_CONVERTER`를 startup에서 거부한다. enum 상수 자신이 "selecting this without registering the named converter is a startup failure"라고 적어 둔 규칙을 실제로 집행하는 지점이다.
|
||
- `BigIntegerRepresentationConverters.forRepresentation(...)`은 manifest의 BigInteger 축을 세 변환기 쌍으로 컴파일한다. 주석이 과거 상태를 기록한다 — 이 축은 선언만 있고 컴파일되지 않아 `STRING`과 `DECIMAL128`이 동일한 document를 만들었고, 하나는 사전식으로 다른 하나는 수치로 정렬된다.
|
||
- `LocalDateTimeMappingGuard`는 `MongoMappingConfiguration`이 **실제 등록된 변환기**로 만든다. javadoc이 이전 결함을 적는다 — guard를 `withoutConverters()`로 만들고 manifest를 검증하게 해서, 명명된 변환기를 등록한 배포와 등록하지 않은 배포를 똑같이 거부했다.
|
||
- `BigDecimalToDecimal128Converter`는 driver 호출 전에 34 유효숫자·지수 범위를 검사한다. `Decimal128`은 초과 정밀도를 조용히 반올림하므로, 검사가 없으면 금액이 다른 값으로 저장되고 아무 오류도 나지 않는다.
|
||
|
||
`PolicyAwareMongoTypeMapper`의 alias 규칙도 견고하다. alias에 점을 금지하고, 읽을 때 점의 유무로 "legacy class name"과 "alias"를 구분한다 — 그래서 미등록 alias가 class loading으로 fallback해 저장된 문자열이 어떤 클래스를 인스턴스화할지 결정하는 일이 없다. `readType(source, basicType)`은 저장된 타입이 caller의 기대 타입과 호환되지 않으면 조용히 caller 타입으로 읽지 않고 schema 오류를 던진다.
|
||
|
||
## 25. Confirmed P2 — D3 gateway가 문서화한 검사 순서에 존재하지 않는 단계가 있다
|
||
|
||
`PolicyAwareMongoNativeGateway`의 javadoc은 이렇게 쓴다.
|
||
|
||
> Runs the design's stated sequence and stops at the first refusal: registration, capability, database profile, collection profile, **timeout**, category, then execution.
|
||
|
||
README는 더 긴 목록을 제시한다.
|
||
|
||
> `PolicyAwareMongoNativeGateway`가 capability → database profile → collection allowlist → operation name → **timeout** → **consistency** → **result limit** → **trace** → **redaction** → command category → D4 차단 순서를 고정한다.
|
||
|
||
실제로 `MongoNativeOperationPolicy.require(...)`가 수행하는 거부는 여섯 개다 — 등록 여부, 등록된 capability와 제출된 capability의 일치, capability support level, database profile allowlist, collection profile allowlist, category(ADMIN 차단). gateway 자신은 `policy.require(operation)` → body 실행 → audit 기록만 한다.
|
||
|
||
빠진 것 중 두 개는 `ApprovedMongoNativeOperation`이 **필드로 선언까지 해 둔** 값이다.
|
||
|
||
```text
|
||
$ git grep -n 'operation.timeout()\|\.hasBody()' -- src/main
|
||
…/nativecap/ApprovedMongoNativeOperation.java:64: public boolean hasBody() { ← 정의뿐, 호출자 없음
|
||
$ git grep -n 'operation.maxResults()' -- src/main
|
||
exit=1
|
||
```
|
||
|
||
`timeout`은 생성자에서 음수만 거부하고 어디서도 적용되지 않으며, `maxResults`는 production에서 한 번도 읽히지 않는다(같은 이름의 `maxResults()` 호출들은 전부 `MongoOperationBudget`이라는 **다른** 타입의 것이다). consistency·result limit·trace·redaction 단계는 코드에 존재하지 않는다.
|
||
|
||
현재 노출은 없다 — `MongoNativeCapabilityGateway`와 `PolicyAwareMongoNativeGateway`는 production 참조가 0이고 어떤 configuration도 bean으로 만들지 않는다(§22). 그러나 README는 이 클래스를 "D3는 raw client escape가 아니다"라는 주장의 근거로 제시한다. fork가 이것을 그대로 배선하면 문서가 약속한 11단계 중 6단계만 동작하고, 그 사실은 코드를 읽어야만 드러난다.
|
||
|
||
**판정: P2.** 수정은 문서를 실제 검사로 줄이거나(정직), 선언된 `timeout`/`maxResults`를 gateway가 실제로 적용하도록 만드는 것이다. 후자를 택하면 `hasBody()`가 처음으로 호출자를 갖게 된다.
|
||
|
||
## 26. geo는 index 전제를 스스로 확인하지만 배선되지 않았다
|
||
|
||
`SpringMongoGeospatialOperations`는 dispatch 전에 manifest에서 해당 필드의 `2dsphere` index를 찾고 없으면 거부한다. 이유가 정확하다 — MongoDB는 index 없는 `$near`는 거부하지만 `$geoWithin`은 거부하지 않고 collection scan으로 조용히 성공한다. 두 경우를 같은 시점에 같은 메시지로 실패시키는 것이 이 검사의 목적이다.
|
||
|
||
`MongoGeoPoint`는 GeoJSON의 longitude-first 순서를 record component 이름으로 못박고 범위를 검증한다. `MongoGeoDistance`는 단위를 타입에 넣는다 — spherical 연산자는 미터, legacy 연산자는 radian, Spring Data는 metric을 받으므로 맨 `double`은 600만 배 틀린 채로도 결과를 돌려준다. `toMeters()`와 `toSpringDistance()`의 두 단위 변환을 직접 검산했고 오류는 없다.
|
||
|
||
`MongoGeoQuery`는 최대 거리와 결과 상한(≤500)을 둘 다 필수로 만든다. `$near`는 collection 전체를 거리순으로 정렬해 스트리밍하므로 거리 경계가 없으면 "가까운 것부터 반환하는 full scan"이 된다.
|
||
|
||
이 package 역시 production 참조 0이다. geo는 README의 package 지도에 "GeoJSON / 2dsphere"로만 적혀 있고 배선을 주장하지 않으므로, nativecap과 달리 **문서와 코드가 어긋나지는 않는다**. 기록만 한다.
|
||
|
||
## 27. Negative-space probes — sub-scope 03
|
||
|
||
- **8.1 reachability**: `mapping`은 platform auto-configuration이 import(배선됨), `geo`·`nativecap`은 production 참조 0(미배선). 세 결과 모두 `130-...` §8.1에 명령·exit code와 함께 있다.
|
||
- **8.2 sibling comparison**: 같은 "미등록 타입" 질문에 대한 세 답(§23). 그리고 `mapping`의 두 guard(`LocalDateTimeMappingGuard`, `requireEveryAxisImplemented`)는 startup에서 거부하는 반면 type metadata 정책은 write 시점에 거부한다 — 같은 종류의 계약 위반이 서로 다른 시점에 잡힌다.
|
||
- **8.3 duplicate mechanism**: 결과 상한을 뜻하는 `maxResults()`가 두 타입에 있다 — `ApprovedMongoNativeOperation`(미사용)과 `MongoOperationBudget`(query·aggregation·cursor에서 실제 사용). 이름이 같고 하나만 살아 있다.
|
||
- **8.4 documentation drift**: §25의 D3 순서. 그 밖에 이 sub-scope 범위에서 새 수치 drift는 없다.
|
||
|
||
## 28. Sub-scope 03 findings backlog
|
||
|
||
| 우선순위 | finding | reachability |
|
||
|---|---|---|
|
||
| **P1 conditional-production** | shipped default(빈 type metadata registry + 무조건 설치되는 policy-aware mapper)에서 미등록 타입의 write가 `IllegalStateException`. 같은 컴포넌트가 미등록 타입에 대해 세 가지로 답한다 | platform을 켠 모든 배포의 첫 write; 저장소 안에는 document type이 없어 내부 재현 없음 |
|
||
| **P2** | D3 gateway가 문서화한 검사 순서(javadoc 7단계 / README 11단계) 중 실제 존재하는 것은 6개. 선언된 `timeout`·`maxResults`는 production에서 한 번도 읽히지 않음 | gateway 자체가 미배선이므로 현재 노출 0 |
|
||
| **P3/기록** | `geo` package가 완전히 미배선(bean 0, 소비자 0) — 다만 문서가 배선을 주장하지 않아 drift는 아님 | fork가 배선할 때 사용 |
|
||
| **P3/기록** | `maxResults()`라는 같은 이름의 결과 상한이 두 타입에 존재하고 하나만 사용됨 | 혼동 |
|
||
|
||
## 29. Sub-scope 03 완료 조건
|
||
|
||
- denominator 27 / 27 FULL_READ (`130-...`)
|
||
- reachability·sibling·duplicate·drift 4종 probe 수행
|
||
- P1을 실행 probe로 확정(`129-...`, `129a-...`), 임시 test 삭제 후 `git status --short` clean
|
||
- geo 단위 변환 2종은 코드로 직접 검산했고 오류 없음을 기록
|
||
|
||
---
|
||
|
||
## 30. Sub-scope 04 범위와 denominator
|
||
|
||
> 내부 상태: COMPLETE — **61 / 61 FULL_READ**
|
||
> 범위: `imperative/**` 34 + `reactive/**` 13 (production 47, 3,369 LOC) + 전용 test 14
|
||
> 역할: 모든 operation이 통과하는 실행 scope — collection 해석, consistency 바인딩, 관측, 실패 번역, 그리고 atomic/bulk/revision/cursor 경로
|
||
|
||
manifest와 probe: `evidence/raw/131-mongo-execution-paths-manifest-and-probes.txt`.
|
||
|
||
배선 상태(§8.1):
|
||
|
||
| 타입 | production bean |
|
||
|---|---|
|
||
| `DefaultMongoImperativeExecutor` | ✓ `MongoPlatformAutoConfiguration:114` |
|
||
| `MongoAtomicOperationsTemplate` | ✓ `:148` |
|
||
| `MongoBulkExecutor` | ✓ `:166` |
|
||
| `DefaultReactiveMongoExecutor` | ✓ `:293` (reactive template이 bean일 때) |
|
||
| `VersionedMongoUpdater` | ✗ bean 없음 |
|
||
| `MongoCursorGuard` | ✗ bean 없음 |
|
||
|
||
## 31. 실행 scope의 고정된 순서가 이 sub-scope의 중심이다
|
||
|
||
`DefaultMongoImperativeExecutor.executeInternal(...)`은 순서를 고정한다 — collection profile 해석 → observation 개시 → consistency 바인딩 → callback 실행 → 실패 번역(최대 한 번) → observation 종료. javadoc이 이유를 적는다: "Fixing it here is what makes the invariants hold for operations nobody has written yet."
|
||
|
||
세 가지 방어가 눈에 띈다.
|
||
|
||
- 이미 번역된 `MongoPersistenceException`은 그대로 통과시킨다. 재번역하면 bulk partial failure나 guardrail 거절처럼 **그것을 던진 계층이 더 잘 아는** category를, driver 코드에서 유도한 일반 category로 덮어쓰게 된다.
|
||
- Spring이 감싼 driver 예외를 `unwrap(...)`으로 되꺼낸다. Spring의 번역은 error label을 잃는데, label이야말로 replayable transaction과 unknown commit을 가르는 값이다.
|
||
- `MongoCompletion.successOutcomeFor(operationType)`가 read와 write의 성공 outcome을 나눈다. 과거에는 두 executor 모두 성공을 `WRITE_CONFIRMED`로 기록해, "write가 acknowledge되고 있는가"를 답하는 지표가 read 트래픽의 함수가 됐다. `default` 분기가 `READ_CONFIRMED`로 떨어지는 것도 의도적이다 — "the honest answer is the one that claims least".
|
||
|
||
`MongoCollectionProfileRegistry`가 "동적 collection 이름 금지"를 강제 가능하게 만드는 지점이다. 애플리케이션은 profile을 부르고 물리 이름은 이 registry만 안다. `ScopedAccess.collection(String)`은 요청된 collection이 scope의 것과 다르면 거부하고, `ScopedMongoOperations`의 어떤 메서드도 collection 인자를 받지 않으므로 그 검사를 우회할 방법이 없다.
|
||
|
||
`MongoConsistencyBinder`는 profile마다 **파생 template**을 생성 시점에 한 번 만든다. `MongoTemplate.setWriteConcern`은 애플리케이션이 공유하는 bean을 변형하므로, 호출마다 설정했다면 다른 스레드의 durability를 바꿨을 것이다. 파생은 Spring Data의 public setter로 원본의 contract(entity callback, auditing, event publisher, write-concern resolver, write-result checking)를 옮긴다 — javadoc이 과거 결함을 기록한다: bare `new MongoTemplate(factory, converter)`로 파생해 같은 entity가 platform executor 경로와 repository 경로에서 서로 다른 document가 됐다.
|
||
|
||
## 32. Confirmed P2 — 서버 측 deadline이 경로마다 다르게 적용되고, 문서가 지목한 메커니즘은 production 호출자가 0이다
|
||
|
||
`BoundScopedOperations`의 javadoc은 이 클래스의 존재 이유를 명확히 쓴다.
|
||
|
||
> Every query-shaped method also carries the operation's deadline as `maxTimeMS`, and **that is the difference between a deadline and a report about one**. The blocking executor could only measure elapsed time after the callback returned … so an operation that ran past its budget was detected, never stopped. Sent to the server, the same number ends the work.
|
||
|
||
측정 결과 이 메커니즘은 `MongoPlatformCollectionAccess.scoped()`를 통해서만 도달하고, **production에서 `scoped()`를 부르는 곳은 0개**다(`131-...` §8.2). 반면 platform이 소유한 세 executor는 전부 `rawOperations()`를 쓴다 — `MongoAtomicOperationsTemplate`(2곳), `MongoBulkExecutor`(1곳), `SpringMongoGeospatialOperations`(2곳). `rawOperations()`는 경계 없는 `MongoOperations`를 그대로 돌려준다.
|
||
|
||
서버 측 deadline을 실제로 붙이는 다른 경로들은 **다른 어휘**를 쓴다.
|
||
|
||
| 경로 | 서버에 보내는 deadline |
|
||
|---|---|
|
||
| aggregation (`PolicyAwareMongoAggregationExecutor:96,106`) | `Math.min(registered.maxTimeMillis(), contextMillis)` — 둘을 조정 |
|
||
| query builder (`PolicyAwareMongoQueryBuilder:200`) | `budget.maxTimeMillis()` 단독 |
|
||
| reactive cursor (`MongoReactiveCursorPublisher:58`) | `budget.maxTimeMillis()` 단독 |
|
||
| atomic / bulk / geospatial | **없음** |
|
||
| caller callback via `scoped()` | `context.timeout()` — production 호출자 0 |
|
||
|
||
즉 `MongoOperationContext.timeout`(모든 operation이 반드시 선언하는 값)이 서버에 도달하는 경로는 aggregation 하나뿐이고, 그것도 budget과의 최소값으로만 도달한다. atomic·bulk·geospatial에서는 executor의 사후 elapsed 검사만 남는데, 그 검사의 주석 자신이 "detected, never stopped"라고 인정한다.
|
||
|
||
**판정: P2.** 데이터 손상은 아니지만 platform이 스스로 선언한 자원 경계가 자신의 세 실행 경로에서 서버에 도달하지 않는다. 수정은 `MongoPlatformCollectionAccess`가 `rawOperations()` 대신 deadline이 붙은 접근자를 내보내거나, 세 executor가 query를 만들 때 `context.timeout()`을 붙이는 것이다.
|
||
|
||
## 33. P3 — timeout 초과 경로가 한 observation에 success와 failure를 모두 기록한다
|
||
|
||
같은 executor의 elapsed 검사 분기는 이렇게 쓰여 있다.
|
||
|
||
```java
|
||
if (elapsed.compareTo(context.timeout()) > 0) {
|
||
observation.success(outcome);
|
||
throw MongoOperationRejectedException.of(...);
|
||
}
|
||
```
|
||
|
||
`MongoOperationRejectedException`은 `MongoPersistenceException`의 하위 타입이고, 이 throw는 같은 `try` 블록 안에 있으므로 바로 다음 `catch (MongoPersistenceException alreadyTranslated)`가 잡아 `observation.failure(...)`를 호출한 뒤 다시 던진다. 결과적으로 하나의 observation에 `success`와 `failure`가 차례로 호출된다.
|
||
|
||
shipped 구현에서는 무해하다. `MicrometerMongoOperationObserver`의 observation은 `success`/`failure`가 `outcomeTags` 필드를 덮어쓸 뿐이고 timer는 `close()`에서 한 번만 정지하므로, 마지막 호출인 failure의 tag로 한 번 기록된다. 문제는 계약이다 — `MongoOperationObservation` 인터페이스는 둘 중 하나만 호출해야 한다거나 마지막 호출이 이긴다는 규칙을 말하지 않는다. 두 호출을 각각 계수하는 구현을 fork가 만들면 이 경로의 operation이 두 번 계수된다. P3.
|
||
|
||
## 34. atomic / bulk / revision — 닫힌 우회로들
|
||
|
||
이 세 package는 과거에 열려 있던 우회로를 닫은 기록을 코드에 남긴다.
|
||
|
||
- **bulk가 atomic의 정책을 우회하던 문제.** `MongoBulkExecutor`의 생성자 javadoc이 기록한다 — 단일 문서 경로는 filter/update를 collection 정책에 대조했고 bulk 경로는 정책을 보지 않았으며, 정책은 기본값 없음인 **선택적** 생성자 인자였다. 같은 update를 배치에 넣으면 보호 필드와 미등록 연산자에 도달할 수 있었다. 지금은 생성자가 하나뿐이고 배치 전체를 dispatch 전에 검증한다("an ordered batch that fails halfway leaves the earlier items applied").
|
||
- **bulk 실패에서 per-item 정보를 잃던 문제.** `catch (MongoBulkWriteException)`는 Spring Data가 감싼 실패를 놓쳤고, caller에게는 per-item index 없는 일반 오류 하나가 갔다 — 이 result 타입이 존재하는 바로 그 이유가 사라진 셈이다. 지금은 `RuntimeException`을 잡고 `SpringDataBulkFailureExtractor`로 안쪽의 driver 실패를 찾는다.
|
||
- **unacknowledged bulk 결과.** `wasAcknowledged()`가 false면 성공 0으로 보고하지 않고 `MongoBulkResult.unknown(...)`을 돌려준다. 주석: "Reporting zero successes would be a claim, and re-sending on that claim duplicates whatever did apply."
|
||
- **revision 재시도.** `VersionedMongoUpdater.applyWithRetry`는 시도마다 문서를 다시 읽고 caller의 계산을 다시 실행한다. 이전에 계산된 update를 재전송하는 재시도는 stale state에서 유도된 값을 쓰는 것이고, 그것이 revision predicate가 막으려던 lost update가 재시도 경로로 되돌아오는 형태다.
|
||
|
||
**두 개의 빈 registry 기본값이 서로 다른 실패 모양을 갖는다**(§8.3). `MongoAtomicPolicyRegistry.empty()`는 `MongoPlatformAutoConfiguration`이 기본 bean으로 등록하고, javadoc이 "empty means every atomic and bulk operation is refused rather than permitted"라고 명시하며, 실제 거부도 platform 어휘인 `MongoOperationRejectedException`이다. 같은 configuration이 등록하는 `MongoTypeMetadataRegistry.empty()`는 §23에서 본 대로 Spring Data converter 깊은 곳에서 `IllegalStateException`으로 실패하고, 그 사실은 어디에도 적혀 있지 않다. 같은 설계 의도(미등록은 거부)가 한쪽에서는 문서화된 fail-closed로, 다른 쪽에서는 문서화되지 않은 런타임 예외로 나타난다.
|
||
|
||
## 35. reactive 경로가 명시적으로 배치한 세 가지
|
||
|
||
`DefaultReactiveMongoExecutor`의 javadoc이 blocking 경로가 공짜로 얻는 것과 여기서 직접 배치해야 하는 것을 대비한다.
|
||
|
||
- observation scope를 Reactor 자원(`Mono.using`/`Flux.using`)으로 두어 완료·오류·**취소** 모두에서 닫는다. HTTP 클라이언트 연결 해제가 취소를 일으키므로 취소가 흔한 경우다.
|
||
- timeout을 조립된 publisher에 적용한다. 구독 전에 적용하면 "람다를 만드는 데 걸린 시간"을 재게 된다.
|
||
- context를 Reactor Context로 옮긴다(`ReactiveMongoContextKeys`). 체인은 operator 경계마다 스레드를 바꾸므로 구독 시점의 `ThreadLocal`은 driver 응답 시점에 이미 없다.
|
||
|
||
기록해 둘 관측 하나: `executeMany(...)`는 성공을 `doOnComplete`로 기록하므로 **취소된 stream은 success도 failure도 기록하지 않는다.** observation은 `close()`되고 초기 tag(`result=unknown`, `failureCategory=none`)로 한 번 계수된다. 취소가 흔한 경로라는 점을 감안하면 이는 의도된 분류로 보이지만, `result=unknown` bucket이 "취소"와 "관측 시작 직후 예외"를 함께 담는다는 사실은 계약에 없다. P3/기록.
|
||
|
||
## 36. Negative-space probes — sub-scope 04
|
||
|
||
- **8.1 reachability**: 6개 주요 타입 중 4개가 bean, `VersionedMongoUpdater`·`MongoCursorGuard`는 미배선(fork 공급).
|
||
- **8.2 deadline**: §32. `scoped()` production 호출자 0, `rawOperations()` 5곳, `maxTime` 계열 6곳이 세 어휘로 갈림.
|
||
- **8.2b observation**: §33.
|
||
- **8.3 duplicate/sibling**: 두 빈 registry 기본값의 실패 모양 차이(§34). 그리고 atomic·bulk가 **같은** `MongoAtomicPolicyRegistry`를 공유하도록 강제된 것은 닫힌 우회로의 증거로 기록.
|
||
- **8.4 drift**: 이 sub-scope 범위에서 새 수치 drift 없음.
|
||
|
||
## 37. Sub-scope 04 findings backlog
|
||
|
||
| 우선순위 | finding | reachability |
|
||
|---|---|---|
|
||
| **P2** | `context.timeout()`이 서버에 도달하는 경로가 aggregation 하나뿐. atomic·bulk·geospatial은 `rawOperations()`로 deadline 없이 실행되고, 이를 위해 만들어진 `BoundScopedOperations`는 production 호출자가 0 | platform이 소유한 세 실행 경로 전부 |
|
||
| **P3** | timeout 초과 분기가 한 observation에 `success`와 `failure`를 연달아 호출. 인터페이스는 어느 쪽이 이기는지 말하지 않으며 shipped observer만 마지막 호출로 해소 | 모든 timeout 초과 operation |
|
||
| **P3/기록** | 취소된 reactive stream이 `result=unknown` bucket에 들어가며 그 사실이 계약에 없음 | 취소가 흔한 reactive 경로 |
|
||
| **P3/기록** | 같은 configuration이 등록하는 두 빈 registry 기본값의 실패 모양이 다르다(atomic=문서화된 platform 거부, type metadata=문서화되지 않은 `IllegalStateException`) | §23의 P1과 같은 뿌리 |
|
||
|
||
## 38. Sub-scope 04 완료 조건
|
||
|
||
- denominator 61 / 61 FULL_READ (`131-...`)
|
||
- reachability·deadline·observation·sibling 4종 probe 수행, 모든 명령과 exit code 보존
|
||
- P2는 `scoped()`/`rawOperations()`/`maxTime` 세 검색의 교차로 확정했고 실행 probe 없이 정적으로 결정 가능
|
||
- 소스 미변경, `git status --short` clean 유지
|
||
|
||
---
|
||
|
||
## 39. Sub-scope 05 범위와 denominator
|
||
|
||
> 내부 상태: COMPLETE — **29 / 29 FULL_READ**
|
||
> 범위: `query/**` 17 + `aggregation/**` 5 (production 22, 2,082 LOC) + 전용 test 7
|
||
> 역할: 동적 query를 allowlist로 표현 가능하게 만들고, budget·keyset pagination·aggregation stage 정책을 고정한다
|
||
|
||
manifest와 probe: `evidence/raw/132-mongo-query-aggregation-manifest-and-probes.txt`.
|
||
|
||
## 40. 이 sub-scope의 설계는 "표현 가능한 query 집합 = 검토된 집합"이다
|
||
|
||
`MongoQueryPolicy`와 `PolicyAwareMongoQueryBuilder`가 이 leaf에서 가장 직접적인 보안 장치다. builder는 caller가 준 BSON/JSON을 **파싱하지 않는다**. 모든 predicate는 등록된 field path와 등록된 operator를 지목하고, 그 둘이 policy에 없으면 로컬에서 거부된다 — 그래서 NoSQL operator injection이 검증 문제가 아니라 표현 불가능성이 된다. denylist가 아니라 allowlist인 이유도 적혀 있다: "A denylist has to anticipate the next operator MongoDB adds; an allowlist does not."
|
||
|
||
세부도 촘촘하다.
|
||
|
||
- `requireSortable`은 등록된 필드라도 sortable이 아니면 거부한다 — 인덱스 없는 sort는 메모리에서 수행되고 sort buffer를 넘기면 실패하기 때문이다.
|
||
- `requireSkipWithinThreshold`는 deep skip(기본 1000 초과)을 keyset pagination으로 밀어낸다.
|
||
- `build(budget)`가 유일한 종료 지점이고, 거기서 `limit` / `maxTimeMsec` / `cursorBatchSize`가 반드시 붙는다 — "a query without a result limit and a `maxTimeMS` is a query with no upper bound on what it can consume".
|
||
- regex는 세 갈래로 나뉜다. `whereStartsWith`/`whereContains`는 caller의 텍스트를 `Pattern.quote`로 escape해 **문법을 기여할 수 없게** 만들고, 전자는 anchored(인덱스 사용 가능), 후자는 unanchored(scan)로 비용이 호출 지점에 드러난다. `whereMatches`만 문법을 받는다.
|
||
|
||
`MongoRegexPolicy`의 정직함은 기록해 둘 만하다. javadoc이 nested-quantifier 검사가 **안전 증명이 아니라 필터**라고 명시하고, alternation·`?`·back-reference로 생기는 catastrophic backtracking을 보지 못한다고 스스로 적는다. 이런 자기 한정은 이 저장소 전체에서 드물지 않지만, 보안 경계에서 특히 유용하다.
|
||
|
||
`MongoKeysetCursorCodec`도 마찬가지로 촘촘하다. cursor는 클라이언트를 왕복하는 attacker-controlled 입력이므로 HMAC-SHA256으로 서명하고 상수시간 비교로 검증하며, 실패 메시지를 하나로 통일해 오류로부터 키나 형식을 배우지 못하게 한다. 값은 **타입 태그 + 길이 프레이밍**으로 인코딩된다 — 과거에는 `toString()`으로 렌더링하고 `String`으로 복원해서, `Instant`/`ObjectId`/UUID/숫자가 텍스트로 비교되어 다음 페이지가 비거나 행을 건너뛰거나 반복했고 아무 오류도 나지 않았다. 구분자 대신 길이 프레이밍인 이유도 같다: "a delimiter chosen from an alphabet a value can contain is not a delimiter".
|
||
|
||
`MongoKeysetQueryBuilder.resumeCriteria`는 사전식 "strictly after"를 전개해서 쓴다. javadoc이 흔한 축약형(`a <= A AND _id < I`)이 왜 틀리는지 적는다 — `a`가 더 작고 `_id`가 더 큰 행을 전부 잃고, 그 증상은 목록 중간에 행이 사라지는 형태라 production에서 오래 살아남는다.
|
||
|
||
## 41. Confirmed — 이 sub-scope는 정책과 값 객체이고, 배선된 것은 하나뿐이다
|
||
|
||
auto-configuration이 이 sub-scope에서 만드는 bean은 **`MongoBudgetEnforcer` 하나**다(`132-...` §8.1). `MongoQueryPolicy`·`PolicyAwareMongoQueryBuilder`·`MongoRegexPolicy`·`MongoBudgetPolicyRegistry`·`MongoKeysetCursorCodec`·`PolicyAwareMongoAggregationExecutor`는 bean도 아니고 `main` 안에 소비자도 없다(§8.1 세 번째 검색 exit=1).
|
||
|
||
그 하나조차 짝이 없다. `MongoBudgetEnforcer`의 유일한 production 소비자는 `PolicyAwareMongoAggregationExecutor`인데 그것이 미배선이므로, 배선된 enforcer는 현재 아무도 호출하지 않는다. `MongoKeysetCursorCodec`은 32바이트 이상 서명 키를 요구하는데 그 키를 공급하는 production 코드가 없다 — 생성자 호출은 test 3곳뿐이다.
|
||
|
||
이것 자체는 결함이 아니다. 이 leaf는 가짜 도메인을 두지 않고 collection profile·field descriptor·budget을 fork가 선언하도록 설계돼 있으며, CLAUDE.md가 "Real forks add their own document, repository, mapper"라고 명시한다. 기록하는 이유는 두 가지다. (a) README의 D1/D2 표는 "typed query, mapping manifest, atomic update, optimistic revision"을 노출 계층의 내용으로 제시하는데, 그중 typed query 계열은 배선 없이 fork가 조립해야 한다는 사실이 그 표에 없다. (b) §41의 다음 항목이 그 조립 시점에만 문제가 된다.
|
||
|
||
## 42. P2 — collection 이름 불변식이 aggregation executor의 서명에서 깨진다
|
||
|
||
`MongoCollectionProfileRegistry`의 javadoc은 이 leaf의 가장 강한 주장 중 하나를 편다.
|
||
|
||
> A collection name assembled from a request value therefore cannot reach the driver, because **there is no path from a string to a collection that does not pass through here.**
|
||
|
||
`PolicyAwareMongoAggregationExecutor.execute(...)`의 서명은 그 경로다.
|
||
|
||
```java
|
||
public <T> List<T> execute(
|
||
MongoOperationContext context,
|
||
MongoAggregationProfile profile,
|
||
MongoAggregationPlan plan,
|
||
String collection, // ← registry를 거치지 않는다
|
||
Class<T> outputType)
|
||
…
|
||
AggregationResults<T> results = operations.aggregate(aggregation, collection, outputType);
|
||
```
|
||
|
||
`context`가 `collectionProfile`을 이미 들고 있는데도 collection은 별도 `String` 인자로 받고, 그 값이 그대로 `MongoOperations.aggregate(...)`에 간다. 같은 클래스가 `MongoOperations`를 **직접** 주입받으므로 imperative 실행 scope도 통과하지 않는다 — collection profile 해석, observation, 실패 번역이 모두 없다(`132-...` §8.2b: 이 클래스에 `observer`·`observation`·`translator` 참조 0).
|
||
|
||
현재 노출은 없다(§41: 미배선). 그러나 fork가 이 executor를 배선하는 순간 두 가지가 동시에 생긴다 — registry가 보장한다고 적힌 불변식의 예외 하나, 그리고 관측·실패번역 없이 도는 실행 경로 하나. **판정: P2.** 수정은 서명에서 `String collection`을 없애고 `context.collectionProfile()`을 registry로 해석하는 것, 그리고 실행을 `DefaultMongoImperativeExecutor.executeInternal(...)` 안으로 옮기는 것이다. 후자는 §32에서 본 deadline 문제도 함께 해결한다(현재 aggregation은 `maxTime`을 스스로 붙이므로 그 부분만은 이미 옳다).
|
||
|
||
## 43. P3 — `MongoRegexPolicy.forbidden()`은 금지하지 않는다
|
||
|
||
```java
|
||
public static MongoRegexPolicy forbidden() {
|
||
return new MongoRegexPolicy(1, Set.of(), true);
|
||
}
|
||
```
|
||
|
||
"금지"가 별도 상태가 아니라 **최대 길이 1**로 표현돼 있다. `validate(pattern, flags)`의 네 검사를 길이 1짜리 패턴 `^`에 대해 따라가면 — 길이 1 ≤ 1 통과, flags 없음 통과, `requireAnchored && startsWith("^")` 통과, `hasNestedQuantifier("^")`는 그룹이 없으므로 false 통과 — **수용된다**. 그리고 `^`는 모든 문자열에 매치된다.
|
||
|
||
`prefixPattern`/`containsPattern`은 escape 결과가 항상 5자 이상이라 길이에서 걸리므로, 이 정책 아래서는 오히려 안전한 두 helper만 막히고 `whereMatches(path, "^", "")`는 통과한다. 도달하려면 해당 필드가 `MongoOperator.REGEX`를 등록해야 하므로 조합이 필요하지만, "regex를 금지했다"고 선언한 collection이 모든 문서에 매치되는 패턴을 받는 상태는 정책 이름이 약속하는 것과 다르다. **P3.** 수정은 policy에 명시적 "regex 불허" 상태를 두고 `validate`가 그것을 먼저 보게 하는 것이다.
|
||
|
||
## 44. Negative-space probes — sub-scope 05
|
||
|
||
- **8.1 reachability**: 배선된 bean은 `MongoBudgetEnforcer` 하나. 나머지 전부 미배선이고 그 하나의 소비자도 미배선(§41).
|
||
- **8.2 collection 불변식**: §42. registry javadoc의 주장과 aggregation executor 서명의 대조.
|
||
- **8.2b 실행 scope 이탈**: aggregation은 `MongoOperations`를 직접 받아 observation/translator 없이 실행.
|
||
- **8.3 regex 정책**: §43.
|
||
- **8.4 서명 키 출처**: `MongoKeysetCursorCodec`의 32바이트 키를 공급하는 production 코드 0 — cursor 서명은 fork가 키를 배선해야 성립한다.
|
||
|
||
## 45. Sub-scope 05 findings backlog
|
||
|
||
| 우선순위 | finding | reachability |
|
||
|---|---|---|
|
||
| **P2** | `PolicyAwareMongoAggregationExecutor`가 collection을 `String`으로 받아 registry를 우회하고, `MongoOperations`를 직접 받아 실행 scope(관측·실패번역)도 우회한다. registry javadoc은 그런 경로가 없다고 적는다 | 현재 미배선; fork가 배선하는 순간 발생 |
|
||
| **P3** | `MongoRegexPolicy.forbidden()`이 길이 1 정책이라 `^`(모든 문자열 매치)를 수용한다 | 필드가 REGEX operator를 등록한 경우 |
|
||
| **P3/기록** | query·aggregation·keyset 전부 미배선이고 배선된 `MongoBudgetEnforcer`는 소비자가 없다. README D1/D2 표는 typed query를 노출 계층 내용으로 제시하나 조립이 fork 몫이라는 사실은 적지 않는다 | 문서/조립 |
|
||
| **P3/기록** | keyset cursor 서명 키를 공급하는 production 경로 없음(생성자 호출은 test 3곳) | fork 배선 시점 |
|
||
|
||
## 46. Sub-scope 05 완료 조건
|
||
|
||
- denominator 29 / 29 FULL_READ (`132-...`)
|
||
- reachability·불변식·실행 scope·regex 정책·키 출처 5종 probe 수행
|
||
- 두 finding 모두 정적으로 결정 가능하여 실행 probe 불필요, 소스 미변경
|
||
|
||
---
|
||
|
||
## 47. Sub-scope 06 범위와 denominator
|
||
|
||
> 내부 상태: COMPLETE — **27 / 27 FULL_READ**
|
||
> 범위: `transaction/**` 20 (production, 1,617 LOC) + 전용 test 7
|
||
> 역할: body 재시도와 commit 재시도를 **서로 다른 루프**로 유지하는 것 — 이 leaf에서 가장 결과가 무거운 규칙
|
||
|
||
manifest와 probe: `evidence/raw/133-mongo-transaction-manifest-and-probes.txt`.
|
||
|
||
## 48. 설계의 중심 규칙이 실제로 구현돼 있다
|
||
|
||
`MongoTransactionRetryCoordinator`의 javadoc이 규칙과 그 대가를 함께 적는다.
|
||
|
||
> `TransientTransactionError` means nothing was committed, so the body may run again — from a new session… `UnknownTransactionCommitResult` means the commit may already have succeeded, so the body must **not** run again… Getting this wrong does not fail loudly. It produces a second order, a double refund, or a duplicate ledger entry — during a failover, when nobody is reading the logs.
|
||
|
||
구현은 그 규칙을 구조로 만든다.
|
||
|
||
- **두 루프.** `execute(...)`의 바깥 루프는 `MongoTransactionTransientException`에서만 `continue`하고, 매 시도마다 `sessions.open(profile)`로 **새 세션**을 연다. `commitWithRetry(...)`는 이미 계산된 `value`를 인자로 받아 그대로 반환하며, javadoc이 "nothing here may recompute it, because recomputing is indistinguishable from replaying"라고 못박는다.
|
||
- **Spring의 transaction 추상화를 쓰지 않는다.** `SpringMongoTransactionSessionFactory`가 이유를 적는다 — `MongoTransactionManager`와 `TransactionTemplate`은 callback이 반환되면 암묵적으로 commit하므로 body와 commit을 한 단계로 접는데, 설계 전체가 그 둘이 **다르게 실패하고 다르게 재시도된다**는 데 서 있다.
|
||
- **분류는 label이 살아 있는 경계에서 한다.** driver 실패는 session factory 안에서 분류되고, 위층 coordinator는 platform의 두 transaction 예외만 본다. `classify(...)`는 `Throwable`을 받는다 — Spring Data가 감싼 실패는 같은 label과 server code를 갖지만 다른 타입으로 도착해 분류를 통째로 건너뛰었고, 그래서 transient 오류가 terminal로 처리돼 재시도되지 않았다.
|
||
- **context를 scope에서 유도한다.** commit-unknown context를 먼저 만들고 classifier가 고른 예외로 감싸는 대신, scope가 `COMMIT_ONLY`면 commit-unknown context를, `WHOLE_TRANSACTION`이면 transient context를 만든다(§15의 두 예외 생성자 불변식과 맞물린다).
|
||
- **reactive도 같은 규칙.** `SpringReactiveMongoTransactionExecutor`는 body 재시도에서 caller의 publisher를 재구독하고 commit 재시도에서는 `commit()`만 재구독한다 — "re-subscribing a publisher is exactly how a reactive codebase replays work that may already have been committed". 정리(cleanup)도 phase-aware다: commit-unknown이면 `abort()`하지 않고 `release()`만 한다.
|
||
|
||
주변 결함 이력도 촘촘히 기록돼 있다.
|
||
|
||
- `startTransaction()` 실패 시 세션을 닫지 않아 시도마다 pool 항목이 샜다 → 이제 실패 경로에서 close하고 close 실패는 원인에 suppressed로 붙인다.
|
||
- `MongoTransactionScope.bind`가 `set`/`remove`였다 → 중첩 시 안쪽 `remove`가 바깥 body의 바인딩을 지워, 이후 `require()`가 실패하거나 평범한 template으로 fallback한 코드가 **transaction 밖에** 썼다. 지금은 이전 값을 복원한다.
|
||
- reactive executor가 budget 검사에 `Duration.ZERO.plusNanos(1)`을 넘겨 `maxElapsed`가 영원히 도달 불가였다 → 이제 주입 가능한 `LongSupplier nanoTime`으로 실제 경과를 잰다.
|
||
- `delayBefore`를 두 번 호출해 metric에 기록된 지연과 실제로 기다린 지연이 달랐다 → 한 번 계산해 재사용.
|
||
- `MongoRetryBudget.allowsAttempt`가 첫 시도에도 `elapsed < maxElapsed`를 요구해, `none()`(maxElapsed=0)이 body 자체를 거부했다 → 첫 시도는 재시도가 아니므로 무조건 허용.
|
||
|
||
`MongoTransactionProfile`은 secondary read profile을 생성자에서 거부하고 timeout이 서버의 `transactionLifetimeLimitSeconds`(기본 60초)를 넘지 못하게 한다.
|
||
|
||
## 49. Confirmed P2 — 이 subsystem 전체가 배선돼 있지 않은데, 그것을 켜는 flag는 startup 검사를 수행한다
|
||
|
||
`MongoPlatformAutoConfiguration`에서 `Transaction`/`CausalSession`/`RetryCoordinator`를 찾으면 **매치 0**이다(`133-...` §8.1, exit=1). transaction package 밖의 production 참조도 0이다. 즉 `MongoTransactionExecutor`·`MongoTransactionRetryCoordinator`·`SpringMongoTransactionSessionFactory`·causal session executor 어느 것도 bean이 아니고, 이 leaf의 다른 production 코드가 부르지도 않는다.
|
||
|
||
그런데 `MongoPlatformSettings.transactions`는 살아 있는 flag다. §6의 probe에서 `platform.transactions=true`가 그대로 bound되는 것을 확인했고, `MongoPlatformAutoConfiguration:362`가 그 값을 `MongoStartupValidator`에 넘기며, validator는 `transactionsEnabled && !capabilities.isStable(TRANSACTION)`이면 startup을 거부한다(`MongoStartupValidator:97`).
|
||
|
||
결과적으로 `ca-skeleton.persistence-mongo.platform.transactions=true`를 설정한 배포는 — topology probe와 나머지 startup 입력이 모두 갖춰졌다면 — **topology가 transaction을 지원하는지 검증받고, 그 다음 transaction을 실행할 bean은 하나도 받지 못한다.** flag는 capability 요구만 만들고 capability를 제공하지 않는다.
|
||
|
||
이것을 §6의 `changeStreams`와 나란히 놓으면 대비가 분명하다. change stream은 실행체가 없다는 사실을 인정하고 flag 값을 강제로 `false`로 만든다(그 방식의 문제는 §6에서 따로 지적했다). transaction은 실행체가 없는데 flag는 살아서 startup 요구를 만든다. 같은 상황에 대해 두 가지 다른 처리가 한 record 안에 있다.
|
||
|
||
**판정: P2.** 데이터 위험은 없다 — 없는 것을 쓸 수는 없다. 위험은 운영자의 기대다. 수정은 셋 중 하나다: transaction executor를 조건부 bean으로 조립하거나, flag가 무엇을 켜는지(=startup 검증만) 문서에 적거나, `changeStreams`처럼 명시적으로 거부하거나. 셋 중 어느 것도 지금은 되어 있지 않다.
|
||
|
||
## 50. Negative-space probes — sub-scope 06
|
||
|
||
- **8.1 reachability**: 배선 0, cross-package 참조 0(§49).
|
||
- **8.1b flag ↔ 조립 불일치**: §49. `transactions`는 검증만 만들고, `changeStreams`는 값을 삼키며, 둘 다 실행체가 없다.
|
||
- **8.2 규칙 검증**: 두 루프의 분리를 코드 구조로 확인(§48). blocking·reactive 양쪽 모두.
|
||
- **8.3 scope 바인딩**: 중첩 bind가 복원 방식인지 확인. 두 개의 `ThreadLocal`이 존재한다 — `MongoTransactionScope.CURRENT`와 `SpringMongoCausalSessionExecutor.CURRENT` — 서로 독립이고 각자의 `require*()`를 갖는다. causal session 안에서 transaction scope를 물으면 "no MongoDB transaction is active"가 나오고 그 반대도 마찬가지다. 의도된 분리로 보이나 두 scope가 겹칠 때 어느 쪽 operations를 써야 하는지에 대한 계약은 어디에도 없다. P3/기록.
|
||
- **8.4 profile 경계**: 60초 서버 한계와 secondary read 거부 확인.
|
||
|
||
## 51. Sub-scope 06 findings backlog
|
||
|
||
| 우선순위 | finding | reachability |
|
||
|---|---|---|
|
||
| **P2** | transaction subsystem 전체가 미배선(bean 0, cross-package 참조 0)인데 `platform.transactions=true`는 startup에서 TRANSACTION capability를 요구한다 — 요구만 만들고 제공하지 않는 flag | flag를 켠 모든 배포 |
|
||
| **P3/기록** | `MongoTransactionScope`와 `SpringMongoCausalSessionExecutor`가 각자 독립된 `ThreadLocal`을 갖고, 두 scope가 중첩될 때 어느 operations가 유효한지에 대한 계약이 없다 | fork가 둘을 함께 배선할 때 |
|
||
|
||
## 52. Sub-scope 06 완료 조건
|
||
|
||
- denominator 27 / 27 FULL_READ (`133-...`)
|
||
- reachability·flag 정합·규칙 구조·scope 바인딩·profile 경계 5종 probe 수행
|
||
- 두 재시도 루프의 분리, 세션 수명, 실패 분류 경계를 blocking·reactive 양쪽에서 코드로 추적
|
||
- 소스 미변경
|
||
|
||
---
|
||
|
||
## 53. Sub-scope 07 범위와 denominator
|
||
|
||
> 내부 상태: COMPLETE — **58 / 58 FULL_READ**
|
||
> 범위: `schema/**` 30 + `migration/**` 19 (production 49, 3,124 LOC) + 전용 test 9
|
||
> 역할: collection의 index·validator·문서 모델을 **선언**으로 만들고, migration을 lease와 ledger 위에서 한 번만 돌게 한다
|
||
|
||
manifest와 정적 probe: `evidence/raw/134-mongo-schema-migration-manifest-and-probes.txt`.
|
||
실행 probe: `evidence/raw/134a-mongo-schema-migration-execution-probes.txt`.
|
||
|
||
## 54. 설계의 두 축 — 선언이 진실이고, 적용은 D4다
|
||
|
||
`MongoCollectionManifest`의 javadoc이 첫 번째 축을 적는다.
|
||
|
||
> Deliberately not derived from annotations. Spring Data's `@Indexed` can create an index as a side effect of a class being on the classpath, which means production index state depends on deployment order and on which module happened to be loaded.
|
||
|
||
그래서 index·validator·문서 모델이 전부 명시적 선언이고, 검증은 **집합이 다 모인 뒤에** `MongoManifestRegistry`에서 일어난다 — collection 이름 중복, 한 collection 안의 index 이름 중복, 문서 모델의 budget 초과는 선언 시점에는 조용하고 비교 시점에만 보이기 때문이다. `MongoIndexManifest`가 `expectedUsage`를 **APPLICATION 소유일 때 필수로** 요구하는 것도 같은 계열이다: "an index nobody can name a query for cannot be reviewed for removal later".
|
||
|
||
두 번째 축은 적용 권한이다. `MongoIndexApplyPolicy`는 APPLY → APPLY_WITH_DIFF → DIFF_WITH_APPROVED_APPLY → REPORT_ONLY 사다리를 두고 production에서 runtime의 index 변경을 금지한다. `MongoValidatorApplyPolicy.runtimeMayApply()`는 **항상 false**다 — validator 변경은 이후 모든 write의 수용 규칙을 다시 쓰므로 D4다. `MongoIndexRetirementState`는 DEPRECATED → USAGE_OBSERVED → HIDDEN → REGRESSION_CHECKED → APPROVED → DROPPED를 한 칸씩만 전진시키고, `successor()`를 ordinal이 아니라 switch로 적는 이유까지 남긴다("an ordinal-based successor silently changes meaning the moment someone inserts a constant, and this sequence is a safety procedure").
|
||
|
||
문서 모델 쪽도 촘촘하다. `MongoDocumentSizeBudget`은 MongoDB의 16 MiB 한계가 아니라 그 1/4인 4 MiB를 상한으로 강제한다 — "the write that fails is the first symptom". `MongoDocumentModelValidator`는 위반을 전부 모아서 한 번에 던진다("a modelling review that surfaces one problem per run turns a five-minute fix into five rounds"). `EmbeddedCollectionDescriptor.unbounded()`는 **거부되기 위해** 존재한다 — "우리는 모른다"를 생략이 아니라 기록으로 표현하게 한다. `worstCaseDocumentBytes()`는 overflow 대신 포화한다("a silent wraparound would turn 'infinitely large' into 'comfortably small'").
|
||
|
||
`MongoValidatorApplyPolicy`의 `CERTIFIED_RELEASE_LINES`에는 이미 한 번 고쳐진 결함이 주석으로 남아 있다: 과거의 `Set.of("7.0","8.0").contains(serverVersion)`은 서버가 `"8.0.4"`를 보고하므로 **모든 실제 배포에서 false**였다 — "the certified lane was a lane nothing was ever in". 지금은 `MongoServerVersion.parse`로 major/minor를 비교한다(§18.1에서 본 `MongoServerVersion`의 유일한 production 소비자가 바로 이 줄이다).
|
||
|
||
## 55. migration은 fencing을 정면으로 다룬다
|
||
|
||
`MongoMigrationLock.fence()`의 javadoc이 이 sub-scope에서 가장 정확한 문장을 담고 있다.
|
||
|
||
> A lease expiring is not the same as its holder stopping. A runner paused inside a long `execute` — a stop-the-world pause, a stalled network write — loses the lease on the server while its thread is still alive and still writing… **Refreshing more often does not fix that: the first runner is not running at the moment it would refresh.**
|
||
|
||
그래서 lease 위에 monotonic fencing token을 얹고, `MongoCollectionMigrationLock.tryAcquire`가 그 token을 **lease를 부여하는 같은 조건부 update 안에서 서버가 증가**시킨다("A token handed out anywhere else could be handed out twice"). `held()`는 owner 이름이 같아도 fence가 다르면 false를 반환한다 — 프로세스가 재시작했거나 운영자가 owner 문자열을 재사용한 경우다. `matchedCount`를 쓰는 이유(같은 값을 다시 쓰면 `modifiedCount`가 0이라 소유권 판정이 뒤집힌다)도 두 곳에 적혀 있다.
|
||
|
||
`MongoMigrationHeartbeat`은 이미 고쳐진 결함의 산물이다: runner가 `execute`가 **반환된 뒤에** 한 번만 refresh했으므로, 40분짜리 `execute`는 35분 동안 만료된 lease를 들고 있었고 그 사이 두 번째 runner가 정당하게 획득해 같은 migration을 동시에 돌렸다. 이제 heartbeat이 migration에게 넘겨진다 — batch 경계를 아는 것은 migration뿐이기 때문이다.
|
||
|
||
`MongoCollectionMigrationLedger.saveCheckpoint`에는 **두 개의** 결함 이력이 주석으로 남아 있다. upsert 하나로는 "매치할 게 없었다"와 "fence filter가 배제했다"를 구분할 수 없어 *모든 migration의 첫 checkpoint*가 "a newer migration runner owns the lease"로 거부됐고, 동시에 진짜 배제 경로는 unique index의 duplicate-key로 죽어 그 문장을 만드는 분기가 **도달 불가**였다. 지금은 replace-then-insert로 두 경우를 분리한다.
|
||
|
||
`MongoMigration`에 `rollback`이 없는 것도 명시적 결정이다 — "A rollback method implies the reverse operation is always safe and always possible, and for a backfill that dropped a column's old values it is neither." 실패한 production 변경은 forward-fix migration으로 고친다.
|
||
|
||
`mongoMigrationTest` lane은 HEAD에서 green이다: 1 class / **8 tests** / 0 failures (`134a-...` §8.4b).
|
||
|
||
## 56. P2 — `recordApplied`는 문서화된 fence 계약을 구현하지 않고, 보호를 역전시킨다
|
||
|
||
`MongoMigrationLedger.recordApplied`의 javadoc은 계약을 분명히 적는다.
|
||
|
||
> Records a completed migration, **only if the fence is still the current one**… A ledger entry from a superseded runner says a migration completed when the work it describes was overwritten by the runner that replaced it.
|
||
> `@throws MongoOperationRejectedException` when a newer acquisition exists
|
||
|
||
구현은 그렇지 않다. `MongoCollectionMigrationLedger.recordApplied:93`은 `requireCurrentFence(fence, …)`를 부르는데, 그 메서드가 하는 검사는 **`fence == UNFENCED`인지 하나뿐**이다(`134-...` §8.2). 저장된 fence와의 비교도, 서버측 조건도 없고, fence는 그냥 문서의 한 필드로 들어간다. 이름이 하는 말("current")과 코드가 하는 일("fenced")이 다르다. `FlamingockLedgerAdapter.recordApplied`는 fence 인자를 아예 무시한다.
|
||
|
||
실제 서버(MongoDB 8.0 replica set)에서 확인했다(`134a-...` PROBE A). live runner가 fence 5로 checkpoint `o-900`을 쓴 상태에서 fence 1을 든 superseded runner가 두 번 쓴다.
|
||
|
||
```
|
||
PROBE saveCheckpoint(fence=1 over stored 5) -> REFUSED MongoOperationRejectedException
|
||
PROBE recordApplied(fence=1 over stored 5) -> ACCEPTED
|
||
PROBE ledger entry now = { migrationId=20260829-001, checksum=superseded,
|
||
operator=stale-runner, fence=1 }
|
||
PROBE recordApplied(live fence=5, after stale wrote) -> REFUSED MongoWriteException:
|
||
E11000 duplicate key error … index: migrationId_1
|
||
```
|
||
|
||
같은 fence 계약이 `saveCheckpoint`에서는 지켜지고 `recordApplied`에서는 지켜지지 않는다. 결과는 단순한 누락이 아니라 **역전**이다 — 밀려난 runner가 ledger를 차지하고, 실제로 작업한 runner는 platform의 lease 문장 대신 driver의 duplicate-key 예외를 받는다. 그리고 이것은 이 파일이 `saveCheckpoint`에서 **이미 한 번 고친 바로 그 형태**다(§55: "a superseded runner got a driver-level duplicate-key error instead of the sentence written for it"). 수정이 한쪽에만 적용됐다.
|
||
|
||
**도달성.** 조립된 경로에서는 `MongoMigrationRunner.applyOne`이 `recordApplied` **직전에** `lock.refresh(...)`를 부르고, `MongoCollectionMigrationLock.refresh`는 owner+fence 조건부라 stale이면 던진다. 그래서 기본 조합에서는 인접한 다른 장치가 막아 준다 — 다만 (a) refresh와 insert 사이에 TOCTOU 창이 남고, (b) 그 보호는 `MongoCollectionMigrationLock`을 쓸 때만 존재하며, (c) `MongoMigrationLedger`는 fork가 구현하도록 공개된 인터페이스인데 그 인터페이스가 약속하는 보호는 어느 구현에도 없다.
|
||
|
||
**판정: P2.** 수정은 `saveCheckpoint`와 같은 모양이다 — `recordApplied`도 저장된 fence를 조건으로 삼고, duplicate-key를 잡아 platform 예외로 번역하는 것. 지금은 test도 이 경계를 보지 않는다: `MongoMigrationFencingTest.ledgerWritesCarryTheirFence`는 fence 값이 **전달되는지**만 보고, `MongoMigrationLaneTest`의 superseded 테스트는 checkpoint만 다룬다.
|
||
|
||
## 57. P2 — index diff가 실제로 비교하는 것은 두 필드뿐이다
|
||
|
||
`MongoIndexManifest`는 14개 요소를 선언한다 — keys, unique, sparse, hidden, deprecated, partialFilterExpression, collationProfile, **expireAfter**, wildcardProjection, shardKeySupport, expectedUsage, owner, metadataOwnership. `MongoIndexDescriptorView`는 6개만 나르고, `MongoIndexDiffEngine.compare`가 실제로 비교하는 것은 **`keySignature`와 `unique` 두 개**다(`134-...` §8.2b, grep 결과 49–50행이 전부).
|
||
|
||
게다가 `hidden`은 **한 방향으로만** 본다: `declared.hidden() && !actual.hidden()`(55행). 반대 — 서버에서는 숨겨져 있는데 manifest는 보인다고 선언한 index — 에 해당하는 분기가 없다. 그것은 planner가 manifest가 살아 있다고 적은 index를 **쓰지 않고 있는** 상태이고, 정확히 은퇴 워크플로가 HIDDEN에 세워 둔 index를 다시 살리기로 한 뒤에 생기는 상태다.
|
||
|
||
hermetic probe로 확인했다(`134a-...` PROBE B). 선언은 `ix_ttl`(expireAfter=30일)과 `ix_active`(sparse + partialFilter + collation, 보임), 서버는 같은 이름·같은 키·같은 uniqueness에 `ix_active`만 숨겨져 있다.
|
||
|
||
```
|
||
PROBE diff.isClean() -> true
|
||
PROBE diff.render() -> [] (빈 문자열)
|
||
```
|
||
|
||
TTL 보존기간 변경, sparse/partialFilter/collation 변경, 그리고 "서버에서 숨겨진 채 선언은 보임"이 **전부 drift 없음**으로 렌더링된다. 이 중 TTL이 가장 무겁다 — 30일을 1일로 바꾸는 것은 대량 삭제이고, drift 보고서는 그것을 clean이라고 말한다.
|
||
|
||
`MongoIndexDescriptorView`의 javadoc이 "reduced to the fields a diff can compare"라고 스스로 한정하는 것은 사실이지만, 그 축소의 **결과**(무엇이 감지 불가가 되는지)는 어디에도 적혀 있지 않고, `MongoIndexDiff.render()`가 CI artifact로 쓰이도록 설계돼 있으므로 "빈 보고서 = 일치"로 읽힌다. **판정: P2.** 최소 수정은 `MongoIndexDescriptorView`에 `expireAfter`와 `sparse`를 추가하고 `compare`에서 비교하는 것, 그리고 `actual.hidden() && !declared.hidden()`에 대한 `unhide` 항목을 두는 것이다. 그것이 과하다면 최소한 비교 대상 필드 집합을 diff 출력에 함께 적어 "빈 보고서"가 무엇을 뜻하는지 읽는 사람이 알 수 있게 해야 한다.
|
||
|
||
## 58. P3 — TTL이 두 곳에 선언되고, 규칙을 가진 쪽은 아무도 쓰지 않는다
|
||
|
||
TTL을 표현하는 방법이 이 sub-scope 안에 둘 있다.
|
||
|
||
1. `MongoIndexManifest.expireAfter(Duration)` — 검증은 생성자의 `isNegative()` 하나.
|
||
2. `MongoTtlPolicy` / `MongoTtlIndexDescriptor` + `MongoTtlPolicyValidator` — 세 가지 실질 규칙: 최소 보존기간 1분(그 아래는 한 번의 sweep으로 전체 population을 지운다), expiry 필드의 BSON 타입이 `date`인지(아니면 MongoDB가 **조용히 무시**한다), 그리고 읽기가 `expiresAt > applicationNow`를 거는지(TTL monitor는 임의 간격으로 돌므로 만료된 문서는 그때까지 계속 읽힌다).
|
||
|
||
둘 사이에 참조가 **하나도 없다**(`134-...` §8.3: `schema/ttl` 밖의 production 참조 검색 exit=1). `MongoIndexManifest.isTtlIndex()`와 `ttl()`은 선언부 말고 호출자가 아예 없다. 그래서 manifest 경로로 선언된 TTL index는 위 세 규칙 중 어느 것도 통과하지 않는다. probe로 확인:
|
||
|
||
```
|
||
PROBE MongoIndexManifest.expireAfter(1s) built -> PT1S isTtlIndex=true
|
||
```
|
||
|
||
`MongoTtlPolicyValidator.MINIMUM_SAFE_RETENTION`이 1분인데, manifest는 1초를 그대로 만든다. 그리고 `schema/ttl`의 네 타입은 이 leaf의 production 어디에서도 쓰이지 않는다 — 규칙을 가진 표현은 아무도 안 쓰고, 쓰이는 표현은 규칙이 없다. **P3.** (지금 결함이 아닌 이유는 §59와 같다: manifest를 조립하는 production 코드 자체가 없다. fork가 조립하는 순간 결함이 된다.)
|
||
|
||
## 59. P3 — Flamingock lease로는 어떤 migration도 실행할 수 없고, javadoc은 다르게 적는다
|
||
|
||
`FlamingockLockAdapter.fence()`는 `UNFENCED`(-1)를 반환하고, 그 이유를 정직하게 적는다 — 로컬 카운터로 fencing을 흉내내면 "look like fencing and protect nothing". 여기까지는 옳다. 문제는 그 다음 문장이다.
|
||
|
||
> The runner refuses **resumable** migrations under an unfenced lease for exactly this reason.
|
||
|
||
`MongoMigrationRunner.apply:82`의 검사는 stream보다 **앞에** 있고 migration의 성질을 보지 않는다. hermetic probe에서 checkpoint를 만들지 않는(=resumable이 아닌) migration을 넣어 확인했다(`134a-...` PROBE C).
|
||
|
||
```
|
||
PROBE FlamingockLockAdapter.fence() = -1
|
||
PROBE runner.apply(non-resumable migration, Flamingock lease) -> REFUSED
|
||
MongoOperationRejectedException: this migration lease exposes no fencing token …
|
||
```
|
||
|
||
즉 engine-agnostic 경로 전체 — Mongock을 새 프로젝트에서 채택하지 않겠다는 결정을 되돌릴 수 있게 만들어 둔 그 경계 — 는 `MongoMigrationRunner`를 통해 **아무것도 실행할 수 없다**. `FlamingockMongoMigrationAdapterTest`도 이 조합을 시험하지 않는다(adapter lock으로 `apply`를 부르는 테스트가 없다). **P3.** 수정은 둘 중 하나다: javadoc을 실제 동작("every migration")에 맞추거나, unfenced lease에서 non-resumable migration을 허용하도록 검사를 옮기거나. 전자가 정직하고 후자는 별도 판단이 필요하다.
|
||
|
||
## 60. Confirmed — 이 sub-scope도 선언 라이브러리이고, ledger의 유일성 장치는 production에서 만들어지지 않는다
|
||
|
||
auto-configuration이 `schema/**`·`migration/**`에서 만드는 bean은 **0개**다(`134-...` §8.1: `MongoPlatformAutoConfiguration`에서 걸리는 것은 `api.mapping.MongoTypeRepresentationManifest`와 `api.schema.MongoSchemaVersionRange`뿐 — 둘 다 sub-scope 02 소속). 그리고 정책 계층은 소비자조차 없다:
|
||
|
||
| 타입 | production 소비자 |
|
||
|---|---|
|
||
| `MongoIndexApplyPolicy`, `requireRuntimeApplyAllowed` | **0** (test 1곳) |
|
||
| `MongoValidatorApplyPolicy` | **0** (test 2곳) |
|
||
| `MongoIndexDiffEngine`, `MongoValidatorDiffEngine` | **0** (`new`는 test에서만) |
|
||
| `MongoTtlPolicyValidator` 외 `schema/ttl` 4종 | **0** |
|
||
| `MongoManifestRegistry` | 1 — `geo/SpringMongoGeospatialOperations`(그 자체가 미배선, §26) |
|
||
| `MongoMetadataOwnership` | `advanced/encryption/qe`, `advanced/search`(sub-scope 10) |
|
||
| `MongoMigrationCheckpoint` | `advanced/tenancy/database` 2개(sub-scope 10) |
|
||
| `MongoMigrationRunner`/`Ledger`/`Lock` | **0** |
|
||
|
||
즉 D4 admin plane의 "runtime은 index/validator를 바꿀 수 없다"는 규칙은 현재 **runtime이 그 코드를 부르지 않는 방식으로** 지켜지고 있다. 사다리는 만들어져 있고 올라서는 사람이 없다.
|
||
|
||
한 가지는 따로 적어 둘 만하다. `MongoCollectionMigrationLedger.ensureIndexes()` — javadoc이 "The unique index on the migration id is the part that matters"라고 말하고, 실제로 §56의 duplicate-key도 그 index가 만든 것이다 — 를 부르는 곳은 **test 6곳뿐**이다(`134-...` §8.3c). 생성자와 분리한 이유는 명시돼 있다("a ledger that silently creates indexes on first use is the auto-index-creation behaviour the platform refuses everywhere else"). 옳은 결정이지만, 그 결과 ledger의 중복 방지는 fork가 admin plane에서 명시적으로 만들어 줘야 성립하는 전제가 되고, 그 전제는 `MongoMigrationRunner`나 module README 어디에도 적혀 있지 않다. 만들지 않은 채 운영하면 §56의 경합은 duplicate-key 예외조차 없이 **두 개의 ledger 항목**으로 끝난다. P3/기록.
|
||
|
||
## 61. Negative-space probes — sub-scope 07
|
||
|
||
- **8.1 reachability**: bean 0, 정책 계층 소비자 0(§60). cross-package 소비자는 geo·advanced 계열뿐이고 그중 geo는 미배선.
|
||
- **8.2 계약 ↔ 구현 대조**: `recordApplied`의 javadoc 계약과 두 구현(§56). 실서버 실행 probe로 확정.
|
||
- **8.2b 비교 필드 집합**: 선언 14 vs 관측 6 vs 실제 비교 2(§57). hermetic 실행 probe로 확정.
|
||
- **8.2c 조건부 형제**: `hidden`이 한 방향만 비교됨(§57). `saveCheckpoint`는 fence 조건부인데 `recordApplied`는 아님(§56) — 같은 파일 안의 형제 비교.
|
||
- **8.3 중복 메커니즘**: TTL 두 표현(§58), ledger 두 구현·lock 두 구현(§56·§59), `ensureIndexes` 호출자 부재(§60).
|
||
- **8.4 문서/개수 drift**: `mongoMigrationTest` lane은 build.gradle:119에 존재하고 tag는 `mongodb-migration`, HEAD에서 1 class / 8 tests / 0 failures. module README에는 manifest·runner 언급 없음. `docs/superpowers/plans/…-implementation-plan.md`는 이 코드를 `modules/mongodb/mongodb-migration-core` 아래 별도 모듈로 적고 있으나 실제 위치는 단일 leaf 안의 package다(§0의 모듈 배치 drift와 같은 계열).
|
||
|
||
## 62. Sub-scope 07 findings backlog
|
||
|
||
| 우선순위 | finding | reachability |
|
||
|---|---|---|
|
||
| **P2** | `MongoMigrationLedger.recordApplied`의 javadoc은 fence 조건부 쓰기와 `MongoOperationRejectedException`을 약속하지만, `MongoCollectionMigrationLedger`는 `UNFENCED`만 검사하고 `FlamingockLedgerAdapter`는 fence를 무시한다. 실서버 probe에서 밀려난 runner가 ledger를 차지하고 live runner가 driver duplicate-key를 받는다 | runner 경로는 인접한 `lock.refresh`가 막아 줌(TOCTOU 창 존재); ledger를 직접 쓰거나 다른 lock 구현을 쓰는 fork는 무방비 |
|
||
| **P2** | index diff가 비교하는 것은 `keySignature`·`unique` 둘뿐이라 TTL 보존기간·sparse·partialFilter·collation 변경과 "서버에서 숨겨짐 + 선언은 보임"이 전부 clean으로 보고된다 (probe: `isClean()=true`, `render()=""`) | drift 보고서를 CI artifact로 쓰는 모든 배포 |
|
||
| **P3** | TTL이 `MongoIndexManifest.expireAfter`와 `MongoTtlPolicy` 두 곳에 있고 서로 참조가 없다. 규칙(최소 1분·BSON date·읽기 술어)을 가진 쪽은 production 소비자 0, 쓰이는 쪽은 `isNegative()`만 본다 (probe: 1초 TTL이 그대로 생성됨) | fork가 manifest를 조립하는 시점 |
|
||
| **P3** | `FlamingockLockAdapter`의 javadoc은 runner가 "resumable migrations"만 거부한다고 적지만 실제로는 **모든** migration을 거부한다 — engine-agnostic 경로로는 아무것도 실행할 수 없다 (probe로 확인) | Flamingock 어댑터를 쓰려는 모든 시점 |
|
||
| **P3/기록** | `ensureIndexes()`(ledger의 유일성 장치)의 호출자가 test뿐이고, admin plane에서 만들어야 한다는 전제가 문서화돼 있지 않다 | 운영 배포 시점 |
|
||
| **P3/기록** | `schema`·`migration` 전체가 bean 0이고 apply policy·diff engine·TTL validator는 production 소비자 0. D4 규칙이 "runtime이 그 코드를 부르지 않는 방식"으로 지켜지고 있다 | 문서/조립 |
|
||
|
||
## 63. Sub-scope 07 완료 조건
|
||
|
||
- denominator 58 / 58 FULL_READ (`134-...` OWNED FILES)
|
||
- reachability·계약대조·비교필드집합·조건부형제·중복메커니즘·문서drift 6종 probe 수행
|
||
- 정적으로 결정 불가한 세 지점(recordApplied fence, index diff 사각지대, Flamingock lease)을 실행 probe로 확정(`134a-...`)
|
||
- 임시 probe class 2개 추가 후 제거, `git status --short` = 0 (`134a-...` 말미)
|
||
|
||
---
|
||
|
||
## 64. Sub-scope 08 범위와 denominator
|
||
|
||
> 내부 상태: COMPLETE — **26 / 26 FULL_READ**
|
||
> 범위: `changestream/**` 21 (production, 1,317 LOC) + 전용 test 5 (996 LOC)
|
||
> 역할: at-least-once change stream 소비 — 저장된 위치에서 열고, 순서대로 투영하고, **투영이 성공한 뒤에** 위치를 쓴다
|
||
|
||
manifest와 정적 probe: `evidence/raw/135-mongo-changestream-manifest-and-probes.txt`.
|
||
실행 probe: `evidence/raw/135a-mongo-changestream-execution-probes.txt`.
|
||
|
||
## 65. 이 sub-scope는 이 leaf에서 유일하게 "조립까지 된" 대형 서브시스템이다
|
||
|
||
앞선 sub-scope들과 다르다. `MongoPlatformAutoConfiguration`이 두 개의 bean을 실제로 만든다.
|
||
|
||
- `mongoChangeStreamSource`(209행) — `SpringReactiveChangeStreamSource`, 무조건.
|
||
- `reactiveMongoChangeStreamConsumer`(235행) — fork만 공급할 수 있는 5종(`MongoChangeStreamSubscription`, `MongoResumeCheckpointStore`, `MongoResumeTokenCodec`, `MongoChangeProjector`, `MongoChangeDeduplicationStore`)에 `@ConditionalOnBean`. pipeline·runner·recovery policy·invalidate recovery는 auto-configuration이 직접 `new`한다.
|
||
|
||
즉 fork가 설계가 요구하는 다섯 개를 그대로 제공하면 **완성된 소비자가 돈다**. 이 사실이 아래 §67의 심각도를 결정한다.
|
||
|
||
설계 자체는 이 leaf에서 가장 정교한 축에 속한다.
|
||
|
||
- **순서가 계약이다.** `MongoChangeStreamRunner`: 투영 먼저, checkpoint 나중. "Checkpointing first would mean a crash between the two loses the event permanently, with no trace." 그래서 중복을 택하고 중복을 제거한다.
|
||
- **claim은 3-state다.** 과거 `alreadyProjected` + `markProjected`(읽고-쓰기)는 동시에 `false`를 읽은 두 subscriber가 둘 다 투영했다 — "the deduplication that exists precisely because redelivery is guaranteed did not survive concurrency". 지금은 `CLAIMED`/`ALREADY_COMPLETED`/`BUSY`의 원자적 전이다.
|
||
- **빈 완료는 프로토콜 위반이다.** `Mono<Boolean>`이 empty로 완료되면 `flatMap`을 그냥 통과해 "투영도 checkpoint도 없이 아무도 문제를 보고하지 않는" 상태가 됐다. 이제 `switchIfEmpty(Mono.error(...))`로 잡는다.
|
||
- **identity는 재전달에 안정적이고 documentKey를 감춘다.** SHA-256, 구분자는 ASCII unit separator(0x1F) — namespace/clusterTime/operationType에 나타날 수 없으므로 필드 재배열로 다른 이벤트의 identity를 위조할 수 없다. 한 transaction이 같은 문서를 두 번 고치면 앞 네 필드가 모두 같아지므로 `txnNumber`+`lsid` discriminator를 추가로 넣는다 — 없으면 두 번째가 첫 번째의 재전달로 **버려진다**.
|
||
- **resume token은 절대 렌더링하지 않는다.** `MongoResumeCheckpoint.toString()`은 길이만 보고한다. token은 clusterTime과 documentKey를 인코딩하므로 로그에 찍는 순간 production write의 모양과 타이밍이 샌다.
|
||
- **`MongoResumeTokenCodec`에는 기본 구현이 없다.** "a built-in that merely encoded would be worse than none: it would satisfy the type and none of the reason for it."
|
||
- **`HISTORY_LOST`는 자동 복구하지 않는다.** "resuming from now… the projection then looks healthy and is quietly wrong, which is worse than a stopped consumer somebody has to look at."
|
||
- **`MongoClusterTime`은 숫자로 비교한다.** 텍스트 비교는 `1700000000.10`을 `1700000000.9`보다 앞에 놓는데, 그것은 바쁜 1초가 정확히 만드는 경우다.
|
||
|
||
## 66. Confirmed — `MongoChangeStreamPipeline`은 존재 이유가 명확한 클래스다
|
||
|
||
javadoc이 자신이 고친 결함을 적는다: runner가 이벤트당 `runOne`만 노출하고 순서를 아무도 소유하지 않았으므로, 평범하게 `flatMap`으로 구독한 caller는 A가 투영 중일 때 B·C를 동시에 날렸고 각자 완료 시 checkpoint를 전진시켰다. B의 checkpoint 뒤 A 완료 전에 프로세스가 죽으면 resume 위치는 이미 A를 지나쳤다 — "**A was lost permanently and nothing recorded that it had been.**"
|
||
|
||
`concatMap`이 그 순서를 파이프라인의 성질로 만든다. 그리고 그 위에 high-water mark를 얹어 뒤로 가는 checkpoint를 막는다. 두 장치 모두 의도가 옳다.
|
||
|
||
## 67. P1 — high-water mark가 재전달된 이벤트를 삼켜, failover 중이던 변경이 조용히 영구 소실된다
|
||
|
||
`MongoChangeStreamPipeline.processOne`은 이벤트를 받자마자 `advancesPosition(event.clusterTime())`을 부르고, 그 메서드는 `getAndAccumulate`로 **mark를 먼저 전진시킨 뒤** 전진 여부를 반환한다(49·58–63행). 즉 mark는 "**투영이 완료된 위치**"가 아니라 "**본 적 있는 위치**"다. 그리고 `ReactiveMongoChangeStreamConsumer.recoverFrom`은 resume 시 `Flux.defer(this::openAndConsume)`로 **같은 pipeline 인스턴스**를 다시 쓴다(199행) — mark는 그대로 남는다.
|
||
|
||
이 둘이 만나면, `MongoChangeStreamPipeline`이 고쳤다고 적은 바로 그 손실이 다른 경로로 돌아온다.
|
||
|
||
**실행 probe C**(`135a-...`) — worker 하나, dedup은 항상 claim을 내준다(BUSY 없음). stream 1이 E(clusterTime 5.1)를 내보내고 projector가 200ms를 쓰는 동안, 50ms 시점에 primary가 내려앉는다(`errorLabels=[ResumableChangeStreamError]`, code 133). stream 2는 서버가 resume했을 때 보낼 것 — checkpoint가 E를 지나친 적이 없으므로 E를 재전달하고, 이어서 F(6.1)를 보낸다.
|
||
|
||
```
|
||
PROBE-C terminal=COMPLETED opens=2
|
||
PROBE-C projector started=2 completed=1
|
||
PROBE-C results=[MongoChangeProjectionResult[outcome=APPLIED, detail=]]
|
||
PROBE-C checkpoints saved=[token-6]
|
||
PROBE-C highWaterMark=6.1
|
||
PROBE-C state=RUNNING runbook=
|
||
```
|
||
|
||
E의 투영은 시작됐다가 failover에 취소됐다. resume 후 재전달된 E는 **pipeline이 삼켰다** — mark가 E의 첫 전달 때(투영 전에) 이미 5.1로 올라갔기 때문이다. 그 다음 F가 투영되고 checkpoint가 token-6으로 저장되면서, 저장 위치는 E를 지나쳤다. change stream은 checkpoint가 지나친 것을 다시 보내지 않는다. **E는 영구히 사라졌고, 구독은 `RUNNING`에 runbook은 비어 있고, caller의 `Flux`는 정상 완료한다.**
|
||
|
||
같은 손실이 다른 두 경로로도 확인된다.
|
||
|
||
- **probe A**: E가 BUSY(다른 worker가 claim 보유)로 checkpoint 없이 지나간 뒤 resumable 실패 → resume → E 재전달 → 삼켜짐 → F가 checkpoint를 E 너머로 옮김. `token-5 projected? false ; checkpoint moved past it? true`.
|
||
- **probe B**: **실패도 resume도 없이**. 하나의 정상 stream에서 E가 BUSY, 이어서 F가 성공. `checkpoints saved=[token-6]` — E의 checkpoint는 안 썼는데 F의 checkpoint가 E를 지나쳤다. `MongoChangeProjectionResult.busy()`의 javadoc이 명시한 불변식 — "The checkpoint must not advance past it: the holder may still fail, and a checkpoint that has passed the event is a change the stream will never replay" — 을 **바로 다음 이벤트가** 깬다. runner는 그 불변식을 지키고, pipeline이 무효화한다.
|
||
|
||
**왜 test가 못 잡았나.** 세 테스트가 각각 절반씩 본다. `MongoChangeStreamRunnerTest.aBusyClaimNeverAdvancesTheCheckpoint`는 이벤트 **하나**만 돌려서 "그 이벤트의 checkpoint가 안 써졌다"까지만 본다. `MongoChangeStreamPipelineTest.anEventBehindTheHighWaterMarkIsDropped`는 늦은 이벤트를 버리는 것이 옳다고 단언하는데, 그 시나리오의 늦은 이벤트는 **이미 완료된** 위치 뒤에 있고, checkpoint store는 `NoOpCheckpoints`라 상호작용이 보이지 않는다. `ChangeStreamConsumerLifecycleTest.aResumableFailureReopensFromTheCheckpoint`는 첫 stream을 `Flux.error(...)`로 시작해 **이벤트를 하나도 전달하지 않고** 실패시키므로 mark가 설정되지 않는다. "본 적 있지만 완료되지 않은 위치"라는 제3의 상태가 어느 테스트에도 없다.
|
||
|
||
**판정: P1.** 조립된 bean에서, 특별한 전제 없이(worker 하나, 평범한 failover), 조용하고 영구적인 변경 소실이 일어나고 시스템은 스스로를 정상이라고 보고한다. 수정 방향은 mark의 의미를 "본 위치"에서 "**checkpoint가 저장된 위치**"로 바꾸는 것이다 — `runOne`이 `allowsCheckpointAdvance()`인 결과를 낸 뒤에만 mark를 올리고, `CLAIMED_ELSEWHERE`/`PARKED`가 나온 위치에서는 mark를 멈춘 채 이후 이벤트의 checkpoint 저장도 그 위치를 넘지 못하게 하는 것(= checkpoint를 순서대로만 전진시키는 것). 최소 수정만으로도 probe C는 막힌다: resume 시 pipeline의 mark를 저장된 checkpoint 위치로 되돌리면 된다.
|
||
|
||
## 68. P2 — `changeStreams` flag는 `false`로 고정돼 있는데, 소비자 bean은 그것과 무관하게 조립된다
|
||
|
||
`MongoPlatformSettings`의 compact 생성자가 `changeStreams = false`를 강제하고(55행), 그 주석은 이렇게 적는다.
|
||
|
||
> The driver-side source — watch, resumeAfter/startAfter, cursor lifetime, reconnection — **is not shipped**; what exists is policy and value objects that do not add up to a running consumer… so the value is refused rather than stored: **zero beans, zero threads**.
|
||
|
||
HEAD에서 그 전제는 더 이상 사실이 아니다. driver-side source는 `SpringReactiveChangeStreamSource`로 **출하돼 있고**(auto-configuration의 무조건 bean), 완전한 소비자도 조립된다(§65). 주석은 이 코드가 존재하기 전 상태를 서술한다.
|
||
|
||
결과는 §49의 transaction과 정확히 **거울상**이다.
|
||
|
||
| | flag | startup capability 검사 | 실행체 |
|
||
|---|---|---|---|
|
||
| `transactions` | 살아 있음 | `TRANSACTION` 요구 | **bean 0** |
|
||
| `changeStreams` | **강제 false** | 절대 실행 안 됨 | **bean 조립됨** |
|
||
|
||
`MongoStartupValidator:104`의 `changeStreamsEnabled && !capabilities.isStable(CHANGE_STREAM)` 검사는 좌항이 영구히 false이므로 도달 불가다. 그래서 change stream을 지원하지 않는 topology(standalone 등)에 완성된 소비자를 배포해도 startup은 통과한다. 실패는 stream을 여는 시점에 driver 오류로 나타나고, `MongoChangeStreamRecoveryPolicy.onFailure`가 그것을 `FAILED` + `docs/mongodb/runbooks/failover.md`로 분류한다 — failover runbook은 "이 topology에는 change stream이 없다"를 설명하지 않는다.
|
||
|
||
**판정: P2.** 수정은 셋 중 하나다: `changeStreams`를 실제 flag로 되살려 소비자 조립의 조건으로 쓰거나, 소비자 bean이 조립될 때 CHANGE_STREAM capability를 startup에서 검증하거나, 최소한 `MongoPlatformSettings`의 주석을 현재 사실("source는 출하됐고 소비자도 조립된다")로 고치는 것. 지금 주석은 운영자가 읽으면 틀린 결론에 도달한다.
|
||
|
||
## 69. P3 — recovery package에 쓰이는 어휘와 쓰이지 않는 어휘가 나란히 있다
|
||
|
||
`135-...` §8.3의 검색 결과를 정리하면, 소비자가 실제로 쓰는 것과 아닌 것이 갈린다.
|
||
|
||
| 타입/메서드 | production 호출 |
|
||
|---|---|
|
||
| `MongoChangeStreamRecoveryPolicy.onFailure` | 1 (소비자) |
|
||
| `MongoInvalidateRecovery.requireCorrectResumeOption` | 1 (소비자) |
|
||
| `onHistoryLost`, `onResumableFailure`, `onInvalidate` | **0** (test만) |
|
||
| `MongoInvalidateRecovery.checkpointFor` | **0** — 소비자는 `tokens.encode(..., START_AFTER)`로 직접 만든다 |
|
||
| `MongoChangeStreamState.autoResumable()` | **0** — 소비자는 `decision.autoResume()`을 쓴다 |
|
||
| `MongoChangeHistoryLostException` | **0 — 어디에서도 생성되지 않는다** |
|
||
|
||
마지막 항목이 가장 무겁다. 이 예외의 javadoc은 왜 전용 타입이어야 하는지를 설명한다("the recovery is a business decision, not a technical one"). 그런데 실제로 history lost가 감지되면(`onFailure` → server code 286/280) 소비자는 state를 `HISTORY_LOST`로 놓고 **driver의 원본 예외를 그대로 재방출**한다. lifecycle test가 그것을 고정한다: `verifyError(MongoQueryException.class)`. 그래서 caller가 `catch (MongoChangeHistoryLostException)`로 이 상황을 구분하려 하면 절대 잡히지 않는다.
|
||
|
||
그리고 소비자의 유일한 `requireCorrectResumeOption` 호출은 **자기 자신과 비교한다**(`ReactiveMongoChangeStreamConsumer:119`: `requireCorrectResumeOption(checkpoint, checkpoint.position())`). probe D로 확인했다 — 이 호출 형태는 구조적으로 던질 수 없고, 다른 `intended`를 넘기는 production 호출은 없다. 안전장치처럼 읽히지만 검사하는 것이 없다. **P3.**
|
||
|
||
## 70. Negative-space probes — sub-scope 08
|
||
|
||
- **8.1 reachability**: 이 sub-scope는 조립돼 있다 — source bean 무조건, consumer bean은 fork의 5종 SPI에 조건부(§65). platform이 제공하는 SPI 구현은 **0**(전부 test fixture) — 설계상 fork 몫.
|
||
- **8.2 계약 ↔ 구현**: `busy()`가 선언한 불변식을 pipeline이 깬다(§67, probe B). `MongoChangeStreamPipeline` javadoc이 고쳤다고 적은 손실이 mark의 의미 때문에 되돌아온다(probe A·C).
|
||
- **8.2b 테스트 사각지대**: "본 적 있지만 완료되지 않은 위치"가 세 테스트 어디에도 없다(§67).
|
||
- **8.3 중복 메커니즘**: recovery의 두 어휘(§69). checkpoint 생성 경로 둘(`checkpointFor` vs `tokens.encode`). 자기 자신과 비교하는 guard.
|
||
- **8.4 문서 drift**: `MongoPlatformSettings`의 "zero beans, zero threads" 주석이 현재 코드와 어긋난다(§68). 반면 policy가 지목하는 두 runbook(`docs/mongodb/runbooks/history-lost.md`, `failover.md`)은 **실재한다** — confirmed match.
|
||
|
||
## 71. Sub-scope 08 findings backlog
|
||
|
||
| 우선순위 | finding | reachability |
|
||
|---|---|---|
|
||
| **P1** | pipeline의 high-water mark가 "투영 완료 위치"가 아니라 "본 위치"이고 resume에도 유지되므로, failover 중이던 이벤트가 재전달 시 삼켜지고 이후 이벤트의 checkpoint가 그것을 지나친다 — 조용한 영구 소실, state는 `RUNNING` (probe C) | 조립된 소비자 + 임의의 resumable failover. worker 하나로 재현 |
|
||
| **P1(동일 결함, 별 경로)** | 실패가 전혀 없어도 `CLAIMED_ELSEWHERE`(및 `PARKED`) 위치를 이후 이벤트의 checkpoint가 지나친다 — `busy()`의 javadoc이 명시한 불변식 위반 (probe B) | 다중 worker 배포 |
|
||
| **P2** | `changeStreams`가 `false`로 고정돼 startup의 CHANGE_STREAM capability 검사가 도달 불가인데 소비자 bean은 조립된다. `MongoPlatformSettings`의 "not shipped / zero beans" 주석이 현재 코드와 어긋난다 | change stream 미지원 topology에 배포하는 모든 fork |
|
||
| **P3** | `MongoChangeHistoryLostException`이 어디에서도 생성되지 않는다 — history lost는 driver 원본 예외로 재방출된다 | 이 상황을 타입으로 구분하려는 caller |
|
||
| **P3** | `requireCorrectResumeOption(checkpoint, checkpoint.position())` — 자기 자신과 비교하는 guard | 소비자의 유일한 호출 |
|
||
| **P3/기록** | `onHistoryLost`·`onResumableFailure`·`onInvalidate`·`checkpointFor`·`autoResumable()` production 호출 0 — 쓰이는 어휘와 쓰이지 않는 어휘가 나란히 있다 | 유지보수 |
|
||
|
||
## 72. Sub-scope 08 완료 조건
|
||
|
||
- denominator 26 / 26 FULL_READ (`135-...` OWNED FILES)
|
||
- reachability·계약대조·테스트사각지대·중복메커니즘·문서drift 5종 probe 수행
|
||
- P1을 세 개의 독립적인 실행 probe(A·B·C)로 확정, auto-configuration과 동일한 조립으로 재현(`135a-...`)
|
||
- 임시 probe class 2개 추가 후 제거, `mongoStableContractTest` 재실행 green, `git status --short` = 0
|
||
|
||
---
|
||
|
||
## 73. Sub-scope 09 범위와 denominator
|
||
|
||
> 내부 상태: COMPLETE — **44 / 44 FULL_READ**
|
||
> 범위: `security/**` 13 + `failure/**` 8 + `observation/**` 7 + `client/**` 1 (production 30, 2,244 LOC) + 전용 test 14 (1,885 LOC)
|
||
> 역할: 자격증명 분리와 D4 admin plane, driver 실패의 단일 번역 지점, 태그 allowlist 기반 관측, 그리고 프로파일 → driver 설정 변환
|
||
|
||
manifest와 정적 probe: `evidence/raw/136-mongo-security-failure-observation-client-probes.txt`.
|
||
실행 probe: `evidence/raw/136a-mongo-client-settings-execution-probe.txt`.
|
||
|
||
## 74. `failure`는 이 leaf에서 가장 잘 배선되고 가장 잘 논증된 부분이다
|
||
|
||
`MongoFailureClassifier`와 `MongoFailureTranslator`는 auto-configuration의 실제 bean이고(`MongoPlatformAutoConfiguration:85·92`), imperative·reactive 두 executor가 모두 그것을 통해 번역한다. 규칙 사슬도 순서까지 논증돼 있다 — **label → phase → 적용 가능성 → code table → fail closed**.
|
||
|
||
> Phase sits above the code table because a failure that never reached a server is safe to repeat whatever code accompanies it, and a commit failure is unsafe to replay whatever code accompanies it — **both were decided by the code table before, and the code table knows neither.**
|
||
|
||
고쳐진 결함 이력이 촘촘하다.
|
||
|
||
- **번역기가 phase를 버렸다.** `DefaultMongoFailureTranslator`가 operationType을 들고도 context-free overload를 불러서, "FIND의 응답 유실 = 재현 가능한 읽기 / UPDATE의 같은 유실 = 결과 불명 쓰기"라는 구분이 **transaction이 아닌 모든 경로에서** 버려졌다 — 즉 모든 평범한 연산에서. 실패한 읽기가 ambiguous write로 보고됐다.
|
||
- **server-selection이 terminal이었다.** label도 code도 없는 실패가 `UNCLASSIFIED`로 떨어져 재시도 불가로 처리됐다 — 재시도가 명백히 안전한 유일한 경우인데.
|
||
- **Spring 래핑이 분류를 통째로 건너뛰었다.** `MongoFailureExtractor`가 그 수리다. cause 사슬을 깊이 16까지, `IdentityHashMap`으로 순환 안전하게 탐색한다("a cycle is about the same object appearing twice").
|
||
- **message는 절대 읽지 않는다.** `MongoDriverFailureView`가 driver 예외를 label·code·boolean 둘로 좁히는 지점이고, 그 이후 어느 계층도 나머지에 닿을 수 없다 — "no later layer can reach the rest, because no later layer is ever handed it".
|
||
|
||
`MongoFailureClassification`의 생성자가 `COMMIT_ONLY`를 `TRANSACTION_COMMIT_UNKNOWN`에만 허용하는 것도 §15의 불변식과 맞물린다.
|
||
|
||
`security`도 대부분 배선돼 있다. `MongoStartupValidator:62`가 `MongoSecurityProfileValidator().validate(runtimeSecurity)`를, `:117`이 `requireDistinctCredentials`를 부른다. `MongoPlatformAutoConfiguration:341–350`은 셋 중 하나라도 없으면 **부분 검증 대신 startup을 거부**한다("A partial startup check reports success for the parts nobody supplied"). `MongoCredentialReference.fingerprint()`의 주석은 이 leaf에서 가장 좋은 결함 서술 중 하나다 — role을 해시에 섞은 탓에 "같은 secret, 다른 role"이 다른 지문을 냈고, 그 지문을 쓰는 유일한 검사인 `requireDistinctCredentials`는 **항상 runtime role과 admin role로 호출되므로 결코 발화할 수 없었다**.
|
||
|
||
`observation`의 태그 allowlist와 `MongoObservationRedactor`의 allowlist 방향("a denylist would have to anticipate the next command MongoDB adds that happens to carry a secret")도 일관된다. driver 리스너는 `MongoDriverObservabilityAutoConfiguration`이 `MongoClientSettingsBuilderCustomizer`로 등록해 실제로 설치된다 — 그 파일의 javadoc이 자기 존재 이유를 적는다: "`MongoDriverObservabilityConfiguration` could add command, SDAM and pool listeners to a settings builder, **and nothing ever called it**… the pool-checkout, server-selection and primary-change metrics the operations documentation refers to were never emitted."
|
||
|
||
## 75. P1 — 프로파일의 TLS·타임아웃·풀·Stable API가 driver에 도달하지 않는다
|
||
|
||
`MongoClientSettingsFactory`의 javadoc은 자신이 무엇을 고치려고 만들어졌는지 적는다.
|
||
|
||
> The profile, the credential resolver, the TLS and Stable-API flags and the pool and timeout policy all existed and were all unit-tested. **None of them reached a `MongoClientSettings`**… A policy that nothing applies reads exactly like a policy that is applied — the tests pass, the record is populated, and the client connects with a three-second timeout it inherited from the driver rather than the two the profile states.
|
||
|
||
HEAD에서 이 클래스는 **저장소 전체에서 호출자가 없다**(`136-...` §8.1b·§8.1e: 자기 파일과 자기 test 외의 참조 0, `app-bootstrap` 포함 repo-wide 0). bean도 아니다. `MongoCredentialResolver`도 production에서 한 번도 호출되지 않는다 — 유일한 외부 언급은 모듈 `CLAUDE.md`의 산문이다. 실제 client는 Spring Boot가 `spring.data.mongodb.uri`에서 만든다(모듈 README:37이 그 형태를 그대로 보여 준다). 즉 **수리 코드는 작성됐고 배선되지 않았다.**
|
||
|
||
hermetic 실행 probe(`136a-...`)로 결과를 측정했다.
|
||
|
||
```
|
||
PROBE profile.tlsRequired=true -> validator ACCEPTED
|
||
PROBE settings Boot builds from the README's URI:
|
||
sslEnabled=false
|
||
connectTimeoutMs=10000 serverSelectionTimeoutMs=30000
|
||
poolMaxSize=100 serverApi=null
|
||
uuidRepresentation=UNSPECIFIED
|
||
PROBE settings MongoClientSettingsFactory would build: sslEnabled=true
|
||
```
|
||
|
||
가장 무거운 줄은 첫 두 줄이다. `MongoSecurityProfileValidator`는 production 프로파일이 TLS를 요구한다고 선언하면 통과시키고, 선언하지 않으면 startup을 거부한다 — 그리고 그 선언을 연결에 적용하는 코드는 없다. TLS가 켜지는 것은 오직 fork의 URI에 `tls=true`가 들어 있을 때뿐이다. **프로파일이 "TLS 필수"라고 말하고, 검증기가 그것을 확인하고, 연결은 평문으로 나갈 수 있다.** 나머지 줄들도 같은 성질이다 — 타임아웃·풀 상한·Stable API strict·고정 UUID 표현이 전부 driver 기본값이다(`serverApi=null`은 strict Stable API가 없다는 뜻이고, `uuidRepresentation=UNSPECIFIED`는 `MongoClientSettingsFactory`가 "a value that moves under a stored document is a migration nobody wrote"라며 고정하려던 바로 그 값이다).
|
||
|
||
**같은 결함의 형제가 이미 고쳐져 있다는 점이 이 finding을 결정적으로 만든다.** 관측 쪽도 "설정 빌더에 적용하는 메서드에 호출자가 없다"는 똑같은 형태였고, 그쪽은 `MongoDriverObservabilityAutoConfiguration`이 `MongoClientSettingsBuilderCustomizer`를 등록해서 고쳤다. **동일한 메커니즘이 같은 패키지에 있고, 설정 절반에는 쓰이지 않았다.**
|
||
|
||
기존 test는 이 경계를 보지 못한다. `MongoClientSettingsFactoryTest`는 factory를 **직접 생성해서** 프로파일이 설정에 도달하는지 확인한다 — factory가 호출된다는 전제 아래. `MongoTlsLaneTest`는 `applyToSslSettings(ssl -> ssl.enabled(true))`로 **손수 만든 설정**으로 서버가 TLS를 강제하는지 확인한다(`:137`). 어느 쪽도 "프로파일의 `tlsRequired`가 실제 연결을 TLS로 만드는가"를 묻지 않는다.
|
||
|
||
**판정: P1.** 수리는 이미 있는 형태를 따르면 된다 — `MongoClientSettingsBuilderCustomizer` bean 하나가 `MongoClientSettingsFactory`(또는 그 `build` 로직)를 Boot의 빌더에 적용하게 하는 것. 그때 `MongoCredentialResolver`도 비로소 경로에 들어온다.
|
||
|
||
## 76. P3 — admin gateway의 두 audit 경로 중 하나만 fail-closed다
|
||
|
||
`MongoAdminGateway.execute`는 모든 audit 쓰기를 `audit(...)` 헬퍼로 보내고, 그 헬퍼는 sink 실패를 `MongoOperationRejectedException`으로 바꾼다 — "an administrative operation that cannot be audited does not run". `MongoAdminAuditStateMachineTest.anUnauditableCommandDoesNotRun`이 그것을 고정한다.
|
||
|
||
`dryRun(...)`(`:145–149`)은 `auditSink.accept(...)`를 **직접** 부른다. 헬퍼를 거치지 않으므로 sink 실패가 platform 예외로 번역되지 않고 raw로 전파된다. 그리고 dry run은 장식이 아니다 — 고위험 작업의 **전제 조건**이고, 그래서 "a first-class call rather than a flag somebody remembers to pass"로 만들어졌다. 감사되지 않은 dry run 위에 승인이 얹히면 승인 사슬의 첫 칸에 기록이 없다. **P3**(전파는 되므로 조용히 통과하지는 않는다; 다만 형제 경로와 동작이 다르고 그 차이가 문서화돼 있지 않다).
|
||
|
||
## 77. P3 — 태그 allowlist는 규약이지 강제가 아니다
|
||
|
||
`MongoObservationConvention`의 javadoc은 강제라고 말한다.
|
||
|
||
> a tag not on this list cannot be attached, so **the mistake has to be made in this file** rather than at a call site.
|
||
|
||
실제로는 `requireAllowed(...)`를 부르는 production 코드가 **없다**(`136-...` §8.2). 네 개의 관측 클래스는 전부 `Tags.of("...", ...)`로 문자열을 직접 넣는다. 현재 값들은 모두 allowlist 안에 있으므로 지금은 어긋남이 없지만, 그 사실은 코드가 아니라 리뷰와 `MongoObservationConventionTest`가 지키고 있다. 새 리스너를 추가하는 사람은 이 파일을 열 이유가 없다.
|
||
|
||
`MongoObservationRedactor.describe(...)`도 production 호출자가 0이다 — `MongoCommandObservationListener`는 `isAlwaysRedacted`만 쓴다. 세 갈래(안전/기본/항상 가림) 중 실제로 쓰이는 것은 "항상 가림" 하나다. **P3.**
|
||
|
||
## 78. Confirmed — 세 곳의 대비: 배선된 것, 부분적으로 배선된 것, 배선되지 않은 것
|
||
|
||
이 sub-scope는 앞선 sub-scope들과 달리 세 상태가 한 화면에 있다.
|
||
|
||
| 패키지 | 상태 |
|
||
|---|---|
|
||
| `failure` | **완전 배선.** classifier·translator 모두 bean, 두 executor가 사용, `MongoFailureExtractor`는 두 session factory가 사용 |
|
||
| `security` | **검증 경로 배선.** `MongoStartupValidator`가 profile validator와 자격증명 분리 검사를 실행. 다만 그 검증 대상 선언이 driver에 적용되지 않는다(§75) |
|
||
| `observation` | **부분 배선.** driver 리스너는 customizer로 설치됨. allowlist 강제와 `describe`는 미사용(§77) |
|
||
| `client` | **미배선.** 호출자 0(§75) |
|
||
|
||
호출자 없는 잔여물도 정리해 둔다: `MongoFailureClassification.unrecognisedServerCode()` 0, `MongoAdminAuditRecord.applied(...)`("legacy shape, kept for callers that do not build a command") production 0 / test 2, `MongoAdminRuntimeGuard.adminGatewayAllowed()` production 0, `MongoDriverObservabilityConfiguration.convention()` 0. 어느 것도 결함은 아니지만, 이 leaf가 "쓰이는 어휘와 쓰이지 않는 어휘를 나란히 둔다"는 §69의 패턴이 여기서도 반복된다.
|
||
|
||
## 79. Negative-space probes — sub-scope 09
|
||
|
||
- **8.1 reachability**: 네 패키지의 상태가 서로 다르다(§78). `MongoClientSettingsFactory` repo-wide 호출자 0(§75).
|
||
- **8.2 계약 ↔ 구현**: `MongoClientSettingsFactory` javadoc이 서술한 결함이 그 클래스 자체에 대해 성립한다(§75). allowlist javadoc의 "cannot be attached"와 실제 강제 부재(§77).
|
||
- **8.2b 조건부 형제**: 같은 결함(설정 빌더 메서드에 호출자 없음)의 두 수리 중 관측 쪽만 배선(§75). `execute`와 `dryRun`의 audit 경로 차이(§76).
|
||
- **8.3 중복/미사용 메커니즘**: §78 말미 목록. redactor의 세 갈래 중 하나만 사용(§77).
|
||
- **8.4 lane drift**: build.gradle에 6개 lane(`mongoReplicaSetTest`·`mongoFailoverTest`·`mongoMigrationTest`·`mongoCompatibilityTest`·`mongoSecurityIntegrationTest`·`mongoPerformanceTest`) 정의, tag는 각각 대응. `MongoTlsLaneTest`·`MongoSecurityIntegrationLaneTest`는 `mongodb-security-integration`, `MongoNetworkFaultLaneTest`는 `mongodb-failover` — 전부 정의된 lane에 매핑된다. **confirmed match.**
|
||
|
||
## 80. Sub-scope 09 findings backlog
|
||
|
||
| 우선순위 | finding | reachability |
|
||
|---|---|---|
|
||
| **P1** | `MongoClientSettingsFactory`가 저장소 전체에서 호출되지 않아 프로파일의 `tlsRequired`·타임아웃·풀 상한·Stable API·UUID 표현이 driver에 도달하지 않는다. 검증기는 "TLS 필수" 선언을 통과시키고 연결은 평문일 수 있다 (probe: `tlsRequired=true` → validator ACCEPTED, Boot 설정 `sslEnabled=false`) | 이 leaf를 켠 모든 배포 |
|
||
| **P3** | `MongoAdminGateway.dryRun`이 fail-closed `audit(...)` 헬퍼를 우회해 sink 실패를 raw로 전파한다 — `execute`와 동작이 다르다 | dry run을 감사하는 배포 |
|
||
| **P3** | 태그 allowlist(`requireAllowed`)와 `MongoObservationRedactor.describe`의 production 호출자 0 — javadoc이 주장하는 강제는 규약과 test가 지킨다 | 리스너를 추가하는 시점 |
|
||
| **P3/기록** | 호출자 없는 잔여 API: `unrecognisedServerCode()`, `MongoAdminAuditRecord.applied(...)`, `adminGatewayAllowed()`, `MongoDriverObservabilityConfiguration.convention()` | 유지보수 |
|
||
|
||
## 81. Sub-scope 09 완료 조건
|
||
|
||
- denominator 44 / 44 FULL_READ (`136-...` OWNED FILES)
|
||
- reachability·계약대조·조건부형제·중복메커니즘·lane drift 5종 probe 수행
|
||
- P1을 hermetic 실행 probe로 확정하고, 기존 두 test(`MongoClientSettingsFactoryTest`·`MongoTlsLaneTest`)가 왜 그 경계를 보지 못하는지 코드로 확인(`136a-...`)
|
||
- 임시 probe class 1개 추가 후 제거, `git status --short` = 0
|
||
|
||
---
|
||
|
||
## 82. Sub-scope 10 범위와 denominator
|
||
|
||
> 내부 상태: COMPLETE — **75 / 75 FULL_READ**
|
||
> 범위: `advanced/**` 65 (production, 3,439 LOC) + 전용 test 10 (1,365 LOC)
|
||
> 하위 영역: root(6) · autoconfigure(2) · bridge(7) · encryption/csfle(5) · encryption/qe(6) · gridfs(4) · search(5) · sharding(6) + sharding/admin(3) · tenancy/database(5) + tenancy/shared(4) · timeseries(6) · vector(5)
|
||
> 역할: Stable lane이 갖지 못한 것(샤딩 클러스터·Atlas·KMS·별도 자격증명)을 요구하는 능력들을 **명시적 opt-in**으로 격리한다
|
||
|
||
manifest와 probe: `evidence/raw/137-mongo-advanced-manifest-and-probes.txt`.
|
||
|
||
## 83. opt-in 구조 자체가 이 sub-scope의 본체다
|
||
|
||
세 겹으로 되어 있다.
|
||
|
||
1. **분류 어노테이션 둘.** `@MongoAdvancedEntryPoint(capability)`는 *실행하는* 타입, `@MongoAdvancedPolicy`는 *판단·기술·검증만 하는* 타입. 후자를 flag 뒤에 두지 않는 이유가 적혀 있다 — "gating it behind a capability flag would only make a shard-key analysis or a manifest check unavailable to the very people deciding whether to turn the capability on."
|
||
2. **guard.** entry point는 `MongoAdvancedCapabilityGuard`를 생성자 인자로 받아 **자기 자신을 넘겨** 검사시킨다. 필요한 capability는 타입 위의 어노테이션에서 읽으므로 호출자마다 복사되지 않는다. 어노테이션 없는 타입이 guard에 물으면 `IllegalArgumentException`이다 — "defaulting to 'allowed' is how the invariant was lost in the first place."
|
||
3. **ArchUnit 규칙 둘.** `MongoAdvancedRules.everyAdvancedTypeIsClassified()`와 `everyEntryPointConsultsTheGuard()`. "Two rules, because one alone is escapable."
|
||
|
||
`MongoAdvancedEntryPoint`의 javadoc이 이 구조가 왜 생겼는지 적는다.
|
||
|
||
> The module documentation claimed that "every Advanced entry point refuses construction unless its capability is enabled". Of the concrete classes under this package **only four referenced the flags at all**; the rest — a change-stream-to-messaging bridge, a per-tenant client registry, a tenant migration coordinator — were constructible and runnable with every Advanced capability switched off. **The invariant was documentation, not behaviour.**
|
||
|
||
그리고 `MongoAdvancedSettings`가 그 위의 결함을 고친다 — flag는 "무엇이 켜졌나"를 답할 줄 알았지만 **그 property를 읽는 코드가 없었다**. 그래서 `ca-skeleton.persistence-mongo.advanced.sharding.enabled=true`를 설정해도 아무 일도 일어나지 않았다. 이제 `@ConfigurationProperties`로 바인딩되고, 바인딩 키가 `MongoAdvancedCapabilityFlags.propertyFor(...)`가 거부 메시지에 적는 경로와 같은지 test가 고정한다.
|
||
|
||
`MongoAdvancedConfiguration`은 **의도적으로 auto-configuration이 아니다** — `AutoConfiguration.imports`에 없고(`137-...` §8.1: grep exit=1), 이 leaf의 `main` 안에서 `MongoAdvancedCapabilityGuard`를 참조하는 non-advanced 코드도 0이다. composition root가 이름으로 import해야 하고, 그 import 자체가 opt-in이다.
|
||
|
||
## 84. Confirmed — 분류 불변식이 실제로 성립한다
|
||
|
||
세어 봤다(`137-...` §8.1b·§8.1c).
|
||
|
||
- `@MongoAdvancedEntryPoint` **7개**: `MongoChangeMessagingBridge`(CHANGE_STREAM), `MongoCsfleClientFactory`(CSFLE), `MongoQueryableEncryptionCollectionManager`(QUERYABLE_ENCRYPTION), `MongoGridFsMigrationJob`(GRIDFS_COMPATIBILITY), `MongoShardingAdminGateway`(SHARDING), `MongoTenantClientRegistry`·`MongoTenantMigrationCoordinator`(DATABASE_PER_TENANT).
|
||
- `@MongoAdvancedPolicy` **11개**.
|
||
- 어느 쪽도 아닌 구체 클래스 **1개**: `MongoAdvancedConfiguration`. 이것은 누락이 아니다 — `MongoAdvancedRules.concreteClass()`가 `@Configuration`을 명시적으로 제외하며 이유를 적는다: "A `@Configuration` class is the package's composition root: it builds entry points through the guard rather than being one, and **gating it would gate the thing that supplies the guard**." interface·enum·record·익명·private 중첩·abstract도 같은 방식으로 제외되고 각각 근거가 붙어 있다.
|
||
|
||
즉 §83이 말하는 불변식은 문서가 아니라 코드로 서 있다. 이 leaf에서 "문서가 주장하고 코드가 지키지 않는다"를 여러 번 본 뒤라, 여기서는 그 반대가 성립한다는 것을 명시해 둘 가치가 있다.
|
||
|
||
또 하나의 confirmed: **`throw new UnsupportedOperationException`만 하는 public 메서드를 값으로 바꾼 수리**가 두 곳에서 같은 형태로 이루어졌다. `MongoTimeSeriesCapabilityValidator`는 네 개의 던지기만 하는 메서드를 `supportFor(capability) → MongoTimeSeriesSupport(지원 여부 + 이유)`로 바꿨고, `MongoQueryableEncryptionProfile`은 세 개의 던지기만 하는 static factory를 `supportFor(MongoQueryShape) → MongoQueryShapeSupport`로 바꿨다. 근거도 동일하다 — "A factory that never returns is not an API: it cannot appear in working code, so its only reachable use is a test asserting that it throws, and the design-time question it was meant to answer is only answered by running it."
|
||
|
||
## 85. P2 — sharding admin gateway의 네 작업 중 셋은 어떤 입력으로도 완료될 수 없다
|
||
|
||
`MongoShardingAdminGateway`는 네 메서드 모두 `MongoAdminGateway`의 **5인자 편의 오버로드**를 부른다(`:64`, `:75`, `:86`, `:92`). 그 오버로드는 `MongoAdminCommand.routine(...)`을 만들고 **`approval = null`**을 넘긴다(`MongoAdminGateway:63–67`). 그리고 실제 실행 경로는 고위험 작업에 대해 `approval == null`이면 거부한다(`:94–97`).
|
||
|
||
`MongoAdminOperation`에서 `SHARD_COLLECTION`·`REFINE_SHARD_KEY`·`RESHARD_COLLECTION`은 전부 `highRisk(true)`이고, `BALANCER_CONTROL`만 `false`다.
|
||
|
||
실행 probe로 확인했다(`137-...` PROBE). 입력은 통과할 수 있는 모든 증거를 갖췄다 — SHARDING capability 활성화, `MongoAdminAuthorization.approved(네 작업, "release-engineer")`(= 이름 있는 승인자 + 완료된 dry run), 승인된 `ShardKeyReadinessReport`, 완전한 `ReshardApproval`(승인된 readiness + dry run 완료 + 승인자 + 문서화된 forward strategy), shard key로 시작하는 지원 인덱스, 만료되지 않은 command clock.
|
||
|
||
```
|
||
PROBE shardCollection -> REFUSED: admin operation SHARD_COLLECTION destroys data or rewrites
|
||
a collection; it runs under an approval bound to this exact command
|
||
or not at all
|
||
PROBE refineShardKey -> REFUSED (REFINE_SHARD_KEY, 같은 메시지)
|
||
PROBE reshardCollection-> REFUSED (RESHARD_COLLECTION, 같은 메시지)
|
||
PROBE controlBalancer -> APPLIED
|
||
PROBE bodies actually executed = 1 of 4
|
||
```
|
||
|
||
구조적 원인은 **승인 어휘가 둘이라는 것**이다. sharding 모듈은 자기 몫의 완전한 승인 객체(`ReshardApproval`, 네 가지 증거)를 만들어 스스로 검사한 뒤, 실제로 결정하는 D4 plane에는 **그 중 아무것도 넘기지 않는다**. D4가 요구하는 것은 `MongoAdminApproval`(command digest에 바인딩된 단일 사용 승인)이고, 그것을 만드는 코드가 sharding 쪽에 없다.
|
||
|
||
test도 이 경계를 보지 않는다: `MongoShardingAdminGateway`를 참조하는 곳은 **자기 선언 세 줄뿐**이다(`137-...` §8.2c). sharding 관련 test 둘(`ShardKeyAnalyzerTest`·`ShardAwareQueryValidatorTest`)은 policy 계층만 다룬다.
|
||
|
||
**판정: P2.** 데이터 위험은 없다 — 거부는 fail-closed이고, 오히려 안전한 방향으로 틀렸다. 위험은 능력이 문서상 존재하고 실제로는 없다는 것이며, 그 사실이 발견되는 시점은 운영자가 프로덕션 클러스터에서 reshard를 실행하려는 순간이다. 수정은 세 메서드가 `MongoAdminCommand.over(...)` + `MongoAdminApproval.of(command, approver, expiry)`를 만들어 2인자 `execute`에 넘기고, `ReshardApproval`의 증거를 그 승인의 전제로 쓰는 것이다.
|
||
|
||
## 86. P3 — promotion 증거 어휘가 둘이고, gate는 하나만 검사한다
|
||
|
||
`MongoAdvancedPromotionEvidence.REQUIRED`는 여섯 범주다: `stable-platform`, `actual-topology`, `security`, `migration`, `failure`, `runbook`. `MongoAdvancedPromotionGate.verify(...)`가 그 여섯을 전부 검사한다 — 그리고 그 파일에는 고쳐진 결함이 주석으로 남아 있다: "`migration` was in `MongoAdvancedPromotionEvidence.REQUIRED` and not here, so the gate demanded five of the six categories it declares… which is the shape MNG-008 names: a gate that certifies more than it ran."
|
||
|
||
그런데 `MongoVectorSearchBenchmarkGate.requiredEvidence()`는 **완전히 다른 다섯 범주**를 반환한다: `index-readiness`, `recall`, `latency`, `memory`, `actual-topology`. 겹치는 것은 `actual-topology` 하나뿐이고, 이 집합을 읽는 production 코드는 없다(`137-...` §8.3). `MongoAdvancedPromotionGate`는 이 집합을 모른다.
|
||
|
||
그래서 vector search를 promotion하는 경로는 `MongoAdvancedPromotionGate.verify`를 통과할 수 있고, 그 통과는 recall·latency·index memory에 대해 **아무것도 말하지 않는다** — `MongoVectorSearchBenchmarkGate`의 javadoc이 정확히 그 위험을 적는데도: "Functional success is not evidence for vector search. An approximate index returns results for any query; whether they are the right results depends on recall." 방금 `migration` 누락으로 고쳤던 것과 같은 모양(선언한 것보다 적게 검사하는 gate)이 모듈 경계를 건너 다시 나타난다. **P3.**
|
||
|
||
## 87. P3/기록 — change stream checkpoint를 쓰는 곳이 둘이고, 서로를 모른다
|
||
|
||
`MongoResumeCheckpointStore.save(...)`를 부르는 production 코드는 둘이다(`137-...` §8.3c).
|
||
|
||
- `MongoChangeStreamRunner:75` — 투영이 성공한 뒤.
|
||
- `MongoChangeMessagingBridge:60·84` — 매핑하지 않은 변경(`:60`)과 broker가 수락한 변경(`:84`) 뒤.
|
||
|
||
둘 다 옳게 설계돼 있고(bridge는 `MongoPublishResult`가 broker의 실제 답을 나르게 만들어, 상수 때문에 두 분기가 모두 도달 불가였던 결함을 고쳤다), 각자 "손실보다 중복"을 택한다. 문제는 **한 subscription에 둘 다 배선되는 경우 서로의 진행을 모른다**는 것이다. 각자 자기 성공에서 checkpoint를 전진시키므로, bridge가 앞서면 projector가 아직 처리하지 않은 변경을 지나치고 그 반대도 마찬가지다. §67에서 본 pipeline의 high-water mark 문제와 합쳐지면 결과는 같은 방향 — 조용한 소실 — 이다.
|
||
|
||
두 클래스 어디에도 "한 subscription에 하나만 배선하라"는 진술이 없다. `MongoChangeMessagingBridge`가 `MongoChangeProjector`가 아니라 별도 타입이라는 사실 자체가 둘을 함께 쓸 수 있다는 신호로 읽힌다. **P3/기록** — fork의 조립 결정이므로 지금 결함은 아니지만, 계약이 어디에도 없다.
|
||
|
||
## 88. P3 — 구현 없는 4개의 계약 중 셋은 그 사실을 적고, 하나는 적지 않는다
|
||
|
||
`MongoSearchOperations`·`MongoTimeSeriesOperations`·`MongoVectorSearchOperations`는 모두 동일한 문단을 담는다.
|
||
|
||
> **Scaffold.** This repository ships no implementation… Read a method signature as a specification, not as an available capability — an interface with no implementation cannot be injected, and treating it as shipped behaviour is how "the platform supports search" becomes true in a document and false in a deployment.
|
||
|
||
훌륭한 자기 한정이고, 이 leaf에서 반복적으로 필요했던 종류의 정직함이다. 그런데 `TenantScopedMongoOperations`도 구현이 **0**인데(`137-...` §8.3d: 네 interface 모두 `implements` 검색 exit=1) 그 문단이 없다. 그리고 이 넷 중 오해가 가장 비싼 것이 바로 그것이다 — javadoc이 "Operations that cannot run without a tenant predicate"라고 시작하므로, 능동적인 안전장치로 읽힌다. 실제로 그 보장을 제공하는 것은 `MongoTenantPredicateInjector`(policy, 구현 있음)이고, 이 interface는 fork가 구현했을 때만 그 injector를 부르게 되는 **형태**일 뿐이다. **P3.**
|
||
|
||
## 89. Negative-space probes — sub-scope 10
|
||
|
||
- **8.1 reachability**: guard bean은 `MongoAdvancedConfiguration`에만 있고 그것은 auto-load되지 않는다 — 저장소 안에 이것을 import하는 곳이 없으므로 **모든 Advanced entry point는 기본 배선에서 도달 불가**다. 이것은 설계이고 문서와 일치한다(confirmed).
|
||
- **8.1b 분류 완전성**: 7 entry point + 11 policy + 1 의도적 제외 = 19개 구체 클래스 전부 설명됨(§84). ArchUnit 규칙이 양쪽을 강제.
|
||
- **8.2 공개 표면 도달성**: `MongoShardingAdminGateway`의 4개 중 3개가 어떤 입력으로도 완료 불가(§85, 실행 probe).
|
||
- **8.2b 중복 로직**: shard key ↔ 유니크 인덱스 호환성 검사가 `ShardKeyDescriptor.supportsUniqueIndexOn`과 `MongoShardingAdminGateway.shardCollection` 안에 각각 있다(후자는 전자를 부르지 않고 sublist 비교를 다시 쓴다). 두 구현의 결과는 현재 같다.
|
||
- **8.3 중복 메커니즘**: promotion 증거 어휘 둘(§86), checkpoint 작성자 둘(§87), 승인 어휘 둘(§85).
|
||
- **8.4 문서 drift**: 모듈 `CLAUDE.md:142`가 "`MongoAdvancedConfiguration` is imported by name, never auto-loaded"라고 적고 실제로 그렇다 — **confirmed match**. `build.gradle`에 advanced 전용 lane은 없고, advanced test는 hermetic `mongodb-contract` 레인에서 돈다.
|
||
|
||
## 90. Sub-scope 10 findings backlog
|
||
|
||
| 우선순위 | finding | reachability |
|
||
|---|---|---|
|
||
| **P2** | `MongoShardingAdminGateway`의 `shardCollection`·`refineShardKey`·`reshardCollection`이 5인자 `execute`(approval=null)를 쓰므로 고위험 작업 거부에 걸려 **완료 불가**. 자기 몫의 `ReshardApproval`을 만들고도 D4가 요구하는 `MongoAdminApproval`은 만들지 않는다. gateway를 구동하는 test 0 | SHARDING을 켠 fork가 샤딩을 실제로 수행하려는 시점 |
|
||
| **P3** | promotion 증거 어휘가 둘(`MongoAdvancedPromotionEvidence.REQUIRED` 6종 vs `MongoVectorSearchBenchmarkGate.requiredEvidence()` 5종, 교집합 1)이고 gate는 전자만 검사한다 — vector 승격이 recall·latency·memory 증거 없이 통과한다 | vector search 승격 절차 |
|
||
| **P3** | `TenantScopedMongoOperations`는 구현이 없는데 형제 셋과 달리 Scaffold 고지가 없고, javadoc은 능동적 안전장치처럼 읽힌다 | 문서/조립 |
|
||
| **P3/기록** | `MongoChangeStreamRunner`와 `MongoChangeMessagingBridge`가 같은 `MongoResumeCheckpointStore`를 독립적으로 전진시키며, 한 subscription에 둘을 배선하지 말라는 계약이 없다 | 두 소비자를 함께 배선하는 fork |
|
||
| **P3/기록** | shard key ↔ 유니크 인덱스 호환성 검사가 두 곳에 중복 구현돼 있다 | 유지보수 |
|
||
|
||
## 91. Sub-scope 10 완료 조건
|
||
|
||
- denominator 75 / 75 FULL_READ (`137-...` OWNED FILES)
|
||
- reachability·분류완전성·공개표면도달성·중복로직·중복메커니즘·문서drift 6종 probe 수행
|
||
- P2를 hermetic 실행 probe로 확정(모든 승인 증거를 갖춘 입력에서 4개 중 1개만 실행)
|
||
- ArchUnit 분류 규칙의 예외(`@Configuration`)가 의도된 것임을 규칙 소스로 확인
|
||
- 임시 probe class 1개 추가 후 제거, `git status --short` = 0
|
||
|
||
---
|
||
|
||
## 92. Sub-scope 11 범위와 denominator
|
||
|
||
> 내부 상태: COMPLETE — **49 / 49 FULL_READ**
|
||
> 범위: `src/testkit` 35 (3,036 LOC) + `src/test`의 미배정 13 (architecture 4, rs 2, compat 1, release 1, testkit-검증 3, 루트 2 — 1,466 LOC) + `src/mongoPerformanceTest` 1 (194 LOC)
|
||
> 역할: 이 leaf의 **인증 장치** — 실제 토폴로지 fixture, 아키텍처 규칙, 릴리스 증거 검증
|
||
|
||
manifest와 probe: `evidence/raw/138-mongo-testkit-release-lanes-probes.txt`.
|
||
|
||
## 93. Confirmed — testkit은 흉내내지 않고 진짜를 만든다
|
||
|
||
이 sub-scope에서 가장 인상적인 것은 fixture들이 **어려운 쪽을 선택했다**는 점이다.
|
||
|
||
- `MongoThreeNodeReplicaSet`은 `MongoDBContainer`를 **쓰지 않는다** — 그 컨테이너는 시작할 때 자기만의 단일 노드 set을 initiate하므로 "세 개를 띄우면 아무것도 선출하지 않는 세 개의 별도 클러스터"가 된다. 대신 `--replSet`만 주고 하나의 `rs.initiate`로 묶는다. primary는 **묻는다**(`db.hello().primary`), 어느 컨테이너가 살아 있는지로 추론하지 않는다 — "inferring it from which containers are still running produces a fixture that reports an election that never happened."
|
||
- `ToxiproxyMongoNetworkFaultController`는 **응답 방향만** 끊는다(`ToxicDirection.DOWNSTREAM`). 그것이 `WRITE_RESULT_UNKNOWN`을 만드는 유일한 방법이다 — 컨테이너를 죽이면 클라이언트는 쓰기가 일어나지 않았음을 알게 되고, 그것은 이미 다루어진 쉬운 실패다. `MongoProxiedReplicaSetNode`는 같은 서버로 가는 **두 경로**(직접/프록시)를 둔다 — 주입한 결함이 서버 결함이 아니라 경로 결함임을 보이려면 프록시를 우회한 두 번째 클라이언트가 서버를 건강하다고 확인해 주어야 하기 때문이다.
|
||
- `MongoAuthenticatedReplicaSetContainer`는 `--auth`와 keyfile을 컨테이너 안에서 생성한다 — "`MongoDBContainer` starts mongod without `--auth`. Users can be created on it and every one of them can do everything, so a least-privilege test against it passes no matter how wrong the roles are. **A security lane that cannot fail is not a security lane.**" root 비밀번호는 인스턴스마다 `SecureRandom`으로 만든다(과거에는 소스 상수였고, 그 주석이 왜 그것이 문제인지 적는다).
|
||
- `MongoSingleReplicaSetContainer.providesFailoverEvidence()`는 **항상 false**를 반환하며 그 이유를 문서화한다 — 단일 노드 set은 선출을 하지 않는다.
|
||
- `MongoBsonSnapshot`은 JSON으로 변환하지 않고 BSON 타입을 보존한 채 정규화한다 — JSON으로 가면 `Decimal128`과 문자열이 같아지고, missing과 explicit null이 같아진다. 키 집합을 정규형의 일부로 렌더링해 그 둘을 분리한다.
|
||
|
||
`MongoAccessRules`의 존재 이유도 이 leaf의 반복 주제다: `MongoRepositoryArchitectureRules`는 타입 이름의 `Set<String>`을 반환했고 그 test는 **집합의 내용만 단언했다**. "a controller must not hold a MongoTemplate"은 `Set<String>`에 대한 통과하는 test였고 컨트롤러는 아무 규칙의 지배도 받지 않았다 — "and Boot's own auto-configuration supplies exactly those beans, so the injection was one constructor parameter away." 지금은 ArchUnit 규칙이 실제 클래스 그래프에 적용된다.
|
||
|
||
`MongoModuleBoundaryTest`도 confirmed다. 닫힌 edge 행렬을 트리와 **정확히 일치**하는지 비교하고, DAG 밖의 네 간선(`reactive → imperative`, `reactive → query`, `transaction → reactive`, `geo → imperative`)을 **제거하는 대신 기록한다** — "Each is a real coupling the code relies on, and pretending otherwise is what the previous rules did; recording them makes the next one a decision instead of an accident."
|
||
|
||
## 94. P2 — 커버리지 gate 둘이 나란히 있고, 하나는 발화할 수 없다
|
||
|
||
`MongoStableContractSuite`의 javadoc이 존재 이유를 적는다.
|
||
|
||
> The report distinguishes a failed contract from a contract that never ran. A suite that reports "no failures" because half of it was skipped is exactly the shape of green build that certifies nothing, **so a missing contract is a failure here.**
|
||
|
||
구현은 그 구분을 만들 수 없다(`138-...` §8.2).
|
||
|
||
```java
|
||
Set<MongoReplicaSetContract> executed = new LinkedHashSet<>();
|
||
for (MongoReplicaSetContract contract : MongoReplicaSetContract.all()) {
|
||
executed.add(contract); // ← 루프가 무조건 채운다
|
||
if (!contractRunner.test(contract)) { failures.add(...); }
|
||
}
|
||
Set<MongoReplicaSetContract> missing = new LinkedHashSet<>(MongoReplicaSetContract.all());
|
||
missing.removeAll(executed); // ← 항상 비어 있다
|
||
missing.forEach(contract -> failures.add(... + " (not executed)"));
|
||
```
|
||
|
||
`executed`는 `all()`과 언제나 같으므로 `missing`은 언제나 비고, `(not executed)` 항목은 **어떤 입력으로도 생성되지 않는다**. `certified()`의 `executed.containsAll(all())`(78행)도 마찬가지로 항상 참이다.
|
||
|
||
**조건부 형제**가 같은 testkit 안에 있다. `MongoChaosGate.report()`는 같은 일을 옳게 한다 — `executed`는 명시적 `record(scenario, passed)` 호출로만 채워지는 map이고, `missing`은 `all()`에서 기록되지 않은 것을 뺀 것이다. 그리고 그 test가 그것을 증명한다: `aScenarioThatNeverRanIsAFailureRatherThanASilence`는 13개 시나리오 중 **하나만** 기록하고 나머지가 `(not executed)`로 나타나는지 단언한다.
|
||
|
||
contract suite의 대응 test는 그렇게 하지 않는다. `stableContractsRunOnEverySupportedLane`은 모든 contract에 `contract -> true`를 주고 나서 `report.executed()`가 전부를 담는지 단언한다 — 구조상 참인 명제다.
|
||
|
||
**판정: P2.** 두 인증 lane(7.0/8.0)의 커버리지 주장이 무효다. 수정은 형제를 따르면 된다 — `run(...)`이 실행할 contract 집합을 인자로 받거나, runner가 실제로 호출된 것만 `executed`에 넣는 것.
|
||
|
||
## 95. P2 — release gate가 실제로 차단하는 것은 hermetic test 3개이고, mongo용 CI workflow는 없다
|
||
|
||
이 leaf는 릴리스 증거 장치를 정성껏 만들었다. `MongoReleaseEvidenceVerifier`는 exit code 대신 **JUnit XML을 읽고**, testsuite 이름이 contract의 클래스와 일치하는지 확인하고, 파일이 실행 시작 시각보다 오래됐으면 거부하고, 전부 skip된 lane을 거부한다. 그 근거도 정확하다.
|
||
|
||
> A Gradle test task exits zero when it runs the tests and also when its selector matched a different test… So "sharded topology certified" was satisfied by a hermetic unit test whose name happened to contain `Shard`.
|
||
|
||
그런데 그 장치가 실제로 지키는 목록을 열어 보면(`138-...` §8.3c, `src/config/mongodb/release-contracts.json`):
|
||
|
||
| | id | task | class | topology |
|
||
|---|---|---|---|---|
|
||
| **blocking** | MONGO-REL-001 | `mongoStableContractTest` | `MongoModuleBoundaryTest` | none |
|
||
| | MONGO-REL-002 | `mongoStableContractTest` | `MongoAdvancedRulesTest` | none |
|
||
| | MONGO-REL-003 | `mongoStableContractTest` | `MongoTransactionRetryCoordinatorTest` | none |
|
||
| **experimental** | MONGO-REL-010 | `mongoShardedTest` | `MongoShardedTopologyContractTest` | sharded |
|
||
| | MONGO-REL-011 | `mongoAtlasTest` | `MongoAtlasContractTest` | atlas |
|
||
| | MONGO-REL-012 | `mongoKmsTest` | `MongoKmsContractTest` | kms |
|
||
|
||
차단 계약 **셋 전부가 `topology=none`**, 즉 컨테이너가 필요 없는 hermetic 클래스다. experimental 셋은 **어느 build 파일에도 등록되지 않은 task**를 가리킨다(`grep mongoShardedTest build.gradle` → 매치 0; 스크립트가 그 사실을 스스로 적는다: "registered by no build file"). 그리고 컨테이너가 필요한 여섯 lane — `mongoReplicaSetTest`·`mongoFailoverTest`·`mongoMigrationTest`·`mongoCompatibilityTest`·`mongoSecurityIntegrationTest`·`mongoPerformanceTest` — 은 **차단 목록에 하나도 없다**.
|
||
|
||
그 위에 CI가 얹히지 않는다. `.github/workflows`에 26개 workflow가 있고 **mongo를 언급하는 것은 0개**다(`138-...` §8.4b, grep 매치 없음). 형제 leaf인 JPA는 일곱 개를 갖는다 — `jpa-pr`, `jpa-nightly`, `jpa-release`, `jpa-r2-evidence`, 그리고 `jpa-next-*` 세 개의 전방 호환 workflow. 여섯 mongo lane은 전부 기본 `test` task에서 제외돼 있으므로(§8.4), **사람이 손으로 부르지 않으면 아무 때도 돌지 않는다.**
|
||
|
||
**판정: P2.** 이것은 개별 코드 결함이 아니라 이 leaf의 검증 지형이다. 그리고 앞선 sub-scope들에서 찾은 것들 — §67의 change stream 소실, §75의 TLS 미적용, §85의 sharding 미완료, §56의 fence 계약 — 이 왜 살아남았는지를 설명한다: **그것들을 잡을 lane은 릴리스를 막지 않고 CI에서 돌지 않는다.** 수정은 두 갈래다. (a) 컨테이너 lane 중 최소한 `mongoReplicaSetTest`·`mongoMigrationTest`·`mongoSecurityIntegrationTest`를 blocking contract로 승격하고, (b) JPA와 같은 형태의 workflow를 추가하는 것.
|
||
|
||
## 96. P3 — 소비자가 없는 fixture 셋
|
||
|
||
`138-...` §8.1의 소비자 계수에서 test·testkit 양쪽 모두 0인 타입이 셋이다.
|
||
|
||
| 타입 | 무엇을 위한 것인가 |
|
||
|---|---|
|
||
| `MongoAtlasLocalContainer` | Atlas Local 컨테이너 — search·vector 계약의 빠른 피드백용. `MongoAtlasCapabilityContractSuite`(report 타입)는 test 1곳에서 쓰이지만, **실제 컨테이너를 띄우는 곳은 없다** |
|
||
| `MongoChunkMigrationController` | 트래픽 중 청크 이동 — "production hits during a rebalance"를 재현하는 유일한 장치 |
|
||
| `MongoRoundTripContract` | Java → BSON → **서버** → raw BSON → Java 왕복. javadoc: "Half a round trip proves nothing… only the raw BSON in the middle shows it" |
|
||
|
||
셋째가 가장 무겁다. `MongoReleaseContract`의 형제인 `MongoReplicaSetContract`는 `GOLDEN_BSON`을 열거된 계약으로 두는데, 그 계약을 실행하도록 만들어진 타입에 호출자가 없다. `MongoBsonSnapshot`·`MongoBsonSnapshotAssert`는 쓰이므로 **정규형 단언은 존재하지만 서버를 통과하는 왕복은 돌지 않는다** — 그리고 그 차이가 정확히 이 클래스가 존재하는 이유다. **P3.**
|
||
|
||
## 97. Negative-space probes — sub-scope 11
|
||
|
||
- **8.1 reachability**: 33개 testkit 타입의 소비자를 계수. 셋이 0(§96). 나머지는 test 또는 testkit 안에서 사용됨.
|
||
- **8.2 조건부 형제**: 같은 testkit의 두 커버리지 gate 중 하나만 "실행되지 않음"을 표현할 수 있다(§94). 각자의 test가 그 차이를 그대로 반영한다.
|
||
- **8.3 계약 목록의 소재**: `new MongoReleaseContract`는 test에만 있고, 정본은 `src/config/mongodb/release-contracts.json`(§95). experimental 셋은 존재하지 않는 task를 가리키며 스크립트가 그 사실을 명시한다 — **정직한 기록**이므로 결함이 아니라 confirmed.
|
||
- **8.4 lane / CI drift**: 여섯 lane 정의는 있고 CI workflow는 없다(§95). build.gradle:87의 "382 hermetic contract tests"는 §0에서 측정한 **526**과 어긋난다(sub-scope 01의 문서 drift 항목과 동일 사안).
|
||
|
||
## 98. Sub-scope 11 findings backlog
|
||
|
||
| 우선순위 | finding | reachability |
|
||
|---|---|---|
|
||
| **P2** | release gate의 차단 계약 3개가 전부 `topology=none` hermetic 클래스이고, 컨테이너가 필요한 여섯 lane은 차단 목록에도 CI에도 없다(mongo workflow 0개, JPA는 7개) | 모든 릴리스 |
|
||
| **P2** | `MongoStableContractSuite`의 `(not executed)` 분기와 `certified()`의 커버리지 검사가 구조적으로 도달 불가 — 형제 `MongoChaosGate`는 같은 일을 옳게 한다 | 7.0/8.0 인증 lane |
|
||
| **P3** | 소비자 0인 fixture 셋: `MongoRoundTripContract`(GOLDEN_BSON 계약의 실행체), `MongoAtlasLocalContainer`, `MongoChunkMigrationController` | 해당 계약을 실제로 돌리려는 시점 |
|
||
| **P3/기록** | `experimental_contracts`가 가리키는 세 task(`mongoShardedTest`·`mongoAtlasTest`·`mongoKmsTest`)가 어느 build 파일에도 없다 — 스크립트가 명시적으로 기록하고 있어 은폐는 아니다 | Advanced 승격 시점 |
|
||
|
||
## 99. Sub-scope 11 완료 조건
|
||
|
||
- denominator 49 / 49 FULL_READ (`138-...` OWNED FILES)
|
||
- reachability(33종 소비자 계수)·조건부형제·계약목록 소재·lane/CI drift 4종 probe 수행
|
||
- 두 finding 모두 정적으로 결정 가능하여 실행 probe 불필요, 소스 미변경(`git status --short` = 0)
|
||
|
||
---
|
||
|
||
## 100. 모듈 원장 대조
|
||
|
||
`§0`의 denominator 497을 하위 범위 실측과 대조한다.
|
||
|
||
| # | 하위 범위 | main | test | testkit | perf | 합 | 실측 근거 |
|
||
|---|---|---|---|---|---|---|---|
|
||
| 1 | governance / build / root / autoconfigure | 15 | 12 | – | 4 | 31 | `121`·`122` |
|
||
| 2 | `api/**` | 61 | 9 | – | – | 70 | `127` |
|
||
| 3 | `mapping`+`nativecap`+`geo` | 23 | 4 | – | – | 27 | `130` |
|
||
| 4 | `imperative`+`reactive` | 47 | 14 | – | – | 61 | `131` |
|
||
| 5 | `query`+`aggregation` | 22 | 7 | – | – | 29 | `132` |
|
||
| 6 | `transaction` | 20 | 7 | – | – | 27 | `133` |
|
||
| 7 | `schema`+`migration` | 49 | 9 | – | – | 58 | `134` |
|
||
| 8 | `changestream` | 21 | 5 | – | – | 26 | `135` |
|
||
| 9 | `security`+`failure`+`observation`+`client` | 30 | 14 | – | – | 44 | `136` |
|
||
| 10 | `advanced/**` | 65 | 10 | – | – | 75 | `137` |
|
||
| 11 | testkit + 미배정 test + perf | – | 13 | 35 | 1 | 49 | `138` |
|
||
| | **합계** | **353** | **104** | **35** | **5** | **497** | |
|
||
|
||
- main 353 = 351 Java + 2 비-Java(§0). 실측 LOC 합계 22,927.
|
||
- test 104, testkit 35(3,036 LOC), perf 1(194 LOC), 기타 4(build/config/docs).
|
||
- **unclassified 0, structural-only 0, excluded 0.** 11개 하위 범위 모두 FULL_READ.
|
||
|
||
## 101. 모듈 findings 종합
|
||
|
||
| 우선순위 | 개수 | 항목 |
|
||
|---|---|---|
|
||
| **P1** | 3 | §67 change stream pipeline의 high-water mark로 인한 조용한 영구 소실(실행 probe 3종) · §67의 두 번째 경로(실패 없이도 `CLAIMED_ELSEWHERE` 위치를 지나침) · §75 `MongoClientSettingsFactory` 미호출로 프로파일의 TLS·타임아웃·풀·Stable API가 driver에 도달하지 않음 |
|
||
| **P2** | 9 | §42 aggregation executor의 collection registry·실행 scope 우회 · §49 transaction flag가 요구만 만들고 실행체 없음 · §56 `recordApplied`의 fence 계약 미구현(보호 역전) · §57 index diff가 두 필드만 비교 · §68 `changeStreams` 고정 false와 조립된 소비자의 불일치 · §85 sharding admin gateway 3/4 완료 불가 · §94 `MongoStableContractSuite` 커버리지 검사 도달 불가 · §95 release gate가 hermetic 3개만 차단하고 mongo CI workflow 0개 |
|
||
| **P3 / 기록** | 20 | 각 sub-scope의 backlog 표 참조 |
|
||
|
||
가장 자주 반복된 형태는 셋이다.
|
||
|
||
1. **선언과 조립의 분리.** 정책·값 객체는 완성돼 있고 그것을 driver나 실행 경로에 붙이는 한 줄이 없다(§41·§60·§75·§78). 이 leaf가 fork를 위한 템플릿이라는 성격 때문에 상당 부분은 의도된 것이지만, §75처럼 **수리 코드 자체가 배선되지 않은** 경우와 §49·§68처럼 **flag와 실행체가 어긋난** 경우는 다르다.
|
||
2. **발화할 수 없는 guard.** `requireCorrectResumeOption`(자기 자신과 비교, §69), `MongoStableContractSuite`의 `(not executed)`(§94), 과거의 `requireDistinctCredentials`(role을 지문에 섞어 항상 통과 — 이미 수리됨, §74). 이 저장소는 이 패턴을 여러 번 스스로 찾아 고쳤고, 남은 것들은 같은 계열이다.
|
||
3. **문서가 코드보다 오래 산다.** `MongoPlatformSettings`의 "zero beans"(§68), build.gradle의 "382 hermetic tests"(실측 526), `FlamingockLockAdapter`의 "resumable migrations만 거부"(§59), README의 API surface 318/324(실측 338/350). 반대로 `MongoAdvancedEntryPoint`·`MongoAccessRules`·`MongoModuleBoundaryTest`는 문서였던 주장을 실행 가능한 규칙으로 바꾼 사례다(§84·§93).
|
||
|
||
## 102. 모듈 완료 조건
|
||
|
||
- denominator **497 / 497 FULL_READ** — 11개 하위 범위 전부 COMPLETE(§100)
|
||
- 하위 범위마다 §8.1~§8.4 네 종 negative-space probe 수행, 증거는 `evidence/raw/121`–`138a`
|
||
- 정적으로 결정 불가한 지점은 실행 probe로 확정: `124/124a`(설정 바인딩), `129/129a`(빈 타입 레지스트리 쓰기), `134a`(migration fence·index diff·Flamingock lease), `135a`(change stream 소실 3종), `136a`(TLS 미적용), `137`(sharding 4작업)
|
||
- 임시 probe class는 모두 제거, 매 실행 후 `git status --short` = 0, `mongoStableContractTest` 재실행 green
|
||
- 소스 미변경 — 문서화 작업만 수행
|
||
|
||
## Source anchors
|
||
|
||
이 문서가 backtick으로 인용한 타입·경로를 저장소 트리에 대고 해석한 결과다. 해석된 것만 싣는다 — 총 **230개** (main 180 · test 41 · 기타 9).
|
||
|
||
```
|
||
src/adapter/outbound/persistence-mongo/build.gradle
|
||
src/config/architecture/modules.json (adapter-outbound-persistence-mongo 항목)
|
||
|
||
main:
|
||
src/app-bootstrap/src/main/resources/application.yml
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/MongoOptInAutoConfigurationImportFilter.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/MongoPersistenceConfig.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/MongoPersistenceSettings.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/MongoRootAutoConfiguration.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/advanced/MongoAdvancedCapabilityFlags.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/advanced/MongoAdvancedCapabilityGuard.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/advanced/MongoAdvancedEntryPoint.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/advanced/MongoAdvancedPromotionEvidence.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/advanced/MongoAdvancedPromotionGate.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/advanced/autoconfigure/MongoAdvancedConfiguration.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/advanced/autoconfigure/MongoAdvancedSettings.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/advanced/bridge/MongoChangeMessagingBridge.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/advanced/bridge/MongoPublishResult.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/advanced/encryption/csfle/MongoCsfleClientFactory.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/advanced/encryption/qe/MongoQueryableEncryptionCollectionManager.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/advanced/encryption/qe/MongoQueryableEncryptionProfile.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/advanced/gridfs/MongoGridFsMigrationJob.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/advanced/search/MongoSearchOperations.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/advanced/sharding/ShardKeyDescriptor.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/advanced/sharding/admin/MongoShardingAdminGateway.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/advanced/sharding/admin/ReshardApproval.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/advanced/sharding/admin/ShardKeyReadinessReport.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/advanced/tenancy/database/MongoTenantClientRegistry.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/advanced/tenancy/database/MongoTenantMigrationCoordinator.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/advanced/tenancy/shared/MongoTenantPredicateInjector.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/advanced/tenancy/shared/TenantScopedMongoOperations.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/advanced/timeseries/MongoTimeSeriesCapabilityValidator.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/advanced/timeseries/MongoTimeSeriesOperations.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/advanced/vector/MongoVectorSearchBenchmarkGate.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/advanced/vector/MongoVectorSearchOperations.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/aggregation/PolicyAwareMongoAggregationExecutor.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/api/CollectionProfileName.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/api/DatabaseProfileName.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/api/MongoOperationContext.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/api/MongoOperationScope.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/api/capability/MongoServerVersion.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/api/consistency/MongoConsistencyDescriptor.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/api/consistency/MongoConsistencyProfile.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/api/consistency/MongoConsistencyRegistry.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoDataSchemaUnsupportedException.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoDocumentTooLargeException.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoExecutionOutcome.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoFailureCategory.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoFailureContext.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoOperationRejectedException.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoPersistenceException.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoRetryScope.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoTimeoutException.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoTransactionCommitUnknownException.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoTransactionTransientException.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/api/mapping/MongoTypeRepresentationManifest.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/api/observation/MongoOperationObservation.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/api/observation/MongoOperationObserver.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/api/observation/NoOpMongoOperationObserver.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/api/package-info.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/api/schema/MongoSchemaVersionPolicy.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/autoconfigure/MongoDriverObservabilityAutoConfiguration.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/autoconfigure/MongoPlatformAutoConfiguration.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/autoconfigure/MongoPlatformHealthIndicator.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/autoconfigure/MongoPlatformSettings.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/autoconfigure/MongoProfileProperties.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/autoconfigure/MongoStartupValidator.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/autoconfigure/MongoTopologyProbe.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/changestream/MongoChangeStreamPipeline.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/changestream/MongoChangeStreamState.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/changestream/MongoChangeStreamSubscription.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/changestream/MongoClusterTime.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/changestream/MongoResumeCheckpoint.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/changestream/MongoResumeCheckpointStore.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/changestream/MongoResumeTokenCodec.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/changestream/consumer/ReactiveMongoChangeStreamConsumer.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/changestream/consumer/SpringReactiveChangeStreamSource.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/changestream/projector/MongoChangeDeduplicationStore.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/changestream/projector/MongoChangeProjectionResult.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/changestream/projector/MongoChangeProjector.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/changestream/projector/MongoChangeStreamRunner.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/changestream/recovery/MongoChangeHistoryLostException.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/changestream/recovery/MongoChangeStreamRecoveryPolicy.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/changestream/recovery/MongoInvalidateRecovery.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/client/MongoClientSettingsFactory.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/failure/DefaultMongoFailureTranslator.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/failure/MongoDriverFailureView.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/failure/MongoFailureClassification.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/failure/MongoFailureClassifier.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/failure/MongoFailureExtractor.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/failure/MongoFailureTranslator.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/geo/MongoGeoDistance.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/geo/MongoGeoPoint.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/geo/MongoGeoQuery.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/geo/SpringMongoGeospatialOperations.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/imperative/BoundScopedOperations.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/imperative/DefaultMongoImperativeExecutor.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/imperative/MongoCollectionProfileRegistry.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/imperative/MongoCompletion.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/imperative/MongoConsistencyBinder.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/imperative/MongoPlatformCollectionAccess.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/imperative/MongoTemplateSupportContract.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/imperative/ScopedMongoOperations.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/imperative/atomic/MongoAtomicOperationsTemplate.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/imperative/atomic/MongoAtomicPolicyRegistry.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/imperative/bulk/MongoBulkExecutor.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/imperative/bulk/MongoBulkResult.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/imperative/bulk/SpringDataBulkFailureExtractor.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/imperative/revision/VersionedMongoUpdater.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/mapping/BigDecimalToDecimal128Converter.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/mapping/BigIntegerRepresentationConverters.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/mapping/LocalDateTimeMappingGuard.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/mapping/MongoCustomConversionsFactory.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/mapping/MongoMappingConfiguration.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/mapping/MongoTypeMetadataConfigurer.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/mapping/type/MongoTypeMetadataRegistry.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/mapping/type/PolicyAwareMongoTypeMapper.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/migration/MongoCollectionMigrationLedger.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/migration/MongoCollectionMigrationLock.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/migration/MongoMigration.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/migration/MongoMigrationCheckpoint.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/migration/MongoMigrationHeartbeat.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/migration/MongoMigrationLedger.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/migration/MongoMigrationLock.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/migration/MongoMigrationRunner.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/migration/flamingock/FlamingockLedgerAdapter.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/migration/flamingock/FlamingockLockAdapter.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/nativecap/ApprovedMongoNativeOperation.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/nativecap/MongoNativeCapabilityGateway.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/nativecap/MongoNativeOperationPolicy.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/nativecap/PolicyAwareMongoNativeGateway.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/observation/MicrometerMongoOperationObserver.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/observation/MongoCommandObservationListener.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/observation/MongoDriverObservabilityConfiguration.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/observation/MongoObservationConvention.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/observation/MongoObservationRedactor.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/query/MongoOperator.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/query/MongoQueryPolicy.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/query/MongoRegexPolicy.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/query/PolicyAwareMongoQueryBuilder.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/query/budget/MongoBudgetEnforcer.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/query/budget/MongoBudgetPolicyRegistry.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/query/budget/MongoOperationBudget.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/query/pagination/MongoKeysetCursorCodec.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/query/pagination/MongoKeysetQueryBuilder.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/reactive/DefaultReactiveMongoExecutor.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/reactive/ReactiveMongoConsistencyBinder.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/reactive/ReactiveMongoContextKeys.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/reactive/cursor/MongoCursorGuard.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/schema/index/MongoIndexApplyPolicy.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/schema/index/MongoIndexDescriptorView.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/schema/index/MongoIndexDiff.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/schema/index/MongoIndexDiffEngine.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/schema/index/MongoIndexRetirementState.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/schema/manifest/MongoCollectionManifest.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/schema/manifest/MongoIndexManifest.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/schema/manifest/MongoManifestRegistry.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/schema/manifest/MongoMetadataOwnership.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/schema/model/EmbeddedCollectionDescriptor.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/schema/model/MongoDocumentModelValidator.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/schema/model/MongoDocumentSizeBudget.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/schema/ttl/MongoTtlIndexDescriptor.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/schema/ttl/MongoTtlPolicy.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/schema/ttl/MongoTtlPolicyValidator.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/schema/validation/MongoValidatorApplyPolicy.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/schema/validation/MongoValidatorDiffEngine.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/security/MongoCredentialReference.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/security/MongoCredentialResolver.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/security/MongoSecurityProfileValidator.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/security/admin/MongoAdminApproval.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/security/admin/MongoAdminAuditRecord.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/security/admin/MongoAdminAuthorization.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/security/admin/MongoAdminCommand.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/security/admin/MongoAdminGateway.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/security/admin/MongoAdminOperation.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/security/admin/MongoAdminRuntimeGuard.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/transaction/MongoTransactionExecutor.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/transaction/MongoTransactionProfile.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/transaction/MongoTransactionScope.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/transaction/SpringMongoTransactionSessionFactory.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/transaction/SpringReactiveMongoTransactionExecutor.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/transaction/retry/MongoRetryBudget.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/transaction/retry/MongoTransactionRetryCoordinator.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/mongo/transaction/session/SpringMongoCausalSessionExecutor.java
|
||
|
||
test:
|
||
src/test/java/dev/caskeleton/adapter/outbound/mongo/MongoNamespaceContractTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/mongo/MongoPersistenceConfigTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/mongo/advanced/sharding/ShardAwareQueryValidatorTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/mongo/advanced/sharding/admin/ShardKeyAnalyzerTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoFailureContextTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/mongo/architecture/MongoAdvancedRulesTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/mongo/architecture/MongoModuleBoundaryTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/mongo/autoconfigure/MongoStartupValidatorTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/mongo/changestream/MongoChangeStreamPipelineTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/mongo/changestream/consumer/ChangeStreamConsumerLifecycleTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/mongo/changestream/projector/MongoChangeStreamRunnerTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/mongo/client/MongoClientSettingsFactoryTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/mongo/failure/MongoNetworkFaultLaneTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/mongo/mapping/type/PolicyAwareMongoTypeMapperTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/mongo/migration/MongoMigrationFencingTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/mongo/migration/MongoMigrationLaneTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/mongo/migration/flamingock/FlamingockMongoMigrationAdapterTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/mongo/observation/MongoObservationConventionTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/mongo/security/MongoSecurityIntegrationLaneTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/mongo/security/MongoTlsLaneTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/mongo/security/admin/MongoAdminAuditStateMachineTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/mongo/transaction/retry/MongoTransactionRetryCoordinatorTest.java
|
||
src/testkit/java/dev/caskeleton/adapter/outbound/mongo/architecture/MongoRepositoryArchitectureRules.java
|
||
src/testkit/java/dev/caskeleton/adapter/outbound/mongo/testkit/arch/MongoAccessRules.java
|
||
src/testkit/java/dev/caskeleton/adapter/outbound/mongo/testkit/arch/MongoAdvancedRules.java
|
||
src/testkit/java/dev/caskeleton/adapter/outbound/mongo/testkit/atlas/MongoAtlasCapabilityContractSuite.java
|
||
src/testkit/java/dev/caskeleton/adapter/outbound/mongo/testkit/atlas/MongoAtlasLocalContainer.java
|
||
src/testkit/java/dev/caskeleton/adapter/outbound/mongo/testkit/compat/MongoStableContractSuite.java
|
||
src/testkit/java/dev/caskeleton/adapter/outbound/mongo/testkit/failover/MongoProxiedReplicaSetNode.java
|
||
src/testkit/java/dev/caskeleton/adapter/outbound/mongo/testkit/failover/MongoThreeNodeReplicaSet.java
|
||
src/testkit/java/dev/caskeleton/adapter/outbound/mongo/testkit/failover/ToxiproxyMongoNetworkFaultController.java
|
||
src/testkit/java/dev/caskeleton/adapter/outbound/mongo/testkit/mapping/MongoBsonSnapshot.java
|
||
src/testkit/java/dev/caskeleton/adapter/outbound/mongo/testkit/mapping/MongoBsonSnapshotAssert.java
|
||
src/testkit/java/dev/caskeleton/adapter/outbound/mongo/testkit/mapping/MongoRoundTripContract.java
|
||
src/testkit/java/dev/caskeleton/adapter/outbound/mongo/testkit/performance/MongoChaosGate.java
|
||
src/testkit/java/dev/caskeleton/adapter/outbound/mongo/testkit/release/MongoReleaseContract.java
|
||
src/testkit/java/dev/caskeleton/adapter/outbound/mongo/testkit/release/MongoReleaseEvidenceVerifier.java
|
||
src/testkit/java/dev/caskeleton/adapter/outbound/mongo/testkit/rs/MongoAuthenticatedReplicaSetContainer.java
|
||
src/testkit/java/dev/caskeleton/adapter/outbound/mongo/testkit/rs/MongoReplicaSetContract.java
|
||
src/testkit/java/dev/caskeleton/adapter/outbound/mongo/testkit/rs/MongoSingleReplicaSetContainer.java
|
||
src/testkit/java/dev/caskeleton/adapter/outbound/mongo/testkit/sharded/MongoChunkMigrationController.java
|
||
|
||
기타:
|
||
CLAUDE.md
|
||
README.md
|
||
docs/architecture/mongo-api-surface.txt
|
||
docs/mongodb/repository-adaptation.md
|
||
docs/mongodb/runbooks/failover.md
|
||
docs/mongodb/runbooks/history-lost.md
|
||
docs/registries/env-keys.yaml
|
||
src/build.gradle
|
||
src/config/mongodb/release-contracts.json
|
||
|
||
해석되지 않은 인용 (12종) — 외부 타입·문서상 약칭 등:
|
||
evidence/raw/121-persistence-mongo-module-inventory.txt
|
||
state.json
|
||
evidence/raw/122-mongo-governance-optin-manifest.txt
|
||
*.md
|
||
evidence/raw/123-mongo-optin-reachability-and-siblings.txt
|
||
application.yml
|
||
evidence/raw/125-mongo-governance-doc-count-drift.txt
|
||
126-mongo-hermetic-lane-original-verification.txt
|
||
*.java
|
||
evidence/raw/126-mongo-hermetic-lane-original-verification.txt
|
||
evidence/raw/127-mongo-api-scope-manifest.txt
|
||
evidence/raw/128-mongo-api-negative-space-probes.txt
|
||
|
||
```
|