도입
초기에는 모든 코드를 하나의 build.gradle과 하나의 소스 트리에 넣는 편이 단순해 보입니다. 하지만 프로젝트가 커질수록 책임이 다른 코드가 한데 섞이고, 변경 영향 범위도 커지며, 빌드 설정과 의존성 관리 역시 빠르게 복잡해집니다.
이때 멀티 모듈은 코드를 여러 경계로 나누고, 각 모듈이 어떤 역할을 가지는지, 어떤 방향으로만 의존해야 하는지, 어떤 빌드 규칙을 적용받는지를 구조적으로 드러내는 방식이 됩니다.
특히 Gradle에서는 멀티 모듈이 단순 디렉터리 분리가 아니라 root project + subprojects + project dependency + task graph로 구성된 빌드 모델이라는 점이 중요합니다.
필요성
멀티 모듈의 첫 번째 장점은 책임 분리입니다. 웹 진입점, 도메인 모델, 인프라 어댑터, 공통 라이브러리처럼 서로 다른 관심사를 분리하면 코드 탐색과 유지보수가 쉬워집니다.
두 번째 장점은 변경 영향 범위가 더 명확해진다는 점입니다. 어떤 모듈이 바뀌었을 때 어디까지 다시 빌드하고 테스트해야 하는지 판단하기 쉬워지고, 팀 단위로 소유권을 나누기도 좋아집니다.
세 번째는 빌드와 의존성 관리 측면입니다. 공통 규칙을 모듈마다 복붙하는 대신 공통 build logic을 분리할 수 있고, 버전과 저장소 정책도 중앙에서 통제하기 쉬워집니다.
물론 모든 프로젝트가 처음부터 멀티 모듈이어야 하는 것은 아닙니다. 중요한 것은 “모듈 수를 늘리는 것”이 아니라, 경계를 명확히 하고 구조를 예측 가능하게 만드는 일입니다.
- 관심사별 책임을 모듈 단위로 분리하기 쉬움
- 변경 영향 범위와 의존 방향을 눈에 보이게 만들 수 있음
- 공통 빌드 로직과 버전 관리를 중앙화하기 좋음
- 재사용 가능한 라이브러리 모듈을 분리하기 쉬움
- 코드베이스와 팀 규모가 커져도 구조를 유지하기 좋음
정의
실무에서는 흔히 “멀티 모듈 프로젝트”라고 부르지만, Gradle 문서에서는 보통 multi-project build라는 표현을 사용합니다. 즉 하나의 루트 프로젝트 아래 여러 서브프로젝트가 포함되는 빌드 구조라고 이해하면 됩니다.
settings.gradle(.kts)는 어떤 모듈이 이 빌드에 포함되는지 정의하고, 각 서브프로젝트의 build.gradle(.kts)는 그 모듈이 어떤 플러그인, 의존성, 태스크를 가지는지 정의합니다.
그리고 모듈 간 연결은 보통 project(":domain") 같은 project dependency로 표현됩니다. 이 구조 덕분에 Gradle은 단순 파일 묶음이 아니라 모듈 간 관계가 반영된 빌드 모델을 이해하게 됩니다.
"좋은 멀티 모듈은 폴더가 많은 구조가 아니라
변경 영향과 의존 방향을 예측 가능하게 만드는 구조에 가깝습니다."
핵심 원리
멀티 모듈은 패키지명만 나누는 작업이 아닙니다. 각 모듈은 독립적인 플러그인, 의존성, 태스크를 가진 서브프로젝트이며, 어떤 모듈이 다른 모듈을 참조하는지 또한 빌드 시스템이 이해할 수 있어야 합니다.
예를 들어 :app이 :domain과 :infra:db에 의존한다면, 이 관계는 단순 import 수준이 아니라 build model의 일부가 됩니다. 그래서 Gradle은 어떤 모듈이 먼저 준비되어야 하는지, 어떤 태스크가 선행되어야 하는지를 그래프 기준으로 해석할 수 있습니다.
즉 멀티 모듈은 소스 구조 설계이면서 동시에 빌드 구조 설계입니다. 둘 중 하나만 맞고 다른 하나가 무너지면, 프로젝트는 빠르게 복잡해집니다.
결국 중요한 것은 “몇 개 모듈로 나눴는가”보다 “어떤 방향으로만 의존하게 만들었는가”입니다.
dependencies {
implementation(project(":domain"))
implementation(project(":infra:db"))
}
구성 요소
| 요소 | 역할 | 실무 해석 |
|---|---|---|
| Root Project | 전체 빌드의 진입점 | 루트 build script는 가능하면 얇게 유지하는 편이 좋음 |
| Subproject | 개별 모듈 | 플러그인, 의존성, 태스크를 각 모듈이 독립적으로 가짐 |
| settings.gradle(.kts) | 모듈 구조와 빌드 참여 범위를 정의 | 멀티 모듈의 실제 경계가 선언되는 파일 |
| Project Path | 모듈의 고유 경로 | :app, :infra:db 같은 형태로 모듈을 식별 |
| Project Dependency | 같은 빌드 안의 다른 모듈 참조 | project(":domain")처럼 선언해 모듈 관계를 명시 |
| Convention Plugin / build-logic | 공통 빌드 규칙의 재사용 | 모듈 수가 많아질수록 복붙 대신 공통화가 중요해짐 |
| Version Catalog / Platform | 버전 및 의존성 정책 중앙화 | 모듈별 버전 흔들림을 줄이고 일관성을 높임 |
초보자가 가장 많이 놓치는 부분은, 멀티 모듈의 중심이 build.gradle 하나가 아니라 settings + subprojects + dependency model 전체라는 점입니다.
기본 구조
shop-platform/
├── settings.gradle.kts
├── build.gradle.kts
├── gradle/
│ └── libs.versions.toml
├── build-logic/
│ ├── settings.gradle.kts
│ └── conventions/
│ └── build.gradle.kts
├── app/
│ └── build.gradle.kts
├── domain/
│ └── build.gradle.kts
├── common/
│ └── build.gradle.kts
├── infra/
│ ├── db/
│ │ └── build.gradle.kts
│ └── web/
│ └── build.gradle.kts
└── platform/
└── build.gradle.kts
settings.gradle(.kts): 어떤 모듈이 빌드에 포함되는지와 repository 정책을 정의build.gradle(.kts): 루트 레벨의 최소 공통 빌드 엔트리, 가능하면 가볍게 유지- 각 모듈의
build.gradle(.kts): 해당 모듈의 plugin, dependency, task만 정의 gradle/libs.versions.toml: 의존성 좌표와 버전 별칭을 중앙에서 관리build-logic: convention plugin 등 공통 build logic을 별도 build로 분리platform: 여러 모듈에서 버전을 정렬하거나 constraints를 공유할 때 사용
특히 루트 프로젝트가 모든 설정을 직접 떠안기 시작하면 멀티 모듈의 장점이 빠르게 사라집니다. 구조 정의는 settings, 공통 규칙은 build-logic, 모듈별 설정은 각 서브프로젝트에 두는 쪽이 장기적으로 훨씬 안정적입니다.
기본 구현
// settings.gradle.kts
rootProject.name = "shop-platform"
pluginManagement {
includeBuild("build-logic")
}
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
mavenCentral()
}
}
include("app", "domain", "common", "infra:db", "infra:web", "platform")
// app/build.gradle.kts
plugins {
id("myapp.java-application")
}
dependencies {
implementation(platform(project(":platform")))
implementation(project(":domain"))
implementation(project(":infra:db"))
}
// domain/build.gradle.kts
plugins {
id("myapp.java-library")
}
dependencies {
api(project(":common"))
}
settings.gradle.kts는 구조와 저장소를 고정하고, 각 모듈 build script는 자기 책임에 맞는 설정만 남기고, 공통 규칙은 build-logic 쪽으로 이동시키는 흐름이 기본입니다.패턴 1. 계층형 분리
계층형 분리는 가장 이해하기 쉽고, 많은 팀에서 가장 먼저 도입하는 방식입니다. 진입점은 app, 핵심 도메인 규칙은 domain, 외부 기술 의존은 infra, 여러 모듈에서 공통으로 쓰는 최소 타입은 common으로 나누는 식입니다.
이 구조의 핵심은 단순히 이름이 아니라 의존 방향입니다. 보통 domain은 infra를 모르고, infra가 domain에 의존하는 방향이 더 자연스럽습니다. 그래야 핵심 로직이 외부 기술 변화에 덜 흔들립니다.
:app -> :domain, :infra:db, :infra:web
:infra:db -> :domain
:infra:web -> :domain
:domain -> :common
:common -> (none)
이 패턴은 구조가 비교적 단순하고 설명하기 쉬운 대신, 시간이 지나면 common이나 domain에 너무 많은 것이 몰리기 쉽다는 점은 주의해야 합니다.
패턴 2. 기능형 분리
프로젝트가 커지고 팀별 책임 범위가 분명해질수록, 기술 계층보다 기능 단위로 자르는 편이 더 자연스러울 수 있습니다. 예를 들어 회원, 주문, 결제, 상품처럼 비즈니스 기능 자체를 모듈 경계로 삼는 방식입니다.
이 경우 장점은 소유권과 변경 영향 범위가 기능 단위로 모인다는 점입니다. 특정 기능을 수정할 때 어떤 모듈을 봐야 하는지 분명해지고, 팀 단위 작업도 더 자연스러워집니다.
다만 기능형 분리는 계층형보다 경계 설계가 더 어렵습니다. 각 기능 모듈이 서로 너무 많이 참조하면 오히려 거대한 모놀리스가 모듈 껍데기만 쓴 상태가 되기 쉽습니다.
shop-platform
├── app
├── member
├── order
├── payment
└── shared-kernel
패턴 3. 공통 build logic과 의존성 중앙화
멀티 모듈의 초반에는 각 모듈에 비슷한 설정을 복사해 붙여 넣는 경우가 많습니다. 하지만 모듈 수가 늘어나면 Java toolchain, 테스트 규칙, Kotlin 옵션, 코드 품질 설정이 여러 파일에 흩어지고, 작은 변경 하나도 대량 수정으로 이어집니다.
그래서 공통 build logic은 convention plugin으로 빼고, 저장소 정책은 settings에서 중앙화하고, 버전 별칭은 version catalog로 묶는 편이 좋습니다. 의존성 버전을 더 강하게 맞춰야 하는 경우에는 java-platform 기반의 platform 모듈도 실무적으로 유용합니다.
// build-logic/conventions/src/main/kotlin/myapp.java-library.gradle.kts
plugins {
`java-library`
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
tasks.test {
useJUnitPlatform()
}
# gradle/libs.versions.toml
[versions]
slf4j = "2.0.17"
junit = "5.13.4"
[libraries]
slf4j-api = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit" }
// platform/build.gradle.kts
plugins {
`java-platform`
}
dependencies {
constraints {
api("org.slf4j:slf4j-api:2.0.17")
api("org.junit.jupiter:junit-jupiter:5.13.4")
}
}
패턴 4. 멀티 모듈과 Composite Build 구분
| 구분 | 멀티 모듈 | Composite Build |
|---|---|---|
| 기본 단위 | 하나의 root build 아래 여러 subproject | 여러 개의 독립된 build를 포함 |
| 구조 | settings.gradle(.kts)에서 모듈을 include()로 선언 |
includeBuild()로 다른 build를 포함 |
| 적합한 경우 | 함께 개발되고 함께 빌드되는 모듈 집합 | 서로 독립적인 build를 조합하거나 build logic을 분리할 때 |
| 주의점 | 루트에 모든 설정을 몰아넣기 쉬움 | 구성이 더 유연하지만 초기 이해 난이도는 더 높음 |
즉 “모듈이 많다”는 이유만으로 composite build가 필요한 것은 아닙니다. 하나의 제품을 함께 빌드하는 코드베이스라면 멀티 모듈이 먼저이고, 별도의 build를 조합해야 할 이유가 분명할 때 composite build를 검토하는 편이 자연스럽습니다.
한계와 주의점
멀티 모듈이 항상 정답은 아닙니다. 모듈을 나눈 기준이 불명확하면 모듈 간 참조만 늘어나고, 오히려 코드를 따라가기가 더 어려워집니다.
특히 common, shared, util 같은 이름의 모듈은 잘못 관리하면 모든 코드가 모여드는 쓰레기통이 되기 쉽습니다. 이렇게 되면 분리한 의미가 거의 사라집니다.
또한 너무 이른 시점에 모듈을 과도하게 세분화하면 빌드 설정, 테스트 구성, 버전 관리, 팀 간 조율 비용이 더 커질 수 있습니다. 구조적 이득보다 운영 비용이 커지면 다시 단순화하는 편이 낫습니다.
- 기준 없이 모듈 수만 늘리면 의존 관계가 오히려 더 복잡해짐
common,shared,util모듈이 만능 저장소가 되기 쉬움- 루트 프로젝트가 모든 설정을 떠안으면 모듈화 이점이 약해짐
- 모듈 분리는 설계 경계이면서 빌드 경계이므로 둘 다 함께 설계해야 함
자주 하는 실수
- 패키지 수가 많다는 이유만으로 기준 없이 모듈을 분리함
app모듈이 거의 모든 모듈에 직접 의존하면서 중앙 집결지가 됨- 라이브러리 모듈에서
api와implementation을 구분하지 않아 의존성이 과도하게 전파됨 - 저장소와 버전 선언을 각 모듈에 흩뿌려 정책이 통일되지 않음
- 여러 모듈에 같은 build logic을 복붙하고 convention plugin으로 정리하지 않음
- 멀티 모듈과 multi-repo, composite build를 같은 개념처럼 섞어 생각함
common모듈에 아무 타입이나 넣어 실제 경계가 흐려짐
실무 루틴
- 먼저 기능이나 계층 중 어떤 기준이 더 안정적인지 보고 모듈 경계를 정한다.
- 모듈별 허용 의존 방향을 글로라도 먼저 정한다.
- 초기에는 3~5개 정도의 의미 있는 모듈부터 시작하고 과도한 세분화는 피한다.
- 저장소 정책은
settings.gradle(.kts), 버전 별칭은libs.versions.toml또는 platform으로 모은다. - 공통 build logic은 루트 script 복붙 대신 convention plugin 또는
build-logic로 이동한다. - 전체 빌드만 보지 말고
:app:build,:domain:test같은 모듈 단위 실행 습관을 함께 들인다.
디버깅
./gradlew projects로 실제 포함된 모듈과 project path를 확인한다../gradlew :app:tasks, ./gradlew :domain:build처럼 fully qualified task name으로 모듈 단위 실행 범위를 좁혀 본다../gradlew :app:dependencies --configuration runtimeClasspath로 실제 의존성 그래프를 확인한다.dependencyInsight로 특정 라이브러리가 왜 들어왔는지, 어떤 버전이 선택됐는지 추적한다.help --warning-mode=all 경로로 deprecation 경고를 먼저 수집한다.자주 쓰는 명령어
./gradlew projects
./gradlew :app:tasks
./gradlew :domain:build
./gradlew :app:dependencies --configuration runtimeClasspath
./gradlew dependencyInsight --dependency slf4j --configuration runtimeClasspath
./gradlew help --warning-mode=all
요약
- ✅ 멀티 모듈은 Gradle에서 보통 multi-project build로 설명된다.
- ✅ 하나의 root project 아래 여러 subproject를
settings.gradle(.kts)로 묶는다. - ✅ 모듈 간 연결은
project(":module")같은 project dependency로 표현한다. - ✅ 중요한 것은 모듈 개수보다 의존 방향과 변경 영향 범위를 명확히 하는 일이다.
- ✅ 계층형 분리와 기능형 분리는 모두 가능하지만, 경계 기준은 일관되어야 한다.
- ✅ 공통 build logic은 convention plugin 또는
build-logic로 분리하는 편이 유지보수에 유리하다. - ✅ 버전과 저장소 정책은 중앙화할수록 멀티 모듈의 운영 난이도가 낮아진다.
- ✅ 멀티 모듈과 composite build는 비슷해 보여도 목적과 단위가 다르다.