feat: redis, fileserver, httpclient 런타임 시점 구현 추가

This commit is contained in:
donghyeon-ka
2026-07-28 14:26:54 +09:00
parent 7363b2aa1e
commit b3add0162d
257 changed files with 30430 additions and 1357 deletions
+132 -4
View File
@@ -1,5 +1,6 @@
import groovy.json.JsonSlurper
import org.gradle.api.artifacts.dsl.LockMode
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
import org.gradle.api.tasks.bundling.AbstractArchiveTask
import org.gradle.api.tasks.bundling.Jar
@@ -232,8 +233,13 @@ configure(subprojects.findAll { it.childProjects.isEmpty() }) {
}
dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
if (project.path in [':domain-core', ':application-core', ':shared-contract']) {
testImplementation 'org.junit.jupiter:junit-jupiter'
testImplementation 'org.assertj:assertj-core'
} else {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
}
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.14.0' // D4 code-level security
@@ -555,7 +561,7 @@ tasks.register('verifyCleanArchitectureDependencies') {
group = 'verification'
description = 'Verifies Clean Architecture project dependency direction.'
File moduleRegistryFile = new File(rootProject.projectDir.parentFile, '.harness/project/modules.yaml')
File moduleRegistryFile = new File(rootProject.projectDir, 'config/architecture/modules.json')
inputs.file(moduleRegistryFile)
doLast {
@@ -619,18 +625,140 @@ tasks.register('verifyCleanArchitectureDependencies') {
}
.toSet()
if (moduleName != 'sample-portfolio' && actual.contains('sample-portfolio')) {
throw new GradleException(
"Module ':${moduleName}' has a forbidden production dependency on " +
"':sample-portfolio'. The sample module may only be consumed through " +
"non-production fixture configurations."
)
}
Set<String> forbidden = actual - allowed
if (!forbidden.isEmpty()) {
throw new GradleException(
"Module ':${moduleName}' has forbidden project dependencies ${forbidden}. " +
"Allowed dependencies are ${allowed}. " +
"Production modules must not depend on ':sample-ticket', and adapter modules must not depend on each other."
"Production modules must not depend on ':sample-portfolio'; " +
"all project edges must be explicitly registered."
)
}
}
}
}
def verifyApplicationCoreDependencyPurity = tasks.register('verifyApplicationCoreDependencyPurity') {
group = 'verification'
description = 'Verifies application-core has only project production dependencies and no diagnostic frameworks on application classpaths.'
doLast {
Project application = project(':application-core')
List<String> violations = []
['api', 'implementation', 'compileOnly', 'runtimeOnly'].each { configurationName ->
def configuration = application.configurations.findByName(configurationName)
if (configuration == null) {
return
}
configuration.dependencies.each { dependency ->
if (!(dependency instanceof ProjectDependency)) {
violations << "${configurationName}: non-project production dependency " +
"${dependency.group ?: '<no-group>'}:${dependency.name}"
}
}
}
Closure<Boolean> forbiddenGroup = { String groupName ->
groupName != null && (
groupName.startsWith('org.springframework') ||
groupName == 'org.slf4j' ||
groupName == 'ch.qos.logback' ||
groupName == 'org.apache.logging.log4j' ||
groupName == 'io.micrometer')
}
['compileClasspath', 'runtimeClasspath', 'testCompileClasspath', 'testRuntimeClasspath']
.each { configurationName ->
def configuration = application.configurations.getByName(configurationName)
configuration.incoming.resolutionResult.allComponents.each { component ->
if (component.id instanceof ModuleComponentIdentifier &&
forbiddenGroup(component.id.group)) {
violations << "${configurationName}: forbidden resolved dependency " +
"${component.id.group}:${component.id.module}:${component.id.version}"
}
}
}
if (!violations.isEmpty()) {
throw new GradleException(
"verifyApplicationCoreDependencyPurity: ${violations.size()} violation(s):\n " +
violations.toSorted().join('\n '))
}
logger.lifecycle(
'verifyApplicationCoreDependencyPurity: OK — application-core production declarations are project-only and application classpaths contain no Spring/logging/metrics frameworks.')
}
}
project(':application-core').tasks.named('check') {
dependsOn verifyApplicationCoreDependencyPurity
}
def verifyConfigurationPropertiesProcessor = tasks.register('verifyConfigurationPropertiesProcessor') {
group = 'verification'
description = 'Verifies every registered leaf declares the Spring configuration processor exactly when its main source owns @ConfigurationProperties.'
File moduleRegistryFile = new File(rootProject.projectDir, 'config/architecture/modules.json')
inputs.file(moduleRegistryFile)
doLast {
def moduleRegistry = new JsonSlurper().parse(moduleRegistryFile)
List<String> violations = []
def processorDeclaration = ~/^\s*annotationProcessor\s+['"]org\.springframework\.boot:spring-boot-configuration-processor['"]\s*$/
moduleRegistry.modules.each { module ->
File leafDirectory = rootProject.projectDir.parentFile.toPath()
.resolve(module.source_path as String)
.normalize()
.toFile()
File mainSource = new File(leafDirectory, 'src/main')
File buildFile = new File(leafDirectory, 'build.gradle')
int propertyAnnotationCount = 0
if (mainSource.isDirectory()) {
mainSource.eachFileRecurse { File sourceFile ->
if (sourceFile.name.endsWith('.java')) {
propertyAnnotationCount += sourceFile.text.count('@ConfigurationProperties(')
}
}
}
int processorCount = buildFile.readLines().count { String line ->
processorDeclaration.matcher(line).matches()
}
boolean ownsConfigurationProperties = propertyAnnotationCount > 0
if (ownsConfigurationProperties && processorCount != 1) {
violations << "${module.id}: ${propertyAnnotationCount} @ConfigurationProperties occurrence(s), " +
"but ${processorCount} configuration-processor declaration(s)"
} else if (!ownsConfigurationProperties && processorCount != 0) {
violations << "${module.id}: no @ConfigurationProperties occurrence, but " +
"${processorCount} configuration-processor declaration(s)"
}
}
if (!violations.isEmpty()) {
throw new GradleException(
"verifyConfigurationPropertiesProcessor: ${violations.size()} parity violation(s):\n " +
violations.toSorted().join('\n '))
}
logger.lifecycle(
"verifyConfigurationPropertiesProcessor: OK — all ${moduleRegistry.modules.size()} registered leaves have exact configuration-processor parity.")
}
}
configure(subprojects.findAll { it.childProjects.isEmpty() }) {
tasks.named('check') {
dependsOn verifyConfigurationPropertiesProcessor
}
}
// verifyOneTypePerFile — one public top-level type per file, file name == type name
// (code-conventions I6). Rationale in README.md.
tasks.register('verifyOneTypePerFile') {