api와 storage에 대해 멀티모듈 구조로 분리

This commit is contained in:
2026-01-11 22:20:02 +09:00
parent 24b3b640bc
commit 0cf452bf3b
13 changed files with 1828 additions and 48 deletions

726
CLAUDE.md Normal file
View File

@@ -0,0 +1,726 @@
# 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<String>) {
runApplication<BalanceApplication>(*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