70 KiB
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으로 판정하면 안 된다. 특히 다음 계약은 문서나 타입 이름과 실제 실행 코드가 다르다.
- reactive transaction body가 session-bound
ReactiveMongoOperations를 실제로 전달받지 못한다. - reactive retry의 wall-clock budget이 계산되지 않고, transaction failure context가 서로 모순될 수 있다.
- keyset cursor가 BSON 값을 문자열로 바꾸어 타입과 정렬 의미를 잃는다.
maxResultBytes, imperative timeout, 일부 null-order 정책은 선언만 있고 실행 시 강제되지 않는다.- README가 약속한 startup validation, client generation, health wiring이 auto-configuration에 없다.
- package DAG와 repository/controller guard가 문서상 규칙일 뿐, 닫힌 의존 그래프나 실제 ArchUnit 제약으로 적용되지 않는다.
- 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. 유지할 설계
다음은 리팩터링하면서 보존할 가치가 있다.
apipackage에 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
ReactiveMongoTransactionExecutor.execute를 다음 의미로 바꾼다.Function<ReactiveMongoOperations, ? extends Publisher<T>> work.ReactiveMongoTransactionSession.runBody도 같은 function을 받고 자기bound를 인자로 전달한다.- callback이 arbitrary template을 얻지 못하도록 application adapter의 transaction helper는 전달받은 operations만 사용한다.
- blocking API도 후속 단계에서
Function<MongoOperations,T>로 맞추고ThreadLocal의존을 제거한다. - 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
NanoClock또는Tickerinterface를 주입하고 최초 subscription에서 deadline을 계산한다.- transaction profile과 platform budget의 각 제한에서 최소값을 취한
EffectiveTransactionRetryBudget을 한 번 만든다. BackoffStrategy.nextDelay(attempt, remaining)가 delay를 한 번만 계산하고 decision과Mono.delay가 같은 값을 사용하게 한다.TransactionPhase를ACQUIRING,BODY,COMMITTING,COMMIT_UNKNOWN,TERMINAL로 둔다.- cleanup policy는 phase별로 결정한다.
BODY실패/cancel은 abort, commit-unknown은 abort하지 않고 reconciliation metadata를 반환한다. - transaction 결과 API가 다건을 정말 요구하지 않으면
Mono<T>로 좁힌다. 다건이 필요하면maxBufferedResults/maxBufferedBytes를 profile에 추가하고 초과 시 commit 전 실패한다. 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하며 SpringDataAccessExceptioncause 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
MongoFailureClassification하나에서 category, retryScope, outcome, retryable, ambiguous를 파생한다.MongoFailureContext.from(classification, operation, code, elapsed, attempt)factory만 public으로 둔다.- record constructor에서 다음 불변식을 검증한다.
COMMIT_ONLY↔TRANSACTION_COMMIT_UNKNOWNWHOLE_TRANSACTION↔ transient transaction categoryoutcome.isAmbiguous()↔ambiguous=true
MongoTransactionTransientException과MongoTransactionCommitUnknownExceptionconstructor는 예상 classification이 아니면 즉시 거부한다.MongoFailureExtractorStrategy를 만들고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안의 labelledMongoException이 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-117decode는 모든 값을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
- token header를
version,keyId,sortVersion,issuedAt으로 고정한다. - payload는 canonical BSON 또는 explicit type-tag + length-prefixed binary로 encode한다.
- 허용 타입을 String, boolean, signed numeric variants, Decimal128, ObjectId, UUID, Instant/Date로 닫고 알 수 없는 type은 encode 시 거부한다.
- HMAC은 raw canonical bytes에 적용하고 key rotation을 위해
keyId를 포함한다. - maximum token bytes, field count, duplicate field, malformed length를 decode 전에 검증한다.
- null을 허용한다면 sort key descriptor가 nullability/order를 소유하고 builder가 null branch를 명시적으로 생성한다. 그렇지 않으면 nullable field를 keyset sort에서 construction-time 거부한다.
- 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 +
_idtie-breaker, ascending/descending, nullable field의 실제 Mongo server pagination. Integer.MAX_VALUEpage 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을 적용하지만 genericTimeoutException을 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
MongoExecutionPolicy를 만들어 deadline, result count/bytes, success outcome을 operation type에 따라 계산한다.- query/aggregation에는 server
maxTimeMS와limit을 context/budget의 최소값으로 적용한다. - blocking arbitrary callback에 hard timeout을 약속하지 못하면 API를 typed operation으로 좁혀 driver timeout을 설정한다. 별도 thread interrupt로 Mongo I/O를 취소한다고 가정하지 않는다.
- reactive path에서
TimeoutException을MongoTimeoutException으로 변환하고 operation phase에 따라NOT_SENT또는 unknown outcome을 선택한다. MongoResultBudgetTracker가 encoded BSON byte와 count를 누적하고 초과 시 cursor를 cancel/close한다.- 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
MongoMappingPolicy를 manifest에서 한 번 compile하고 conversions, codec settings, type mapper가 모두 이를 참조한다.- manifest의 각 축에 구현이 없으면 startup에서 실패한다. 선언만 있는 option을 허용하지 않는다.
- long-lived document는 명시적 alias registry 없이는 fail closed한다. ephemeral/internal type만 별도 allow 정책을 둔다.
- incompatible stored type을
basicType으로 조용히 fallback하지 말고 schema/type metadata exception으로 올린다. - type metadata registry builder는 alias/type 충돌을 모두 선검사한 뒤 두 map을 원자적으로 갱신한다.
LocalDateTimeMappingGuard를 단독 bean 이름이 아니라 실제MongoMappingContext와MongoCustomConversions에 연결한다. standard mode에서는 persistentLocalDateTime을 startup에서 거부하거나 명시적 UTC converter를 사용한다.- 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
_classfail-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
MongoPlatformAutoConfiguration하나만 public auto-config entry point로 둔다.- child mapping configuration은 component scan 대상이 아닌 imported nested config로 바꾼다.
@Validated와 nested Jakarta validation 또는 명시적 validator bean을 사용해 refresh 중 설정을 검증한다.MongoTopologyProbe는 실제 client의hello/buildInfo등에서 structured capability를 구한다.MongoClientFactory가 runtime/admin/capability plane client를 credential reference로 생성하고MongoClientGenerationRegistry는 metadata가 아니라 실제 handle lifecycle을 관리한다.- health는 Spring Boot
HealthContributorSPI에 연결하고 liveness와 readiness를 분리한다. - 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
CaSkeletonApplicationscan에서 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
- 각 release contract에 unique ID, test task/FQCN/method, topology, required artifact를 매핑한다.
- JUnit XML에서 발견/실행/skip/failure를 검사하고 task 시작 전의 stale XML은 거부한다.
- sharded/Atlas/KMS lane은 dedicated source set/task에서 실제 command/round-trip을 실행한다.
- secret URI를 JVM argument/process list에 직접 넣지 않고 file/credential provider reference를 쓴다.
- performance는 warm-up + 반복 sample + histogram, pool listener, aggregation
explainspill field, concurrent pagination invariant를 측정한다. - 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
- reactive → imperative:
구현 결정: closed allowed-edge matrix
- logical slice를 최상위 package + 필요한 하위 slice로 명시한다.
sourceSlice -> allowedTargetSlices를 단일 map으로 만들고 발견한 모든 production dependency edge가 map에 있어야 통과하게 한다.- 현재 illegal edge를 먼저 test로 red 상태로 만든다.
- collection profile/budget/context key처럼 여러 실행 경로가 쓰는 contract를
api또는internal.common의 정확한 owner로 이동한다. - 문서 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
- production의 inbound/controller/bootstrap/application/domain package가 Mongo driver, Spring Data Mongo repository/template type에 의존하거나 field/constructor parameter로 받지 못하게 한다.
- 허용 범위는 Mongo leaf의 구체 implementation package와 명시적 composition config뿐이다.
- generic
CommonMongoRepository,BaseMongoRepository, raw collection gateway 이름/상속을 실제 class scan에 적용한다. - “domain repositories may extend Spring Data” 문구는 “adapter-local Spring Data repositories”로 고친다. domain/application port는 framework-free다.
- 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
- descriptor/value object와 executable entry point를 명시적으로 분류한다.
- 모든 executable implementation은
AdvancedCapabilityGuarddecorator/factory를 통해서만 생성한다. - flag를 typed configuration으로 binding하고 disabled/enabled composition test를 둔다.
- 항상 실패하는 method는 제거한다. capability matrix가 지원/미지원과 이유를 반환하고 descriptor validation 단계에서 조합을 거부하게 한다.
- implementation이 없는 search/vector/time-series interface는 문서에서 scaffold로 표시하거나 experimental artifact로 물리 분리한다.
- 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-81map 안에서startTransaction이 던지면 session을 release하지 않는다. MongoTransactionScope와SpringMongoCausalSessionExecutor는ThreadLocal.set/remove로 outer scope를 저장하지 않아 nested bind가 outer context를 잃는다.
구현 결정
- acquisition은
try/catch close또는usingWhenresource acquisition으로 감싼다. - transaction API의 bound operations 인자화로 ambient
ThreadLocal을 제거한다. - 당장 제거하지 못하면 bind 시 existing value를 감지해 nested usage를 명시적으로 거부하거나 stack token으로 restore한다.
- abort/release failure가 original failure를 덮지 않도록 suppressed/observation policy를 고정한다.
필수 테스트
startTransactionsync/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
- store API를 atomic
tryClaim(identity, lease)→CLAIMED|ALREADY_COMPLETED|BUSY로 바꾼다. - 성공 후
complete, 재시도 가능한 실패/lease expiry는releaseOrExpire한다. - projector 자체 idempotency key는 계속 요구하되 dedupe claim이 중복 동시 실행도 줄인다.
- empty publisher는
switchIfEmpty로 protocol violation을 발생시킨다. - 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
- lock acquisition이 monotonically increasing fencing token을 반환한다.
- runner는 lease의 일정 비율마다 heartbeat하고 ownership/fence mismatch 시 작업을 중단한다.
- migration은 bounded batch/checkpoint API를 사용한다. 임의의 장시간 단일
execute는 certification 대상에서 제외하거나 별도 no-expiry maintenance window 정책을 요구한다. - 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이 없다.
구현 결정
- migration job이 source stream을 try-with-resources로 소유한다.
- checkpoint를
lastSuccessfullyProcessed와failedObjects로 분리한다. - target write는 checksum/idempotency key를 사용하고 checkpoint는 성공 후 저장한다.
- 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
- command를 type, target digest, plan digest, dry-run, approver, expiry가 있는 immutable request로 만든다.
- approval token이 같은 digest/dry-run/expiry에 binding되게 한다.
- audit는
INTENT_RECORDED후SUCCEEDED또는 sanitizedFAILEDterminal record를 append한다. - audit sink 실패 정책을 command 종류별 fail-closed로 고정한다.
- 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는 mutableLinkedHashMap을 synchronization 없이 사용한다.MongoTenantMigrationCoordinator는 tenant checkpoint를 in-memoryLinkedHashMap에 둔다.MongoTenantClientRegistry도 mutable access state를 보유한다.
구현 결정
- client generation은 profile별 immutable aggregate를
ConcurrentHashMap.compute로 원자 교체한다. - generation state에 actual client handle, active lease count, retiring timestamp를 함께 둔다.
- tenant migration checkpoint는
MongoTenantMigrationCheckpointStoreport에 영속화하고 coordinator는 stateless orchestration으로 바꾼다. - 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에 둔다.
구현 결정
MongoCredentialResolverport는 secret reference만 받고 value는 client factory의 최소 scope에서만 사용한다.- production profile은 TLS, authentication, stable API, finite connect/server-selection/socket timeout을 필수로 검증한다.
- credential value/URI는
toString, exception, JVM args, audit/metric에 들어가지 않게 한다. - integration test credential은 runtime random으로 생성하고 fixture가 전달한다.
- TLS lane에 trusted CA success, wrong CA, hostname mismatch, expired cert를 포함한다.
- 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 실행에서
test386개,mongoStableContractTest382개가 각각 실행됐다.
구현 결정
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
implementationdependency다.
판정
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로 이동 권장
- external contract만
api에 남기고 concrete implementation은internal로 이동한다. - 같은 package에서만 쓰는 implementation/constructor는 package-private로 낮춘다.
- public API allowlist snapshot과 “module 외부에서 internal 접근 금지” ArchUnit rule을 추가한다.
architectureString rules와 release evidence DTO는 production classpath가 아니라 testkit/build support로 옮긴다.- 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을 권장한다.
- README에 consumer가 registry/runtime composition을 승인해 추가하기 전 shipped runtime에는 포함되지 않는다고 적는다.
- Mongo leaf의 현재
allowed_dependencies는[]로 줄여 fail closed한다. - 실제 도메인 Mongo adapter가 필요할 때 별도 approved leaf/구조에서 application/domain port를 구현한다.
- 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은 각각 rawMongoOperations/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
- public callback에서 raw Spring Data operations를 제거한다.
ScopedMongoOperations<T>와 reactive counterpart가 collection name을 받지 않는 typedfindOne,findMany,insert,updateOne,deleteOne,aggregate만 노출한다.- implementation이 physical collection, read/write concern, deadline, result budget, observation을 자동 적용한다.
- native escape가 필요한 capability는 별도
PolicyAwareMongoNativeGateway에서 allowlisted command로만 제공한다. - 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의 저장 결과가 호출 경로에 따라 달라진다.
구현 결정
- 먼저 per-operation read/write concern 적용으로 원 template을 재사용할 수 있는지 검토한다.
- 별도 template이 필수라면
MongoConsistencyOperationsFactory가 원 template의 converter, entityCallbacks, eventPublisher, writeConcernResolver, writeResultChecking 등 지원 계약을 복제한다. - reflection 기반 field copy는 금지하고 Spring Data가 제공하는 public extension point만 사용한다.
- 지원할 수 없는 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
MongoFailurePhase를CLIENT_VALIDATION,SERVER_SELECTION,COMMAND_SEND,RESPONSE_WAIT,TRANSACTION_BODY,TRANSACTION_COMMIT으로 둔다.- driver subtype과 bounded labels/codes를 view에 보존한다.
NoWritesPerformed와 response loss를 별도 rule로 처리한다.- 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을 다시 실행할 위험이 있다.
구현 결정
SpringDataBulkFailureExtractor가 Spring wrapper cause chain에서 driver bulk result를 추출한다.- atomic/bulk가 같은
MongoAtomicOperationValidator를 사용한다. - item result를
APPLIED,MATCHED_UNCHANGED,FAILED,NOT_ATTEMPTED,UNKNOWN으로 모델링한다. - ordered/unordered semantics와 write-concern ambiguity를 보존한다.
- 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으로
나간다.
구현 결정
- public record constructor 대신 검증된 static factory를 가진 final class로 바꾼다.
- revision field를 건드리는 update가 정확히 하나이고 numeric
$inc 1인지 확인한다. - 같은 field의 conflicting operator/duplicate update를 거부한다.
- expected revision은
MongoRevisionvalue 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 계약은 실행되지 않는다.
구현 결정
ReactiveMongoChangeStreamConsumer.run(Flux<Envelope>)가 checkpoint load → resume mode → driver stream → projection → dedupe completion → checkpoint를 하나의 lifecycle로 소유한다.- partition당
concatMap으로 순차 처리하거나 checkpoint store에saveIfNewer(expectedPrevious, next)CAS를 둔다. - identity에는 stable resume token을 우선 사용하고, 필요 시 lsid/txnNumber/operation index를 보조한다.
- raw token을 숨기려면 key ID가 있는 HMAC을 사용한다.
- 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가 준 mutableQuery에 batch/maxTime을 직접 설정한다.- 같은 Query를 재사용하거나 concurrent subscription하면 설정이 서로 누출될 수 있다.
MongoDriverObservabilityConfiguration은 listener 적용 method를 제공하지만 auto-config에서MongoClientSettingsBuilderCustomizer로 연결되지 않는다.
구현 결정
Query.of(query)등 지원되는 copy API로 defensive copy 후 budget을 적용한다.- operation request에는 mutable Spring Query 대신 immutable platform descriptor를 우선 사용한다.
- MeterRegistry가 있을 때 command/pool/SDAM listener를 등록하는 Boot client-settings customizer bean을 제공한다.
- 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
- 이 문서의 MNG ID를 issue/commit 메시지 추적 키로 사용한다.
MongoModuleBoundaryTest에 현재 illegal edge를 먼저 재현하되, P0 semantic 수정 branch와 package 이동 branch는 분리한다.- Docker 없이 실행 가능한 unit/contract baseline을 저장한다.
- traceable JAR hygiene blocker는 사용자 artifact 소유권을 확인한 별도 작업에서 정리한다.
Phase 1 — transaction correctness (MNG-001~003, 012)
- reactive callback signature와 session binding을 먼저 변경한다.
- real replica set rollback/commit test를 red → green으로 만든다.
- monotonic deadline/backoff Strategy와 transaction phase state machine을 도입한다.
- failure classification-derived context와 common extractor를 적용한다.
- resource acquisition/nested scope/cancellation test를 보강한다.
완료 전 다음 phase로 넘어가지 않는다. transaction contract가 잘못된 상태에서 auto-config를 연결하면 결함의 사용 범위만 넓어진다.
Phase 2 — query/mapping/execution contract (MNG-004006, 023027, 029)
- versioned typed cursor codec과 null policy를 구현한다.
- deadline/result-count/result-byte decorator를 모든 query/cursor/executor에 적용한다.
- mapping manifest를 compiled policy로 바꾸고 real converter/driver golden test를 추가한다.
- raw operations callback을 scoped capability API로 바꾸고 Spring callback/consistency 보존을 검증한다.
- failure classifier에 operation type/phase를 추가한다.
- bulk/revision/result semantics를 닫고 mutable Query/driver observability wiring을 보강한다.
Phase 3 — composition/security (MNG-007, 018, 021)
- library-only runtime 계약을 README/registry에 명확히 한다.
- auto-config entry를 하나로 통합하고 settings validation을 refresh에 연결한다.
- client factory/secret resolver/topology probe/health를 실제 bean graph에 연결한다.
- broad application scan disabled test와 container enabled test를 추가한다.
Phase 4 — architecture/API surface (MNG-009~011, 020)
- exact allowed-edge matrix를 먼저 적용한다.
- shared policy owner를
api/internal.common으로 이동해 illegal edge를 제거한다. - root raw-Mongo injection ArchUnit rule을 추가한다.
- executable Advanced guard를 통일한다.
- 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)
- test/contract 중복을 제거한다.
- Stable contract ID → exact test artifact mapping을 추가한다.
- real topology/chaos/security/performance lane을 강화한다.
- 마지막에만 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 skippedmongoStableContractTest: 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-001
018, 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 결과,
checkblocker, Docker/Advanced 미실행 범위, 증거 등급. - 파생 raw 문서: 없음. 구현 전 read-only finding이므로 interview/blog/canonical로 승격하지 않았다.
- link-only structure lint: PASS.
- full single-file structure lint: 기존
main.mdnaming conflict로NAMING_VIOLATION1건. 제품AGENTS.md가 실제 branch-name path를 요구하고 vault naming rule은 prefix를 요구하므로 임의 rename하지 않았다.