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

70 KiB
Raw Blame History

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 중복 실행 testmongoStableContractTest가 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:84session.runBody(work, session.operations())를 호출한다.
  • SpringReactiveMongoTransactionSessionFactory.java:115-119operations 인자를 사용하지 않고 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. TransactionPhaseACQUIRING, 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_ONLYTRANSACTION_COMMIT_UNKNOWN
    • WHOLE_TRANSACTION ↔ transient transaction category
    • outcome.isAmbiguous()ambiguous=true
  4. MongoTransactionTransientExceptionMongoTransactionCommitUnknownException 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:17nullOrdering은 builder에서 읽히지 않는다.
  • MongoKeysetQueryBuilder.java:100pageSize + 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도 기록되지 않을 수 있다.
  • MongoOperationBudgetmaxResultBytes는 비교/교집합에는 쓰이지만 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 maxTimeMSlimit을 context/budget의 최소값으로 적용한다.
  3. blocking arbitrary callback에 hard timeout을 약속하지 못하면 API를 typed operation으로 좁혀 driver timeout을 설정한다. 별도 thread interrupt로 Mongo I/O를 취소한다고 가정하지 않는다.
  4. reactive path에서 TimeoutExceptionMongoTimeoutException으로 변환하고 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 이름이 아니라 실제 MongoMappingContextMongoCustomConversions에 연결한다. 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-10docs/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하지 않는다.
  • MongoTransactionScopeSpringMongoCausalSessionExecutorThreadLocal.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다

근거

  • MongoChangeDeduplicationStorealreadyProjectedmarkProjected를 분리한다.
  • 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-108migration.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를 lastSuccessfullyProcessedfailedObjects로 분리한다.
  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_RECORDEDSUCCEEDED 또는 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이 완성되지 않았다

근거

  • MongoProfilePropertiesuriSecret을 가지지만 실제 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하고 checktest + 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 리팩터링

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-bootstrapsample-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:27ReactiveMongoCollectionAccess.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

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. MongoFailurePhaseCLIENT_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 MongoBulkWriteExceptionBulkOperationException 또는 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-004006, 023027, 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 매 변경의 기본 검증

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 변경

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 변경

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 후보

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. 이번 리뷰에서 실행한 검증

성공

./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 검증

./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-001018, MNG-023028) 완료 및 관련 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하지 않았다.