ABOUT

성능과 운영 안정성을 함께 끌어올리는 개발자입니다.

92% Positional Error Reduction
79% p95 Latency Improvement
90%+ Long Tasks Reduction

2022.02 · 한국장학재단

우수 멘티

한국장학재단 사회 리더 대학생 멘토링 IT

2022.10 · 동작구청

우수 인재상

동작구청 우수 SW 인재

2025.05 · (주) 그랩

프로그래밍 우수상

(주) 그랩 우수 프로그램 개발

2025.05 · AWSKRUG

AWS한국사용자모임 발표

AI agent 스크립트 튜닝 관련 발표

ComputerScience

Development

Engineering

Trouble Shooting

GUESTBOOK

첫 마음부터
함께 나누는 온기

방명록 작성하러 가기

SUBSCRIBE

최신소식을
편하게 만나보세요.

멀티 모듈 (multi module)

도입

멀티 모듈은 프로젝트를 폴더 몇 개로 나누는 기법이 아니라, 코드와 빌드와 의존성 경계를 여러 서브프로젝트로 모델링해 변경 영향과 복잡도를 통제하는 구조다.

초기에는 모든 코드를 하나의 build.gradle과 하나의 소스 트리에 넣는 편이 단순해 보입니다. 하지만 프로젝트가 커질수록 책임이 다른 코드가 한데 섞이고, 변경 영향 범위도 커지며, 빌드 설정과 의존성 관리 역시 빠르게 복잡해집니다.

이때 멀티 모듈은 코드를 여러 경계로 나누고, 각 모듈이 어떤 역할을 가지는지, 어떤 방향으로만 의존해야 하는지, 어떤 빌드 규칙을 적용받는지를 구조적으로 드러내는 방식이 됩니다.

특히 Gradle에서는 멀티 모듈이 단순 디렉터리 분리가 아니라 root project + subprojects + project dependency + task graph로 구성된 빌드 모델이라는 점이 중요합니다.

필요성

코드베이스가 커질수록 하나의 거대한 모듈보다, 책임과 의존 방향이 분명한 여러 모듈로 나누는 편이 변경 범위와 빌드 운영을 다루기 쉬워진다

멀티 모듈의 첫 번째 장점은 책임 분리입니다. 웹 진입점, 도메인 모델, 인프라 어댑터, 공통 라이브러리처럼 서로 다른 관심사를 분리하면 코드 탐색과 유지보수가 쉬워집니다.

두 번째 장점은 변경 영향 범위가 더 명확해진다는 점입니다. 어떤 모듈이 바뀌었을 때 어디까지 다시 빌드하고 테스트해야 하는지 판단하기 쉬워지고, 팀 단위로 소유권을 나누기도 좋아집니다.

세 번째는 빌드와 의존성 관리 측면입니다. 공통 규칙을 모듈마다 복붙하는 대신 공통 build logic을 분리할 수 있고, 버전과 저장소 정책도 중앙에서 통제하기 쉬워집니다.

물론 모든 프로젝트가 처음부터 멀티 모듈이어야 하는 것은 아닙니다. 중요한 것은 “모듈 수를 늘리는 것”이 아니라, 경계를 명확히 하고 구조를 예측 가능하게 만드는 일입니다.

멀티 모듈이 특히 강한 지점
  • 관심사별 책임을 모듈 단위로 분리하기 쉬움
  • 변경 영향 범위와 의존 방향을 눈에 보이게 만들 수 있음
  • 공통 빌드 로직과 버전 관리를 중앙화하기 좋음
  • 재사용 가능한 라이브러리 모듈을 분리하기 쉬움
  • 코드베이스와 팀 규모가 커져도 구조를 유지하기 좋음

정의

Gradle에서 멀티 모듈은 공식적으로 multi-project build에 가깝고, 하나의 root project 아래 여러 subproject를 settings.gradle(.kts)로 묶어 관리하는 구조다

실무에서는 흔히 “멀티 모듈 프로젝트”라고 부르지만, 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, subproject, settings, project path, 공통 build logic의 역할을 함께 봐야 한다
요소 역할 실무 해석
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 전체라는 점입니다.

기본 구조

실무의 멀티 모듈은 settings 파일로 구조를 고정하고, 각 모듈 build script는 자기 역할만 가지도록 유지하는 편이 가장 안정적이다
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에서 모듈과 저장소를 고정하고, 각 모듈은 자기 역할에 맞는 plugin과 project dependency만 선언하게 만드는 것이다
// 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 같은 계층형 모듈 구성이 가장 먼저 시도하기 좋은 구조다

계층형 분리는 가장 이해하기 쉽고, 많은 팀에서 가장 먼저 도입하는 방식입니다. 진입점은 app, 핵심 도메인 규칙은 domain, 외부 기술 의존은 infra, 여러 모듈에서 공통으로 쓰는 최소 타입은 common으로 나누는 식입니다.

이 구조의 핵심은 단순히 이름이 아니라 의존 방향입니다. 보통 domaininfra를 모르고, infradomain에 의존하는 방향이 더 자연스럽습니다. 그래야 핵심 로직이 외부 기술 변화에 덜 흔들립니다.

:app      -> :domain, :infra:db, :infra:web
:infra:db -> :domain
:infra:web -> :domain
:domain   -> :common
:common   -> (none)

이 패턴은 구조가 비교적 단순하고 설명하기 쉬운 대신, 시간이 지나면 common이나 domain에 너무 많은 것이 몰리기 쉽다는 점은 주의해야 합니다.

패턴 2. 기능형 분리

도메인과 팀 경계가 명확하면 member, order, payment 같은 기능형 모듈 분리가 계층형보다 유지보수에 유리할 수 있다

프로젝트가 커지고 팀별 책임 범위가 분명해질수록, 기술 계층보다 기능 단위로 자르는 편이 더 자연스러울 수 있습니다. 예를 들어 회원, 주문, 결제, 상품처럼 비즈니스 기능 자체를 모듈 경계로 삼는 방식입니다.

이 경우 장점은 소유권과 변경 영향 범위가 기능 단위로 모인다는 점입니다. 특정 기능을 수정할 때 어떤 모듈을 봐야 하는지 분명해지고, 팀 단위 작업도 더 자연스러워집니다.

다만 기능형 분리는 계층형보다 경계 설계가 더 어렵습니다. 각 기능 모듈이 서로 너무 많이 참조하면 오히려 거대한 모놀리스가 모듈 껍데기만 쓴 상태가 되기 쉽습니다.

shop-platform
├── app
├── member
├── order
├── payment
└── shared-kernel
핵심 포인트
계층형이든 기능형이든 중요한 것은 모듈 이름이 아니라 의존 방향입니다. 멀티 모듈은 예쁘게 쪼개 보이는 구조보다, 실제 변경이 어디까지 전파되는지 예측 가능한 구조여야 합니다.

패턴 3. 공통 build logic과 의존성 중앙화

모듈이 늘어날수록 build.gradle 복붙보다 convention plugin, version catalog, platform 같은 공통화 장치가 훨씬 중요해진다

멀티 모듈의 초반에는 각 모듈에 비슷한 설정을 복사해 붙여 넣는 경우가 많습니다. 하지만 모듈 수가 늘어나면 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가 더 맞는 경우가 많다
구분 멀티 모듈 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 모듈이 만능 저장소가 되기 쉬움
  • 루트 프로젝트가 모든 설정을 떠안으면 모듈화 이점이 약해짐
  • 모듈 분리는 설계 경계이면서 빌드 경계이므로 둘 다 함께 설계해야 함

자주 하는 실수

멀티 모듈이 어려워지는 이유는 Gradle 문법보다, 무엇을 경계로 나누고 어떤 방향으로만 의존하게 할지를 정하지 않기 때문이다
  • 패키지 수가 많다는 이유만으로 기준 없이 모듈을 분리함
  • app 모듈이 거의 모든 모듈에 직접 의존하면서 중앙 집결지가 됨
  • 라이브러리 모듈에서 apiimplementation을 구분하지 않아 의존성이 과도하게 전파됨
  • 저장소와 버전 선언을 각 모듈에 흩뿌려 정책이 통일되지 않음
  • 여러 모듈에 같은 build logic을 복붙하고 convention plugin으로 정리하지 않음
  • 멀티 모듈과 multi-repo, composite build를 같은 개념처럼 섞어 생각함
  • common 모듈에 아무 타입이나 넣어 실제 경계가 흐려짐

실무 루틴

새 프로젝트를 멀티 모듈로 시작하거나 기존 프로젝트를 나눌 때는 모듈 개수보다 경계, 의존 방향, 공통 빌드 로직 위치를 먼저 고정하는 편이 훨씬 중요하다
  1. 먼저 기능이나 계층 중 어떤 기준이 더 안정적인지 보고 모듈 경계를 정한다.
  2. 모듈별 허용 의존 방향을 글로라도 먼저 정한다.
  3. 초기에는 3~5개 정도의 의미 있는 모듈부터 시작하고 과도한 세분화는 피한다.
  4. 저장소 정책은 settings.gradle(.kts), 버전 별칭은 libs.versions.toml 또는 platform으로 모은다.
  5. 공통 build logic은 루트 script 복붙 대신 convention plugin 또는 build-logic로 이동한다.
  6. 전체 빌드만 보지 말고 :app:build, :domain:test 같은 모듈 단위 실행 습관을 함께 들인다.

디버깅

멀티 모듈 디버깅은 에러 한 줄만 보는 것이 아니라, 어떤 모듈이 포함됐고 어떤 project path로 연결됐는지부터 확인하는 과정이다
1
먼저 ./gradlew projects로 실제 포함된 모듈과 project path를 확인한다.
2
./gradlew :app:tasks, ./gradlew :domain:build처럼 fully qualified task name으로 모듈 단위 실행 범위를 좁혀 본다.
3
./gradlew :app:dependencies --configuration runtimeClasspath로 실제 의존성 그래프를 확인한다.
4
dependencyInsight로 특정 라이브러리가 왜 들어왔는지, 어떤 버전이 선택됐는지 추적한다.
5
업그레이드 전후에는 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

요약

멀티 모듈의 핵심은 프로젝트를 여러 개로 쪼개는 것이 아니라, settings로 구조를 정의하고 project dependency로 관계를 모델링하며 공통 build logic과 버전 정책을 중앙화해 복잡도를 통제하는 데 있다
  • ✅ 멀티 모듈은 Gradle에서 보통 multi-project build로 설명된다.
  • ✅ 하나의 root project 아래 여러 subproject를 settings.gradle(.kts)로 묶는다.
  • ✅ 모듈 간 연결은 project(":module") 같은 project dependency로 표현한다.
  • ✅ 중요한 것은 모듈 개수보다 의존 방향과 변경 영향 범위를 명확히 하는 일이다.
  • ✅ 계층형 분리와 기능형 분리는 모두 가능하지만, 경계 기준은 일관되어야 한다.
  • ✅ 공통 build logic은 convention plugin 또는 build-logic로 분리하는 편이 유지보수에 유리하다.
  • ✅ 버전과 저장소 정책은 중앙화할수록 멀티 모듈의 운영 난이도가 낮아진다.
  • ✅ 멀티 모듈과 composite build는 비슷해 보여도 목적과 단위가 다르다.

 

 

728x90