Files
clean-architecture-backend-…/docs/reviews/2026-08-14-mongodb-module-code-review.md

1228 lines
70 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MongoDB persistence 모듈 상세 코드·아키텍처 리뷰
- 기준 일자: 2026-08-14
- 기준 Git HEAD: `92744c57dee5dcd9aa1b12475f1d2d8d16294bc2`
- 대상 Gradle leaf: `:adapter:outbound:persistence-mongo`
- 대상 경로: `src/adapter/outbound/persistence-mongo`
- 판정: **CHANGES REQUIRED**
- 검토 방식: 전체 트리 정적 탐색 + 핵심 실행 경로 정독 + 병렬 교차 리뷰 + fresh unit/contract 검증
- 변경 범위: 이 리뷰 문서만 추가했으며 production/test 코드는 수정하지 않았다.
## 1. 결론
이 모듈은 단순 Mongo repository adapter가 아니라 mapping, imperative/reactive 실행기, transaction,
query/aggregation, schema/index/migration, change stream, security/observability, auto-configuration,
Advanced capability와 release evidence까지 한 leaf에 담은 persistence platform이다. 정책을 값 객체와
명시적 타입으로 모델링하고, Mongo를 기본 비활성으로 둔 방향은 좋다.
그러나 현재 상태를 production-ready Stable platform으로 판정하면 안 된다. 특히 다음 계약은 문서나
타입 이름과 실제 실행 코드가 다르다.
1. reactive transaction body가 session-bound `ReactiveMongoOperations`를 실제로 전달받지 못한다.
2. reactive retry의 wall-clock budget이 계산되지 않고, transaction failure context가 서로 모순될 수 있다.
3. keyset cursor가 BSON 값을 문자열로 바꾸어 타입과 정렬 의미를 잃는다.
4. `maxResultBytes`, imperative timeout, 일부 null-order 정책은 선언만 있고 실행 시 강제되지 않는다.
5. README가 약속한 startup validation, client generation, health wiring이 auto-configuration에 없다.
6. package DAG와 repository/controller guard가 문서상 규칙일 뿐, 닫힌 의존 그래프나 실제 ArchUnit
제약으로 적용되지 않는다.
7. compatibility/failover/performance/Advanced release gate 일부가 실행한 것보다 강한 증거를 만든다.
따라서 즉시 운영 원칙은 다음과 같이 고정한다.
- `ca-skeleton.persistence-mongo.enabled`는 계속 기본 `false`로 유지한다.
- 이 문서의 P0/P1 수정 전에는 Stable release evidence를 새로 발행하지 않는다.
- Advanced는 구현 완료가 아니라 contract scaffold/experimental로 표시한다.
- transaction, cursor, mapping 문제를 먼저 고친 뒤 auto-configuration과 구조 리팩터링을 진행한다.
- 폴더 이동이나 디자인 패턴 도입만으로 동작 결함을 가리지 않는다.
## 2. 검토 범위와 증거 경계
### 2.1 현재 규모
| 항목 | 현재 값 |
|---|---:|
| production Java 파일 | 313 |
| production Java LOC | 18,059 |
| 일반 test Java 파일 | 64 |
| testkit Java 파일 | 26 |
| performance test Java 파일 | 1 |
| `@Test` 메서드 | 411 |
| public top-level type가 있는 production 파일 | 311 / 313 |
### 2.2 깊게 확인한 영역
| 영역 | 상태 | 대표 근거 |
|---|---|---|
| module registry/build/runtime membership | READ_FULL | `src/config/architecture/modules.json`, Mongo `build.gradle`, bootstrap build |
| module/package architecture tests | READ_FULL | `MongoModuleBoundaryTest`, `MongoRepositoryArchitectureRules*`, root `CleanArchitectureTest` 관련 규칙 |
| imperative/reactive execution | READ_FULL | 두 default executor, operation context/result/outcome, cursor publisher |
| transaction/retry/session | READ_FULL | blocking/reactive executor·session factory·retry coordinator·scope와 관련 tests |
| query/budget/keyset | READ_FULL | policy builder, budget types, cursor codec/page builder와 관련 tests |
| mapping/type metadata | READ_FULL | representation manifest, conversions, type mapper, snapshot/round-trip testkit |
| auto-configuration/settings/health | READ_FULL | opt-in filter, persistence config, platform auto-config/properties/validator/probe/health |
| architecture/README/가이드 주장 | READ_PARTIAL | Mongo README·CLAUDE와 관련 ADR/guide의 해당 계약 구간 |
| migration/change stream/admin/GridFS | READ_PARTIAL | coordinator/runner/gateway/job 및 직접 관련 tests/docs |
| Stable/Advanced release lane | READ_FULL | Mongo build task와 두 verification scripts, lane fixtures/tests |
| Advanced 전체 54파일 | READ_PARTIAL | 전수 import/type/flag 사용 탐색 + 실행 진입점 표본 정독 |
`READ_PARTIAL` 영역은 모든 메서드의 품질을 승인했다는 뜻이 아니다. 이 보고서의 결론은 확인한 계약과
실행 seam에 한정한다. Docker-backed lane과 실제 Atlas/KMS/sharded topology는 이번 재검증에서 실행하지
않았으므로 해당 운영 결과는 `UNVERIFIED`다.
## 3. 유지할 설계
다음은 리팩터링하면서 보존할 가치가 있다.
- `api` package에 Spring, Mongo driver, BSON, Reactor, Micrometer import가 없고 ArchUnit으로 이를
검사한다.
- 현재 production code에서 `domain-core`, `application-core`, 다른 adapter production import가
발견되지 않았다.
- Mongo auto-configuration 후보를 기본 비활성화하는 import filter의 Boot 4.0.0 대상 목록은 현재
dependency JAR의 Mongo auto-configuration 목록과 일치한다.
- failure classification에서 label을 code보다 먼저 판단하려는 정책은 Mongo transaction semantics에
맞다.
- transaction body retry와 commit-only retry를 별도 개념으로 둔 것은 반드시 유지해야 한다.
- immutable record, defensive copy, 입력 검증을 일관되게 사용한다.
- cursor HMAC에 constant-time comparison과 32-byte 이상 key를 요구한다.
- query field/operator/sort allowlist와 keyset의 unique tie-breaker 원칙은 적절하다.
- change projection 후 checkpoint를 저장하려는 순서, stable에서 advanced import를 금지한 규칙,
Docker lane을 기본 `test`와 분리한 선택은 유지한다.
- container image tag와 driver version을 중앙에서 관리하려는 방향은 좋다. 다만 release evidence에는
digest와 실제 실행 artifact가 추가로 필요하다.
## 4. 우선순위 요약
| ID | 우선순위 | 심각도 | 주제 | 완료 조건 |
|---|---|---|---|---|
| MNG-001 | P0 | Critical | reactive transaction body의 session 미바인딩 | bound operations를 callback 인자로 강제하고 실제 rollback test 통과 |
| MNG-002 | P0 | High | retry deadline/backoff/result buffering/phase cleanup | monotonic deadline과 단일 backoff 계산, phase test 통과 |
| MNG-003 | P0 | High | transaction failure context 불변식 붕괴 | category/outcome/retry scope가 한 classifier 결과에서 생성됨 |
| MNG-004 | P0 | High | typed keyset cursor가 BSON type을 잃음 | versioned typed codec round-trip/server pagination 통과 |
| MNG-005 | P0 | High | 선언된 timeout/result budget이 미강제 | 모든 entry path가 실제 deadline/byte/result bound를 적용 |
| MNG-006 | P0 | High | BSON representation manifest와 실제 converter 불일치 | manifest 모든 축의 real converter round-trip/golden test 통과 |
| MNG-007 | P0 | High | startup/health/client generation auto-config 부재 | enabled/disabled/invalid context에서 실제 bean/lifecycle 검증 |
| MNG-008 | P0 | High | release lane가 과도한 증거를 생성 | contract-to-artifact mapping과 실 topology 결과로만 promotion 가능 |
| MNG-009 | P1 | High | package DAG가 닫힌 그래프로 강제되지 않음 | exact allowed-edge test가 현재 illegal edge를 탐지 |
| MNG-010 | P1 | High | repository/controller guard가 실행되지 않음 | root ArchUnit negative fixture가 raw Mongo injection을 거부 |
| MNG-011 | P1 | High | Advanced opt-in 불변식이 사실이 아님 | 모든 실행 entry point가 동일 guard/decorator 경유 |
| MNG-012 | P1 | High | session/resource/ambient scope lifecycle 결함 | acquisition 실패·nested scope·cancellation leak test 통과 |
| MNG-013 | P1 | High | change stream dedupe가 atomic하지 않음 | claim/complete state machine과 동시성 test 통과 |
| MNG-014 | P1 | High | migration lease heartbeat/fencing 부재 | 느린 migration 중 lease 갱신과 경쟁 runner 차단 |
| MNG-015 | P1 | High | GridFS stream/checkpoint 의미 결함 | close와 restart/failed-id semantics test 통과 |
| MNG-016 | P1 | High | admin audit가 실패 결과를 표현하지 못함 | intent/succeeded/failed terminal audit와 approval binding |
| MNG-017 | P1 | Medium | mutable registry의 thread safety/durability 부족 | atomic immutable state 또는 durable store로 교체 |
| MNG-018 | P1 | Medium | production security settings/secret wiring 불완전 | prod TLS/auth validation과 secret resolver/client factory 연결 |
| MNG-019 | P2 | Medium | contract suite 중복 실행 | `test``mongoStableContractTest`가 disjoint |
| MNG-020 | P2 | Medium | 311개 public type/god leaf | public API allowlist와 internal package 경계 확보 |
| MNG-021 | P2 | Medium | runtime membership/README 계약 불일치 | library-only 또는 shipped runtime 중 하나를 명시적으로 선택 |
| MNG-022 | P2 | Medium | regex/health/version capability 판정이 과장됨 | 안전한 subset/structured probe/semantic version으로 교체 |
| MNG-023 | P0 | High | collection-scoped callback이 raw operations로 경계를 우회 | collection 인자를 숨기는 scoped capability API로 교체 |
| MNG-024 | P1 | High | consistency template 재생성이 callback/runtime 설정을 잃음 | 원 template의 Spring runtime contract 보존 test 통과 |
| MNG-025 | P1 | High | failure classifier가 operation type/phase를 모름 | 같은 오류의 read/write/body/commit 분류를 구분 |
| MNG-026 | P1 | High | bulk failure/policy/result semantics 붕괴 | Spring wrapper 추출, atomic policy 공유, item state 완전 표현 |
| MNG-027 | P1 | High | optimistic revision invariant 우회 | revision field를 정확히 한 번 `$inc 1`만 허용 |
| MNG-028 | P1 | High | change-stream ordering/identity/source wiring 불완전 | 순차 처리 또는 monotonic CAS와 실제 resume consumer 검증 |
| MNG-029 | P2 | Medium | mutable Query와 driver observability dead wiring | Query copy와 client customizer context test 통과 |
## 5. 상세 발견 사항과 구현 명세
### MNG-001 — reactive transaction body가 session에 묶이지 않는다
**근거**
- `ReactiveMongoTransactionExecutor.java:15`는 body를
`Supplier<? extends Publisher<T>>`로 받는다.
- `SpringReactiveMongoTransactionExecutor.java:84``session.runBody(work, session.operations())`
호출한다.
- `SpringReactiveMongoTransactionSessionFactory.java:115-119``operations` 인자를 사용하지 않고
`Supplier::get`만 실행한다.
**실패 모드**
호출자는 session-bound operations를 전달받을 방법이 없다. 일반 `ReactiveMongoTemplate`을 closure로
캡처하면 write가 transaction session 밖에서 실행되고 executor는 비어 있는 transaction을 commit할 수
있다. API 이름과 주석만 보고 atomicity를 신뢰한 use case에서 부분 write가 남을 수 있으므로 Critical이다.
**구현 결정: Context Object + Unit of Work**
1. `ReactiveMongoTransactionExecutor.execute`를 다음 의미로 바꾼다.
`Function<ReactiveMongoOperations, ? extends Publisher<T>> work`.
2. `ReactiveMongoTransactionSession.runBody`도 같은 function을 받고 자기 `bound`를 인자로 전달한다.
3. callback이 arbitrary template을 얻지 못하도록 application adapter의 transaction helper는 전달받은
operations만 사용한다.
4. blocking API도 후속 단계에서 `Function<MongoOperations,T>`로 맞추고 `ThreadLocal` 의존을 제거한다.
5. binary compatibility가 필요하면 기존 overload를 바로 유지하지 않는다. 기존 overload는 atomicity를
강제할 수 없으므로 한 release 동안 `@Deprecated(forRemoval=true)` + 명시적 `unsafeExecute` 이름으로만
두고, 기본 `execute`는 새 계약으로 전환한다.
**필수 테스트**
- `SpringReactiveMongoTransactionExecutorReplicaSetTest.rollsBackTwoWritesUsingBoundOperations`
- `...commitsTwoWritesUsingTheSameSession`
- `...bodyRetryOpensANewSessionAndDoesNotReuseBoundOperations`
- callback이 외부 template을 사용한 경우를 API/architecture test에서 금지하거나 unsafe API로 명시한다.
### MNG-002 — reactive retry budget과 transaction phase가 실제 시간/상태를 반영하지 않는다
**근거**
- `SpringReactiveMongoTransactionExecutor.java:96,113`은 매 retry마다 elapsed로
`Duration.ZERO.plusNanos(1)`을 넘긴다.
- 같은 파일 `:99-102`, `:120-123`은 jitter가 있는 `delayBefore`를 기록용과 실행용으로 두 번 호출한다.
- blocking coordinator도 decision과 sleep에 `delayBefore`를 별도로 호출하며, transaction profile의
`maxAttempts`와 coordinator의 별도 retry budget을 하나의 effective budget으로 합치지 않는다.
- transaction profile timeout은 body deadline이 아니라 driver `maxCommitTime`에만 적용된다.
- `:84-89`는 transaction body의 모든 값을 `collectList()`로 heap에 모은 뒤 commit한다.
- `:90-92`의 generic error cleanup은 transaction phase를 표현하지 않고 abort/release한다.
**실패 모드**
- `maxElapsed`가 사실상 무시되어 caller deadline을 넘겨 retry한다.
- metric에 기록된 delay와 실제 delay가 다를 수 있다.
- 다건 Publisher는 commit 전에 unbounded memory를 사용할 수 있다.
- commit 결과가 모호한 단계에서도 generic cleanup이 abort를 시도해 원래 오류를 가리거나 상태 해석을
더 어렵게 만들 수 있다.
**구현 결정: Strategy + explicit state machine**
1. `NanoClock` 또는 `Ticker` interface를 주입하고 최초 subscription에서 deadline을 계산한다.
2. transaction profile과 platform budget의 각 제한에서 최소값을 취한
`EffectiveTransactionRetryBudget`을 한 번 만든다.
3. `BackoffStrategy.nextDelay(attempt, remaining)`가 delay를 한 번만 계산하고 decision과 `Mono.delay`
같은 값을 사용하게 한다.
4. `TransactionPhase``ACQUIRING`, `BODY`, `COMMITTING`, `COMMIT_UNKNOWN`, `TERMINAL`로 둔다.
5. cleanup policy는 phase별로 결정한다. `BODY` 실패/cancel은 abort, commit-unknown은 abort하지 않고
reconciliation metadata를 반환한다.
6. transaction 결과 API가 다건을 정말 요구하지 않으면 `Mono<T>`로 좁힌다. 다건이 필요하면
`maxBufferedResults`/`maxBufferedBytes`를 profile에 추가하고 초과 시 commit 전 실패한다.
7. `MongoRetryBudget.none()`은 첫 실행은 허용하고 추가 retry만 금지하도록 `nextAttempt == 1`을 별도로
처리한다. 현재 `maxElapsed=ZERO``< maxElapsed` 조합은 첫 attempt조차 거부할 수 있다.
**필수 테스트**
- virtual clock으로 deadline 직전/동일/초과, backoff 포함 deadline 초과를 검증한다.
- fixed random으로 recorder delay와 실제 scheduler delay가 같은지 검증한다.
- commit-unknown 뒤 body 재구독 0회, commit 재구독 N회, abort 0회를 검증한다.
- `MongoRetryBudget.none()`이 body를 정확히 1회 실행하는 test를 추가한다.
- result bound 초과 시 commit/side effect가 없는지 검증한다.
### MNG-003 — transaction failure context가 exception type과 모순될 수 있다
**근거**
- sync/reactive session factory는 각각 `SpringMongoTransactionSessionFactory.java:147-161`,
`SpringReactiveMongoTransactionSessionFactory.java:144-159`에서 먼저
`MongoFailureContext.commitUnknown(...)`을 만든다.
- classifier가 `WHOLE_TRANSACTION`을 반환하면 그 context를
`MongoTransactionTransientException`에 넣는다.
- `MongoTransactionRetryCoordinatorTest.java:182-188`도 transient exception에 commit-unknown context를
직접 넣어 이 모순을 고정한다.
- 두 session factory는 driver `MongoException`만 직접 mapping하며 Spring `DataAccessException` cause
chain을 공통 방식으로 추출하지 않는다.
**실패 모드**
exception type은 whole transaction retry를 뜻하지만 context category/outcome은
`TRANSACTION_COMMIT_UNKNOWN`, `retryable=false`, `ambiguous=true`가 될 수 있다. telemetry, retry policy,
caller reconciliation이 서로 다른 결론을 내린다. Spring Data가 감싼 label-bearing driver failure는
retry 분류를 건너뛸 수 있다.
**구현 결정: classification-derived factory + Strategy**
1. `MongoFailureClassification` 하나에서 category, retryScope, outcome, retryable, ambiguous를 파생한다.
2. `MongoFailureContext.from(classification, operation, code, elapsed, attempt)` factory만 public으로 둔다.
3. record constructor에서 다음 불변식을 검증한다.
- `COMMIT_ONLY``TRANSACTION_COMMIT_UNKNOWN`
- `WHOLE_TRANSACTION` ↔ transient transaction category
- `outcome.isAmbiguous()``ambiguous=true`
4. `MongoTransactionTransientException``MongoTransactionCommitUnknownException` constructor는 예상
classification이 아니면 즉시 거부한다.
5. `MongoFailureExtractor` Strategy를 만들고 `MongoException`, `DataAccessException`, nested cause,
Reactor timeout을 모든 executor/session에서 동일하게 처리한다. raw message/cause를 외부 계약에
노출하지 않고 code/label/type만 bounded metadata로 보존한다.
**필수 테스트**
- exception type × category × outcome × retry scope invariant parameterized test.
- Spring `DataAccessException` 안의 labelled `MongoException`이 whole/commit-only retry로 분류되는 test.
- cyclic/deep cause chain, no-cause, non-Mongo cause의 fail-closed test.
### MNG-004 — keyset cursor가 BSON type과 framing을 보존하지 않는다
**근거**
- `MongoKeysetCursorCodec.java:88-99`는 값에 `toString()`을 사용한다.
- `:102-117` decode는 모든 값을 `String`으로 복원한다.
- payload는 제어문자 separator와 `String.split`에 의존한다.
- `MongoKeysetQueryBuilderTest.java:77-87`은 문자열 값만 검증한다.
- `MongoKeysetPageRequest.java:17``nullOrdering`은 builder에서 읽히지 않는다.
- `MongoKeysetQueryBuilder.java:100``pageSize + 1`은 상한이 없고 overflow 가능하다.
**실패 모드**
`Instant`, `Date`, `ObjectId`, UUID, numeric/Decimal128 cursor가 String으로 바뀌면 Mongo 비교 BSON type이
달라져 다음 page가 비거나 중복/누락될 수 있다. 문자열 자체에 separator가 들어가면 framing이 깨진다.
nullable sort는 선언된 null order가 predicate에 반영되지 않는다.
**구현 결정: versioned typed value codec**
1. token header를 `version`, `keyId`, `sortVersion`, `issuedAt`으로 고정한다.
2. payload는 canonical BSON 또는 explicit type-tag + length-prefixed binary로 encode한다.
3. 허용 타입을 String, boolean, signed numeric variants, Decimal128, ObjectId, UUID, Instant/Date로
닫고 알 수 없는 type은 encode 시 거부한다.
4. HMAC은 raw canonical bytes에 적용하고 key rotation을 위해 `keyId`를 포함한다.
5. maximum token bytes, field count, duplicate field, malformed length를 decode 전에 검증한다.
6. null을 허용한다면 sort key descriptor가 nullability/order를 소유하고 builder가 null branch를 명시적으로
생성한다. 그렇지 않으면 nullable field를 keyset sort에서 construction-time 거부한다.
7. page size를 registered query budget 이하로 제한하고 `Math.addExact` 또는
`pageSize <= maxPageSize` 선검증을 사용한다.
**필수 테스트**
- 위 모든 허용 BSON type round-trip.
- separator/control/Unicode 문자열, duplicate field, unknown version/key, expired/oversized/tampered token.
- 동일 sort value + `_id` tie-breaker, ascending/descending, nullable field의 실제 Mongo server pagination.
- `Integer.MAX_VALUE` page size 거부.
### MNG-005 — operation timeout과 result budget이 계약대로 강제되지 않는다
**근거**
- `MongoOperationContext.java:8-30`은 모든 operation에 positive timeout을 요구한다.
- `DefaultMongoImperativeExecutor.java:51-86`은 timeout을 읽지 않고 elapsed만 사후 측정한다.
- `DefaultReactiveMongoExecutor.java:76-83,103-109`은 Reactor `.timeout`을 적용하지만 generic
`TimeoutException`을 Mongo failure로 변환하지 않아 observer failure도 기록되지 않을 수 있다.
- `MongoOperationBudget``maxResultBytes`는 비교/교집합에는 쓰이지만 result consumption 경로에서
측정되지 않는다.
- `MongoReactiveCursorPublisher.java:47`은 batch/maxTime만 적용하고 total result/bytes limit을 두지 않는다.
- 두 generic executor는 read 성공도 `WRITE_CONFIRMED`로 기록한다
(`DefaultMongoImperativeExecutor.java:73-77`, `DefaultReactiveMongoExecutor.java:78-80,104-106`).
**실패 모드**
blocking callback은 선언한 deadline을 넘길 수 있고, reactive timeout은 raw Reactor exception으로
유출된다. 대용량 result stream은 byte budget을 넘는다. FIND metric/result가 write confirmed로 기록되어
운영 지표가 잘못된다.
**구현 결정: execution policy decorator**
1. `MongoExecutionPolicy`를 만들어 deadline, result count/bytes, success outcome을 operation type에 따라
계산한다.
2. query/aggregation에는 server `maxTimeMS``limit`을 context/budget의 최소값으로 적용한다.
3. blocking arbitrary callback에 hard timeout을 약속하지 못하면 API를 typed operation으로 좁혀 driver
timeout을 설정한다. 별도 thread interrupt로 Mongo I/O를 취소한다고 가정하지 않는다.
4. reactive path에서 `TimeoutException``MongoTimeoutException`으로 변환하고 operation phase에 따라
`NOT_SENT` 또는 unknown outcome을 선택한다.
5. `MongoResultBudgetTracker`가 encoded BSON byte와 count를 누적하고 초과 시 cursor를 cancel/close한다.
6. read/write completion을 분리한다. 권장안은 `MongoCompletion { READ_CONFIRMED,
WRITE_CONFIRMED, ... }`이며, write ambiguity enum을 억지로 read에 재사용하지 않는다.
**필수 테스트**
- fake/virtual time 기반 reactive timeout translation + observer failure exactly once.
- blocking/query maxTime propagation과 effective minimum deadline test.
- multi-batch result byte/count 초과 시 cancel/close.
- FIND/COUNT/AGGREGATE와 write별 completion metric parameterized test.
### MNG-006 — representation manifest가 실제 mapping policy를 고정하지 않는다
**근거**
- `MongoTypeRepresentationManifest.standard()`은 UUID, decimal, BigInteger, temporal, enum,
type metadata 정책을 선언한다.
- `MongoCustomConversionsFactory.java:38-46`이 등록하는 것은 Decimal128과 DomainId converter뿐이다.
- BigInteger, enum, temporal, UUID 축을 manifest로부터 compile하는 converter/configuration이 없다.
- `PolicyAwareMongoTypeMapper.java:67-78`은 registry에 없는 type을
`CLASS_METADATA_ALLOWED`로 처리하여 long-lived alias 기본 정책과 어긋난다.
- testkit `MongoRoundTripContract`는 production test에서 사용되지 않고, BSON snapshot은 test codec에서
UUID representation을 직접 지정한다.
**실패 모드**
manifest를 바꾸거나 standard를 사용해도 실제 `MappingMongoConverter`/driver codec이 같은 정책을 쓰는지
보장되지 않는다. `_class`, UUID, BigInteger, temporal representation이 환경 기본값에 따라 달라질 수
있고 기존 document를 조용히 오독할 수 있다.
**구현 결정: compiled mapping policy**
1. `MongoMappingPolicy`를 manifest에서 한 번 compile하고 conversions, codec settings,
type mapper가 모두 이를 참조한다.
2. manifest의 각 축에 구현이 없으면 startup에서 실패한다. 선언만 있는 option을 허용하지 않는다.
3. long-lived document는 명시적 alias registry 없이는 fail closed한다. ephemeral/internal type만 별도
allow 정책을 둔다.
4. incompatible stored type을 `basicType`으로 조용히 fallback하지 말고 schema/type metadata exception으로
올린다.
5. type metadata registry builder는 alias/type 충돌을 모두 선검사한 뒤 두 map을 원자적으로 갱신한다.
6. `LocalDateTimeMappingGuard`를 단독 bean 이름이 아니라 실제 `MongoMappingContext`와
`MongoCustomConversions`에 연결한다. standard mode에서는 persistent `LocalDateTime`을 startup에서
거부하거나 명시적 UTC converter를 사용한다.
7. testkit contract를 실제 `MappingMongoConverter` + driver round-trip에 사용한다.
**필수 테스트**
- 실제 replica set에 UUID, BigInteger, BigDecimal, enum, Instant/Date/OffsetDateTime, DomainId를 저장하고
raw BSON과 Java round-trip을 동시에 확인한다.
- alias rename/unknown alias/incompatible `_class` fail-closed test.
- golden BSON snapshot은 production configuration에서 생성하고 MongoDB 7/8 lane에서 비교한다.
### MNG-007 — documented startup/health/client generation wiring이 없다
**근거**
- `README.md:21-23`은 auto-configuration이 startup validator, client generation registry,
health indicator를 등록한다고 말한다.
- `MongoPlatformAutoConfiguration.java:49-123`에는 이 세 bean과 reactive executor가 없다.
- `MongoPlatformProperties.java:16-18`은 binding-time validation을 주장하지만 `validate()`는 수동 method다.
- `MongoStartupValidator`, `MongoTopologyProbe`, `MongoClientGenerationRegistry`,
`MongoPlatformHealthIndicator`는 test에서 직접 생성되며 lifecycle/Actuator SPI에 연결되지 않는다.
- `MongoMappingConfiguration`은 component-scannable `@Configuration`이고 platform condition 밖에서
발견될 수 있다.
**실패 모드**
설정을 켜도 문서상 startup checks와 health/client generation이 실행되지 않는다. 반대로 leaf를 실제
bootstrap scan에 넣으면 master flag가 false여도 mapping/configuration 일부가 생성될 가능성이 있다.
**구현 결정: 단일 composition root + Abstract Factory**
1. `MongoPlatformAutoConfiguration` 하나만 public auto-config entry point로 둔다.
2. child mapping configuration은 component scan 대상이 아닌 imported nested config로 바꾼다.
3. `@Validated`와 nested Jakarta validation 또는 명시적 validator bean을 사용해 refresh 중 설정을
검증한다.
4. `MongoTopologyProbe`는 실제 client의 `hello`/`buildInfo` 등에서 structured capability를 구한다.
5. `MongoClientFactory`가 runtime/admin/capability plane client를 credential reference로 생성하고
`MongoClientGenerationRegistry`는 metadata가 아니라 실제 handle lifecycle을 관리한다.
6. health는 Spring Boot `HealthContributor` SPI에 연결하고 liveness와 readiness를 분리한다.
7. imperative/reactive auto-config를 class presence 조건의 nested configuration으로 분리한다.
**필수 테스트**
- `ApplicationContextRunner`: disabled, enabled-imperative, enabled-reactive, both, missing URI/secret,
insecure production, topology mismatch, user override bean.
- broad `CaSkeletonApplication` scan에서 disabled 시 Mongo platform bean 0개.
- container context에서 startup validator가 실제 topology mismatch를 거부.
- health UP/DEGRADED/DOWN과 credential rotation generation drain.
### MNG-008 — release lane가 실행한 것보다 강한 증거를 만든다
**근거**
- Advanced script는 `-Dmongodb.sharded.uri`를 넘기지만 Java production/test source가 이를 읽지 않는다.
- `--tests '*Shard*'`는 실제 sharded topology operation이 아닌 unit selector도 만족한다.
- Atlas/KMS는 environment variable 존재만으로 evidence에 포함되고, security/migration evidence는 script가
missing 목록에 무조건 추가하여 gate가 완결될 수 없다.
- `MongoAdvancedPromotionEvidence`가 요구하는 migration 항목과 `MongoAdvancedPromotionGate`가 검사하는
required 항목도 서로 다르다.
- complete sharded URI를 JVM system property argument로 전달하여 process inspection/실패 출력에 credential이
노출될 수 있다.
- Stable contract tag에는 Advanced GridFS test도 포함되어 “Stable은 Advanced 제외”라는 release script
분류와 어긋난다.
- performance lane은 한 번의 count를 p50/p95/p99 모두에 넣고 pool wait=0, spill=false를 상수로 기록하며
timing assertion 기본값은 false다.
- version matrix contract predicate가 실동작 없이 true를 반환할 수 있고, 3-node failover test는 election
관측보다 강한 unknown-commit/resume 계약을 직접 검증하지 않는다.
**실패 모드**
test process의 exit 0 또는 env 존재가 feature certification으로 승격된다. support matrix와 release
evidence가 실제로 실행하지 않은 transaction/change stream/security/performance 동작을 증명한 것처럼
보일 수 있다.
**구현 결정: evidence manifest + contract-to-artifact mapping**
1. 각 release contract에 unique ID, test task/FQCN/method, topology, required artifact를 매핑한다.
2. JUnit XML에서 발견/실행/skip/failure를 검사하고 task 시작 전의 stale XML은 거부한다.
3. sharded/Atlas/KMS lane은 dedicated source set/task에서 실제 command/round-trip을 실행한다.
4. secret URI를 JVM argument/process list에 직접 넣지 않고 file/credential provider reference를 쓴다.
5. performance는 warm-up + 반복 sample + histogram, pool listener, aggregation `explain` spill field,
concurrent pagination invariant를 측정한다.
6. promotion manifest에 image digest, server/driver version, commit SHA, test result hash, topology probe를
넣고 누락된 required evidence가 있으면 fail한다.
**필수 테스트/게이트**
- selector mutation, zero tests, all skipped, stale XML, wrong topology, missing artifact가 모두 gate를
실패시키는 test.
- real election 중 driver operation continuity, unknown commit reconciliation, change stream resume.
- performance assertion을 release gate에서 항상 true로 강제하고 percentile sample 수 하한을 검증한다.
### MNG-009 — package dependency DAG가 닫힌 그래프로 강제되지 않는다
**근거**
- `build.gradle:6-10`과 `docs/mongodb/repository-adaptation.md:21-25`는 원 설계의 package DAG를
`MongoModuleBoundaryTest`가 강제한다고 주장한다.
- 현재 test는 선택된 역방향 의존만 금지한다(`MongoModuleBoundaryTest.java:95-168`).
- 현재 존재하지만 원 설계 allowed edge에 없는 import 예:
- reactive → imperative: `DefaultReactiveMongoExecutor.java:13`
- reactive cursor → query budget: `MongoCursorGuard.java:4`
- transaction session → reactive: `ReactiveMongoCausalSessionExecutor.java:6`
- geo → imperative/schema: `SpringMongoGeospatialOperations.java:6-10`
**구현 결정: closed allowed-edge matrix**
1. logical slice를 최상위 package + 필요한 하위 slice로 명시한다.
2. `sourceSlice -> allowedTargetSlices`를 단일 map으로 만들고 발견한 모든 production dependency edge가
map에 있어야 통과하게 한다.
3. 현재 illegal edge를 먼저 test로 red 상태로 만든다.
4. collection profile/budget/context key처럼 여러 실행 경로가 쓰는 contract를 `api` 또는
`internal.common`의 정확한 owner로 이동한다.
5. 문서 DAG를 바꾸어야 한다면 test와 adaptation doc을 같은 변경에서 갱신한다.
**필수 테스트**
- unknown package slice와 unknown edge가 실패하는 negative fixture.
- Stable → Advanced, runtime → testkit, API → framework 금지 유지.
- exact matrix와 문서 표가 동일 source에서 생성/검증되는 drift test.
### MNG-010 — repository/controller guardrail이 실제 codebase에 적용되지 않는다
**근거**
- `MongoRepositoryArchitectureRules.java:35-56`은 금지 이름/type의 String set만 반환한다.
- `MongoRepositoryArchitectureRulesTest.java:20-41`은 set 내용만 assert한다.
- root controller architecture rule은 JPA/Spring Data repository를 막지만 MongoTemplate,
ReactiveMongoTemplate, MongoClient/Database/Collection injection을 포괄하지 않는다.
- Boot auto-configuration은 raw client/template bean을 제공하므로 composition 후 우회가 가능하다.
**구현 결정: executable root ArchUnit rule**
1. production의 inbound/controller/bootstrap/application/domain package가 Mongo driver, Spring Data Mongo
repository/template type에 의존하거나 field/constructor parameter로 받지 못하게 한다.
2. 허용 범위는 Mongo leaf의 구체 implementation package와 명시적 composition config뿐이다.
3. generic `CommonMongoRepository`, `BaseMongoRepository`, raw collection gateway 이름/상속을 실제 class
scan에 적용한다.
4. “domain repositories may extend Spring Data” 문구는 “adapter-local Spring Data repositories”로
고친다. domain/application port는 framework-free다.
5. String catalog helper는 testkit으로 옮기거나 actual ArchRule factory로 바꾼다.
**필수 테스트**
- controller가 MongoTemplate/MongoRepository/MongoClient를 주입하는 negative fixture 각각 실패.
- outbound adapter implementation과 auto-config의 필요한 reference는 허용.
- root `CleanArchitectureTest`에서 Mongo leaf가 composition되지 않아도 class import로 검사한다.
### MNG-011 — Advanced opt-in invariant와 always-throw API
**근거**
- `CLAUDE.md:69-70`은 모든 Advanced entry point가 flag 없이는 construction을 거부한다고 말한다.
- 54개 Advanced production file 중 flag를 직접 참조하는 것은 일부뿐이다.
- 실행 가능한 `MongoChangeMessagingBridge.java:29-59`, tenancy/search/vector 관련 여러 entry point는
동일 guard를 강제하지 않는다.
- `MongoTimeSeriesCapabilityValidator`의 네 public method와
`MongoQueryableEncryptionProfile`의 일부 query method는 항상 `UnsupportedOperationException`을 던진다.
**구현 결정: capability guard decorator + Specification**
1. descriptor/value object와 executable entry point를 명시적으로 분류한다.
2. 모든 executable implementation은 `AdvancedCapabilityGuard` decorator/factory를 통해서만 생성한다.
3. flag를 typed configuration으로 binding하고 disabled/enabled composition test를 둔다.
4. 항상 실패하는 method는 제거한다. capability matrix가 지원/미지원과 이유를 반환하고 descriptor
validation 단계에서 조합을 거부하게 한다.
5. implementation이 없는 search/vector/time-series interface는 문서에서 scaffold로 표시하거나
experimental artifact로 물리 분리한다.
6. change-to-messaging bridge가 publish 결과를 상수 `published=true, ambiguous=false`로 만들지 않고 broker
adapter의 confirmed/ambiguous/failed 결과를 받아 checkpoint/outbox policy를 실제로 분기하게 한다.
**필수 테스트**
- `..advanced..`의 executable concrete type이 guard/factory를 경유하는 ArchUnit rule.
- capability별 disabled construction/operation, enabled supported operation, unsupported combination.
- stable auto-config graph에 Advanced bean/type dependency가 없는지 검증.
### MNG-012 — session acquisition과 ambient scope lifecycle이 안전하지 않다
**근거**
- sync factory는 session을 연 뒤 `SpringMongoTransactionSessionFactory.java:73`에서 transaction을
시작한다. 시작 실패 시 close하는 보호 구문이 없다.
- reactive factory도 `SpringReactiveMongoTransactionSessionFactory.java:76-81` map 안에서
`startTransaction`이 던지면 session을 release하지 않는다.
- `MongoTransactionScope`와 `SpringMongoCausalSessionExecutor`는 `ThreadLocal.set/remove`로 outer scope를
저장하지 않아 nested bind가 outer context를 잃는다.
**구현 결정**
1. acquisition은 `try/catch close` 또는 `usingWhen` resource acquisition으로 감싼다.
2. transaction API의 bound operations 인자화로 ambient `ThreadLocal`을 제거한다.
3. 당장 제거하지 못하면 bind 시 existing value를 감지해 nested usage를 명시적으로 거부하거나 stack
token으로 restore한다.
4. abort/release failure가 original failure를 덮지 않도록 suppressed/observation policy를 고정한다.
**필수 테스트**
- `startTransaction` sync/reactive throw 시 close 1회.
- cancel/body failure/commit failure/cleanup failure 조합별 abort/release 횟수와 원 exception 보존.
- nested scope rejection 또는 outer restoration.
### MNG-013 — change-stream dedupe는 check-then-act race다
**근거**
- `MongoChangeDeduplicationStore`는 `alreadyProjected`와 `markProjected`를 분리한다.
- `MongoChangeStreamRunner.java:52-72`는 check → project → mark 순서다.
- 두 subscriber가 동시에 false를 읽으면 둘 다 projection을 실행할 수 있다.
- store/projector가 empty `Mono`를 반환할 때 일부 chain은 terminal action 없이 끝날 수 있다.
**구현 결정: durable state machine**
1. store API를 atomic `tryClaim(identity, lease)` → `CLAIMED|ALREADY_COMPLETED|BUSY`로 바꾼다.
2. 성공 후 `complete`, 재시도 가능한 실패/lease expiry는 `releaseOrExpire`한다.
3. projector 자체 idempotency key는 계속 요구하되 dedupe claim이 중복 동시 실행도 줄인다.
4. empty publisher는 `switchIfEmpty`로 protocol violation을 발생시킨다.
5. checkpoint는 projection/dedupe completion 성공 뒤에만 advance한다.
**필수 테스트**
- 2개 concurrent runner에서 projector exactly once.
- crash after claim/before project, after project/before complete, after complete/before checkpoint.
- empty store/projector, lease expiration, history lost recovery.
### MNG-014 — migration lock lease를 긴 batch 중 갱신하지 않는다
**근거**
- schema migration guide와 lock 주석은 between-batch refresh/restart를 약속한다.
- `MongoMigrationRunner.java:107-108`은 `migration.execute(context)` 전체가 끝난 뒤 refresh한다.
- context에는 heartbeat/fencing token이 없어 오래 걸리는 execute 중 lease가 만료될 수 있다.
**실패 모드**
두 번째 runner가 만료된 lock을 획득한 뒤 첫 runner가 계속 쓰면 migration이 중첩된다. 단순 refresh의
`matchedCount` 수정만으로 이 문제를 해결하지 못한다.
**구현 결정: lease heartbeat + fencing**
1. lock acquisition이 monotonically increasing fencing token을 반환한다.
2. runner는 lease의 일정 비율마다 heartbeat하고 ownership/fence mismatch 시 작업을 중단한다.
3. migration은 bounded batch/checkpoint API를 사용한다. 임의의 장시간 단일 `execute`는 certification
대상에서 제외하거나 별도 no-expiry maintenance window 정책을 요구한다.
4. ledger/checkpoint write에도 fence를 조건으로 사용한다.
**필수 테스트**
- mutable clock + blocking batch + competing runner.
- heartbeat success/failure, stale fence write rejection, process kill 후 checkpoint restart.
- 실제 replica set migration lane에서 long batch와 lease contention.
### MNG-015 — GridFS stream과 checkpoint의 의미가 불일치한다
**근거**
- `MongoGridFsCompatibilityReader.java:20-25`는 caller가 stream을 닫아야 한다고 명시한다.
- `MongoGridFsMigrationJob.java:56-66`은 try-with-resources 없이 stream을 넘긴다.
- checkpoint 이름은 `lastMigrated`지만 failure path/test는 failed object ID를 그 자리에 저장한다.
- 문서는 failed IDs 재실행을 말하지만 별도 failed-id collection이 없다.
**구현 결정**
1. migration job이 source stream을 try-with-resources로 소유한다.
2. checkpoint를 `lastSuccessfullyProcessed`와 `failedObjects`로 분리한다.
3. target write는 checksum/idempotency key를 사용하고 checkpoint는 성공 후 저장한다.
4. failed object retry queue의 bounded size/retention과 poison object 정책을 명시한다.
**필수 테스트**
- close-tracking stream: success/failure/cancel 모두 close 1회.
- N번째 실패 후 restart가 N-1 성공 checkpoint부터 재개하고 성공 object를 중복 생성하지 않음.
- failed ID가 별도 보존되고 retry/poison 정책을 따름.
### MNG-016 — admin audit가 command 결과와 approval 대상을 증명하지 못한다
**근거**
- `MongoAdminGateway.java:51-61`은 command supplier 실행 전에 applied audit를 기록한다.
- `MongoAdminAuditRecord`에는 outcome/failure terminal state가 없다.
- `MongoAdminAuthorization.approved`의 dry-run 값과 gateway invocation의 dryRun이 cryptographically 또는
structurally binding되지 않는다.
**구현 결정: typed command + audit state machine**
1. command를 type, target digest, plan digest, dry-run, approver, expiry가 있는 immutable request로 만든다.
2. approval token이 같은 digest/dry-run/expiry에 binding되게 한다.
3. audit는 `INTENT_RECORDED` 후 `SUCCEEDED` 또는 sanitized `FAILED` terminal record를 append한다.
4. audit sink 실패 정책을 command 종류별 fail-closed로 고정한다.
5. raw command string/secret/document data는 audit에 저장하지 않는다.
**필수 테스트**
- supplier throw 시 FAILED terminal audit.
- approval reuse, target/dry-run mismatch, expiry, concurrent double execution 거부.
- audit sink 실패 시 command가 실행되지 않는지 검증.
### MNG-017 — client/tenant registry가 singleton 동시성과 restart를 견디지 못한다
**근거**
- `MongoClientGenerationRegistry`는 mutable `LinkedHashMap`을 synchronization 없이 사용한다.
- `MongoTenantMigrationCoordinator`는 tenant checkpoint를 in-memory `LinkedHashMap`에 둔다.
- `MongoTenantClientRegistry`도 mutable access state를 보유한다.
**구현 결정**
1. client generation은 profile별 immutable aggregate를 `ConcurrentHashMap.compute`로 원자 교체한다.
2. generation state에 actual client handle, active lease count, retiring timestamp를 함께 둔다.
3. tenant migration checkpoint는 `MongoTenantMigrationCheckpointStore` port에 영속화하고 coordinator는
stateless orchestration으로 바꾼다.
4. tenant client cache는 max entries, idle expiry, close-on-evict, single-flight create를 강제한다.
**필수 테스트**
- rotate/require/release 100-way concurrency에서 lost update/early close 없음.
- tenant client same-key single creation, eviction close, max bound.
- coordinator restart 후 durable checkpoint resume.
### MNG-018 — production security config와 secret/client wiring이 완성되지 않았다
**근거**
- `MongoProfileProperties`는 `uriSecret`을 가지지만 실제 secret resolver/client settings로 연결되지 않는다.
- production profile에서 TLS/authentication required와 duration 양수 조건이 충분히 startup validation에
연결되지 않는다.
- security integration fixture는 auth/RBAC/redaction을 일부 검증하지만 TLS/rotation 전체를 검증하지
않으며 test credential literal을 source에 둔다.
**구현 결정**
1. `MongoCredentialResolver` port는 secret reference만 받고 value는 client factory의 최소 scope에서만
사용한다.
2. production profile은 TLS, authentication, stable API, finite connect/server-selection/socket timeout을
필수로 검증한다.
3. credential value/URI는 `toString`, exception, JVM args, audit/metric에 들어가지 않게 한다.
4. integration test credential은 runtime random으로 생성하고 fixture가 전달한다.
5. TLS lane에 trusted CA success, wrong CA, hostname mismatch, expired cert를 포함한다.
6. rotation은 new generation ready → traffic switch → old lease drain → close 순서를 검증한다.
### MNG-019 — Stable contract 382개가 `check`에서 중복 실행된다
**근거**
- default `test`는 Docker tag만 제외하고 `mongodb-contract`를 제외하지 않는다
(`build.gradle:97-105`).
- `check`는 별도 `mongoStableContractTest`에 의존한다(`:181-194`).
- fresh 실행에서 `test` 386개, `mongoStableContractTest` 382개가 각각 실행됐다.
**구현 결정**
default `test`에서 `mongodb-contract`를 exclude하고 `check`가 `test` +
`mongoStableContractTest`를 각각 한 번 실행하게 한다. contract가 대부분 unit test와 같은 class라면 반대로
별도 task를 제거할 수도 있으나, release artifact 분리를 위해 전자를 권장한다.
**검증**
- 두 task의 XML FQCN/method 집합 교집합이 0인지 build contract test로 검사한다.
- `check` 총 discovered 수가 두 disjoint 집합의 합과 같은지 검사한다.
### MNG-020 — public API와 Gradle leaf가 과도하게 넓다
**근거**
- 313 production Java 파일 중 311개가 public top-level type을 노출한다.
- 한 leaf에 Stable/Advanced, sync/reactive, admin/migration, starter, architecture policy가 모두 들어 있다.
- sync/reactive starter가 모두 unconditional `implementation` dependency다.
**판정**
class 수만으로 god module이라고 단정하지 않는다. 그러나 닫히지 않은 package DAG, 거의 전부 public인
surface, inseparable Advanced/admin/starter까지 함께 보면 artifact boundary 기준 god leaf다.
**즉시 구현: 현재 19-leaf 정책을 보존하는 package 리팩터링**
```text
dev.caskeleton.adapter.outbound.mongo
├── api
│ ├── execution
│ ├── consistency
│ ├── failure
│ ├── mapping
│ ├── query
│ └── transaction
├── autoconfigure
├── internal
│ ├── springdata
│ ├── imperative
│ ├── reactive
│ ├── transaction
│ ├── query
│ ├── schema
│ ├── migration
│ ├── changestream
│ ├── security
│ └── observation
├── advanced
│ ├── api
│ └── internal
└── architecture # production이 아니라 testkit/test로 이동 권장
```
1. external contract만 `api`에 남기고 concrete implementation은 `internal`로 이동한다.
2. 같은 package에서만 쓰는 implementation/constructor는 package-private로 낮춘다.
3. public API allowlist snapshot과 “module 외부에서 internal 접근 금지” ArchUnit rule을 추가한다.
4. `architecture` String rules와 release evidence DTO는 production classpath가 아니라 testkit/build
support로 옮긴다.
5. package 이동은 transaction P0 수정 뒤에 진행해 semantic diff와 mechanical diff를 섞지 않는다.
**조건부 장기안**
물리 Gradle module은 `api`, `spring-data-common`, `imperative`, `reactive`, `admin`, `advanced`,
`starter`, `testkit` 정도의 8개가 현실적이다. 다만 현재 repository는 정확히 19 leaf를 canonical로
강제한다. 따라서 이 분리는 일반 리팩터링으로 바로 실행하면 HARD-STOP 위반이다. 별도 architecture
proposal에서 `AGENTS.md`, `modules.json`, settings 검증, runtime membership, dependency tests를 원자적으로
바꾸는 승인이 있을 때만 진행한다.
### MNG-021 — runtime membership과 README activation 계약이 다르다
**근거**
- Mongo registry entry의 `runtime_memberships`는 빈 배열이다.
- shipped `app-bootstrap`과 `sample-portfolio`는 Mongo project dependency가 없다.
- README는 property 설정만으로 활성화되는 것처럼 안내한다.
- registry는 application/shared dependency를 허용하지만 현재 Mongo build는 project dependency가 없다.
**구현 결정**
이번 템플릿에서는 **library-only opt-in**을 권장한다.
1. README에 consumer가 registry/runtime composition을 승인해 추가하기 전 shipped runtime에는 포함되지
않는다고 적는다.
2. Mongo leaf의 현재 `allowed_dependencies`는 `[]`로 줄여 fail closed한다.
3. 실제 도메인 Mongo adapter가 필요할 때 별도 approved leaf/구조에서 application/domain port를 구현한다.
4. property-only activation을 지원하기로 결정한다면 modules registry membership, bootstrap dependency,
enabled/disabled full composition test를 같은 변경으로 추가한다.
### MNG-022 — regex, health, version capability가 실제 보장보다 강하게 표현된다
**근거와 조치**
- `MongoRegexPolicy`의 nested quantifier 검사는 `*`, `+`, `{` 중심의 syntactic 검사다. `?`, alternation,
overlapping group 등 모든 catastrophic pattern을 안전하게 판별하지 못한다.
- 사용자 검색은 기본 literal prefix/escaped contains로 제한한다.
- regex를 열어야 하면 parser 기반 safe subset + maxTime + index/hint policy를 함께 적용한다.
- `MongoPlatformHealthIndicator`의 secondary availability는 topology별 expected secondary 수를 단순화한다.
- 실제 topology probe 결과와 configured threshold를 사용하고 liveness/readiness를 구분한다.
- `MongoTimeSeriesCapabilityValidator`의 version 판정은 문자열 prefix에 의존한다.
- semantic version parser보다 가능하면 실제 server capability/command probe를 권위로 사용한다.
### MNG-023 — collection-scoped callback이 raw operations로 경계를 우회한다
**근거**
- `MongoCollectionAccess.java:27`과 `ReactiveMongoCollectionAccess.java:26`은 각각 raw
`MongoOperations`/`ReactiveMongoOperations`를 반환한다.
- executor의 `ScopedAccess.collection(requested)`는 다른 collection 이름을 거부하지만 caller는
`access.operations().find(..., "anotherCollection")`처럼 이 검사를 호출하지 않고 우회할 수 있다.
- consistency binder의 read concern/query setting helper도 caller가 별도 호출해야 하므로 평범한 callback
read에는 자동 적용되지 않는다.
**실패 모드**
등록된 collection profile과 tenant boundary를 벗어난 read/write가 가능하다. caller가
`PRIMARY_MAJORITY`/causal profile을 선택해도 callback이 일반 operation을 호출하면 requested read concern이
조용히 빠질 수 있다.
**구현 결정: capability-based scoped adapter**
1. public callback에서 raw Spring Data operations를 제거한다.
2. `ScopedMongoOperations<T>`와 reactive counterpart가 collection name을 받지 않는 typed
`findOne`, `findMany`, `insert`, `updateOne`, `deleteOne`, `aggregate`만 노출한다.
3. implementation이 physical collection, read/write concern, deadline, result budget, observation을 자동
적용한다.
4. native escape가 필요한 capability는 별도 `PolicyAwareMongoNativeGateway`에서 allowlisted command로만
제공한다.
5. migration/admin처럼 raw access가 필요한 plane은 runtime callback과 다른 credential/type으로 분리한다.
**필수 테스트**
- public callback API에서 collection 문자열/raw operations를 얻을 수 없는 API surface test.
- 모든 scoped operation이 등록된 physical collection을 사용하고 majority/snapshot concern을 적용하는
argument capture test.
- tenant A callback으로 tenant B collection을 접근할 수 없는 integration test.
### MNG-024 — consistency별 template 재생성이 Spring runtime contract를 잃는다
**근거**
- `MongoConsistencyBinder.java:41-47`과 reactive counterpart는 factory와 converter로 새 template을 만든다.
- 원 Boot template에 붙은 entity callbacks, auditing, event publisher, write concern resolver,
write-result checking 등 다른 runtime 설정을 명시적으로 이전하지 않는다.
**실패 모드**
일반 Spring Data repository/template에서는 실행되던 `BeforeConvertCallback`, auditing, validation/event가
platform executor 경로에서는 빠질 수 있다. 같은 entity의 저장 결과가 호출 경로에 따라 달라진다.
**구현 결정**
1. 먼저 per-operation read/write concern 적용으로 원 template을 재사용할 수 있는지 검토한다.
2. 별도 template이 필수라면 `MongoConsistencyOperationsFactory`가 원 template의 converter,
entityCallbacks, eventPublisher, writeConcernResolver, writeResultChecking 등 지원 계약을 복제한다.
3. reflection 기반 field copy는 금지하고 Spring Data가 제공하는 public extension point만 사용한다.
4. 지원할 수 없는 setting은 startup에서 명시적으로 거부하거나 README에 제한을 적는다.
**필수 테스트**
- `BeforeConvertCallback`/auditing이 base path와 consistency-bound path에서 각각 정확히 1회 실행.
- custom write concern resolver와 application event 설정 보존.
- sync/reactive 양쪽 bean graph test.
### MNG-025 — failure classifier가 operation type과 failure phase를 모른다
**근거**
- `MongoDriverFailureView.from`은 command sent/response 여부를 driver exception subtype과 실제 phase에
충분히 연결하지 않는다.
- classifier는 read/write/body/commit context 없이 label/code/view만으로 outcome을 만든다.
- label/code가 없는 driver timeout/server-selection timeout은 unclassified로 떨어질 수 있다.
**실패 모드**
동일 socket failure가 FIND에서는 안전한 read retry 후보인데 UPDATE에서는 write result unknown일 수 있다.
현재처럼 operation/phase가 없으면 read를 ambiguous write로 분류하거나 server-selection failure를
non-retryable unclassified로 보낼 수 있다.
**구현 결정: ordered classification rule chain**
```text
classify(operationType, failurePhase, driverFailureView)
1. authoritative labels
2. transaction phase-specific rules
3. exact driver subtype / command-sent state
4. server code
5. fail-closed unclassified
```
1. `MongoFailurePhase`를 `CLIENT_VALIDATION`, `SERVER_SELECTION`, `COMMAND_SEND`, `RESPONSE_WAIT`,
`TRANSACTION_BODY`, `TRANSACTION_COMMIT`으로 둔다.
2. driver subtype과 bounded labels/codes를 view에 보존한다.
3. `NoWritesPerformed`와 response loss를 별도 rule로 처리한다.
4. retry scope와 execution outcome은 rule result에서 함께 생성한다.
**필수 테스트**
- 같은 socket exception을 FIND/UPDATE와 send-before/send-after 조합으로 분류.
- server selection timeout, driver timeout, `NoWritesPerformed`, transient transaction,
unknown commit의 exact category/outcome/retry scope.
### MNG-026 — bulk path가 Spring failure wrapper와 atomic policy를 우회한다
**근거**
- `MongoBulkExecutor.java:53-61`은 직접 driver bulk exception 중심으로 처리한다.
- Spring Data bulk 실행은 driver `MongoBulkWriteException`을 `BulkOperationException` 또는
`DataIntegrityViolationException` 계열로 감쌀 수 있다.
- bulk update는 atomic update path가 사용하는 protected-field/operator validator를 공유하지 않는다.
- 현재 result는 upsert, matched-but-unchanged, ordered failure 뒤 not-attempted, write concern unknown을
item별로 완전히 표현하지 못한다.
- item 수 상한만으로 encoded Mongo command/document byte ceiling을 보장할 수 없다.
**실패 모드**
실제 duplicate-key partial failure가 generic translation으로 빠져 성공/실패 index가 사라진다. bulk를 통해
보호 field/operator 정책을 우회할 수 있고, retry 시 이미 성공한 item을 다시 실행할 위험이 있다.
**구현 결정**
1. `SpringDataBulkFailureExtractor`가 Spring wrapper cause chain에서 driver bulk result를 추출한다.
2. atomic/bulk가 같은 `MongoAtomicOperationValidator`를 사용한다.
3. item result를 `APPLIED`, `MATCHED_UNCHANGED`, `FAILED`, `NOT_ATTEMPTED`, `UNKNOWN`으로 모델링한다.
4. ordered/unordered semantics와 write-concern ambiguity를 보존한다.
5. encoded byte budget을 계산해 ordered semantics를 유지하는 chunking Strategy를 적용하거나 초과를
실행 전에 거부한다.
**필수 테스트**
- Spring wrapper 안 duplicate-key partial result와 실제 server bulk failure.
- protected field/operator, successful upsert, no-op, ordered failure의 후속 item, write concern ambiguity.
- item 수는 적지만 encoded bytes가 ceiling을 넘는 계획.
### MNG-027 — optimistic revision invariant를 public constructor로 우회할 수 있다
**근거**
- `VersionedUpdateCommand.java:18-30,49-51`은 public record constructor로 임의 filter/update를 받는다.
- 첫 revision increment만 확인한 뒤 같은 field에 추가 `$inc`, `$set`이 있는지 완전히 닫지 않는다.
- filter revision value의 numeric type도 강제되지 않아 accessor에서 cast failure가 날 수 있다.
**실패 모드**
revision을 1이 아닌 값으로 증가시키거나 set으로 덮어 optimistic locking의 monotonic invariant를 깨뜨릴
수 있다. 잘못된 filter value가 operation 전에 안정된 validation error가 아니라 `ClassCastException`으로
나간다.
**구현 결정**
1. public record constructor 대신 검증된 static factory를 가진 final class로 바꾼다.
2. revision field를 건드리는 update가 정확히 하나이고 numeric `$inc 1`인지 확인한다.
3. 같은 field의 conflicting operator/duplicate update를 거부한다.
4. expected revision은 `MongoRevision` value object만 받는다.
**필수 테스트**
- `$inc 1` 뒤 `$inc 5`, `$set revision`, duplicate operator, String revision 모두 construction-time 거부.
- matched/no-match/conflict 결과가 outer execution outcome과 일치.
### MNG-028 — change-stream ordering, identity, source wiring이 완전하지 않다
**근거**
- 현재 runner는 event 단위 `run`을 노출해 caller가 병렬 호출할 수 있다.
- 뒤 event B의 projection/checkpoint가 앞 event A보다 먼저 완료되면 checkpoint가 앞질러 저장되거나
나중에 뒤로 회귀할 수 있다.
- `MongoChangeEventIdentity`의 clusterTime/namespace/document/operation tuple은 같은 transaction에서 같은
document에 같은 operation을 여러 번 한 event를 충돌시킬 수 있다.
- production source에서 driver `changeStream/watch/resumeAfter/startAfter`를 runner/recovery/checkpoint에
잇는 lifecycle consumer가 확인되지 않는다.
**실패 모드**
process가 B checkpoint 뒤 A 완료 전에 죽으면 A를 영구 건너뛸 수 있다. identity 충돌은 distinct event를
duplicate로 오인한다. policy/value object는 있어도 실제 resume state machine이 조립되지 않으면 문서의
at-least-once consumer 계약은 실행되지 않는다.
**구현 결정**
1. `ReactiveMongoChangeStreamConsumer.run(Flux<Envelope>)`가 checkpoint load → resume mode → driver stream
→ projection → dedupe completion → checkpoint를 하나의 lifecycle로 소유한다.
2. partition당 `concatMap`으로 순차 처리하거나 checkpoint store에
`saveIfNewer(expectedPrevious, next)` CAS를 둔다.
3. identity에는 stable resume token을 우선 사용하고, 필요 시 lsid/txnNumber/operation index를 보조한다.
4. raw token을 숨기려면 key ID가 있는 HMAC을 사용한다.
5. invalidate/history-lost는 explicit state transition으로 halt/rebuild/startAfter를 선택한다.
**필수 테스트**
- A/B completion 순서를 뒤집어도 checkpoint skip/regression 없음.
- same transaction/same document multiple updates의 identity가 다름.
- replica set에서 failover resume, invalidate/startAfter, history-lost halt, crash after projection/before
checkpoint.
### MNG-029 — mutable Query와 driver observability configuration이 호출 경계에 연결되지 않는다
**근거**
- `MongoReactiveCursorPublisher.java:32-49`는 caller가 준 mutable `Query`에 batch/maxTime을 직접 설정한다.
- 같은 Query를 재사용하거나 concurrent subscription하면 설정이 서로 누출될 수 있다.
- `MongoDriverObservabilityConfiguration`은 listener 적용 method를 제공하지만 auto-config에서
`MongoClientSettingsBuilderCustomizer`로 연결되지 않는다.
**구현 결정**
1. `Query.of(query)` 등 지원되는 copy API로 defensive copy 후 budget을 적용한다.
2. operation request에는 mutable Spring Query 대신 immutable platform descriptor를 우선 사용한다.
3. MeterRegistry가 있을 때 command/pool/SDAM listener를 등록하는 Boot client-settings customizer bean을
제공한다.
4. operation observation과 driver observation의 metric/tag ownership을 구분해 double count를 막는다.
**필수 테스트**
- 원 Query가 변경되지 않고 서로 다른 두 subscription의 budget이 독립적임.
- `ApplicationContextRunner`에서 customizer/listener 존재와 disabled/no-meter 조건.
- pool checkout/server selection/primary change metric의 bounded tag test.
## 6. 디자인 패턴 적용 지침
패턴은 package 수를 늘리기 위한 장식이 아니라 현재 실패 모드를 없앨 때만 사용한다.
| 패턴 | 적용 위치 | 해결하는 문제 | 피해야 할 적용 |
|---|---|---|---|
| Context Object / Unit of Work | sync/reactive transaction callback | session-bound operations를 명시적으로 전달 | ThreadLocal을 감춘 facade만 추가 |
| Strategy | failure extractor/classifier, backoff, result budget, success outcome | 분기와 불변식을 한 정책으로 통합 | 모든 작은 validator를 interface로 분해 |
| State Machine | transaction phase, change claim, client rotation, admin audit | 순서·terminal state·재시도 가능성을 명시 | enum만 만들고 transition guard 미구현 |
| Abstract Factory | runtime/admin/capability Mongo client | credential/plane/settings/lifecycle을 한 owner가 구성 | caller에게 raw URI/client settings를 다시 노출 |
| Specification | query allowlist, capability combination, schema/index diff | 조합 가능한 정책과 거부 이유를 표현 | business rule을 persistence Specification으로 이동 |
| Decorator | observation, budget, advanced guard | 모든 executable entry path에 공통 정책 적용 | 일부 constructor만 수동 guard |
| Adapter/Port | secret resolver, durable checkpoint, audit sink | 외부 secret/store/audit backend 교체 | domain/application에 Spring Data type 노출 |
Generic Repository pattern은 권장하지 않는다. Mongo aggregate마다 query/index/atomic update/consistency 요구가
다르므로 domain/application에는 좁은 port를 두고 Mongo leaf에서 Spring Data/template 기반 adapter로
구현한다. `CommonMongoRepository<T,ID>`는 collection/budget/consistency guard를 우회하기 쉽다.
## 7. 구현 순서
### Phase 0 — 변경 전 safety net
1. 이 문서의 MNG ID를 issue/commit 메시지 추적 키로 사용한다.
2. `MongoModuleBoundaryTest`에 현재 illegal edge를 먼저 재현하되, P0 semantic 수정 branch와 package 이동
branch는 분리한다.
3. Docker 없이 실행 가능한 unit/contract baseline을 저장한다.
4. traceable JAR hygiene blocker는 사용자 artifact 소유권을 확인한 별도 작업에서 정리한다.
### Phase 1 — transaction correctness (MNG-001~003, 012)
1. reactive callback signature와 session binding을 먼저 변경한다.
2. real replica set rollback/commit test를 red → green으로 만든다.
3. monotonic deadline/backoff Strategy와 transaction phase state machine을 도입한다.
4. failure classification-derived context와 common extractor를 적용한다.
5. resource acquisition/nested scope/cancellation test를 보강한다.
완료 전 다음 phase로 넘어가지 않는다. transaction contract가 잘못된 상태에서 auto-config를 연결하면
결함의 사용 범위만 넓어진다.
### Phase 2 — query/mapping/execution contract (MNG-004~006, 023~027, 029)
1. versioned typed cursor codec과 null policy를 구현한다.
2. deadline/result-count/result-byte decorator를 모든 query/cursor/executor에 적용한다.
3. mapping manifest를 compiled policy로 바꾸고 real converter/driver golden test를 추가한다.
4. raw operations callback을 scoped capability API로 바꾸고 Spring callback/consistency 보존을 검증한다.
5. failure classifier에 operation type/phase를 추가한다.
6. bulk/revision/result semantics를 닫고 mutable Query/driver observability wiring을 보강한다.
### Phase 3 — composition/security (MNG-007, 018, 021)
1. library-only runtime 계약을 README/registry에 명확히 한다.
2. auto-config entry를 하나로 통합하고 settings validation을 refresh에 연결한다.
3. client factory/secret resolver/topology probe/health를 실제 bean graph에 연결한다.
4. broad application scan disabled test와 container enabled test를 추가한다.
### Phase 4 — architecture/API surface (MNG-009~011, 020)
1. exact allowed-edge matrix를 먼저 적용한다.
2. shared policy owner를 `api`/`internal.common`으로 이동해 illegal edge를 제거한다.
3. root raw-Mongo injection ArchUnit rule을 추가한다.
4. executable Advanced guard를 통일한다.
5. public API allowlist를 만든 뒤 concrete type을 internal/package-private로 축소한다.
### Phase 5 — operational state machines (MNG-013~017, 028)
change dedupe, migration lease, GridFS checkpoint, admin audit, client/tenant registry를 각각 독립 change로
처리한다. 각 change는 concurrent/crash/restart test가 있어야 한다.
### Phase 6 — release evidence (MNG-008, 019, 022)
1. test/contract 중복을 제거한다.
2. Stable contract ID → exact test artifact mapping을 추가한다.
3. real topology/chaos/security/performance lane을 강화한다.
4. 마지막에만 support matrix와 release evidence를 갱신한다.
## 8. 권장 검증 매트릭스
### 8.1 매 변경의 기본 검증
```bash
cd src
./gradlew :adapter:outbound:persistence-mongo:test --rerun-tasks --no-daemon --console=plain
./gradlew :adapter:outbound:persistence-mongo:mongoStableContractTest --rerun-tasks --no-daemon --console=plain
./gradlew :adapter:outbound:persistence-mongo:check --no-daemon --console=plain
./gradlew verifyCleanArchitectureDependencies verifyDependencyLocks verifyEnvKeys verifyPublicPathSnapshot --no-daemon --console=plain
```
### 8.2 transaction/query/mapping 변경
```bash
cd src
./gradlew :adapter:outbound:persistence-mongo:mongoReplicaSetTest --no-daemon --console=plain
./gradlew :adapter:outbound:persistence-mongo:mongoFailoverTest --no-daemon --console=plain
./gradlew :adapter:outbound:persistence-mongo:mongoCompatibilityTest --no-daemon --console=plain
```
검증할 동작은 transaction commit/rollback, whole-body retry, commit-only retry, response-loss reconciliation,
typed cursor pagination, mapping raw BSON이다. task exit code만으로 완료하지 않고 해당 test ID와 JUnit XML
실행 수를 확인한다.
### 8.3 migration/security/performance 변경
```bash
cd src
./gradlew :adapter:outbound:persistence-mongo:mongoMigrationTest --no-daemon --console=plain
./gradlew :adapter:outbound:persistence-mongo:mongoSecurityIntegrationTest --no-daemon --console=plain
./gradlew :adapter:outbound:persistence-mongo:mongoPerformanceTest -Pperformance.assertions.enabled=true --no-daemon --console=plain
```
### 8.4 release 후보
```bash
MONGODB_DOCKER=1 bash scripts/verify-mongodb-platform.sh
bash scripts/verify-mongodb-advanced.sh
```
Advanced script는 MNG-008을 고치기 전에는 promotion 성공 근거로 사용하지 않는다. 현재 설계상 missing
evidence를 보고하는 exit는 실패가 아니라 아직 promotion할 수 없다는 정직한 상태로 해석한다.
## 9. 이번 리뷰에서 실행한 검증
### 성공
```text
./gradlew :adapter:outbound:persistence-mongo:test \
:adapter:outbound:persistence-mongo:mongoStableContractTest \
--rerun-tasks --no-daemon --max-workers=2 --console=plain
BUILD SUCCESSFUL in 1m 5s
6 actionable tasks: 6 executed
```
- `test`: 386 tests, 0 failures, 0 errors, 0 skipped
- `mongoStableContractTest`: 382 tests, 0 failures, 0 errors, 0 skipped
### 별도 repository 검증
```text
./gradlew verifyCleanArchitectureDependencies verifyDependencyLocks \
verifyEnvKeys verifyPublicPathSnapshot \
--rerun-tasks --no-daemon --max-workers=2 --console=plain
BUILD SUCCESSFUL in 49s
30 actionable tasks: 30 executed
```
- 19개 leaf dependency lock 검증을 포함해 모두 실행·성공했다.
- `verifyEnvKeys`: 155 keys, 67 required placeholders, 173 application references,
61 typed properties, 280 registry rows — OK.
- 기존 owner가 아직 소비하지 않는 6개 env key warning은 남았지만 Mongo 변경으로 발생한 failure는 아니다.
- `verifyPublicPathSnapshot`: committed public paths unchanged — OK.
### 차단된 검증
Mongo leaf `check`는 Mongo compile/test failure가 아니라 root 선행 task
`verifyNoStaleTraceableJars`에서 차단됐다. 현재 HEAD `92744c5...`와 다른 source SHA
`99a51e5a1614...`로 만들어진 traceable JAR 14개가 남아 있었다. gate가 안내한
`cleanStaleTraceableJars`는 artifact 삭제 작업이므로 이 read-only review에서 실행하지 않았다.
### 실행하지 않은 검증
- Docker-backed replica set/failover/migration/compatibility/security/performance lane
- 실제 Atlas, KMS, sharded cluster Advanced lane
- 운영 부하와 production topology 검증
따라서 unit/contract green은 이 보고서의 runtime correctness finding을 반박하지 않는다. 해당 tests가
session-bound reactive transaction, typed BSON cursor, real mapping policy, actual auto-config lifecycle,
release evidence fidelity를 아직 검증하지 않기 때문이다.
## 10. Definition of Done
Mongo platform을 Stable/production-ready로 다시 판정하려면 최소한 다음이 모두 필요하다.
- [ ] 모든 P0 및 High finding(MNG-001~018, MNG-023~028) 완료 및 관련 real topology test 통과
- [ ] reactive transaction callback이 bound operations 외 경로를 기본 API로 사용할 수 없음
- [ ] retry category/outcome/scope invariant test 전수 통과
- [ ] cursor 허용 BSON type round-trip + 실제 pagination 전수 통과
- [ ] 모든 timeout/result budget이 실제 실행 path에서 강제됨
- [ ] representation manifest 각 축이 real converter/codec에 연결됨
- [ ] disabled/enabled/invalid auto-configuration context와 health/client lifecycle 통과
- [ ] package exact DAG와 root raw-Mongo injection rule 통과
- [ ] scoped execution API 밖에서 raw operations/다른 collection을 접근할 수 없음
- [ ] consistency-bound path에서 Spring callbacks/auditing/read concern이 보존됨
- [ ] bulk partial result와 optimistic revision invariant test 통과
- [ ] change-stream 순서/identity/resume lifecycle의 crash·failover test 통과
- [ ] Advanced executable entry point 전부 opt-in guard를 경유
- [ ] compatibility/failover/performance/Advanced evidence가 실제 test artifact/topology와 1:1 연결
- [ ] hard-coded test credential 제거, TLS/rotation lane 통과
- [ ] Docker Stable gate fresh 성공 및 JUnit evidence count 확인
- [ ] root `check`와 architecture/dependency/env/public-path 검증 성공
- [ ] README, support matrix, ADR의 보장 수준이 실제 구현·검증 수준과 일치
이 체크리스트를 충족하기 전의 정확한 표현은 “MongoDB persistence platform contract와 일부 실행
경로가 구현된 opt-in experimental leaf”다.
## 11. LLM Wiki capture
- 갱신: `/home/donghyeon/workspace/ai-tool/llm-wiki-private/raw/branch-notes/main.md`
- 기록 내용: 기준 HEAD, 검토 범위, 핵심 finding, 구현 순서, 변경 파일, fresh Gradle 결과,
`check` blocker, Docker/Advanced 미실행 범위, 증거 등급.
- 파생 raw 문서: 없음. 구현 전 read-only finding이므로 interview/blog/canonical로 승격하지 않았다.
- link-only structure lint: PASS.
- full single-file structure lint: 기존 `main.md` naming conflict로 `NAMING_VIOLATION` 1건. 제품
`AGENTS.md`가 실제 branch-name path를 요구하고 vault naming rule은 prefix를 요구하므로 임의 rename하지
않았다.