Files
balance/CLAUDE.md

17 KiB

Spring Boot 멀티모듈 프로젝트 설계 가이드라인

개요

이 문서는 Spring Boot 프로젝트를 멀티모듈 구조로 설계하고 구현하기 위한 가이드라인입니다. 본 프로젝트(balance)의 실제 구조 변경 경험을 바탕으로 작성되었습니다.

목차

  1. 멀티모듈 구조의 이점
  2. 모듈 분리 원칙
  3. 표준 모듈 구조
  4. Gradle 설정
  5. 의존성 관리 전략
  6. 패키지 구조 및 네이밍
  7. 주의사항 및 베스트 프랙티스

멀티모듈 구조의 이점

1. 관심사의 분리 (Separation of Concerns)

  • 각 모듈이 명확한 책임을 가짐
  • 도메인, 데이터 접근, API 레이어를 물리적으로 분리

2. 재사용성 (Reusability)

  • Storage 모듈처럼 독립적인 모듈은 다른 프로젝트에서 재사용 가능
  • 공통 기능을 별도 모듈로 추출하여 여러 프로젝트에서 활용

3. 빌드 최적화

  • 변경된 모듈만 재빌드
  • 병렬 빌드 가능

4. 팀 협업

  • 모듈별로 소유권 분리 가능
  • 충돌 최소화

5. 의존성 제어

  • 모듈 간 의존성을 명시적으로 관리
  • 순환 참조 방지

모듈 분리 원칙

레이어드 아키텍처 기반 분리

┌─────────────────────┐
│   API/Presentation  │  ← balance-core-api
│    (Controller)     │
├─────────────────────┤
│   Business Logic    │  ← balance-core-api
│     (Service)       │
├─────────────────────┤
│   Data Access       │  ← storage
│  (Entity, Repo)     │
└─────────────────────┘

권장 모듈 분리 전략

1. Storage/Domain 모듈

목적: 데이터 엔티티 및 레포지토리

포함 요소:

  • JPA 엔티티 (@Entity)
  • Repository 인터페이스 (JpaRepository)
  • 도메인 모델
  • 데이터 접근 로직

특징:

  • 다른 모듈에 의존하지 않음 (독립적)
  • Spring Boot 실행 불가 (라이브러리 모듈)
  • 다른 프로젝트에서 재사용 가능

2. Core API 모듈

목적: REST API 및 비즈니스 로직

포함 요소:

  • REST 컨트롤러 (@RestController)
  • 서비스 계층 (@Service)
  • DTO (Data Transfer Objects)
  • 애플리케이션 진입점 (@SpringBootApplication)

특징:

  • Storage 모듈에 의존
  • 실행 가능한 Spring Boot 애플리케이션
  • HTTP 엔드포인트 제공

3. (선택) Common 모듈

목적: 공통 유틸리티 및 설정

포함 요소:

  • 공통 유틸리티 클래스
  • 공통 예외 클래스
  • 공통 설정
  • 공통 DTO

표준 모듈 구조

디렉토리 구조 예시

project-root/
├── settings.gradle
├── build.gradle
├── storage/
│   ├── build.gradle
│   └── src/
│       ├── main/
│       │   └── kotlin/
│       │       └── com/company/project/storage/
│       │           ├── entity/
│       │           │   ├── User.kt
│       │           │   └── Order.kt
│       │           └── repository/
│       │               ├── UserRepository.kt
│       │               └── OrderRepository.kt
│       └── test/
│           └── kotlin/
│               └── com/company/project/storage/
│
├── core-api/
│   ├── build.gradle
│   └── src/
│       ├── main/
│       │   ├── kotlin/
│       │   │   └── com/company/project/
│       │   │       ├── Application.kt
│       │   │       ├── controller/
│       │   │       │   ├── UserController.kt
│       │   │       │   └── OrderController.kt
│       │   │       └── service/
│       │   │           ├── UserService.kt
│       │   │           └── OrderService.kt
│       │   └── resources/
│       │       └── application.properties
│       └── test/
│           └── kotlin/
│               └── com/company/project/
│
└── common/ (선택사항)
    ├── build.gradle
    └── src/
        └── main/
            └── kotlin/
                └── com/company/project/common/
                    ├── util/
                    ├── exception/
                    └── config/

Gradle 설정

1. 루트 프로젝트 settings.gradle

rootProject.name = 'project-name'
include 'storage'
include 'core-api'
// include 'common'  // 필요시

2. 루트 프로젝트 build.gradle

plugins {
    id 'org.jetbrains.kotlin.jvm' version '2.2.21' apply false
    id 'org.jetbrains.kotlin.plugin.spring' version '2.2.21' apply false
    id 'org.springframework.boot' version '4.0.1' apply false
    id 'io.spring.dependency-management' version '1.1.7' apply false
    id 'org.hibernate.orm' version '7.2.0.Final' apply false
    id 'org.jetbrains.kotlin.plugin.jpa' version '2.2.21' apply false
}

group = 'com.company'
version = '0.0.1-SNAPSHOT'

subprojects {
    apply plugin: 'org.jetbrains.kotlin.jvm'
    apply plugin: 'io.spring.dependency-management'

    repositories {
        mavenCentral()
    }

    java {
        toolchain {
            languageVersion = JavaLanguageVersion.of(21) // 또는 17, 24
        }
    }

    kotlin {
        compilerOptions {
            freeCompilerArgs.addAll '-Xjsr305=strict', '-Xannotation-default-target=param-property'
        }
    }

    dependencies {
        implementation 'org.jetbrains.kotlin:kotlin-reflect'
    }

    tasks.withType(Test) {
        useJUnitPlatform()
    }
}

3. Storage 모듈 storage/build.gradle

plugins {
    id 'org.jetbrains.kotlin.plugin.spring'
    id 'org.springframework.boot'
    id 'org.hibernate.orm'
    id 'org.jetbrains.kotlin.plugin.jpa'
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

    // 데이터베이스 드라이버
    runtimeOnly 'com.h2database:h2'
    runtimeOnly 'org.postgresql:postgresql'
    // runtimeOnly 'com.mysql:mysql-connector-j'
}

hibernate {
    enhancement {
        enableAssociationManagement = true
    }
}

allOpen {
    annotation 'jakarta.persistence.Entity'
    annotation 'jakarta.persistence.MappedSuperclass'
    annotation 'jakarta.persistence.Embeddable'
}

// 라이브러리 모듈이므로 bootJar 비활성화
bootJar {
    enabled = false
}

jar {
    enabled = true
}

4. Core API 모듈 core-api/build.gradle

plugins {
    id 'org.jetbrains.kotlin.plugin.spring'
    id 'org.jetbrains.kotlin.plugin.jpa'
    id 'org.springframework.boot'
}

dependencies {
    // 내부 모듈 의존성
    implementation project(':storage')

    // Spring Boot Starters
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-webmvc'
    implementation 'org.springframework.boot:spring-boot-h2console'
    implementation 'tools.jackson.module:jackson-module-kotlin'

    // 개발 도구
    developmentOnly 'org.springframework.boot:spring-boot-devtools'

    // 테스트
    testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa-test'
    testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
    testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

// 실행 가능한 jar 생성 (기본값이므로 생략 가능)
// bootJar {
//     enabled = true
// }

의존성 관리 전략

의존성 방향 규칙

core-api  ──→  storage
    ↓
common (선택)

규칙:

  1. 상위 레이어는 하위 레이어에 의존 가능
  2. 하위 레이어는 상위 레이어에 의존하면 안 됨
  3. 순환 의존성 절대 금지

모듈 간 의존성 선언

dependencies {
    // 올바른 예
    implementation project(':storage')
    implementation project(':common')

    // 잘못된 예 (storage 모듈에서)
    // implementation project(':core-api')  // ❌ 순환 의존성
}

외부 의존성 관리

루트 프로젝트에서 버전 관리:

// build.gradle (root)
ext {
    springCloudVersion = '2024.0.0'
}

subprojects {
    apply plugin: 'io.spring.dependency-management'

    dependencyManagement {
        imports {
            mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
        }
    }
}

패키지 구조 및 네이밍

패키지 네이밍 컨벤션

com.company.project.{module}.{layer}

예시:

com.quantbench.balance.storage.entity
com.quantbench.balance.storage.repository
com.quantbench.balance.controller
com.quantbench.balance.service

모듈별 패키지 구조

Storage 모듈

com.quantbench.balance.storage/
├── entity/
│   ├── User.kt
│   ├── Order.kt
│   └── Product.kt
└── repository/
    ├── UserRepository.kt
    ├── OrderRepository.kt
    └── ProductRepository.kt

Core API 모듈

com.quantbench.balance/
├── Application.kt
├── controller/
│   ├── UserController.kt
│   ├── OrderController.kt
│   └── ProductController.kt
├── service/
│   ├── UserService.kt
│   ├── OrderService.kt
│   └── ProductService.kt
└── dto/
    ├── UserDto.kt
    ├── OrderDto.kt
    └── ProductDto.kt

Spring Boot 애플리케이션 클래스 설정

package com.quantbench.balance

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication(scanBasePackages = ["com.quantbench.balance"])
class BalanceApplication

fun main(args: Array<String>) {
    runApplication<BalanceApplication>(*args)
}

중요:

  • scanBasePackages를 명시하여 모든 모듈의 컴포넌트를 스캔
  • 패키지 구조는 일관성 있게 유지 (com.quantbench.balance.*)

주의사항 및 베스트 프랙티스

1. 모듈 설계 원칙

DO (권장)

  • 각 모듈은 단일 책임을 가져야 함
  • 모듈 간 인터페이스는 명확하게 정의
  • Storage 모듈은 가능한 한 독립적으로 유지
  • 공통 기능은 별도 모듈로 분리

DON'T (비권장)

  • 순환 의존성 생성
  • 너무 많은 모듈로 과도하게 분리 (3-5개 권장)
  • 하위 레이어가 상위 레이어에 의존
  • 모듈 간 패키지 직접 접근

2. Gradle 설정 주의사항

bootJar 설정

// Storage 모듈 (라이브러리)
bootJar {
    enabled = false  // 실행 불가능
}
jar {
    enabled = true   // 일반 jar 생성
}

// API 모듈 (실행 가능)
bootJar {
    enabled = true   // 기본값이므로 생략 가능
}

플러그인 적용

  • 루트 프로젝트: apply false 사용
  • 서브 프로젝트: 필요한 플러그인만 적용

3. Spring Boot 4.0+ 사용 시 주의사항

Spring Boot 4.0에서는 일부 어노테이션이 변경되거나 제거되었습니다.

컴포넌트 스캔 설정:

// ✅ 권장 (Spring Boot 4.0+)
@SpringBootApplication(scanBasePackages = ["com.quantbench.balance"])
class BalanceApplication

// ❌ 비권장 (구버전 방식, Spring Boot 4.0에서 문제 발생 가능)
@SpringBootApplication
@EntityScan("com.quantbench.balance.storage.entity")
@EnableJpaRepositories("com.quantbench.balance.storage.repository")
class BalanceApplication

4. 빌드 및 실행

전체 빌드

./gradlew clean build

특정 모듈 빌드

./gradlew :storage:build
./gradlew :core-api:build

애플리케이션 실행

./gradlew :core-api:bootRun

의존성 확인

./gradlew :core-api:dependencies --configuration runtimeClasspath

5. 테스트 전략

Storage 모듈 테스트

@DataJpaTest
class UserRepositoryTest {
    @Autowired
    lateinit var userRepository: UserRepository

    @Test
    fun `should save and find user`() {
        // 테스트 코드
    }
}

API 모듈 통합 테스트

@SpringBootTest
@AutoConfigureMockMvc
class UserControllerIntegrationTest {
    @Autowired
    lateinit var mockMvc: MockMvc

    @Test
    fun `should return user by id`() {
        mockMvc.perform(get("/api/users/1"))
            .andExpect(status().isOk)
    }
}

6. 성능 최적화

Gradle 빌드 최적화

# gradle.properties
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configureondemand=true

모듈별 병렬 빌드

./gradlew build --parallel

실제 적용 예시 (balance 프로젝트)

적용 전 (단일 모듈)

balance/
├── build.gradle
├── settings.gradle
└── src/
    ├── main/
    │   └── kotlin/com/quantbench/balance/
    │       └── BalanceApplication.kt
    └── test/

적용 후 (멀티 모듈)

balance/
├── build.gradle                          # 공통 설정
├── settings.gradle                       # 모듈 정의
├── storage/                              # 데이터 접근 레이어
│   ├── build.gradle
│   └── src/main/kotlin/com/quantbench/balance/storage/
│       ├── entity/
│       └── repository/
└── balance-core-api/                     # API 레이어
    ├── build.gradle
    └── src/
        ├── main/
        │   ├── kotlin/com/quantbench/balance/
        │   │   ├── BalanceApplication.kt
        │   │   ├── controller/
        │   │   └── service/
        │   └── resources/application.properties
        └── test/

핵심 변경 사항

  1. 모듈 분리: storage와 balance-core-api로 분리
  2. 의존성 방향: balance-core-api → storage
  3. 실행 가능성: balance-core-api만 실행 가능, storage는 라이브러리
  4. 재사용성: storage 모듈은 다른 프로젝트에서 재사용 가능

추가 확장 시나리오

1. Admin API 추가

balance/
├── storage/
├── balance-core-api/
└── balance-admin-api/      # 관리자 API
    ├── build.gradle
    └── src/

의존성 구조:

balance-core-api  ──→  storage
balance-admin-api ──→  storage

2. Batch 모듈 추가

balance/
├── storage/
├── balance-core-api/
└── balance-batch/          # 배치 작업
    ├── build.gradle
    └── src/

3. Common 모듈 추가

balance/
├── common/                 # 공통 유틸리티
├── storage/
├── balance-core-api/
└── balance-admin-api/

의존성 구조:

balance-core-api  ──→  storage  ──→  common
balance-admin-api ──→  storage  ──→  common

문제 해결 (Troubleshooting)

1. 컴파일 에러: Unresolved reference

문제:

e: Unresolved reference 'EntityScan'
e: Unresolved reference 'EnableJpaRepositories'

해결:

  • Spring Boot 4.0+에서는 @EntityScan, @EnableJpaRepositories 대신
  • @SpringBootApplication(scanBasePackages = [...])를 사용

2. bootJar 에러

문제:

Main class name has not been configured and it could not be resolved

해결:

// Storage 모듈에서
bootJar {
    enabled = false
}
jar {
    enabled = true
}

3. 컴포넌트 스캔 실패

문제: Repository나 Service가 스캔되지 않음

해결:

@SpringBootApplication(scanBasePackages = ["com.company.project"])

패키지 구조가 일관성 있게 유지되는지 확인

4. 순환 의존성

문제:

Circular dependency between the following tasks:
:module-a:jar
:module-b:jar

해결:

  • 모듈 간 의존성 구조 재검토
  • 공통 기능은 별도 모듈로 분리

마이그레이션 체크리스트

기존 프로젝트를 멀티모듈로 마이그레이션할 때:

  • 1. 모듈 구조 설계 (어떤 모듈로 분리할지 결정)
  • 2. settings.gradle에 모듈 추가
  • 3. 루트 build.gradle 수정 (공통 설정 분리)
  • 4. 각 모듈별 build.gradle 생성
  • 5. 디렉토리 구조 생성
  • 6. 기존 소스 코드를 적절한 모듈로 이동
  • 7. 패키지 구조 정리
  • 8. Application 클래스 수정 (컴포넌트 스캔 설정)
  • 9. 빌드 테스트 (./gradlew clean build)
  • 10. 애플리케이션 실행 테스트
  • 11. 단위 테스트 및 통합 테스트 검증
  • 12. CI/CD 파이프라인 업데이트 (필요시)

참고 자료


버전 정보

  • Spring Boot: 4.0.1
  • Kotlin: 2.2.21
  • Gradle: 9.2.1
  • Java: 24 (또는 21, 17)

작성일: 2026-01-11 프로젝트: balance 작성자: Claude Code