feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가

This commit is contained in:
DongHyeonka
2026-08-15 13:01:58 +09:00
parent ac874e49e6
commit 2f5d2fc219
909 changed files with 62510 additions and 6354 deletions
@@ -6,8 +6,9 @@
// 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
// (src/config/architecture/modules.json) outranks that layout, so the module boundaries are
// packages under dev.caskeleton.adapter.outbound.mongo and MongoModuleBoundaryTest enforces the
// design's module dependency table.
// 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
// equality, and rejects any observed edge that is not declared.
//
// Driver and Spring Data MongoDB versions come from the Spring Boot BOM applied to every module in
// src/build.gradle (design §4: "개별 Driver 버전 override 금지"), so nothing here pins them.
@@ -94,9 +95,16 @@ Closure<Void> applyMongoImageSelection = { task ->
// Docker-backed lanes are excluded from the default unit run: they fail closed without Docker, and
// a `check` that fails on a laptop without Docker teaches people to skip `check`.
//
// `mongodb-contract` is excluded here too. It was not, and `check` depends on both `test` and
// `mongoStableContractTest`, so every one of the 382 hermetic contract tests ran twice on a fresh
// build — once in each task. The two lanes are now disjoint by construction, and
// `MongoTestLaneDisjointnessTest` asserts it against the JUnit XML rather than trusting this
// comment.
tasks.named('test', Test) {
useJUnitPlatform {
excludeTags 'quarantine',
'mongodb-contract',
'mongodb-replicaset',
'mongodb-failover',
'mongodb-migration',
@@ -172,15 +180,55 @@ tasks.register('mongoPerformanceTest', Test) {
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') ?: 'false').toString()
(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'
dependsOn 'mongoStableContractTest', 'verifyMongoTestLaneDisjointness'
}
// The tag exclusion above is a claim about two task configurations. This checks the claim against
// what the two tasks actually ran, because the failure it prevents — every hermetic contract test
// executing twice per `check` — is invisible in a green build and only shows up as time.
tasks.register('verifyMongoTestLaneDisjointness') {
group = 'verification'
description = 'Fails when the unit lane and the stable contract lane execute the same test.'
dependsOn 'test', 'mongoStableContractTest'
def unitResults = layout.buildDirectory.dir('test-results/test')
def contractResults = layout.buildDirectory.dir('test-results/mongoStableContractTest')
inputs.dir(unitResults)
inputs.dir(contractResults)
outputs.file(layout.buildDirectory.file('reports/mongo-test-lane-disjointness.txt'))
doLast {
def executed = { java.io.File directory ->
def names = [] as Set
directory.listFiles({ File file -> file.name.endsWith('.xml') } as FileFilter)
?.each { file ->
new groovy.xml.XmlParser().parse(file).testcase.each { testcase ->
names << "${testcase.@classname}#${testcase.@name}".toString()
}
}
names
}
def unit = executed(unitResults.get().asFile)
def contract = executed(contractResults.get().asFile)
def overlap = unit.intersect(contract)
if (!overlap.isEmpty()) {
throw new GradleException(
"${overlap.size()} tests run in both the unit lane and the stable contract lane, " +
"so `check` executes them twice: ${overlap.take(5)}")
}
def report = outputs.files.singleFile
report.parentFile.mkdirs()
report.text = "unit=${unit.size()} contract=${contract.size()} overlap=0\n"
}
}
tasks.register('mongoStableContractTest', Test) {
@@ -193,3 +241,106 @@ tasks.register('mongoStableContractTest', Test) {
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
}
// verifyMongoApiSurface — every public type this leaf exposes is a committed decision.
//
// 311 of this leaf's 313 production files declare a public top-level type. One jar means `public`
// is public to every adopter, so the intended split between contract and implementation — `api` is
// the surface, the rest is how it is built — is a convention the compiler does not know about.
//
// The full move of implementation packages under an `internal` root is a separate, mechanical
// change; this is what keeps the surface from growing while that is pending. A snapshot does not
// shrink anything. It makes each addition a decision somebody made in review rather than something
// discovered later by an adopter who imported it.
//
// A snapshot does not shrink the surface. It makes each addition visible in review, which is the
// 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')
}
tasks.register('verifyMongoApiSurface') {
group = 'verification'
description = 'Fails without mutation when the committed GraphQL public API surface drifts.'
doLast {
if (project.hasProperty('approveMongoApiSurfaceChange')) {
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')
}