Gradle 멀티모듈 프로젝트 구성 방법

Gradle 멀티모듈 프로젝트 구성 방법

대규모 프로젝트를 효율적으로 관리하고 개발 생산성을 높이기 위해, 많은 기업과 개발자들이 Gradle의 멀티모듈 프로젝트 방식을 도입하고 있습니다. 이 글에서는 멀티모듈의 개념부터 구조 설계, 실습 예제, 그리고 실무 적용 전략까지 단계적으로 깊이 있게 다뤄보겠습니다.


📑 목차


1. 서론: 왜 멀티모듈인가?

현대의 소프트웨어 개발은 점점 더 복잡하고 대형화되어 가고 있습니다. 하나의 프로젝트가 수십 개의 도메인, 수백 개의 클래스와 기능으로 구성되는 것은 이제 흔한 일이 되었죠. 이러한 환경에서 모든 코드를 단일 프로젝트에 담는 것은 개발과 유지보수 측면에서 큰 비효율을 초래할 수 있습니다.

바로 이 지점에서 ‘멀티모듈’이라는 구조적 접근이 빛을 발합니다. Gradle의 멀티모듈 시스템은 프로젝트를 기능 단위로 분리하여, 독립적인 빌드 및 테스트가 가능하도록 돕습니다. 이를 통해 개발자 간의 작업 충돌을 줄이고, 변경 사항의 영향 범위를 최소화하며, 재사용성과 빌드 성능을 극대화할 수 있습니다.

예를 들어, ‘공통 유틸 모듈’, ‘도메인 모듈’, ‘웹 어플리케이션 모듈’처럼 기능별로 모듈을 분리해두면, 한 모듈의 수정이 전체 프로젝트의 빌드에 영향을 주지 않게 됩니다. 이는 특히 CI/CD 파이프라인이나 마이크로서비스 아키텍처와 같은 현대 개발 환경에서 큰 장점으로 작용합니다.

이 글에서는 Gradle 멀티모듈의 개념부터 시작해, 실전에서 바로 활용할 수 있는 설정 방법과 구조 설계 전략까지 단계별로 자세히 살펴보겠습니다. 중급 이상의 사용자는 물론이고, 멀티모듈 구성을 처음 접하는 초보자도 쉽게 따라올 수 있도록 구성했으니, 끝까지 함께 해보시기 바랍니다.


2. Gradle 멀티모듈 프로젝트란?

Gradle 멀티모듈 프로젝트는 하나의 루트(최상위) 프로젝트 아래 여러 개의 하위 프로젝트(서브모듈)를 구성하여, 기능 단위로 코드를 분리하고 각각 독립적으로 관리할 수 있도록 하는 프로젝트 구조입니다. 이 방식은 대규모 애플리케이션 개발에서 코드의 재사용성과 유지보수성을 높이는 데 큰 도움이 됩니다.

각 모듈은 고유한 역할을 담당할 수 있으며, 독립적으로 컴파일 및 테스트가 가능합니다. 예를 들어, 다음과 같이 다양한 역할의 모듈을 구성할 수 있습니다:

  • common: 여러 모듈에서 공통으로 사용하는 유틸리티나 DTO 클래스 등을 포함
  • domain: 핵심 비즈니스 로직과 도메인 모델을 정의
  • service: 비즈니스 로직 처리 및 외부 API 호출 담당
  • web: 사용자 요청을 처리하고 UI 또는 API 인터페이스 제공

Gradle은 이들 모듈 간의 의존성과 빌드를 체계적으로 관리할 수 있게 해주며, 특히 다음과 같은 상황에서 유리합니다:

  • 코드 변경이 전체 빌드에 영향을 주지 않도록 하고 싶은 경우
  • 여러 팀이 동시에 다양한 기능을 독립적으로 개발할 때
  • 공통 라이브러리를 여러 서비스에서 재사용할 때
  • CI/CD 파이프라인에서 병렬 빌드나 변경된 모듈만 테스트하고 싶을 때

2-1. 루트 프로젝트와 서브 모듈의 구조

멀티모듈 프로젝트는 루트 디렉토리를 기준으로 아래와 같은 디렉토리 구조를 갖습니다:

my-multi-project/
├── build.gradle
├── settings.gradle
├── common/
│   └── build.gradle
├── service/
│   └── build.gradle
└── web/
    └── build.gradle

위 구조에서 settings.gradle 파일은 어떤 모듈들이 프로젝트에 포함되어 있는지 선언하는 역할을 합니다. 각 모듈은 자신의 build.gradle 파일을 통해 별도로 의존성과 플러그인을 설정할 수 있으며, 루트 build.gradle에서 공통 설정을 관리할 수도 있습니다.

2-2. 단일 모듈 vs 멀티모듈 비교

항목 단일 모듈 프로젝트 멀티모듈 프로젝트
구조 모든 소스 코드가 하나의 모듈에 포함 역할별로 코드가 나뉘어 독립적인 모듈로 구성
빌드 효율성 작은 프로젝트에 유리, 대규모에서는 빌드 시간이 증가 변경된 모듈만 빌드하여 효율성 향상
재사용성 다른 프로젝트에서 코드 재사용이 어려움 모듈 단위로 분리되어 재사용 용이
테스트 구조 테스트가 서로 얽히기 쉬움 모듈 단위로 테스트를 분리하여 독립성 확보

정리하자면, 단일 모듈은 설정이 간단하지만 규모가 커지면 유지관리가 어려워지고, 멀티모듈은 초기 설정이 다소 복잡하지만 장기적인 관점에서 훨씬 효율적인 구조를 제공합니다. 특히 팀 단위 협업이나 서비스 분리 전략이 필요한 경우, 멀티모듈 구조는 더 이상 선택이 아닌 필수가 되고 있습니다.


3. Gradle 멀티모듈 프로젝트의 기본 구조

Gradle 멀티모듈 프로젝트를 효과적으로 구성하려면 디렉토리 구조와 핵심 설정 파일들에 대한 이해가 필수입니다. 이 단락에서는 실제로 많이 사용하는 구조 예시와 함께 settings.gradle, build.gradle의 역할을 명확히 설명하고, Kotlin DSL을 사용하는 경우 유의해야 할 점도 함께 소개합니다.

3-1. 기본 디렉토리 구조

멀티모듈 프로젝트의 전형적인 디렉토리 구조는 아래와 같습니다.

my-multi-project/
├── build.gradle
├── settings.gradle
├── common/
│   ├── build.gradle
│   └── src/main/java/...
├── service/
│   ├── build.gradle
│   └── src/main/java/...
└── web/
    ├── build.gradle
    └── src/main/java/...

각 모듈은 독립된 Gradle 설정 파일을 가지고 있으며, 전체 프로젝트를 통합 관리하기 위해 루트 디렉토리에 settings.gradle과 공통 설정이 들어있는 build.gradle이 존재합니다.

3-2. settings.gradle의 역할

settings.gradle 파일은 루트 프로젝트에서 어떤 서브 모듈들을 포함할 것인지 정의합니다. Gradle은 이 파일을 통해 멀티모듈 프로젝트임을 인식하게 됩니다. 예를 들면 아래와 같이 설정할 수 있습니다.

rootProject.name = 'my-multi-project'

include 'common', 'service', 'web'

이 설정을 통해 Gradle은 common, service, web 디렉토리를 각각 독립된 모듈로 인식하고 빌드 시 함께 처리하게 됩니다.

3-3. 루트 build.gradle의 구성

루트 build.gradle은 전체 프로젝트에 공통적으로 적용될 설정들을 담습니다. 자바 버전, 공통 dependency, repository 설정 등이 여기에 포함되며, 하위 모듈에 자동으로 적용됩니다.

buildscript {
    repositories {
        mavenCentral()
    }
}

allprojects {
    group = 'com.example'
    version = '1.0.0'

    repositories {
        mavenCentral()
    }
}

여기서 allprojects 블록은 모든 모듈에 공통 적용되며, subprojects 블록을 사용하면 하위 모듈에만 적용할 수도 있습니다.

3-4. Kotlin DSL(.kts)을 사용하는 경우

Gradle은 Groovy 기반의 DSL뿐 아니라 Kotlin 기반의 DSL도 지원합니다. Kotlin DSL은 정적 타입 검사와 코드 자동완성 기능을 지원해 현대적인 개발 환경에 더 적합할 수 있습니다. 다만 설정 방식에 약간의 차이가 있으므로 주의가 필요합니다.

Kotlin DSL을 사용하는 경우 설정 파일은 다음과 같이 `.gradle.kts` 확장자를 사용해야 합니다:

  • settings.gradle.kts
  • build.gradle.kts

예시로 settings.gradle.kts는 아래와 같이 작성됩니다.

rootProject.name = "my-multi-project"

include("common", "service", "web")

또한 Kotlin DSL에서는 Groovy DSL에서 사용하는 buildscript 블록 대신, plugins 블록과 dependencyResolutionManagement를 사용하는 것이 일반적입니다.

예를 들어 루트 build.gradle.kts는 다음과 같이 구성할 수 있습니다.

plugins {
    kotlin("jvm") version "1.8.21" apply false
}

allprojects {
    group = "com.example"
    version = "1.0.0"

    repositories {
        mavenCentral()
    }
}

Groovy와 Kotlin DSL 중 어떤 것을 사용할지는 팀의 개발 경험과 프로젝트의 성격에 따라 선택할 수 있으며, 중요한 점은 일관성을 유지하는 것입니다.

이제 기본 구조와 설정 파일의 역할을 파악했으니, 다음 단락에서는 실습을 통해 실제 멀티모듈 프로젝트를 단계적으로 구성해 보겠습니다.


4. 멀티모듈 설정 실습: 간단한 예제 프로젝트 만들기

앞서 멀티모듈 프로젝트의 개념과 구조를 살펴보았다면, 이제는 직접 손으로 만들어보는 시간이 필요합니다. 이 단락에서는 Gradle을 이용해 간단한 멀티모듈 프로젝트를 단계별로 구성해보겠습니다. 특히, 자주 사용되는 공통 모듈과 서비스 모듈, 그리고 웹 모듈의 구성을 통해 실무에서도 바로 활용 가능한 예제를 만드는 것이 목표입니다.

4-1. 프로젝트 구성 목표

우리가 구성할 예제 프로젝트는 다음과 같은 세 가지 모듈로 나뉩니다:

  • common: 공통 유틸리티 및 DTO 등 여러 모듈이 공유하는 코드
  • service: 비즈니스 로직을 처리하는 서비스 모듈
  • web: 사용자 요청을 처리하는 웹 애플리케이션 모듈

4-2. 프로젝트 디렉토리 생성

먼저, 루트 디렉토리를 생성하고 각 모듈 디렉토리를 생성합니다. 터미널 또는 명령어를 사용하여 다음과 같이 구조를 만듭니다.

mkdir my-multi-project
cd my-multi-project
mkdir common service web

4-3. settings.gradle 구성

루트 디렉토리에 settings.gradle 파일을 생성하고, 프로젝트 이름과 하위 모듈을 선언합니다.

rootProject.name = 'my-multi-project'

include 'common', 'service', 'web'

4-4. 루트 build.gradle 작성

모든 모듈에 공통으로 적용될 설정을 build.gradle에 작성합니다.

buildscript {
    repositories {
        mavenCentral()
    }
}

allprojects {
    group = 'com.example'
    version = '1.0.0'

    repositories {
        mavenCentral()
    }
}

4-5. 각 모듈별 build.gradle 설정

각 하위 모듈 디렉토리 안에 build.gradle 파일을 생성하여 해당 모듈에 필요한 설정을 작성합니다.

common/build.gradle

apply plugin: 'java'

dependencies {
    // 공통 의존성 예시
}

service/build.gradle

apply plugin: 'java'

dependencies {
    implementation project(':common')
}

web/build.gradle

apply plugin: 'java'

dependencies {
    implementation project(':service')
    implementation project(':common')
}

위 예제는 가장 기본적인 구조이며, 실제 프로젝트에서는 모듈마다 필요한 라이브러리를 추가로 정의하게 됩니다. 중요한 점은 각 모듈이 필요에 따라 다른 모듈을 project()로 참조함으로써 명확한 의존성을 가진다는 것입니다.

4-6. 모듈별 Java 소스 디렉토리 생성

각 모듈의 내부에 Java 소스 디렉토리를 생성해 실제 코드를 넣을 수 있도록 준비합니다. 예:

mkdir -p common/src/main/java
mkdir -p service/src/main/java
mkdir -p web/src/main/java

이제 기본적인 멀티모듈 프로젝트 구성은 완료되었습니다. IDE(IntelliJ 등)에서 해당 프로젝트를 열면 모듈 간 의존성이 자동으로 인식되며, Gradle 명령을 통해 전체 혹은 특정 모듈만 빌드할 수도 있습니다.

4-7. 빌드 테스트

터미널에서 다음 명령어를 실행해 프로젝트 전체가 정상적으로 빌드되는지 확인합니다.

./gradlew clean build

빌드가 성공했다면, Gradle 멀티모듈 프로젝트의 구성은 올바르게 완료된 것입니다. 이후 각 모듈에 코드를 작성하고 테스트를 추가함으로써 실제 개발 작업을 본격적으로 진행할 수 있습니다.


5. 멀티모듈 간 의존성 설정 방법

멀티모듈 프로젝트의 핵심은 모듈 간의 명확한 의존성 관리에 있습니다. 어떤 모듈이 다른 모듈을 참조하거나 사용하는 경우, 이를 Gradle의 의존성 설정을 통해 명시적으로 연결해야 합니다. 의존성을 적절히 설정하지 않으면 컴파일 오류나 런타임 오류가 발생하게 됩니다.

5-1. project() 함수를 이용한 모듈 의존성 설정

모듈 간 의존성을 설정하는 가장 기본적인 방식은 project() 함수를 사용하는 것입니다. 예를 들어, service 모듈이 common 모듈의 클래스를 사용해야 한다면, build.gradle에서 다음과 같이 선언합니다.

dependencies {
    implementation project(':common')
}

이렇게 설정하면 common 모듈이 먼저 빌드되고, service 모듈에서 해당 코드를 참조할 수 있게 됩니다. 하위 모듈 간에도 동일한 방식으로 의존 관계를 설정할 수 있습니다.

5-2. API vs implementation: 어떤 걸 써야 할까?

Gradle에서는 의존성을 설정할 때 implementation 외에도 api 키워드를 사용할 수 있습니다. 이 둘은 다음과 같은 차이가 있습니다:

구분 implementation api
노출 범위 자신의 모듈 내부에서만 사용 의존한 모듈에서도 사용 가능
컴파일 속도 빠름 (모듈 간 캡슐화) 느림 (모든 모듈에 전파)
추천 상황 대부분의 일반적인 의존성 API를 외부에 노출해야 할 때

예를 들어 common 모듈에서 정의한 DTO나 인터페이스를 service 뿐만 아니라 web에서도 직접 사용해야 한다면, commonbuild.gradle에서 다음과 같이 선언할 수 있습니다:

dependencies {
    api 'com.google.guava:guava:32.1.2-jre'
}

이렇게 하면 해당 라이브러리나 클래스가 이 모듈을 의존하는 다른 모듈에게도 노출됩니다. 반면, 외부에 노출하지 않아도 되는 내부 구현에 대해서는 implementation을 사용하는 것이 성능과 구조 관리 측면에서 유리합니다.

5-3. 의존성 순환(Circular Dependency) 주의

멀티모듈 구조에서 흔히 발생하는 문제 중 하나는 **순환 의존성(circular dependency)** 입니다. 예를 들어, A → B → C → A 와 같은 형태로 모듈 간의 참조가 반복되면 Gradle 빌드 시스템은 에러를 발생시킵니다.

이러한 문제를 방지하기 위해 다음과 같은 전략을 사용할 수 있습니다:

  • 공통 모듈에 너무 많은 책임을 몰아주지 않기
  • 명확한 계층 구조 설계 (하위 모듈 → 상위 모듈 방향만 참조)
  • 인터페이스를 통해 추상화하고, 의존성 주입을 활용

5-4. 의존성 트리 확인하기

모듈 간의 의존성 관계를 명확히 파악하고 싶다면 Gradle의 dependencies 명령어를 사용하여 의존성 트리를 시각적으로 확인할 수 있습니다.

./gradlew :web:dependencies

이 명령어는 web 모듈의 의존성 트리를 출력해 주며, 간접 의존성(transitive dependency)까지 모두 확인할 수 있어 충돌이나 누락 문제를 조기에 발견할 수 있습니다.

이처럼 모듈 간의 의존성 설정은 멀티모듈 프로젝트에서 가장 기본적이면서도 중요한 작업입니다. 체계적이고 명확한 의존성 구조는 빌드 성능을 향상시키고, 유지보수를 쉽게 만들어주는 기반이 됩니다.


6. 멀티모듈에서 공통 라이브러리 및 설정 공유하기

멀티모듈 프로젝트에서 여러 모듈에 중복된 설정이 반복되면 유지보수가 어려워지고, 실수로 버전이 불일치하는 등의 문제가 발생할 수 있습니다. 이런 문제를 해결하기 위해 공통 설정은 별도로 분리하여 일괄 적용하는 것이 일반적인 전략입니다.

이 단락에서는 공통 라이브러리 및 Gradle 설정을 공유하는 방법에 대해 소개합니다. 대표적으로 다음과 같은 항목들을 공유하게 됩니다:

  • 공통 의존성 라이브러리 (예: Lombok, Logback, JUnit 등)
  • Java 버전, Kotlin 버전, 빌드 툴 버전 등 공통 속성
  • 공통 Gradle 플러그인 및 태스크

6-1. subprojects 블록을 통한 공통 설정 적용

가장 일반적인 방식은 루트 build.gradle 파일에서 subprojects 블록을 사용하여 하위 모듈에 공통 설정을 적용하는 것입니다.

subprojects {
    apply plugin: 'java'

    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17

    dependencies {
        implementation 'org.projectlombok:lombok:1.18.30'
        testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
    }

    test {
        useJUnitPlatform()
    }
}

이렇게 하면 별도로 설정하지 않아도 모든 하위 모듈에서 동일한 Java 버전과 테스트 플랫폼, 공통 라이브러리가 적용됩니다. 실무에서 자주 사용하는 방식이며, 설정 누락을 방지할 수 있는 장점이 있습니다.

6-2. buildSrc 디렉토리 활용

공통 의존성과 설정을 더 정교하게 관리하고 싶다면 Gradle의 buildSrc 디렉토리를 활용하는 것이 좋습니다. buildSrc는 Gradle 빌드 스크립트에서 재사용할 수 있는 코드를 정의하는 내부 프로젝트이며, 자동으로 컴파일되고 인식됩니다.

예를 들어, 공통 버전 정보를 상수로 정의하고 싶다면 다음과 같이 작성할 수 있습니다.

  1. 루트 디렉토리에 buildSrc 디렉토리 생성
  2. buildSrc/src/main/java/Dependencies.java 파일 생성
public class Dependencies {
    public static final String LOMBOK = "org.projectlombok:lombok:1.18.30";
    public static final String JUNIT = "org.junit.jupiter:junit-jupiter:5.10.0";
}

이후 하위 모듈에서 다음과 같이 사용할 수 있습니다:

dependencies {
    implementation Dependencies.LOMBOK
    testImplementation Dependencies.JUNIT
}

이 방법은 프로젝트 전체에 걸쳐 버전 정보를 한 곳에서 관리할 수 있어 유지보수가 매우 쉬워집니다. 또한 모듈 수가 많고 팀 규모가 클수록 강력한 이점을 가집니다.

6-3. Gradle Version Catalog 사용 (settings.gradle.kts)

Gradle 7.0 이상에서는 Version Catalog 기능을 활용할 수 있습니다. 이는 libs.versions.toml 파일을 통해 라이브러리와 버전 정보를 외부에서 선언하고, 전체 모듈에 일관되게 적용하는 방식입니다.

1. gradle/libs.versions.toml 파일 생성 후 다음과 같이 작성합니다.

[versions]
junit = "5.10.0"
lombok = "1.18.30"

[libraries]
junit = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
lombok = { module = "org.projectlombok:lombok", version.ref = "lombok" }

2. 하위 모듈에서는 다음과 같이 사용할 수 있습니다.

dependencies {
    implementation(libs.lombok)
    testImplementation(libs.junit)
}

이 방식은 특히 Kotlin DSL과 함께 사용할 때 빛을 발하며, 점점 더 많은 프로젝트에서 Version Catalog를 채택하고 있습니다.

6-4. 공통 Gradle 설정 파일 분리하기

공통 설정이 많아질 경우, 이를 별도의 Gradle 파일로 분리하고 apply from 구문을 통해 불러오는 방식도 있습니다.

  1. gradle 디렉토리 생성 후 common.gradle 작성
  2. 하위 모듈의 build.gradleapply from 사용
// web/build.gradle
apply from: "$rootDir/gradle/common.gradle"

이러한 방식은 설정 파일을 모듈화함으로써 가독성과 재사용성을 높여주며, 복잡한 프로젝트에 유용합니다.

공통 설정을 잘 정리해두면 모듈 추가나 환경 변경 시 훨씬 유연하게 대처할 수 있습니다. 또한, 코드 일관성을 유지하고 오류 발생 가능성을 줄이는 데에도 큰 도움이 됩니다.


7. 멀티모듈과 빌드 최적화 전략

멀티모듈 프로젝트의 구조가 커질수록 빌드 시간이 길어지고, 전체 생산성에 영향을 줄 수 있습니다. 특히 여러 명의 개발자가 동시에 작업하거나 CI/CD 파이프라인에서 반복적으로 빌드가 실행되는 경우, 빌드 최적화는 선택이 아닌 필수가 됩니다.

Gradle은 다양한 빌드 최적화 기능을 제공하며, 이를 적극 활용하면 시간과 자원을 크게 절약할 수 있습니다. 이 단락에서는 실무에서 즉시 적용 가능한 빌드 성능 개선 전략들을 소개합니다.

7-1. Configuration on Demand

Configuration on Demand는 필요한 모듈만 설정(configuration)하고, 사용하지 않는 모듈은 설정을 생략하는 기능입니다. 기본적으로 Gradle은 모든 모듈을 설정하기 때문에 프로젝트 규모가 커질수록 시간이 지연되는데, 이 옵션을 통해 개선할 수 있습니다.

Gradle 7 이후로는 이 기능이 비활성화되었지만, 과거 버전에서는 아래처럼 사용 가능했습니다:

./gradlew build --configure-on-demand

하지만 Gradle 8부터는 Configuration Cache 기능이 이 역할을 대체하며, 더욱 강력한 최적화를 제공합니다.

7-2. Configuration Cache 사용

Configuration Cache는 Gradle이 설정 단계에서 생성한 데이터를 캐시에 저장하고, 다음 빌드 시 이를 재사용하여 빌드 속도를 대폭 향상시키는 기능입니다.

활성화 방법은 아래와 같습니다:

./gradlew build --configuration-cache

이 기능을 사용하려면, 플러그인이나 커스텀 태스크들이 Configuration Cache 호환성을 갖추고 있어야 합니다. 호환성 여부는 아래 명령어로 확인할 수 있습니다:

./gradlew help --configuration-cache

7-3. 병렬 빌드 활성화

Gradle은 기본적으로 모듈을 순차적으로 빌드합니다. 그러나 서로 의존성이 없는 모듈은 병렬로 빌드할 수 있으며, 이를 통해 빌드 속도를 크게 줄일 수 있습니다.

병렬 빌드는 명령줄 옵션으로 활성화할 수 있습니다:

./gradlew build --parallel

또는 gradle.properties 파일에 아래와 같이 설정할 수 있습니다:

org.gradle.parallel=true

병렬 빌드를 사용할 때는 각 모듈 간의 의존성 관계를 명확하게 정리해두어야 하며, 공통 자원을 공유하지 않도록 주의해야 합니다.

7-4. Gradle Build Cache 사용

Gradle Build Cache는 이전 빌드 결과를 캐싱하여 동일한 작업이 반복되지 않도록 하는 기능입니다. 로컬 캐시뿐 아니라 원격 캐시 서버와 연동하여 팀 단위 최적화도 가능합니다.

gradle.properties 파일에서 설정할 수 있습니다:

org.gradle.caching=true

Gradle 캐시는 다음과 같은 작업 결과를 저장합니다:

  • Java 컴파일 결과
  • 테스트 실행 결과
  • 리소스 처리 결과 등

CI 환경에서는 원격 캐시를 사용해 여러 빌드 서버 간에 캐시를 공유하면 훨씬 더 큰 성능 향상을 기대할 수 있습니다.

7-5. 의존성 해석 캐시 (Dependency Resolution Cache)

Gradle은 기본적으로 외부 라이브러리의 버전을 해석할 때, 이를 매번 확인합니다. 하지만 자주 변경되지 않는 라이브러리에 대해 캐시를 활성화하면, 무의미한 네트워크 지연을 줄일 수 있습니다.

이 설정은 settings.gradle 또는 init.gradle에서 다음과 같이 지정할 수 있습니다:

dependencyResolutionManagement {
    repositories {
        mavenCentral()
    }

    versionCatalogs {
        create("libs") {
            // 라이브러리 정의 생략
        }
    }

    resolutionStrategy {
        cacheChangingModulesFor 0, 'seconds'
        cacheDynamicVersionsFor 10, 'minutes'
    }
}

7-6. 테스트 병렬화

JVM 기반의 멀티모듈에서는 테스트 수행이 많은 시간을 차지하는 경우가 많습니다. 이를 최적화하기 위해 테스트 병렬화를 설정할 수 있습니다.

build.gradle 파일에 다음과 같이 테스트 설정을 추가합니다:

test {
    maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
}

이렇게 하면 CPU 코어 수의 절반을 활용해 테스트를 병렬로 실행할 수 있습니다. 성능을 위해 최적의 값은 환경에 따라 조정이 필요합니다.

정리하자면, 멀티모듈 프로젝트에서는 병렬화 + 캐시 + 설정 재사용이라는 세 가지 축을 중심으로 빌드 성능을 최적화할 수 있습니다. 이 전략들을 적절히 조합하면 대형 프로젝트에서도 빠르고 안정적인 빌드 환경을 구축할 수 있습니다.


8. 멀티모듈과 테스트 전략

멀티모듈 프로젝트에서 테스트를 효과적으로 운영하기 위해서는 모듈 간의 책임 분리뿐 아니라, 테스트 구조 또한 전략적으로 설계할 필요가 있습니다. 각각의 모듈은 독립적으로 테스트가 가능해야 하며, 변경 사항에 따른 테스트 영향 범위를 명확히 파악할 수 있어야 합니다.

이 단락에서는 모듈 단위 테스트부터 통합 테스트 전략, 테스트 분리, 그리고 테스트 커버리지 관리까지 다각도로 살펴보겠습니다.

8-1. 모듈 단위의 테스트 실행

Gradle에서는 특정 모듈만 개별적으로 테스트할 수 있습니다. 예를 들어 service 모듈만 테스트하고 싶다면 다음과 같이 명령어를 실행합니다.

./gradlew :service:test

이는 전체 프로젝트를 테스트하는 것보다 훨씬 빠르며, 변경된 부분만 빠르게 검증할 수 있습니다. CI 환경에서도 변경된 모듈만 선택적으로 테스트하는 방식이 많이 사용됩니다.

8-2. 유닛 테스트 vs 통합 테스트 분리

멀티모듈 프로젝트에서는 테스트의 성격을 분리하여 운영하는 것이 매우 중요합니다. 일반적으로 다음과 같이 두 가지로 나눕니다:

구분 유닛 테스트 통합 테스트
테스트 대상 단일 클래스, 메서드 단위 여러 모듈 및 계층 간의 상호작용
의존성 Mock, Stub 등으로 격리 실제 데이터베이스 또는 외부 시스템
속도 빠름 느림

통상적으로 src/test/java에는 유닛 테스트를, src/integrationTest/java와 같이 별도의 소스셋을 정의하여 통합 테스트를 분리 운영하는 방식이 추천됩니다.

8-3. 소스셋을 이용한 테스트 분리

Gradle에서는 통합 테스트를 위해 별도의 소스셋을 추가할 수 있습니다. 예를 들어 service 모듈에 통합 테스트용 디렉토리를 추가하려면 build.gradle에 다음과 같이 설정합니다.

sourceSets {
    integrationTest {
        java {
            compileClasspath += main.output + test.output
            runtimeClasspath += main.output + test.output
            srcDir file('src/integrationTest/java')
        }
        resources.srcDir file('src/integrationTest/resources')
    }
}

configurations {
    integrationTestImplementation.extendsFrom testImplementation
    integrationTestRuntimeOnly.extendsFrom testRuntimeOnly
}

task integrationTest(type: Test) {
    description = 'Runs integration tests.'
    group = 'verification'
    testClassesDirs = sourceSets.integrationTest.output.classesDirs
    classpath = sourceSets.integrationTest.runtimeClasspath
    shouldRunAfter test
}

이제 다음과 같은 명령으로 통합 테스트만 실행할 수 있습니다.

./gradlew integrationTest

유닛 테스트와 통합 테스트를 구분하면 테스트 전략을 더 유연하게 설계할 수 있으며, 빌드 시간 단축과 디버깅 효율성 향상에도 도움이 됩니다.

8-4. 테스트 격리와 테스트 전용 모듈

경우에 따라서는 통합 테스트 코드를 전용 모듈로 분리하는 전략도 유용합니다. 예를 들어, integration-test라는 별도의 모듈을 만들어 전체 서비스나 시스템 통합 테스트만 전담하도록 구성할 수 있습니다.

이렇게 하면 테스트 대상이 되는 모듈들만 의존성으로 추가하고, 실제 서비스 코드와 테스트 코드를 물리적으로도 분리할 수 있어 유지보수가 수월해집니다.

8-5. 테스트 커버리지와 리포트 통합

멀티모듈 프로젝트에서는 각 모듈의 테스트 커버리지를 별도로 산출할 수 있지만, 전체 커버리지를 통합하여 리포트를 생성하면 더욱 유용한 인사이트를 얻을 수 있습니다.

이를 위해 Jacoco 플러그인을 적용하고, 통합 커버리지 리포트를 생성하는 태스크를 루트 프로젝트에 추가할 수 있습니다. 다음은 예시입니다:

subprojects {
    apply plugin: 'jacoco'

    jacoco {
        toolVersion = '0.8.10'
    }

    test {
        finalizedBy jacocoTestReport
    }

    jacocoTestReport {
        dependsOn test
        reports {
            xml.required = true
            html.required = true
        }
    }
}

또한, 모든 모듈의 커버리지 데이터를 합산한 통합 리포트를 만들기 위해 custom 태스크를 루트 프로젝트에 정의할 수도 있습니다.

이와 같은 전략을 통해 멀티모듈 환경에서도 테스트 품질을 체계적으로 유지하고, 신뢰할 수 있는 검증 체계를 만들 수 있습니다.


9. 멀티모듈과 CI/CD 환경 구성

멀티모듈 프로젝트는 그 구조의 복잡성만큼 CI/CD 구성 시 고려할 사항도 많아집니다. 단일 모듈 프로젝트에서는 전체를 빌드하고 배포하면 되었지만, 멀티모듈에서는 변경된 모듈만 테스트하거나 배포하는 전략, 모듈 간 의존성에 따른 빌드 순서 관리 등 추가적인 설계가 필요합니다.

이 단락에서는 GitLab CI, GitHub Actions 등에서 멀티모듈 프로젝트를 효율적으로 다루기 위한 전략을 설명하고, 모듈 단위 빌드 트리거와 테스트 커버리지 통합 등의 실용적인 팁도 함께 소개합니다.

9-1. CI/CD 구성 시 고려사항

멀티모듈 프로젝트를 위한 CI/CD 파이프라인을 설계할 때는 다음과 같은 요소를 고려해야 합니다:

  • 어떤 모듈이 변경되었는지 자동으로 감지할 수 있어야 함
  • 변경된 모듈만 선택적으로 빌드하고 테스트할 수 있어야 함
  • 공통 모듈의 변경이 다른 모듈에 어떤 영향을 주는지 분석할 수 있어야 함
  • 각 모듈별 빌드/테스트 결과를 시각화하거나 통합할 수 있어야 함

9-2. GitLab CI 예제: 변경 모듈만 빌드하기

GitLab에서는 rules를 이용하여 특정 경로가 변경되었을 때만 Job을 실행하도록 설정할 수 있습니다.

예를 들어, common 모듈이 변경되었을 때만 빌드하는 Job은 다음과 같이 정의할 수 있습니다:

build-common:
  stage: build
  script:
    - ./gradlew :common:build
  rules:
    - changes:
        - common/**/*

각 모듈별로 유사한 Job을 만들어 구성하면, 변경된 모듈만 선택적으로 빌드할 수 있어 전체 파이프라인의 속도와 효율성이 크게 향상됩니다.

9-3. GitHub Actions 예제: 디렉토리 변경 감지

GitHub Actions에서는 paths를 활용하여 특정 디렉토리의 변경 여부에 따라 워크플로우를 조건적으로 실행할 수 있습니다.

on:
  push:
    paths:
      - 'web/**'
      - 'common/**'

jobs:
  build-web:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Build Web Module
        run: ./gradlew :web:build

이와 같이 모듈 단위로 테스트와 빌드를 분리하여 설정하면, 변경 범위가 명확하게 구분되고 파이프라인의 실행 시간도 최적화할 수 있습니다.

9-4. 테스트 커버리지 통합 리포트 생성

멀티모듈 프로젝트에서는 각 모듈의 Jacoco 리포트를 단일 HTML 리포트로 통합하여 시각화할 수 있습니다. 이 경우 루트 build.gradle 또는 별도 Gradle 태스크를 만들어 다음과 같이 설정합니다.

task jacocoRootReport(type: JacocoReport) {
    dependsOn = subprojects.test

    subprojects.each { subproject ->
        executionData fileTree(dir: "${subproject.buildDir}/jacoco", include: "*.exec")
        sourceDirectories.from files("${subproject.projectDir}/src/main/java")
        classDirectories.from files("${subproject.buildDir}/classes/java/main")
    }

    reports {
        xml.enabled true
        html.enabled true
    }
}

이렇게 하면 CI 파이프라인에서 실행 후, 하나의 HTML 리포트로 전체 커버리지를 한눈에 파악할 수 있습니다. 팀 코드 품질 관리에 매우 유용한 방법입니다.

9-5. 배포 전략: 모듈별 독립 배포

특정 모듈이 라이브러리나 마이크로서비스 역할을 한다면, 해당 모듈만 독립적으로 배포하는 전략이 필요합니다. Gradle의 publishing 기능을 사용하면 개별 모듈을 Maven 리포지토리나 사내 Nexus 등에 배포할 수 있습니다.

publishing {
    publications {
        mavenJava(MavenPublication) {
            from components.java
        }
    }

    repositories {
        maven {
            url = uri("https://nexus.example.com/repository/maven-releases/")
        }
    }
}

CI/CD 파이프라인에서 각 모듈을 별도로 배포하도록 구성하면, 변경 사항이 없는 모듈까지 불필요하게 배포하지 않아도 되므로 효율적입니다.

결론적으로, 멀티모듈 환경에서는 빌드와 테스트의 선택적 실행, 결과 통합, 독립 배포가 핵심입니다. 이러한 전략은 프로젝트 규모가 커질수록 빛을 발하며, CI/CD 효율성을 극대화하는 기반이 됩니다.


10. 실무에서 자주 겪는 문제와 해결법

Gradle 멀티모듈 프로젝트는 많은 이점을 제공하지만, 실무에서 사용하다 보면 다양한 문제에 직면하게 됩니다. 특히 처음 도입하거나 규모가 커질수록 사소한 설정 오류가 전체 빌드 실패로 이어지기도 합니다.

이 단락에서는 실제로 많이 발생하는 문제 유형과 그 해결 방법을 정리하여, 개발 중 불필요한 시행착오를 줄이고자 합니다.

10-1. 모듈이 인식되지 않는 문제

문제: 특정 모듈이 Gradle 또는 IDE에서 인식되지 않아 빌드가 실패하거나 코드 참조가 안 되는 현상

해결 방법:

  • settings.gradle 파일에 include 지시문이 누락되었는지 확인
  • 모듈 디렉토리 내부에 build.gradle 또는 build.gradle.kts 파일이 존재하는지 확인
  • IntelliJ에서 “File > Invalidate Caches / Restart” 후 Gradle 재동기화
  • ./gradlew clean./gradlew build로 다시 초기화

10-2. 의존성 충돌 (Dependency Conflict)

문제: 여러 모듈 또는 라이브러리 간에 같은 의존성의 서로 다른 버전이 충돌하여 컴파일 또는 런타임 오류 발생

해결 방법:

  • Gradle의 dependencies 태스크로 의존성 트리 확인
  • ./gradlew :web:dependencies
  • 버전 충돌 시 강제로 버전을 고정 (resolutionStrategy 사용)
  • configurations.all {
        resolutionStrategy {
            force 'com.google.guava:guava:32.1.2-jre'
        }
    }
  • 가능하면 BOM(Bill of Materials)을 사용해 의존성 버전을 통일

10-3. 순환 참조(Circular Dependency)

문제: 모듈 간의 의존성이 순환되어 빌드 오류 발생

해결 방법:

  • 공통 코드를 common과 같은 별도 모듈로 추출하여 중간 계층으로 분리
  • 모듈 간의 참조 방향을 “하위 → 상위”로만 유지
  • 의존성 주입을 통해 의존 관계를 역전시키는 전략(DI, 인터페이스 분리)

10-4. 테스트 실패 및 클래스 로드 문제

문제: 테스트 중 클래스 로드 실패 또는 NoClassDefFoundError 등 발생

해결 방법:

  • 테스트 소스셋에 필요한 의존성이 빠졌는지 확인 (testImplementation 누락)
  • 통합 테스트와 유닛 테스트 구분 여부 점검
  • 클래스패스가 올바르게 구성되었는지 Gradle 로그 확인

10-5. IntelliJ에서 프로젝트 구조가 깨지는 현상

문제: Gradle 프로젝트가 IntelliJ에서 모듈로 인식되지 않거나 구조가 깨지는 문제

해결 방법:

  • “File > Project Structure > Modules”에서 각 모듈이 등록되어 있는지 확인
  • “File > Invalidate Caches / Restart” 실행 후, Gradle Sync 수행
  • .idea, .gradle, *.iml 삭제 후 새로 Import
  • 가능하면 Gradle Wrapper 사용 (로컬 설치된 Gradle 대신)

이 외에도 Java 버전 불일치, 플러그인 비호환, DSL 문법 오류 등 다양한 문제가 발생할 수 있습니다. 중요한 것은 각 모듈의 독립성과 책임을 유지하면서, 의존성과 설정을 명확하게 분리하는 것입니다.

Gradle 로그는 대부분의 힌트를 제공하므로, 문제가 발생했을 때는 먼저 --info 또는 --stacktrace 옵션을 활용해 자세한 오류를 확인해 보시기 바랍니다.


11. 결론: 멀티모듈 프로젝트로 한 단계 더 나아가기

Gradle 멀티모듈 프로젝트는 단순한 디렉토리 분할을 넘어, 대규모 시스템의 개발, 유지보수, 테스트, 배포 전반에 걸쳐 효율성을 극대화할 수 있는 강력한 설계 전략입니다. 제대로 설계된 멀티모듈 구조는 코드 품질을 향상시키고, 팀 간 협업을 원활하게 만들며, 빌드 성능과 배포 유연성까지 확보할 수 있게 해줍니다.

이번 글을 통해 다음과 같은 핵심 내용을 단계별로 살펴보았습니다:

  • 멀티모듈 프로젝트의 개념과 구조
  • 실습 기반의 Gradle 설정 방법
  • 모듈 간 의존성 관리 전략
  • 공통 설정 공유 및 빌드 최적화 기법
  • 테스트 분리 전략과 커버리지 통합
  • CI/CD에 멀티모듈을 효과적으로 연동하는 방법
  • 실무에서 마주치는 문제들과 그 해결책

이제 여러분은 단일 모듈 프로젝트를 넘어서, 보다 체계적이고 확장 가능한 멀티모듈 구조를 직접 설계하고 운영할 수 있는 기반을 갖추셨습니다. 남은 것은 이 지식을 실제 프로젝트에 적용해보는 일입니다.

시작은 복잡해 보일 수 있지만, 일단 패턴을 익히고 나면 멀티모듈은 개발자의 가장 강력한 무기가 될 수 있습니다. 작은 모듈 하나하나가 모여 커다란 시스템을 움직이는 것처럼, 여러분의 프로젝트 또한 한층 더 단단하고 유연한 구조로 성장하길 바랍니다.

좋은 구조는 좋은 개발 문화를 만듭니다. 그 시작이 바로 멀티모듈입니다.

댓글 남기기

Table of Contents

Table of Contents