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

View File

@@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(./gradlew balance-core-api:dependencies:*)",
"Bash(./gradlew detekt:*)"
]
}
}

27
.editorconfig Normal file
View File

@@ -0,0 +1,27 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
max_line_length = 120
tab_width = 4
trim_trailing_whitespace = true
[*.{kt,kts}]
indent_size = 4
continuation_indent_size = 4
ij_kotlin_align_multiline_parameters = true
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
[*.gradle]
indent_size = 4
[*.{yml,yaml}]
indent_size = 2
[*.md]
trim_trailing_whitespace = false

3
.gitignore vendored
View File

@@ -38,3 +38,6 @@ out/
### Kotlin ###
.kotlin
### Lint ###
detekt-baseline.xml

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

344
LINT.md Normal file
View File

@@ -0,0 +1,344 @@
# Kotlin Lint 설정 가이드
## 개요
이 프로젝트는 Kotlin 코드 품질을 유지하기 위해 **ktlint**를 사용합니다.
## 사용 가능한 Lint 도구
### 1. ktlint (활성화됨 ✅)
Kotlin 공식 코드 스타일 가이드를 따르는 린터 및 포맷터입니다.
**특징:**
- 코드 스타일 체크
- 자동 포맷팅 기능
- Kotlin 2.2.21과 완벽 호환
### 2. detekt (현재 비활성화됨 ⚠️)
정적 코드 분석 도구로 코드 냄새, 복잡도, 잠재적 버그를 탐지합니다.
**비활성화 이유:**
- Kotlin 2.2.21과 호환성 문제
- detekt 1.23.7은 Kotlin 2.0.10까지만 지원
- 향후 detekt가 Kotlin 2.2.x를 지원하면 재활성화 예정
**활성화 방법:**
1. `build.gradle`에서 detekt 관련 주석 제거
2. Kotlin 버전을 2.0.x로 다운그레이드하거나
3. detekt 최신 버전(Kotlin 2.2.x 지원) 출시 대기
---
## 사용 가능한 Gradle Tasks
### 코드 스타일 체크
```bash
# 모든 모듈의 Kotlin 코드 스타일 체크
./gradlew ktlintCheck
# 특정 모듈만 체크
./gradlew :balance-core-api:ktlintCheck
./gradlew :storage:ktlintCheck
```
### 자동 포맷팅
```bash
# 모든 모듈의 Kotlin 코드 자동 포맷팅
./gradlew ktlintFormat
# 특정 모듈만 포맷팅
./gradlew :balance-core-api:ktlintFormat
./gradlew :storage:ktlintFormat
```
### 빌드와 함께 실행
```bash
# 빌드 전에 자동으로 ktlint 체크 실행
./gradlew clean build
# 또는
./gradlew check # 모든 검증 태스크 실행 (ktlint 포함)
```
---
## ktlint 설정
### 1. 기본 설정 (build.gradle)
```gradle
ktlint {
version = '1.4.1'
android = false
outputToConsole = true
coloredOutput = true
ignoreFailures = false
filter {
exclude('**/generated/**')
exclude('**/build/**')
}
}
```
### 2. .editorconfig 설정
프로젝트 루트의 `.editorconfig` 파일에서 코드 스타일을 세부 설정할 수 있습니다.
```ini
[*.{kt,kts}]
indent_size = 4
continuation_indent_size = 4
max_line_length = 120
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
```
---
## IDE 통합
### IntelliJ IDEA / Android Studio
#### 1. ktlint 플러그인 설치
1. `Preferences``Plugins`
2. "ktlint" 검색
3. 설치 및 재시작
#### 2. EditorConfig 활성화
1. `Preferences``Editor``Code Style`
2. "Enable EditorConfig support" 체크
#### 3. 저장 시 자동 포맷팅 (선택사항)
1. `Preferences``Tools``Actions on Save`
2. "Reformat code" 체크
3. "Optimize imports" 체크
---
## CI/CD 통합
### GitHub Actions 예시
```yaml
name: Lint Check
on:
pull_request:
branches: [ main ]
push:
branches: [ main ]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 24
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '24'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Run ktlint
run: ./gradlew ktlintCheck
- name: Upload ktlint reports
if: failure()
uses: actions/upload-artifact@v3
with:
name: ktlint-reports
path: '**/build/reports/ktlint/'
```
---
## 일반적인 린트 규칙
### 1. 들여쓰기
- 4 스페이스 사용 (탭 사용 금지)
### 2. 최대 줄 길이
- 120자 제한
### 3. 임포트
- 와일드카드 임포트 금지 (java.util.* 제외)
- 사용하지 않는 임포트 제거
### 4. 네이밍
- 클래스: PascalCase
- 함수/변수: camelCase
- 상수: UPPER_SNAKE_CASE
### 5. 공백
- 파일 끝에 빈 줄 추가
- 클래스 본문 시작/끝에 불필요한 빈 줄 제거
- 불필요한 공백 제거
---
## 린트 위반 사항 수정
### 자동 수정 (권장)
대부분의 스타일 위반은 자동으로 수정할 수 있습니다.
```bash
./gradlew ktlintFormat
```
### 수동 수정
자동 수정이 불가능한 경우, 린트 리포트를 확인하고 수동으로 수정합니다.
**리포트 위치:**
```
build/reports/ktlint/
├── ktlintMainSourceSetCheck.txt
└── ktlintTestSourceSetCheck.txt
```
### 특정 규칙 비활성화 (비권장)
정말 필요한 경우에만 사용합니다.
```kotlin
// 한 줄 비활성화
@Suppress("ktlint:standard:max-line-length")
val veryLongVariableName = "..."
// 파일 전체 비활성화
@file:Suppress("ktlint:standard:filename")
```
---
## 커밋 전 체크리스트
코드를 커밋하기 전에 다음을 실행하세요:
```bash
# 1. 코드 포맷팅
./gradlew ktlintFormat
# 2. 린트 체크
./gradlew ktlintCheck
# 3. 빌드 및 테스트
./gradlew clean build
```
---
## Git Hook 설정 (선택사항)
커밋 전에 자동으로 ktlint를 실행하도록 설정할 수 있습니다.
### pre-commit hook 생성
`.git/hooks/pre-commit` 파일 생성:
```bash
#!/bin/sh
echo "Running ktlint check..."
./gradlew ktlintCheck
if [ $? -ne 0 ]; then
echo "❌ ktlint check failed. Please run './gradlew ktlintFormat' to fix."
exit 1
fi
echo "✅ ktlint check passed!"
```
실행 권한 부여:
```bash
chmod +x .git/hooks/pre-commit
```
---
## 문제 해결
### 1. ktlint 버전 호환성 문제
**증상:**
```
Class org.jetbrains.kotlin.lexer.KtTokens does not have member field...
```
**해결:**
- `build.gradle`에서 ktlint 버전을 1.4.1 이상으로 업데이트
- Kotlin 버전과 호환되는 ktlint 버전 확인
### 2. 포맷팅 후에도 체크 실패
**원인:**
- IDE와 ktlint의 포맷팅 규칙 불일치
**해결:**
```bash
# ktlint 포맷팅 사용
./gradlew ktlintFormat
# IDE 포맷팅 사용 금지 또는 .editorconfig 확인
```
### 3. 특정 파일 제외하기
`build.gradle`의 ktlint 설정 수정:
```gradle
ktlint {
filter {
exclude('**/generated/**')
exclude('**/build/**')
exclude('**/MySpecialFile.kt') // 특정 파일 제외
}
}
```
---
## 참고 자료
- [ktlint 공식 문서](https://pinterest.github.io/ktlint/)
- [Kotlin 코드 스타일 가이드](https://kotlinlang.org/docs/coding-conventions.html)
- [EditorConfig](https://editorconfig.org/)
---
## 향후 계획
### detekt 재활성화
Kotlin 2.2.x를 지원하는 detekt 버전이 출시되면:
1. `build.gradle`에서 주석 해제:
```gradle
apply plugin: 'io.gitlab.arturbosch.detekt'
```
2. detekt 설정 활성화
3. 정적 분석 실행:
```bash
./gradlew detekt
```
---
**최종 업데이트:** 2026-01-11
**ktlint 버전:** 1.4.1
**Kotlin 버전:** 2.2.21

View File

@@ -0,0 +1,19 @@
plugins {
id 'org.jetbrains.kotlin.plugin.spring'
id 'org.jetbrains.kotlin.plugin.jpa'
id 'org.springframework.boot'
id 'org.graalvm.buildtools.native'
}
dependencies {
implementation project(':storage')
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'
}

View File

@@ -3,7 +3,7 @@ package com.quantbench.balance
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
@SpringBootApplication(scanBasePackages = ["com.quantbench.balance"])
class BalanceApplication
fun main(args: Array<String>) {

View File

@@ -5,9 +5,7 @@ import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
class BalanceApplicationTests {
@Test
fun contextLoads() {
}
}

View File

@@ -1,60 +1,85 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '2.2.21'
id 'org.jetbrains.kotlin.plugin.spring' version '2.2.21'
id 'org.springframework.boot' version '4.0.1'
id 'io.spring.dependency-management' version '1.1.7'
id 'org.hibernate.orm' version '7.2.0.Final'
id 'org.graalvm.buildtools.native' version '0.11.3'
id 'org.jetbrains.kotlin.plugin.jpa' version '2.2.21'
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.graalvm.buildtools.native' version '0.11.3' apply false
id 'org.jetbrains.kotlin.plugin.jpa' version '2.2.21' apply false
id 'org.jlleitschuh.gradle.ktlint' version '12.1.1' apply false
id 'io.gitlab.arturbosch.detekt' version '1.23.7' apply false
}
group = 'com.quantbench'
version = '0.0.1-SNAPSHOT'
description = 'balance'
java {
subprojects {
apply plugin: 'org.jetbrains.kotlin.jvm'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'org.jlleitschuh.gradle.ktlint'
// detekt는 Kotlin 2.2.21과 호환성 문제가 있어 주석 처리
// Kotlin 2.0.x로 다운그레이드하거나 detekt 최신 버전 출시 시 활성화
// apply plugin: 'io.gitlab.arturbosch.detekt'
repositories {
mavenCentral()
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(24)
}
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-h2console'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
implementation 'org.jetbrains.kotlin:kotlin-reflect'
implementation 'tools.jackson.module:jackson-module-kotlin'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'org.postgresql:postgresql'
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'
}
kotlin {
kotlin {
compilerOptions {
freeCompilerArgs.addAll '-Xjsr305=strict', '-Xannotation-default-target=param-property'
}
}
hibernate {
enhancement {
enableAssociationManagement = true
}
}
allOpen {
annotation 'jakarta.persistence.Entity'
annotation 'jakarta.persistence.MappedSuperclass'
annotation 'jakarta.persistence.Embeddable'
}
dependencies {
implementation 'org.jetbrains.kotlin:kotlin-reflect'
// detektPlugins 'io.gitlab.arturbosch.detekt:detekt-formatting:1.23.7'
}
tasks.named('test') {
tasks.withType(Test) {
useJUnitPlatform()
}
// ktlint 설정
ktlint {
version = '1.4.1'
android = false
outputToConsole = true
coloredOutput = true
ignoreFailures = false
filter {
exclude('**/generated/**')
exclude('**/build/**')
}
}
// detekt 설정 (현재 비활성화 - Kotlin 2.2.21과 호환성 문제)
// detekt {
// buildUponDefaultConfig = true
// allRules = false
// config.setFrom(files("$rootDir/detekt.yml"))
// // baseline은 존재할 때만 사용
// def baselineFile = file("$rootDir/detekt-baseline.xml")
// if (baselineFile.exists()) {
// baseline = baselineFile
// }
// ignoreFailures = false
// }
// tasks.withType(io.gitlab.arturbosch.detekt.Detekt).configureEach {
// reports {
// html.required = true
// xml.required = false
// txt.required = false
// sarif.required = false
// md.required = false
// }
// }
}

595
detekt.yml Normal file
View File

@@ -0,0 +1,595 @@
build:
maxIssues: 0
excludeCorrectable: false
weights:
complexity: 2
LongParameterList: 1
style: 1
comments: 1
config:
validation: true
warningsAsErrors: false
checkExhaustiveness: false
processors:
active: true
console-reports:
active: true
output-reports:
active: true
comments:
active: true
AbsentOrWrongFileLicense:
active: false
CommentOverPrivateFunction:
active: false
CommentOverPrivateProperty:
active: false
DeprecatedBlockTag:
active: false
EndOfSentenceFormat:
active: false
OutdatedDocumentation:
active: false
UndocumentedPublicClass:
active: false
UndocumentedPublicFunction:
active: false
UndocumentedPublicProperty:
active: false
complexity:
active: true
ComplexCondition:
active: true
threshold: 4
ComplexInterface:
active: false
CyclomaticComplexMethod:
active: true
threshold: 15
ignoreSingleWhenExpression: false
ignoreSimpleWhenEntries: false
ignoreNestingFunctions: false
LabeledExpression:
active: false
LargeClass:
active: true
threshold: 600
LongMethod:
active: true
threshold: 60
LongParameterList:
active: true
functionThreshold: 6
constructorThreshold: 7
ignoreDefaultParameters: false
ignoreDataClasses: true
ignoreAnnotatedParameter: []
MethodOverloading:
active: false
NamedArguments:
active: false
NestedBlockDepth:
active: true
threshold: 4
ReplaceSafeCallChainWithRun:
active: false
StringLiteralDuplication:
active: false
TooManyFunctions:
active: true
thresholdInFiles: 20
thresholdInClasses: 20
thresholdInInterfaces: 20
thresholdInObjects: 20
thresholdInEnums: 20
ignoreDeprecated: false
ignorePrivate: false
ignoreOverridden: false
coroutines:
active: true
GlobalCoroutineUsage:
active: false
InjectDispatcher:
active: false
RedundantSuspendModifier:
active: true
SleepInsteadOfDelay:
active: true
SuspendFunSwallowedCancellation:
active: false
SuspendFunWithCoroutineScopeReceiver:
active: false
SuspendFunWithFlowReturnType:
active: true
empty-blocks:
active: true
EmptyCatchBlock:
active: true
allowedExceptionNameRegex: '_|(ignore|expected).*'
EmptyClassBlock:
active: true
EmptyDefaultConstructor:
active: true
EmptyDoWhileBlock:
active: true
EmptyElseBlock:
active: true
EmptyFinallyBlock:
active: true
EmptyForBlock:
active: true
EmptyFunctionBlock:
active: true
ignoreOverridden: false
EmptyIfBlock:
active: true
EmptyInitBlock:
active: true
EmptyKtFile:
active: true
EmptySecondaryConstructor:
active: true
EmptyTryBlock:
active: true
EmptyWhenBlock:
active: true
EmptyWhileBlock:
active: true
exceptions:
active: true
ExceptionRaisedInUnexpectedLocation:
active: true
methodNames:
- 'equals'
- 'finalize'
- 'hashCode'
- 'toString'
InstanceOfCheckForException:
active: true
NotImplementedDeclaration:
active: false
ObjectExtendsThrowable:
active: false
PrintStackTrace:
active: true
RethrowCaughtException:
active: true
ReturnFromFinally:
active: true
ignoreLabeled: false
SwallowedException:
active: true
ignoredExceptionTypes:
- 'InterruptedException'
- 'MalformedURLException'
- 'NumberFormatException'
- 'ParseException'
allowedExceptionNameRegex: '_|(ignore|expected).*'
ThrowingExceptionFromFinally:
active: true
ThrowingExceptionInMain:
active: false
ThrowingExceptionsWithoutMessageOrCause:
active: true
exceptions:
- 'ArrayIndexOutOfBoundsException'
- 'Exception'
- 'IllegalArgumentException'
- 'IllegalMonitorStateException'
- 'IllegalStateException'
- 'IndexOutOfBoundsException'
- 'NullPointerException'
- 'RuntimeException'
- 'Throwable'
ThrowingNewInstanceOfSameException:
active: true
TooGenericExceptionCaught:
active: true
exceptionNames:
- 'ArrayIndexOutOfBoundsException'
- 'Error'
- 'Exception'
- 'IllegalMonitorStateException'
- 'IndexOutOfBoundsException'
- 'NullPointerException'
- 'RuntimeException'
- 'Throwable'
allowedExceptionNameRegex: '_|(ignore|expected).*'
TooGenericExceptionThrown:
active: true
exceptionNames:
- 'Error'
- 'Exception'
- 'RuntimeException'
- 'Throwable'
naming:
active: true
BooleanPropertyNaming:
active: false
ClassNaming:
active: true
classPattern: '[A-Z][a-zA-Z0-9]*'
ConstructorParameterNaming:
active: true
parameterPattern: '[a-z][A-Za-z0-9]*'
privateParameterPattern: '[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
EnumNaming:
active: true
enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
ForbiddenClassName:
active: false
FunctionMaxLength:
active: false
FunctionMinLength:
active: false
FunctionNaming:
active: true
excludeClassPattern: '$^'
functionPattern: '[a-z][a-zA-Z0-9]*'
excludeAnnotatedFunction: ['Composable']
ignoreAnnotated: ['Test']
FunctionParameterNaming:
active: true
parameterPattern: '[a-z][A-Za-z0-9]*'
InvalidPackageDeclaration:
active: true
rootPackage: ''
LambdaParameterNaming:
active: false
MatchingDeclarationName:
active: true
mustBeFirst: true
MemberNameEqualsClassName:
active: true
ignoreOverridden: true
NoNameShadowing:
active: true
NonBooleanPropertyPrefixedWithIs:
active: false
ObjectPropertyNaming:
active: true
constantPattern: '[A-Za-z][_A-Za-z0-9]*'
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
PackageNaming:
active: true
packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
TopLevelPropertyNaming:
active: true
constantPattern: '[A-Z][_A-Z0-9]*'
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
VariableMaxLength:
active: false
VariableMinLength:
active: false
VariableNaming:
active: true
variablePattern: '[a-z][A-Za-z0-9]*'
privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
performance:
active: true
ArrayPrimitive:
active: true
CouldBeSequence:
active: false
ForEachOnRange:
active: true
SpreadOperator:
active: false
UnnecessaryPartOfBinaryExpression:
active: false
UnnecessaryTemporaryInstantiation:
active: true
potential-bugs:
active: true
AvoidReferentialEquality:
active: true
forbiddenTypePatterns:
- 'kotlin.String'
CastNullableToNonNullableType:
active: false
CastToNullableType:
active: false
Deprecation:
active: false
DontDowncastCollectionTypes:
active: false
DoubleMutabilityForCollection:
active: true
ElseCaseInsteadOfExhaustiveWhen:
active: false
EqualsAlwaysReturnsTrueOrFalse:
active: true
EqualsWithHashCodeExist:
active: true
ExitOutsideMain:
active: false
ExplicitGarbageCollectionCall:
active: true
HasPlatformType:
active: true
IgnoredReturnValue:
active: true
ImplicitDefaultLocale:
active: true
ImplicitUnitReturnType:
active: false
InvalidRange:
active: true
IteratorHasNextCallsNextMethod:
active: true
IteratorNotThrowingNoSuchElementException:
active: true
LateinitUsage:
active: false
MapGetWithNotNullAssertionOperator:
active: true
MissingPackageDeclaration:
active: false
NullCheckOnMutableProperty:
active: false
NullableToStringCall:
active: false
UnconditionalJumpStatementInLoop:
active: false
UnnecessaryNotNullCheck:
active: false
UnnecessaryNotNullOperator:
active: true
UnnecessarySafeCall:
active: true
UnreachableCatchBlock:
active: true
UnreachableCode:
active: true
UnsafeCallOnNullableType:
active: true
UnsafeCast:
active: true
UnusedUnaryOperator:
active: true
UselessPostfixExpression:
active: true
WrongEqualsTypeParameter:
active: true
style:
active: true
AlsoCouldBeApply:
active: false
BracesOnIfStatements:
active: false
BracesOnWhenStatements:
active: false
CanBeNonNullable:
active: false
CascadingCallWrapping:
active: false
ClassOrdering:
active: false
CollapsibleIfStatements:
active: false
DataClassContainsFunctions:
active: false
DataClassShouldBeImmutable:
active: false
DestructuringDeclarationWithTooManyEntries:
active: true
maxDestructuringEntries: 3
DoubleNegativeLambda:
active: false
EqualsNullCall:
active: true
EqualsOnSignatureLine:
active: false
ExplicitCollectionElementAccessMethod:
active: false
ExplicitItLambdaParameter:
active: false
ExpressionBodySyntax:
active: false
ForbiddenAnnotation:
active: false
ForbiddenComment:
active: true
values:
- 'FIXME:'
- 'STOPSHIP:'
- 'TODO:'
allowedPatterns: ''
ForbiddenImport:
active: false
ForbiddenMethodCall:
active: false
ForbiddenSuppress:
active: false
ForbiddenVoid:
active: true
ignoreOverridden: false
ignoreUsageInGenerics: false
FunctionOnlyReturningConstant:
active: true
ignoreOverridableFunction: true
ignoreActualFunction: true
excludedFunctions: []
LoopWithTooManyJumpStatements:
active: true
maxJumpCount: 1
MagicNumber:
active: true
excludes:
- '**/test/**'
- '**/androidTest/**'
- '**/commonTest/**'
- '**/jvmTest/**'
- '**/jsTest/**'
- '**/iosTest/**'
ignoreNumbers:
- '-1'
- '0'
- '1'
- '2'
ignoreHashCodeFunction: true
ignorePropertyDeclaration: false
ignoreLocalVariableDeclaration: false
ignoreConstantDeclaration: true
ignoreCompanionObjectPropertyDeclaration: true
ignoreAnnotation: false
ignoreNamedArgument: true
ignoreEnums: false
ignoreRanges: false
ignoreExtensionFunctions: true
MandatoryBracesLoops:
active: false
MaxChainedCallsOnSameLine:
active: false
MaxLineLength:
active: true
maxLineLength: 120
excludePackageStatements: true
excludeImportStatements: true
excludeCommentStatements: false
MayBeConst:
active: true
ModifierOrder:
active: true
MultilineLambdaItParameter:
active: false
MultilineRawStringIndentation:
active: false
NestedClassesVisibility:
active: true
NewLineAtEndOfFile:
active: true
NoTabs:
active: false
NullableBooleanCheck:
active: false
ObjectLiteralToLambda:
active: true
OptionalAbstractKeyword:
active: true
OptionalUnit:
active: false
PreferToOverPairSyntax:
active: false
ProtectedMemberInFinalClass:
active: true
RedundantExplicitType:
active: false
RedundantHigherOrderMapUsage:
active: true
RedundantVisibilityModifierRule:
active: false
ReturnCount:
active: true
max: 2
excludedFunctions:
- 'equals'
excludeLabeled: false
excludeReturnFromLambda: true
excludeGuardClauses: false
SafeCast:
active: true
SerialVersionUIDInSerializableClass:
active: true
SpacingBetweenPackageAndImports:
active: false
StringShouldBeRawString:
active: false
ThrowsCount:
active: true
max: 2
excludeGuardClauses: false
TrailingWhitespace:
active: false
TrimMultilineRawString:
active: false
UnderscoresInNumericLiterals:
active: false
UnnecessaryAbstractClass:
active: true
UnnecessaryAnnotationUseSiteTarget:
active: false
UnnecessaryApply:
active: true
UnnecessaryBackticks:
active: false
UnnecessaryBracesAroundTrailingLambda:
active: false
UnnecessaryFilter:
active: true
UnnecessaryInheritance:
active: true
UnnecessaryInnerClass:
active: false
UnnecessaryLet:
active: false
UnnecessaryParentheses:
active: false
UntilInsteadOfRangeTo:
active: false
UnusedImports:
active: false
UnusedParameter:
active: true
allowedNames: 'ignored|expected'
UnusedPrivateClass:
active: true
UnusedPrivateMember:
active: true
allowedNames: ''
UnusedPrivateProperty:
active: true
allowedNames: '_|ignored|expected|serialVersionUID'
UseAnyOrNoneInsteadOfFind:
active: true
UseArrayLiteralsInAnnotations:
active: true
UseCheckNotNull:
active: true
UseCheckOrError:
active: true
UseDataClass:
active: false
UseEmptyCounterpart:
active: false
UseIfEmptyOrIfBlank:
active: false
UseIfInsteadOfWhen:
active: false
UseIsNullOrEmpty:
active: true
UseOrEmpty:
active: true
UseRequire:
active: true
UseRequireNotNull:
active: true
UseSumOfInsteadOfFlatMapSize:
active: false
UselessCallOnNotNull:
active: true
UtilityClassWithPublicConstructor:
active: true
VarCouldBeVal:
active: true
WildcardImport:
active: true
excludeImports:
- 'java.util.*'

View File

@@ -1 +1,3 @@
rootProject.name = 'balance'
include 'storage'
include 'balance-core-api'

33
storage/build.gradle Normal file
View File

@@ -0,0 +1,33 @@
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'
}
hibernate {
enhancement {
enableAssociationManagement = true
}
}
allOpen {
annotation 'jakarta.persistence.Entity'
annotation 'jakarta.persistence.MappedSuperclass'
annotation 'jakarta.persistence.Embeddable'
}
// 라이브러리 모듈이므로 bootJar 비활성화
bootJar {
enabled = false
}
jar {
enabled = true
}