17 KiB
17 KiB
Spring Boot 멀티모듈 프로젝트 설계 가이드라인
개요
이 문서는 Spring Boot 프로젝트를 멀티모듈 구조로 설계하고 구현하기 위한 가이드라인입니다. 본 프로젝트(balance)의 실제 구조 변경 경험을 바탕으로 작성되었습니다.
목차
멀티모듈 구조의 이점
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 (선택)
규칙:
- 상위 레이어는 하위 레이어에 의존 가능
- 하위 레이어는 상위 레이어에 의존하면 안 됨
- 순환 의존성 절대 금지
모듈 간 의존성 선언
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/
핵심 변경 사항
- 모듈 분리: storage와 balance-core-api로 분리
- 의존성 방향: balance-core-api → storage
- 실행 가능성: balance-core-api만 실행 가능, storage는 라이브러리
- 재사용성: 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