Project Setup Expert (프로젝트 설정 전문가)
목적 (Purpose)
Multi-module 헥사고날 아키텍처 프로젝트 구조를 설계하고,
Version Catalog 기반 의존성 관리와 QA 도구 설정을 담당합니다.
활성화 조건
- 프로젝트 초기 설정 시
/plan 실행 후 모듈 구조 변경 필요 시
- gradle, multi-module, version catalog, 의존성, checkstyle, spotbugs 키워드 언급 시
산출물 (Output)
| 컴포넌트 |
파일명 패턴 |
위치 |
| Version Catalog |
libs.versions.toml |
gradle/ |
| 루트 Gradle |
build.gradle |
프로젝트 루트 |
| 모듈 Gradle |
build.gradle |
각 모듈 루트 |
| 설정 파일 |
settings.gradle |
프로젝트 루트 |
| QA 설정 |
checkstyle.xml, pmd-ruleset.xml |
config/ |
완료 기준 (Acceptance Criteria)
모듈 구조 (6개 프로덕션 모듈)
project/
│
├── domain/ ⭐ Domain Layer (Pure Java)
│ ├── src/main/java/
│ │ └── com/company/domain/
│ │ ├── common/ # DomainEvent, DomainException
│ │ └── {boundedContext}/ # order, product, customer
│ │ ├── aggregate/ # Aggregate Root
│ │ ├── vo/ # Value Object
│ │ ├── event/ # Domain Event
│ │ └── exception/ # Domain Exception
│ └── src/testFixtures/java/ # Test Fixtures
│
├── application/ ⭐ Application Layer
│ ├── src/main/java/
│ │ └── com/company/application/
│ │ ├── common/ # 공통 Port, Config
│ │ └── {boundedContext}/
│ │ ├── assembler/ # Domain ↔ Response
│ │ ├── dto/ # Command, Query, Response, Bundle
│ │ ├── facade/ # 2+ Manager 조합
│ │ ├── factory/ # Command/Query Factory
│ │ ├── manager/ # Transaction/Read Manager
│ │ ├── port/ # Port-In, Port-Out
│ │ └── service/ # UseCase 구현
│ └── src/testFixtures/java/
│
├── adapter-in/ ⭐ Inbound Adapters
│ └── rest-api/
│ ├── src/main/java/
│ │ └── com/company/adapter/in/rest/
│ │ ├── common/ # GlobalExceptionHandler
│ │ └── {boundedContext}/
│ │ ├── controller/ # REST Controller
│ │ ├── dto/ # Request/Response DTO
│ │ ├── mapper/ # API ↔ UseCase DTO
│ │ └── error/ # ErrorMapper
│ └── src/testFixtures/java/
│
├── adapter-out/ ⭐ Outbound Adapters
│ ├── persistence-mysql/
│ │ ├── src/main/java/
│ │ │ └── com/company/adapter/out/persistence/
│ │ │ ├── config/ # JPA, Flyway Config
│ │ │ └── {boundedContext}/
│ │ │ ├── adapter/ # Command/Query Adapter
│ │ │ ├── entity/ # JPA Entity
│ │ │ ├── mapper/ # Entity ↔ Domain
│ │ │ └── repository/ # JPA/QueryDSL
│ │ └── src/testFixtures/java/
│ │
│ └── persistence-redis/
│ ├── src/main/java/
│ │ └── com/company/adapter/out/redis/
│ │ ├── config/ # Lettuce, Redisson
│ │ └── {boundedContext}/
│ │ └── adapter/ # Cache/Lock Adapter
│ └── src/testFixtures/java/
│
└── bootstrap/ ⭐ Bootstrap Module
└── bootstrap-web-api/
└── src/main/java/
└── com/company/
└── BootstrapWebApiApplication.java
코드 템플릿
1. settings.gradle
rootProject.name = 'spring-hexagonal-template'
// ========================================
// Core Modules (Hexagonal Architecture)
// ========================================
include 'domain'
include 'application'
// ========================================
// Adapter Modules (Ports & Adapters)
// ========================================
// Inbound Adapters (Driving)
include 'adapter-in:rest-api'
// Outbound Adapters (Driven)
include 'adapter-out:persistence-mysql'
include 'adapter-out:persistence-redis'
// ========================================
// Bootstrap Modules (Runnable Applications)
// ========================================
include 'bootstrap:bootstrap-web-api'
// ========================================
// Project Structure
// ========================================
project(':domain').projectDir = file('domain')
project(':application').projectDir = file('application')
project(':adapter-in:rest-api').projectDir = file('adapter-in/rest-api')
project(':adapter-out:persistence-mysql').projectDir = file('adapter-out/persistence-mysql')
project(':adapter-out:persistence-redis').projectDir = file('adapter-out/persistence-redis')
project(':bootstrap:bootstrap-web-api').projectDir = file('bootstrap/bootstrap-web-api')
2. libs.versions.toml (Version Catalog)
# gradle/libs.versions.toml
[versions]
# ========================================
# Spring & Framework
# ========================================
springBoot = "3.5.6"
springDependencyManagement = "1.1.5"
# ========================================
# Database & Persistence
# ========================================
querydsl = "5.1.0"
flyway = "10.10.0"
mysql = "42.7.3"
mysql = "8.3.0"
# ========================================
# Redis
# ========================================
redisson = "3.27.0"
# ========================================
# UUID
# ========================================
uuidCreator = "6.0.0"
# ========================================
# Testing
# ========================================
archunit = "1.2.1"
testcontainers = "1.19.7"
# ========================================
# QA Tools
# ========================================
checkstyle = "10.14.0"
spotbugs = "4.8.3"
spotbugsPlugin = "6.0.9"
pmd = "7.0.0"
jacoco = "0.8.11"
spotless = "7.0.0.BETA4"
[libraries]
# ========================================
# Spring Boot Starters (BOM 관리 - 버전 생략)
# ========================================
spring-boot-starter = { module = "org.springframework.boot:spring-boot-starter" }
spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web" }
spring-boot-starter-data-jpa = { module = "org.springframework.boot:spring-boot-starter-data-jpa" }
spring-boot-starter-data-redis = { module = "org.springframework.boot:spring-boot-starter-data-redis" }
spring-boot-starter-validation = { module = "org.springframework.boot:spring-boot-starter-validation" }
spring-boot-starter-test = { module = "org.springframework.boot:spring-boot-starter-test" }
# ========================================
# Spring (Non-Starter)
# ========================================
spring-context = { module = "org.springframework:spring-context" }
spring-tx = { module = "org.springframework:spring-tx" }
# ========================================
# Database
# ========================================
querydsl-jpa-jakarta = { module = "com.querydsl:querydsl-jpa", version.ref = "querydsl" }
querydsl-apt-jakarta = { module = "com.querydsl:querydsl-apt", version.ref = "querydsl" }
flyway-core = { module = "org.flywaydb:flyway-core", version.ref = "flyway" }
flyway-mysql = { module = "org.flywaydb:flyway-mysql", version.ref = "flyway" }
mysql-connector = { module = "com.mysql:mysql-connector-j", version.ref = "mysql" }
mysql = { module = "org.mysql:mysql", version.ref = "mysql" }
# ========================================
# Redis
# ========================================
redisson-spring-boot-starter = { module = "org.redisson:redisson-spring-boot-starter", version.ref = "redisson" }
# ========================================
# UUID
# ========================================
uuid-creator = { module = "com.github.f4b6a3:uuid-creator", version.ref = "uuidCreator" }
# ========================================
# Testing
# ========================================
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" }
assertj-core = { module = "org.assertj:assertj-core" }
mockito-core = { module = "org.mockito:mockito-core" }
archunit-junit5 = { module = "com.tngtech.archunit:archunit-junit5", version.ref = "archunit" }
testcontainers-junit = { module = "org.testcontainers:junit-jupiter", version.ref = "testcontainers" }
testcontainers-mysql = { module = "org.testcontainers:mysql", version.ref = "testcontainers" }
testcontainers-mysql = { module = "org.testcontainers:mysql", version.ref = "testcontainers" }
# ========================================
# Jakarta
# ========================================
jakarta-annotation-api = { module = "jakarta.annotation:jakarta.annotation-api" }
jakarta-persistence-api = { module = "jakarta.persistence:jakarta.persistence-api" }
[bundles]
# ========================================
# Dependency Bundles
# ========================================
testing-basic = ["junit-jupiter", "assertj-core", "mockito-core"]
testcontainers = ["testcontainers-junit", "testcontainers-mysql"]
querydsl = ["querydsl-jpa-jakarta"]
[plugins]
# ========================================
# Gradle Plugins
# ========================================
spring-boot = { id = "org.springframework.boot", version.ref = "springBoot" }
spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "springDependencyManagement" }
spotbugs = { id = "com.github.spotbugs", version.ref = "spotbugsPlugin" }
spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
핵심 규칙:
- [versions]에 버전 정의
- [libraries]에서
version.ref 참조
- Spring Boot BOM 관리 의존성은 버전 생략 가능
- 하드코딩 버전 금지
3. 루트 build.gradle
import org.springframework.boot.gradle.plugin.SpringBootPlugin
plugins {
id 'java'
alias(libs.plugins.spring.boot) apply false
alias(libs.plugins.spring.dependency.management) apply false
id 'checkstyle'
alias(libs.plugins.spotbugs) apply false
id 'pmd'
alias(libs.plugins.spotless) apply false
}
allprojects {
group = 'com.company.template'
version = '1.0.0-SNAPSHOT'
repositories {
mavenCentral()
}
}
subprojects {
apply plugin: 'java'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'checkstyle'
apply plugin: 'com.github.spotbugs'
apply plugin: 'pmd'
apply plugin: 'com.diffplug.spotless'
apply plugin: 'jacoco'
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
dependencyManagement {
imports {
mavenBom SpringBootPlugin.BOM_COORDINATES
}
}
// ========================================
// Checkstyle
// ========================================
checkstyle {
toolVersion = rootProject.libs.versions.checkstyle.get()
configFile = rootProject.file('config/checkstyle/checkstyle.xml')
ignoreFailures = false
maxWarnings = 0
}
// ========================================
// SpotBugs
// ========================================
spotbugs {
toolVersion = rootProject.libs.versions.spotbugs.get()
effort = 'max'
reportLevel = 'low'
excludeFilter = rootProject.file('config/spotbugs/spotbugs-exclude.xml')
}
// ========================================
// PMD (Law of Demeter)
// ========================================
pmd {
toolVersion = rootProject.libs.versions.pmd.get()
consoleOutput = true
ruleSetFiles = files(rootProject.file('config/pmd/pmd-ruleset.xml'))
ruleSets = []
ignoreFailures = false
}
// ========================================
// Spotless (Code Formatting)
// ========================================
spotless {
java {
googleJavaFormat('1.22.0').aosp().reflowLongStrings()
target 'src/*/java/**/*.java'
targetExclude '**/generated/**', '**/Q*.java'
}
}
// ========================================
// JaCoCo (Coverage)
// ========================================
jacoco {
toolVersion = rootProject.libs.versions.jacoco.get()
}
tasks.named('jacocoTestReport') {
dependsOn 'test'
reports {
xml.required = true
html.required = true
}
}
// ========================================
// Common Dependencies
// ========================================
dependencies {
testImplementation rootProject.libs.junit.jupiter
testImplementation rootProject.libs.assertj.core
testImplementation rootProject.libs.archunit.junit5
}
// ========================================
// JavaCompile Options
// ========================================
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
options.compilerArgs.addAll([
'-Xlint:unchecked',
'-Xlint:deprecation',
'-parameters'
])
}
// ========================================
// Build Task Dependencies
// ========================================
tasks.named('build') {
dependsOn 'spotlessCheck'
dependsOn 'jacocoTestReport'
}
}
// ========================================
// Custom Tasks
// ========================================
tasks.register('checkNoLombok') {
group = 'verification'
description = 'Verify Lombok is not used in any module'
doLast {
subprojects.each { project ->
project.configurations.each { config ->
config.dependencies.each { dep ->
if (dep.group == 'org.projectlombok' && dep.name == 'lombok') {
throw new GradleException("""
❌ LOMBOK DETECTED in ${project.name}
Lombok is strictly prohibited in this project.
""")
}
}
}
}
}
}
tasks.register('verifyVersionCatalog') {
group = 'verification'
description = 'Verify all versions use version.ref in libs.versions.toml'
doLast {
def catalogFile = rootProject.file('gradle/libs.versions.toml')
if (!catalogFile.exists()) {
throw new GradleException('libs.versions.toml not found')
}
def content = catalogFile.text
def librariesSection = false
def violations = []
content.eachLine { line, lineNum ->
if (line.trim() == '[libraries]') {
librariesSection = true
} else if (line.trim().startsWith('[') && line.trim() != '[libraries]') {
librariesSection = false
}
if (librariesSection && line.contains('version = "')) {
violations << "Line ${lineNum + 1}: ${line.trim()}"
}
}
if (!violations.isEmpty()) {
throw new GradleException("""
❌ VERSION CATALOG CONSISTENCY VIOLATION
Hardcoded versions found in [libraries] section.
Use 'version.ref' referencing [versions] section.
Violations:
${violations.join('\n')}
""")
}
}
}
4. domain/build.gradle
plugins {
id 'java-library'
id 'java-test-fixtures'
}
dependencies {
// ========================================
// Domain은 외부 의존성 없음 (Pure Java)
// ========================================
// ❌ Lombok 금지
// ❌ Spring 의존성 금지
// ❌ JPA 의존성 금지
// ✅ 허용: UUID 생성 유틸리티
implementation rootProject.libs.uuid.creator
// ========================================
// Test Dependencies
// ========================================
testImplementation rootProject.libs.junit.jupiter
testImplementation rootProject.libs.assertj.core
testImplementation rootProject.libs.archunit.junit5
}
5. application/build.gradle
plugins {
id 'java-library'
id 'java-test-fixtures'
}
dependencies {
// ========================================
// Core Dependencies
// ========================================
api project(':domain')
// Spring (Context, TX only)
implementation rootProject.libs.spring.context
implementation rootProject.libs.spring.tx
// ========================================
// Test Dependencies
// ========================================
testImplementation rootProject.libs.spring.boot.starter.test
testImplementation testFixtures(project(':domain'))
// ========================================
// Test Fixtures Dependencies
// ========================================
testFixturesApi project(':domain')
testFixturesApi testFixtures(project(':domain'))
}
6. adapter-out/persistence-mysql/build.gradle
plugins {
id 'java-library'
id 'java-test-fixtures'
}
dependencies {
// ========================================
// Core Dependencies
// ========================================
api project(':application')
api project(':domain')
// Spring Boot Data JPA
implementation rootProject.libs.spring.boot.starter.data.jpa
// QueryDSL
implementation rootProject.libs.querydsl.jpa.jakarta
annotationProcessor rootProject.libs.querydsl.apt.jakarta
annotationProcessor rootProject.libs.jakarta.annotation.api
annotationProcessor rootProject.libs.jakarta.persistence.api
// Flyway
implementation rootProject.libs.flyway.core
implementation rootProject.libs.flyway.mysql
// Database Drivers
runtimeOnly rootProject.libs.mysql.connector
// ========================================
// Test Dependencies
// ========================================
testImplementation rootProject.libs.spring.boot.starter.test
testImplementation rootProject.libs.testcontainers.mysql
testImplementation rootProject.libs.testcontainers.junit
testImplementation testFixtures(project(':domain'))
// ========================================
// Test Fixtures Dependencies
// ========================================
testFixturesApi project(':domain')
testFixturesApi testFixtures(project(':domain'))
}
7. bootstrap/bootstrap-web-api/build.gradle
plugins {
id 'java'
alias(libs.plugins.spring.boot)
alias(libs.plugins.spring.dependency.management)
}
dependencies {
// ========================================
// All Modules
// ========================================
implementation project(':domain')
implementation project(':application')
implementation project(':adapter-in:rest-api')
implementation project(':adapter-out:persistence-mysql')
implementation project(':adapter-out:persistence-redis')
// Spring Boot
implementation rootProject.libs.spring.boot.starter
// ========================================
// Test Dependencies
// ========================================
testImplementation rootProject.libs.spring.boot.starter.test
testImplementation testFixtures(project(':domain'))
testImplementation testFixtures(project(':application'))
}
bootJar {
enabled = true
}
jar {
enabled = false
}
의존성 흐름
의존성 규칙 매트릭스
| From ↓ / To → |
domain |
application |
rest-api |
persistence |
bootstrap |
| domain |
- |
❌ |
❌ |
❌ |
❌ |
| application |
✅ |
- |
❌ |
❌ |
❌ |
| rest-api |
✅ |
✅ |
- |
❌ |
❌ |
| persistence |
✅ |
✅ |
❌ |
- |
❌ |
| bootstrap |
✅ |
✅ |
✅ |
✅ |
- |
핵심 규칙:
- domain: 외부 의존성 없음 (Pure Java)
- application: domain만 의존
- adapter: application + domain만 의존
- adapter-in ↔ adapter-out: 상호 의존 금지
의존성 다이어그램
┌─────────────────────────┐
│ bootstrap-web-api │
│ (Spring Boot App) │
└───────────┬─────────────┘
│
┌───────────────────────┼───────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────────┐
│ rest-api │ │ persistence │ │ persistence │
│ (adapter-in) │ │ (mysql) │ │ (redis) │
└───────┬───────┘ └───────┬───────┘ └─────────┬─────────┘
│ │ │
└────────────────────┼──────────────────────┘
│
▼
┌─────────────────┐
│ application │
│ (UseCase) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ domain │
│ (Pure Java) │
└─────────────────┘
Zero-Tolerance 규칙
✅ MANDATORY (필수)
| 규칙 |
설명 |
| Version Catalog |
libs.versions.toml 중앙 관리 |
version.ref 사용 |
버전 하드코딩 금지 |
| 단방향 의존성 |
domain ← application ← adapter |
| Domain Pure Java |
외부 의존성 금지 (Spring, JPA, Lombok) |
| testFixtures |
java-test-fixtures 플러그인 |
| ArchUnit 검증 |
모듈 의존성 자동 검증 |
| JaCoCo 커버리지 |
모듈별 최소 커버리지 |
❌ PROHIBITED (금지)
| 항목 |
이유 |
| Lombok |
Plain Java 원칙, checkNoLombok 검증 |
| 버전 하드코딩 |
verifyVersionCatalog 검증 |
| 순환 의존성 |
단방향 의존성 원칙 |
| Domain Spring 의존성 |
Pure Java 원칙 |
| adapter-in ↔ adapter-out |
독립성 원칙 |
QA 도구 설정
도구별 역할
| 도구 |
역할 |
설정 파일 |
| Checkstyle |
코드 스타일 (Lombok 금지 포함) |
config/checkstyle/checkstyle.xml |
| SpotBugs |
버그 패턴 탐지 |
config/spotbugs/spotbugs-exclude.xml |
| PMD |
Law of Demeter 강제 |
config/pmd/pmd-ruleset.xml |
| JaCoCo |
테스트 커버리지 |
build.gradle 내 설정 |
| Spotless |
코드 포맷팅 |
build.gradle 내 설정 |
모듈별 커버리지 기준
| 모듈 |
최소 커버리지 |
근거 |
| domain |
90% |
핵심 비즈니스 로직 |
| application |
80% |
UseCase 로직 |
| adapter-* |
70% |
인프라 통합 코드 |
| bootstrap |
70% |
설정 및 진입점 |
체크리스트 (Output Checklist)
프로젝트 초기화
Version Catalog
의존성 규칙
testFixtures
QA 도구
검증
ArchUnit 모듈 의존성 테스트
package com.company.template.architecture;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
@DisplayName("Multi-Module Dependency ArchUnit Tests")
class ModuleDependencyArchTest {
private static JavaClasses classes;
@BeforeAll
static void setUp() {
classes = new ClassFileImporter()
.importPackages("com.company.template");
}
@Test
@DisplayName("domain은 application, adapter를 의존할 수 없다")
void domain_MustNotDependOnOtherModules() {
ArchRule rule = noClasses()
.that().resideInAPackage("..domain..")
.should().dependOnClassesThat().resideInAnyPackage(
"..application..",
"..adapter.."
);
rule.check(classes);
}
@Test
@DisplayName("application은 adapter를 의존할 수 없다")
void application_MustNotDependOnAdapter() {
ArchRule rule = noClasses()
.that().resideInAPackage("..application..")
.should().dependOnClassesThat().resideInAPackage("..adapter..");
rule.check(classes);
}
@Test
@DisplayName("adapter-in은 adapter-out을 의존할 수 없다")
void adapterIn_MustNotDependOnAdapterOut() {
ArchRule rule = noClasses()
.that().resideInAPackage("..adapter.in..")
.should().dependOnClassesThat().resideInAPackage("..adapter.out..");
rule.check(classes);
}
@Test
@DisplayName("adapter-out은 adapter-in을 의존할 수 없다")
void adapterOut_MustNotDependOnAdapterIn() {
ArchRule rule = noClasses()
.that().resideInAPackage("..adapter.out..")
.should().dependOnClassesThat().resideInAPackage("..adapter.in..");
rule.check(classes);
}
}
빌드 명령어
# 전체 빌드
./gradlew clean build
# Version Catalog 검증
./gradlew verifyVersionCatalog
# Lombok 금지 검증
./gradlew checkNoLombok
# 코드 스타일 검사
./gradlew checkstyleMain checkstyleTest
# 버그 패턴 검사
./gradlew spotbugsMain
# Law of Demeter 검사
./gradlew pmdMain
# 포맷팅 검사/적용
./gradlew spotlessCheck
./gradlew spotlessApply
# 커버리지 리포트
./gradlew jacocoTestReport
# ArchUnit 테스트만
./gradlew test --tests "*ArchTest"
# 모듈별 빌드
./gradlew :domain:build
./gradlew :application:build
참조 문서
- Multi-Module Structure:
docs/coding_convention/00-project-setup/multi-module-structure.md
- Gradle Configuration:
docs/coding_convention/00-project-setup/gradle-configuration.md
- Version Management:
docs/coding_convention/00-project-setup/version-management.md
- Test Fixtures Guide:
docs/coding_convention/05-testing/test-fixtures/01_test-fixtures-guide.md