# Spring Boot 멀티모듈 프로젝트 설계 가이드라인 ## 개요 이 문서는 Spring Boot 프로젝트를 멀티모듈 구조로 설계하고 구현하기 위한 가이드라인입니다. 본 프로젝트(balance)의 실제 구조 변경 경험을 바탕으로 작성되었습니다. ## 목차 1. [멀티모듈 구조의 이점](#멀티모듈-구조의-이점) 2. [모듈 분리 원칙](#모듈-분리-원칙) 3. [표준 모듈 구조](#표준-모듈-구조) 4. [Gradle 설정](#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` ```gradle rootProject.name = 'project-name' include 'storage' include 'core-api' // include 'common' // 필요시 ``` ### 2. 루트 프로젝트 `build.gradle` ```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` ```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` ```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. 순환 의존성 절대 금지 ### 모듈 간 의존성 선언 ```gradle dependencies { // 올바른 예 implementation project(':storage') implementation project(':common') // 잘못된 예 (storage 모듈에서) // implementation project(':core-api') // ❌ 순환 의존성 } ``` ### 외부 의존성 관리 **루트 프로젝트에서 버전 관리:** ```gradle // 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 애플리케이션 클래스 설정 ```kotlin 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) { runApplication(*args) } ``` **중요:** - `scanBasePackages`를 명시하여 모든 모듈의 컴포넌트를 스캔 - 패키지 구조는 일관성 있게 유지 (`com.quantbench.balance.*`) --- ## 주의사항 및 베스트 프랙티스 ### 1. 모듈 설계 원칙 #### ✅ DO (권장) - 각 모듈은 단일 책임을 가져야 함 - 모듈 간 인터페이스는 명확하게 정의 - Storage 모듈은 가능한 한 독립적으로 유지 - 공통 기능은 별도 모듈로 분리 #### ❌ DON'T (비권장) - 순환 의존성 생성 - 너무 많은 모듈로 과도하게 분리 (3-5개 권장) - 하위 레이어가 상위 레이어에 의존 - 모듈 간 패키지 직접 접근 ### 2. Gradle 설정 주의사항 #### bootJar 설정 ```gradle // Storage 모듈 (라이브러리) bootJar { enabled = false // 실행 불가능 } jar { enabled = true // 일반 jar 생성 } // API 모듈 (실행 가능) bootJar { enabled = true // 기본값이므로 생략 가능 } ``` #### 플러그인 적용 - 루트 프로젝트: `apply false` 사용 - 서브 프로젝트: 필요한 플러그인만 적용 ### 3. Spring Boot 4.0+ 사용 시 주의사항 Spring Boot 4.0에서는 일부 어노테이션이 변경되거나 제거되었습니다. **컴포넌트 스캔 설정:** ```kotlin // ✅ 권장 (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. 빌드 및 실행 #### 전체 빌드 ```bash ./gradlew clean build ``` #### 특정 모듈 빌드 ```bash ./gradlew :storage:build ./gradlew :core-api:build ``` #### 애플리케이션 실행 ```bash ./gradlew :core-api:bootRun ``` #### 의존성 확인 ```bash ./gradlew :core-api:dependencies --configuration runtimeClasspath ``` ### 5. 테스트 전략 #### Storage 모듈 테스트 ```kotlin @DataJpaTest class UserRepositoryTest { @Autowired lateinit var userRepository: UserRepository @Test fun `should save and find user`() { // 테스트 코드 } } ``` #### API 모듈 통합 테스트 ```kotlin @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 빌드 최적화 ```properties # gradle.properties org.gradle.parallel=true org.gradle.caching=true org.gradle.configureondemand=true ``` #### 모듈별 병렬 빌드 ```bash ./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 ``` **해결:** ```gradle // Storage 모듈에서 bootJar { enabled = false } jar { enabled = true } ``` ### 3. 컴포넌트 스캔 실패 **문제:** Repository나 Service가 스캔되지 않음 **해결:** ```kotlin @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 Reference Documentation](https://docs.spring.io/spring-boot/index.html) - [Gradle Multi-Project Builds](https://docs.gradle.org/current/userguide/multi_project_builds.html) - [Kotlin and Spring Boot](https://spring.io/guides/tutorials/spring-boot-kotlin) --- ## 버전 정보 - Spring Boot: 4.0.1 - Kotlin: 2.2.21 - Gradle: 9.2.1 - Java: 24 (또는 21, 17) --- **작성일:** 2026-01-11 **프로젝트:** balance **작성자:** Claude Code