Skip to the content.

Multi-Module Structure — 헥사고날 멀티모듈 구조

목적: Spring Boot 3.5.x + Java 21 헥사고날 아키텍처 멀티모듈 구조 및 의존성 규칙


1️⃣ 전체 모듈 구조

프로덕션 모듈 (6개)

project/
│
├── domain/                              ⭐ 도메인 레이어 (핵심 비즈니스 로직)
│   ├── src/main/java/
│   │   └── com/company/template/domain/
│   │       ├── common/                  # 공통 인터페이스 (DomainEvent, DomainException)
│   │       └── {boundedContext}/        # 예: order, product, customer
│   │           ├── aggregate/           # Aggregate Root + Entity
│   │           ├── vo/                  # Value Object
│   │           ├── event/               # Domain Event
│   │           └── exception/           # BC 전용 예외
│   └── src/testFixtures/java/           # Test Fixtures (Gradle 플러그인)
│
├── application/                         ⭐ 애플리케이션 레이어 (UseCase)
│   ├── src/main/java/
│   │   └── com/company/template/application/
│   │       ├── common/                  # 공통 DTO, Config
│   │       └── {boundedContext}/
│   │           ├── assembler/           # Domain ↔ DTO 변환
│   │           ├── dto/                 # Command, Query, Response, Bundle
│   │           ├── facade/              # 여러 Manager 조합
│   │           ├── factory/             # Command/Query Factory
│   │           ├── manager/             # 단일 Port Transaction
│   │           ├── port/                # Port-In (UseCase), Port-Out (Persistence)
│   │           ├── service/             # UseCase 구현체
│   │           └── listener/            # Event Listener
│   └── src/testFixtures/java/
│
├── adapter-in/                          ⭐ Inbound Adapters (Driving)
│   └── rest-api/                        ⭐ REST API Adapter
│       ├── src/main/java/
│       │   └── com/company/template/adapter/in/rest/
│       │       ├── common/              # 공통 DTO, Error Handler
│       │       └── {boundedContext}/
│       │           ├── controller/      # HTTP Controller (CQRS 분리)
│       │           ├── dto/             # API Request/Response
│       │           ├── mapper/          # API ↔ UseCase DTO 변환
│       │           └── error/           # BC 전용 Error Mapper
│       └── src/testFixtures/java/       # (Optional)
│
├── adapter-out/                         ⭐ Outbound Adapters (Driven)
│   ├── persistence-mysql/               ⭐ MySQL Persistence Adapter
│   │   ├── src/main/java/
│   │   │   └── com/company/template/adapter/out/persistence/
│   │   │       ├── config/              # JPA, Flyway Config
│   │   │       ├── common/              # Base Entity
│   │   │       └── {boundedContext}/
│   │   │           ├── adapter/         # Command/Query Adapter
│   │   │           ├── entity/          # JPA Entity
│   │   │           ├── mapper/          # Entity ↔ Domain 변환
│   │   │           └── repository/      # JPA/QueryDSL Repository
│   │   └── src/testFixtures/java/       # (Optional)
│   │
│   └── persistence-redis/               ⭐ Redis Persistence Adapter
│       ├── src/main/java/
│       │   └── com/company/template/adapter/out/redis/
│       │       ├── config/              # Lettuce, Redisson Config
│       │       └── {boundedContext}/
│       │           └── adapter/         # Cache/Lock Adapter
│       └── src/testFixtures/java/       # (Optional)
│
└── bootstrap/                           ⭐ Bootstrap Modules (Application Entry Points)
    └── bootstrap-web-api/               ⭐ Spring Boot Application
        └── src/main/java/
            └── com/company/template/
                └── BootstrapWebApiApplication.java

모듈 역할 요약

모듈 역할 핵심 원칙
domain 순수 비즈니스 로직 Pure Java, Lombok 금지, Law of Demeter
application UseCase + Transaction 관리 CQRS 분리, Port/Adapter 패턴
adapter-in/rest-api HTTP 요청/응답 처리 Thin Controller, Bean Validation
adapter-out/persistence-mysql MySQL 저장/조회 Long FK 전략, CQRS 분리
adapter-out/persistence-redis 캐싱 + 분산락 Lettuce (캐싱), Redisson (분산락)
bootstrap/bootstrap-web-api Spring Boot 진입점 모든 모듈 조립, Config

2️⃣ 의존성 흐름 (Dependency Flow)

프로덕션 모듈 의존성

┌─────────────────────────────────────────────────────────────────────┐
│ bootstrap/bootstrap-web-api (Spring Boot Application)               │
│ ├── adapter-in/rest-api                                             │
│ ├── adapter-out/persistence-mysql                                   │
│ ├── adapter-out/persistence-redis                                   │
│ ├── application                                                     │
│ └── domain                                                          │
└─────────────────────────────────────────────────────────────────────┘
                              ↓ implementation
┌─────────────────────────────────────────────────────────────────────┐
│ adapter-in/rest-api (REST API Adapter)                              │
│ ├── application (Port-In UseCase)                                   │
│ └── domain (DTO 변환용)                                             │
└─────────────────────────────────────────────────────────────────────┘
                              ↓ implementation
┌─────────────────────────────────────────────────────────────────────┐
│ application (UseCase Layer)                                         │
│ └── domain                                                          │
└─────────────────────────────────────────────────────────────────────┘
                              ↓ implementation
┌─────────────────────────────────────────────────────────────────────┐
│ domain (Domain Layer)                                               │
│ └── (No dependencies - Pure Java)                                   │
└─────────────────────────────────────────────────────────────────────┘
                              ↑ implementation
┌─────────────────────────────────────────────────────────────────────┐
│ adapter-out/persistence-mysql (MySQL Adapter)                       │
│ ├── application (Port-Out 구현)                                     │
│ └── domain (Entity ↔ Domain 변환)                                   │
└─────────────────────────────────────────────────────────────────────┘
                              ↑ implementation
┌─────────────────────────────────────────────────────────────────────┐
│ adapter-out/persistence-redis (Redis Adapter)                       │
│ ├── application (Port-Out 구현)                                     │
│ └── domain (Cache DTO 변환)                                         │
└─────────────────────────────────────────────────────────────────────┘

의존성 규칙 매트릭스

From ↓ / To → domain application rest-api persistence-mysql persistence-redis bootstrap
domain -
application -
rest-api -
persistence-mysql -
persistence-redis -
bootstrap -

핵심 규칙:


3️⃣ 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')

4️⃣ 버전 관리 (Version Catalog)

⚠️ 버전 관리 규칙 (Zero-Tolerance)

❌ 금지: build.gradle에 직접 버전 하드코딩 ✅ 필수: gradle/libs.versions.toml에 버전 명시 → libs.xxx 문법으로 참조

libs.versions.toml 구조

# gradle/libs.versions.toml

[versions]
springBoot = "3.5.6"
springDependencyManagement = "1.1.5"
querydsl = "5.1.0"
archunit = "1.2.1"

[libraries]
# Spring Boot Starters (BOM 관리 - 버전 생략)
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" }

# 외부 라이브러리 (version.ref 필수)
querydsl-jpa = { module = "com.querydsl:querydsl-jpa", version.ref = "querydsl" }
archunit-junit5 = { module = "com.tngtech.archunit:archunit-junit5", version.ref = "archunit" }

[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "springBoot" }
spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "springDependencyManagement" }

build.gradle에서 참조

dependencies {
    // ✅ Version Catalog 참조
    implementation libs.spring.boot.starter.web
    implementation libs.querydsl.jpa
    testImplementation libs.archunit.junit5
}

📖 상세 문서: Version Management


5️⃣ 모듈별 build.gradle

domain/build.gradle

plugins {
    id 'java-library'
    id 'java-test-fixtures'  // ⭐ testFixtures 활성화
}

dependencies {
    // ========================================
    // Domain은 외부 의존성 없음 (Pure Java)
    // ========================================
    // ❌ Lombok 금지 (Zero-Tolerance)
    // ❌ Spring 의존성 금지
    // ❌ JPA 의존성 금지

    // ✅ 허용: 순수 Java 유틸리티만
    implementation rootProject.libs.uuid.creator  // UUIDv7 생성

    // ========================================
    // Test Dependencies
    // ========================================
    testImplementation rootProject.libs.junit.jupiter
    testImplementation rootProject.libs.assertj.core
    testImplementation rootProject.libs.archunit.junit5
}

application/build.gradle

plugins {
    id 'java-library'
    id 'java-test-fixtures'
}

dependencies {
    // ========================================
    // Core Dependencies
    // ========================================
    api project(':domain')

    // Spring (Context, TX only - Starter 제외)
    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'))
}

adapter-in/rest-api/build.gradle

plugins {
    id 'java-library'
    id 'java-test-fixtures'
}

dependencies {
    // ========================================
    // Core Dependencies
    // ========================================
    api project(':application')
    api project(':domain')

    // Spring Boot Web
    implementation rootProject.libs.spring.boot.starter.web
    implementation rootProject.libs.spring.boot.starter.validation

    // OpenAPI/Swagger
    implementation rootProject.libs.springdoc.openapi.starter

    // ========================================
    // Test Dependencies
    // ========================================
    testImplementation rootProject.libs.spring.boot.starter.test
    testImplementation rootProject.libs.spring.restdocs.mockmvc
    testImplementation testFixtures(project(':domain'))
    testImplementation testFixtures(project(':application'))

    // ========================================
    // Test Fixtures Dependencies
    // ========================================
    testFixturesApi project(':application')
    testFixturesApi testFixtures(project(':application'))
}

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'))

    testRuntimeOnly rootProject.libs.h2

    // ========================================
    // Test Fixtures Dependencies
    // ========================================
    testFixturesApi project(':domain')
    testFixturesApi testFixtures(project(':domain'))
}

adapter-out/persistence-redis/build.gradle

plugins {
    id 'java-library'
    id 'java-test-fixtures'
}

dependencies {
    // ========================================
    // Core Dependencies
    // ========================================
    api project(':application')
    api project(':domain')

    // Lettuce (캐싱 - Spring Boot 기본)
    implementation rootProject.libs.spring.boot.starter.data.redis

    // Redisson (분산락)
    implementation rootProject.libs.redisson.spring.boot.starter

    // ========================================
    // Test Dependencies
    // ========================================
    testImplementation rootProject.libs.spring.boot.starter.test
    testImplementation rootProject.libs.testcontainers.junit
    testImplementation testFixtures(project(':domain'))

    // ========================================
    // Test Fixtures Dependencies
    // ========================================
    testFixturesApi project(':domain')
    testFixturesApi testFixtures(project(':domain'))
}

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
}

6️⃣ Test Fixtures (Gradle 플러그인)

핵심 원칙

Gradle java-test-fixtures 플러그인을 사용하여 별도 모듈 없이 테스트 픽스쳐를 공유합니다.

디렉토리 구조

domain/
├── src/main/java/                    (Production 코드)
├── src/test/java/                    (단위 테스트)
└── src/testFixtures/java/            ⭐ Test Fixtures
    └── com/company/template/fixture/domain/
        ├── OrderFixture.java
        ├── ProductFixture.java
        └── MoneyFixture.java

의존성 전파

// application/build.gradle
dependencies {
    // Domain Fixture 사용
    testImplementation testFixtures(project(':domain'))

    // Application Fixture 전파
    testFixturesApi project(':domain')
    testFixturesApi testFixtures(project(':domain'))
}

의존성 흐름

domain testFixtures
    ↓ 의존
  domain (Production)

application testFixtures
    ↓ 의존              ↓ 의존
  application      domain testFixtures

adapter-* testFixtures
    ↓ 의존                    ↓ 의존
  adapter-*         application testFixtures (또는 domain testFixtures)

📖 상세 문서: Test Fixtures Guide


7️⃣ ArchUnit 검증

멀티모듈 의존성 검증

위치: bootstrap/bootstrap-web-api/src/test/java/.../ModuleDependencyArchTest.java

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.Tag;
import org.junit.jupiter.api.Test;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;

/**
 * Multi-Module 의존성 규칙 ArchUnit 검증
 *
 * @author development-team
 * @since 1.0.0
 */
@Tag("architecture")
@DisplayName("Multi-Module Dependency ArchUnit Tests")
class ModuleDependencyArchTest {

    private static JavaClasses classes;

    @BeforeAll
    static void setUp() {
        classes = new ClassFileImporter()
            .importPackages("com.company.template");
    }

    /**
     * 규칙 1: domain은 외부 모듈 의존 금지
     */
    @Test
    @DisplayName("[필수] domain은 application, adapter를 의존할 수 없다")
    void domain_MustNotDependOnOtherModules() {
        ArchRule rule = noClasses()
            .that().resideInAPackage("..domain..")
            .should().dependOnClassesThat().resideInAnyPackage(
                "..application..",
                "..adapter.."
            )
            .because("domain은 Pure Java로 작성되어야 하며 외부 모듈 의존이 없어야 합니다");

        rule.check(classes);
    }

    /**
     * 규칙 2: application은 adapter 의존 금지
     */
    @Test
    @DisplayName("[필수] application은 adapter를 의존할 수 없다")
    void application_MustNotDependOnAdapter() {
        ArchRule rule = noClasses()
            .that().resideInAPackage("..application..")
            .should().dependOnClassesThat().resideInAPackage("..adapter..")
            .because("application은 domain만 의존해야 합니다");

        rule.check(classes);
    }

    /**
     * 규칙 3: adapter-in과 adapter-out은 서로 의존 금지
     */
    @Test
    @DisplayName("[금지] adapter-in과 adapter-out은 서로 의존할 수 없다")
    void adapterIn_MustNotDependOnAdapterOut() {
        ArchRule rule = noClasses()
            .that().resideInAPackage("..adapter.in..")
            .should().dependOnClassesThat().resideInAPackage("..adapter.out..")
            .because("adapter-in과 adapter-out은 서로 독립적이어야 합니다");

        rule.check(classes);
    }

    /**
     * 규칙 4: adapter-out은 adapter-in 의존 금지
     */
    @Test
    @DisplayName("[금지] adapter-out은 adapter-in을 의존할 수 없다")
    void adapterOut_MustNotDependOnAdapterIn() {
        ArchRule rule = noClasses()
            .that().resideInAPackage("..adapter.out..")
            .should().dependOnClassesThat().resideInAPackage("..adapter.in..")
            .because("adapter-out과 adapter-in은 서로 독립적이어야 합니다");

        rule.check(classes);
    }
}

8️⃣ 빌드 및 테스트

전체 빌드

# 루트에서 전체 빌드
./gradlew clean build

# Version Catalog 일관성 검증
./gradlew verifyVersionCatalog

# Lombok 금지 검증
./gradlew checkNoLombok

모듈별 빌드

# 특정 모듈만 빌드
./gradlew :domain:build
./gradlew :application:build
./gradlew :adapter-in:rest-api:build
./gradlew :adapter-out:persistence-mysql:build
./gradlew :adapter-out:persistence-redis:build
./gradlew :bootstrap:bootstrap-web-api:build

테스트 실행

# 전체 테스트
./gradlew test

# ArchUnit 테스트만
./gradlew test --tests "*ArchTest"

# 모듈별 테스트
./gradlew :domain:test
./gradlew :application:test

의존성 확인

# 모듈 의존성 트리 확인
./gradlew :application:dependencies
./gradlew :adapter-in:rest-api:dependencies

9️⃣ 체크리스트

멀티모듈 구조 설정 시:

프로젝트 초기화

의존성 규칙

Test Fixtures

검증


📖 관련 문서


작성자: Development Team 최종 수정일: 2025-12-05 버전: 2.0.0