프로젝트가 커지면 모듈을 나누고 싶고, 의존성 버전은 한 곳에서 관리하고 싶습니다. Gradle이 이걸 어떻게 지원할까요?

Gradle 멀티모듈

왜 필요한가요?

하나의 프로젝트가 커지면:

  • 빌드 시간이 길어짐 (변경된 모듈만 빌드하면 빠름)
  • 모듈 간 의존 방향을 명확히 하여 아키텍처 강제
  • 공통 코드를 별도 모듈로 분리하여 재사용

프로젝트 구조

PLAINTEXT
my-project/
├── settings.gradle.kts
├── build.gradle.kts          (루트)
├── core/
│   └── build.gradle.kts
├── api/
│   └── build.gradle.kts
└── batch/
    └── build.gradle.kts

settings.gradle.kts

KOTLIN
rootProject.name = "my-project"

include("core")
include("api")
include("batch")

루트 build.gradle.kts

KOTLIN
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

KOTLIN
// core/build.gradle.kts — 도메인 로직, Spring 의존성 없음
dependencies {
    implementation("com.google.guava:guava:33.0.0-jre")
}
KOTLIN
// 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")
}
KOTLIN
// 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

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" }

사용

KOTLIN
// 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으로 버전을 일괄 관리합니다.

KOTLIN
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 만들기

KOTLIN
// 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")
    }
}
KOTLIN
// api/build.gradle.kts
dependencies {
    implementation(platform(project(":bom"))) // 내부 BOM
    implementation("com.google.guava:guava")   // 버전 생략
}

의존성 스코프

GradleMaven설명
implementationcompile컴파일+런타임, 전이 안 됨
apicompile컴파일+런타임, 전이 됨
compileOnlyprovided컴파일만 (Lombok)
runtimeOnlyruntime런타임만 (JDBC 드라이버)
testImplementationtest테스트만

api vs implementation

KOTLIN
// core 모듈
dependencies {
    api("com.google.guava:guava")        // 전이: api 모듈에서도 Guava 사용 가능
    implementation("org.slf4j:slf4j-api") // 비전이: api 모듈에서 직접 접근 불가
}

implementation이 기본이고, 소비자 모듈에서도 사용해야 하는 라이브러리만 api를 씁니다.

빌드 최적화

KOTLIN
// 병렬 빌드
// 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 Cataloglibs.versions.toml에서 모든 버전 중앙 관리
BOM관련 라이브러리 버전을 일괄 관리 (platform())
api vs implementation전이 여부 차이, 기본은 implementation
빌드 최적화병렬, 캐시, 구성 캐시

References

댓글 로딩 중...