refactor: 각 어댑터터별 리펙토링 진행

This commit is contained in:
DongHyeonka
2026-08-24 18:26:40 +09:00
parent e98b56eb03
commit 0137263441
439 changed files with 31935 additions and 4719 deletions
@@ -4,7 +4,7 @@
// modules map here).
//
// The design models the platform as 19 Stable and 12 Advanced Gradle modules under
// `modules/mongodb` and `modules/mongodb-advanced`. This repository's fail-closed 19-leaf registry
// `modules/mongodb` and `modules/mongodb-advanced`. This repository's fail-closed module registry
// (src/config/architecture/modules.json) outranks that layout, so the module boundaries are
// packages under dev.caskeleton.adapter.outbound.mongo. MongoModuleBoundaryTest holds a closed
// edge matrix — every package and what it may import — compares it against the tree for exact
@@ -43,33 +43,20 @@ dependencies {
// The testkit is its own source set rather than part of `test` because several lanes consume it and
// because the design forbids a production module from depending on the testkit. Declaring its
// dependencies only on the test configurations gives that guarantee without a new Gradle project.
sourceSets {
testkit {
java.srcDir 'src/testkit/java'
resources.srcDir 'src/testkit/resources'
compileClasspath += sourceSets.main.output
runtimeClasspath += output + compileClasspath
}
mongoPerformanceTest {
java.srcDir 'src/mongoPerformanceTest/java'
compileClasspath += sourceSets.main.output + sourceSets.testkit.output
runtimeClasspath += output + compileClasspath
}
}
configurations {
// The testkit compiles against exactly what a test does: testImplementation already extends
// implementation, so this is the module's own dependencies plus the test libraries.
testkitImplementation.extendsFrom testImplementation
testkitRuntimeOnly.extendsFrom testRuntimeOnly
mongoPerformanceTestImplementation.extendsFrom testImplementation
mongoPerformanceTestRuntimeOnly.extendsFrom testRuntimeOnly
strictTestLanes {
// The testkit compiles against exactly what a test does: `implementation` inheritance runs
// through testImplementation, so this is the module's own dependencies plus the test libraries.
sourceSet('testkit') { compilesAgainst 'main' }
sourceSet('mongoPerformanceTest') { compilesAgainst 'main', 'testkit' }
}
// Every test lane compiles and runs against the testkit.
sourceSets.test {
compileClasspath += sourceSets.testkit.output
runtimeClasspath += sourceSets.testkit.output
//
// No publishAs: this leaf's testkit is consumed inside the leaf and is not offered to the
// composition root, unlike the JPA one whose ArchUnit rule pack the root applies to the production
// graph. Publishing is opt-in in the convention precisely so that difference stays a decision.
testkitPublisher {
consumedBy 'test'
}
dependencies {
@@ -113,85 +100,62 @@ tasks.named('test', Test) {
}
}
tasks.register('mongoReplicaSetTest', Test) {
group = 'verification'
description = 'Single-node replica set contract lane: mapping, atomic write, transaction, ' +
'change stream (design §29).'
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
useJUnitPlatform { includeTags 'mongodb-replicaset' }
applyMongoImageSelection(it)
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
// Six lanes, declared rather than assembled. `ca.strict-test-lane` owns testClassesDirs, classpath,
// tag selection, failOnNoDiscoveredTests and the up-to-date refusal — the five lines that used to be
// copied once per lane here and again in four other leaves.
strictTestLanes {
lane('mongoReplicaSetTest') {
tag = 'mongodb-replicaset'
description = 'Single-node replica set contract lane: mapping, atomic write, transaction, ' +
'change stream (design §29).'
customize = { test -> applyMongoImageSelection(test) }
}
lane('mongoFailoverTest') {
tag = 'mongodb-failover'
description = 'Three-node replica set failover lane: primary kill, partition, unknown ' +
'commit, resume (design §29).'
customize = { test -> applyMongoImageSelection(test) }
}
lane('mongoMigrationTest') {
tag = 'mongodb-migration'
description = 'Migration lane: empty / N-1 / oldest-supported snapshots, lock, checkpoint ' +
'restart (design §12).'
customize = { test -> applyMongoImageSelection(test) }
}
lane('mongoCompatibilityTest') {
tag = 'mongodb-compatibility'
description = 'MongoDB 7.0 compatibility and 8.0 primary certification matrix (design §30).'
customize = { test -> applyMongoImageSelection(test) }
}
lane('mongoSecurityIntegrationTest') {
tag = 'mongodb-security-integration'
description = 'RBAC, TLS, injection and redaction release gate against a real server ' +
'(design §26).'
customize = { test -> applyMongoImageSelection(test) }
}
// Driven by its own source set rather than a tag: for this shape the source set is the
// selection, so the convention asks for no tag.
lane('mongoPerformanceTest') {
sourceSet = 'mongoPerformanceTest'
description = 'Certifies contention, aggregation spill, pagination and pool resource ' +
'bounds (design §29).'
customize = { test ->
applyMongoImageSelection(test)
// Assertions on by default. They defaulted to false, so the lane measured numbers and
// compared them to nothing — a performance gate whose bounds are never evaluated is a
// report, and the release evidence called it a certification.
test.systemProperty 'performance.assertions.enabled',
(project.findProperty('performance.assertions.enabled') ?: 'true').toString()
}
}
}
tasks.register('mongoFailoverTest', Test) {
group = 'verification'
description = 'Three-node replica set failover lane: primary kill, partition, unknown commit, ' +
'resume (design §29).'
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
useJUnitPlatform { includeTags 'mongodb-failover' }
applyMongoImageSelection(it)
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
}
tasks.register('mongoMigrationTest', Test) {
group = 'verification'
description = 'Migration lane: empty / N-1 / oldest-supported snapshots, lock, checkpoint ' +
'restart (design §12).'
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
useJUnitPlatform { includeTags 'mongodb-migration' }
applyMongoImageSelection(it)
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
}
tasks.register('mongoCompatibilityTest', Test) {
group = 'verification'
description = 'MongoDB 7.0 compatibility and 8.0 primary certification matrix (design §30).'
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
useJUnitPlatform { includeTags 'mongodb-compatibility' }
applyMongoImageSelection(it)
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
}
tasks.register('mongoSecurityIntegrationTest', Test) {
group = 'verification'
description = 'RBAC, TLS, injection and redaction release gate against a real server ' +
'(design §26).'
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
useJUnitPlatform { includeTags 'mongodb-security-integration' }
applyMongoImageSelection(it)
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
}
tasks.register('mongoPerformanceTest', Test) {
group = 'verification'
description = 'Certifies contention, aggregation spill, pagination and pool resource bounds ' +
'(design §29).'
testClassesDirs = sourceSets.mongoPerformanceTest.output.classesDirs
classpath = sourceSets.mongoPerformanceTest.runtimeClasspath
useJUnitPlatform()
applyMongoImageSelection(it)
// Assertions on by default. They defaulted to false, so the lane measured numbers and compared
// them to nothing — a performance gate whose bounds are never evaluated is a report, and the
// release evidence called it a certification.
systemProperty 'performance.assertions.enabled',
(project.findProperty('performance.assertions.enabled') ?: 'true').toString()
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
}
// `check` gains only the hermetic lanes. The Docker-backed ones stay opt-in for the reason above.
tasks.named('check') {
dependsOn 'mongoStableContractTest', 'verifyMongoTestLaneDisjointness'
dependsOn 'mongoStableContractTest', 'verifyMongoTestLaneDisjointness',
'verifyMongoReleaseContractLanes'
}
// The tag exclusion above is a claim about two task configurations. This checks the claim against
@@ -231,6 +195,55 @@ tasks.register('verifyMongoTestLaneDisjointness') {
}
}
// Splitting the two lanes moved every tagged contract out of `test`, and the release manifest kept
// naming the lane it had left. `MongoReleaseEvidenceVerifier` resolves
// `test-results/<task>/TEST-<className>.xml`, so a contract whose class now runs somewhere else
// resolves to a file that will never exist: the Stable gate reports the transaction retry
// invariants as evidence the run failed to produce, for a suite that ran them.
//
// Checked against the XML the lanes wrote rather than against a tag table, because a tag table here
// would be a second copy of the selection above, and the copy is what drifted the first time.
tasks.register('verifyMongoReleaseContractLanes') {
group = 'verification'
description = 'Fails when a blocking release contract names a lane that did not run its class.'
dependsOn 'test', 'mongoStableContractTest'
def manifest = rootProject.file('../src/config/mongodb/release-contracts.json')
def hermeticLanes = ['test', 'mongoStableContractTest']
def resultsRoot = layout.buildDirectory.dir('test-results')
inputs.file(manifest)
inputs.dir(resultsRoot)
outputs.file(layout.buildDirectory.file('reports/mongo-release-contract-lanes.txt'))
doLast {
def contracts = new groovy.json.JsonSlurper().parse(manifest).contracts
def checked = []
def wrongLane = []
contracts.findAll { hermeticLanes.contains(it.task) }.each { contract ->
def results = resultsRoot.get().dir(contract.task).file(
"TEST-${contract.className}.xml").asFile
if (!results.isFile()) {
wrongLane << "${contract.id} names lane '${contract.task}', which did not run " +
"${contract.className}"
return
}
def suite = new groovy.xml.XmlParser().parse(results)
int executed = (suite.@tests as int) - (suite.@skipped as int)
if (executed < contract.minimumExecuted) {
wrongLane << "${contract.id} requires ${contract.minimumExecuted} executed test(s) " +
"in '${contract.task}' and the lane ran ${executed}"
}
checked << contract.id
}
if (!wrongLane.isEmpty()) {
throw new GradleException(
'the Mongo release manifest points at lanes that cannot produce its evidence: ' +
wrongLane.join('; '))
}
def report = outputs.files.singleFile
report.parentFile.mkdirs()
report.text = "hermetic release contracts verified: ${checked.join(', ')}\n"
}
}
tasks.register('mongoStableContractTest', Test) {
group = 'verification'
description = 'Hermetic stable contract suite: manifests, guardrails, retry scopes, ' +
@@ -257,95 +270,14 @@ tasks.register('mongoStableContractTest', Test) {
// prerequisite for shrinking it: the `api` and `spi` packages are the surface an adopter is meant
// to use, and everything else in this file is a candidate for becoming internal when the leaf is
// split into capability artifacts. Until then the number cannot grow by accident.
def mongoApiSurfaceFile = rootProject.file('../docs/architecture/mongo-api-surface.txt')
Closure<String> renderMongoApiSurface = {
def sourceRoot = file('src/main/java')
def typePattern = ~/(?m)^public\s+(?:final\s+|abstract\s+|sealed\s+|non-sealed\s+)*(class|interface|enum|record|@interface)\s+(\w+)/
def packagePattern = ~/(?m)^package\s+([\w.]+)\s*;/
List<String> types = []
sourceRoot.eachFileRecurse { candidate ->
if (!candidate.isFile() || !candidate.name.endsWith('.java')) {
return
}
String text = candidate.getText('UTF-8')
def packageMatcher = packagePattern.matcher(text)
if (!packageMatcher.find()) {
return
}
String packageName = packageMatcher.group(1)
def typeMatcher = typePattern.matcher(text)
while (typeMatcher.find()) {
types << "${packageName}.${typeMatcher.group(2)}".toString()
}
}
types = types.unique().toSorted()
String header =
"# MongoDB leaf public API surface — every public top-level type in src/main/java.\n" +
"# A public type in a single-jar leaf is reachable from every adopter's code, so\n" +
"# additions are reviewed rather than discovered. `api` is the intended external\n" +
"# surface; the rest is implementation that has not been moved under an internal\n" +
"# root yet.\n" +
"# Update only after review with:\n" +
"# ./gradlew :adapter:outbound:persistence-mongo:updateMongoApiSurface -PapproveMongoApiSurfaceChange\n" +
"# types: ${types.size()}\n"
header + (types.isEmpty() ? '' : types.join('\n') + '\n')
}
// The approval flag is read at configuration time and carried in, not fetched from `project`
// inside doLast. Task.project at execution time is deprecated and fails under Gradle 10, and it is
// incompatible with the configuration cache — which this build will need before it can adopt one.
boolean mongoApiSurfaceUpdateApproved = project.hasProperty('approveMongoApiSurfaceChange')
tasks.register('verifyMongoApiSurface') {
group = 'verification'
description = 'Fails without mutation when the committed GraphQL public API surface drifts.'
doLast {
if (mongoApiSurfaceUpdateApproved) {
throw new GradleException(
'verifyMongoApiSurface is read-only; use updateMongoApiSurface to record an ' +
'approved change.')
}
String rendered = renderMongoApiSurface()
if (!mongoApiSurfaceFile.isFile()) {
throw new GradleException(
"verifyMongoApiSurface: missing committed baseline ${mongoApiSurfaceFile}")
}
String committed = mongoApiSurfaceFile.getText('UTF-8')
if (committed != rendered) {
List<String> committedTypes = committed.readLines().findAll { !it.startsWith('#') }
List<String> renderedTypes = rendered.readLines().findAll { !it.startsWith('#') }
List<String> added = (renderedTypes - committedTypes).toSorted()
List<String> removed = (committedTypes - renderedTypes).toSorted()
throw new GradleException(
"verifyMongoApiSurface: the public API surface changed.\n" +
(added.isEmpty() ? '' : " added:\n " + added.join('\n ') + '\n') +
(removed.isEmpty() ? '' : " removed:\n " + removed.join('\n ') + '\n') +
"Review the change, then record it with:\n" +
" ./gradlew :adapter:outbound:persistence-mongo:updateMongoApiSurface " +
"-PapproveMongoApiSurfaceChange")
}
logger.lifecycle('verifyMongoApiSurface: OK — the committed public API surface is unchanged.')
}
}
tasks.register('updateMongoApiSurface') {
group = 'verification'
description = 'Rewrites the committed GraphQL public API surface baseline after review.'
doLast {
if (!project.hasProperty('approveMongoApiSurfaceChange')) {
throw new GradleException(
'updateMongoApiSurface requires -PapproveMongoApiSurfaceChange: growing the ' +
'public surface is a review decision, not a build step.')
}
mongoApiSurfaceFile.parentFile.mkdirs()
mongoApiSurfaceFile.setText(renderMongoApiSurface(), 'UTF-8')
logger.lifecycle("updateMongoApiSurface: wrote ${mongoApiSurfaceFile}")
}
}
tasks.named('check') {
dependsOn tasks.named('verifyMongoApiSurface')
apiSurface {
label = 'Mongo'
baseline = rootProject.file('../docs/architecture/mongo-api-surface.txt')
description = 'MongoDB leaf public API surface — every public top-level type in src/main/java.'
rationale = [
'A public type in a single-jar leaf is reachable from every adopter\'s code, so',
'additions are reviewed rather than discovered. `api` is the intended external',
'surface; the rest is implementation that has not been moved under an internal',
'root yet.',
]
}