자바 빌드와 의존성 — Gradle 멀티모듈, 버전 카탈로그, BOM
프로젝트가 커지면 모듈을 나누고 싶고, 의존성 버전은 한 곳에서 관리하고 싶습니다. Gradle이 이걸 어떻게 지원할까요?
Gradle 멀티모듈
왜 필요한가요?
하나의 프로젝트가 커지면:
- 빌드 시간이 길어짐 (변경된 모듈만 빌드하면 빠름)
- 모듈 간 의존 방향을 명확히 하여 아키텍처 강제
- 공통 코드를 별도 모듈로 분리하여 재사용
프로젝트 구조
my-project/
├── settings.gradle.kts
├── build.gradle.kts (루트)
├── core/
│ └── build.gradle.kts
├── api/
│ └── build.gradle.kts
└── batch/
└── build.gradle.kts
settings.gradle.kts
rootProject.name = "my-project"
include("core")
include("api")
include("batch")
루트 build.gradle.kts
plugins {
java
id("org.springframework.boot") version "3.3.0" apply false
}
// 모든 서브프로젝트에 공통 설정
subprojects {
apply(plugin = "java")
group = "com.example"
version = "1.0.0"
java {
sourceCompatibility = JavaVersion.VERSION_21
}
repositories {
mavenCentral()
}
// 공통 의존성
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
}
tasks.test {
useJUnitPlatform()
}
}
모듈별 build.gradle.kts
// core/build.gradle.kts — 도메인 로직, Spring 의존성 없음
dependencies {
implementation("com.google.guava:guava:33.0.0-jre")
}
// api/build.gradle.kts — REST API 모듈
plugins {
id("org.springframework.boot")
id("io.spring.dependency-management")
}
dependencies {
implementation(project(":core")) // core 모듈 의존
implementation("org.springframework.boot:spring-boot-starter-web")
}
// batch/build.gradle.kts — 배치 모듈
plugins {
id("org.springframework.boot")
id("io.spring.dependency-management")
}
dependencies {
implementation(project(":core")) // core 모듈 의존
implementation("org.springframework.boot:spring-boot-starter-batch")
}
Version Catalog
왜 필요한가요?
멀티모듈에서 같은 라이브러리의 버전이 모듈마다 다르면 충돌이 발생합니다. Version Catalog로 한 파일에서 모든 의존성 버전을 관리합니다.
gradle/libs.versions.toml
[versions]
spring-boot = "3.3.0"
junit = "5.10.0"
guava = "33.0.0-jre"
lombok = "1.18.32"
mapstruct = "1.5.5.Final"
[libraries]
spring-boot-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot" }
spring-boot-batch = { module = "org.springframework.boot:spring-boot-starter-batch", version.ref = "spring-boot" }
spring-boot-test = { module = "org.springframework.boot:spring-boot-starter-test", version.ref = "spring-boot" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
guava = { module = "com.google.guava:guava", version.ref = "guava" }
lombok = { module = "org.projectlombok:lombok", version.ref = "lombok" }
[bundles]
testing = ["junit-jupiter", "spring-boot-test"]
[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }
사용
// api/build.gradle.kts
plugins {
alias(libs.plugins.spring.boot)
}
dependencies {
implementation(libs.spring.boot.web)
implementation(libs.guava)
compileOnly(libs.lombok)
testImplementation(libs.bundles.testing) // 번들로 여러 의존성 한 번에
}
IDE에서 자동 완성이 되므로 오타도 방지됩니다.
BOM (Bill of Materials)
왜 필요한가요?
Spring Boot, Jackson, AWS SDK처럼 여러 라이브러리의 버전이 함께 맞아야 하는 경우, BOM으로 버전을 일괄 관리합니다.
dependencies {
// BOM 가져오기 — 버전을 명시하지 않아도 됨
implementation(platform("org.springframework.boot:spring-boot-dependencies:3.3.0"))
// 버전 생략 가능 (BOM에서 관리)
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.core:jackson-databind")
}
커스텀 BOM 만들기
// bom/build.gradle.kts
plugins {
`java-platform`
}
dependencies {
constraints {
api("com.google.guava:guava:33.0.0-jre")
api("org.mapstruct:mapstruct:1.5.5.Final")
api("io.jsonwebtoken:jjwt-api:0.12.5")
}
}
// api/build.gradle.kts
dependencies {
implementation(platform(project(":bom"))) // 내부 BOM
implementation("com.google.guava:guava") // 버전 생략
}
의존성 스코프
| Gradle | Maven | 설명 |
|---|---|---|
| implementation | compile | 컴파일+런타임, 전이 안 됨 |
| api | compile | 컴파일+런타임, 전이 됨 |
| compileOnly | provided | 컴파일만 (Lombok) |
| runtimeOnly | runtime | 런타임만 (JDBC 드라이버) |
| testImplementation | test | 테스트만 |
api vs implementation
// core 모듈
dependencies {
api("com.google.guava:guava") // 전이: api 모듈에서도 Guava 사용 가능
implementation("org.slf4j:slf4j-api") // 비전이: api 모듈에서 직접 접근 불가
}
implementation이 기본이고, 소비자 모듈에서도 사용해야 하는 라이브러리만 api를 씁니다.
빌드 최적화
// 병렬 빌드
// gradle.properties
org.gradle.parallel=true
// 빌드 캐시
org.gradle.caching=true
// 구성 캐시 (Gradle 8.1+)
org.gradle.configuration-cache=true
자주 헷갈리는 포인트
- api vs implementation:
api를 남용하면 의존성이 전이되어 빌드 시간이 늘어납니다. 꼭 필요한 경우만api를 쓰세요. - Version Catalog 파일 위치:
gradle/libs.versions.toml이 정확한 경로입니다. 이름이나 위치가 다르면 인식되지 않습니다. - BOM과 Version Catalog 함께 쓰기: Spring Boot BOM은
spring-boot-dependencies로 가져오고, 나머지 라이브러리는 Version Catalog로 관리하면 깔끔합니다. - 순환 의존: 모듈 A가 B에 의존하고 B가 A에 의존하면 빌드가 실패합니다. 공통 코드를 별도 모듈로 분리하세요.
정리
| 항목 | 설명 |
|---|---|
| 멀티모듈 | settings.gradle에 include, 모듈 간 project(":name") 의존 |
| Version Catalog | libs.versions.toml에서 모든 버전 중앙 관리 |
| BOM | 관련 라이브러리 버전을 일괄 관리 (platform()) |
| api vs implementation | 전이 여부 차이, 기본은 implementation |
| 빌드 최적화 | 병렬, 캐시, 구성 캐시 |