1228 lines
70 KiB
Markdown
1228 lines
70 KiB
Markdown
# 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하지
|
||
않았다.
|