복잡한 프로젝트, Gradle로 일관되게 관리하는 멀티 모듈 설계법

복잡한 프로젝트, Gradle로 일관되게 관리하는 멀티 모듈 설계법

멀티 모듈은 왜 필요한가?

멀티 모듈 프로젝트란, 하나의 큰 프로젝트를 여러개의 작은 모듈로 나누어 관리하는 방식입니다.

멀티 모듈을 택할 경우 다음의 이점을 가져갈 수 있습니다.

  • 유지보수성의 향상
  • 명확한 의존성 관리
  • 코드의 재사용성 증가

이 외에도 다양한 이점이 존재하기에 프로젝트 진행시에 멀티 모듈 프로젝트를 택합니다.

이번 글에서는 모듈은 무엇이고, 보다 효율적으로 모듈의 의존성들을 관리하는 방법을 알아보고자 합니다.

모듈이란?

소프트웨어 개발에서 모듈이란, 독립적인 기능 또는 역할을 수행하는 하나의 논리적 단위를 의미합니다.
모듈은 시스템을 구성하는 작은 단위로서, 다음과 같은 특성을 지닙니다.

  • 독립성 :

모듈은 독립적으로 개발, 테스트, 배포가 가능합니다.

  • 명확한 책임 :

각 모듈은 명확한 역할과 책임을 지니기에, 코드의 변경이 시스템에 미치는 영향을 최소화 할 수 있습니다.

  • 재사용성 :

잘 정의된 모듈은 동일한 기능이 다양한 곳에서 반복적으로 활용될 수 있어, 개발 생산성과 유지보수성이 향상됩니다.

모듈의 이러한 특성 덕분에, 소프트웨어 시스템은 더욱 유연하고 확장 가능하도록 설계할 수 있습니다.
따라서 프로젝트의 규모나 복잡성에 따라 단일 프로젝트 혹은 멀티 모듈 프로젝트 구조를 선택하게 됩니다.

단일 프로젝트 VS 멀티 모듈 프로젝트

소프트웨어 프로젝트를 설계할 때, 전체 기능을 하나의 프로젝트로 관리할지, 여러 모듈로 나누어 관리할지를 결정해야 합니다.
각 방식은 다음과 같은 특성을 지닙니다.

구분 단일 프로젝트 멀티 모듈 프로젝트
모듈 구조 하나의 모듈 내 모든 기능 관리 도메인/기능별로 모듈화
빌드와 배포 전체 빌드로 시간 증가 개별 모듈 빌드로 시간 절약
코드 재사용성 제한적 높음
의존성 관리 규모 증가시 어려움 명확하고 체계적인 관리
적합한 프로젝트 소규모, 간단한 애플리케이션 중/대규모, 마이크로서비스 기반

프로젝트 규모가 커지고 복잡성이 증가함에 따라, 멀티 모듈 프로젝트로 선택하는 경우가 많습니다.
Gradle은 이러한 멀티 모듈 구조를 효과적으로 지원하기 위해 다양한 빌드 방식을 제공합니다.

Multi-Project Build Basics

Gradle 공식 문서에 따르면, Gradle은 Multi-project Build 방식을 통해 대규모 프로젝트를 효과적으로 관리할 수 있도록 지원합니다.

Gradle Build Flow

소규모 개발이나 코드가 단순한 경우에는 Monolithic 방식으로 진행할 수 있지만, 프로젝트 규모가 커질수록 작은 단위의 모듈로 분리하는 것이 일반적입니다.

Gradle은 여러 하위 모듈을 한 번의 빌드 과정에서 함께 연결하여 빌드할 수 있도록 지원합니다.

Gradle에서는 멀티 프로젝트 빌드 구조에 대해 두가지 표준을 제시합니다.
바로 buildSrc를 이용한 방식과 Composite Builds 방식입니다.

Multi-Project Builds using buildSrc

Gradle에서 buildSrc 디렉터리는 특별한 의미를 갖는 디렉터리입니다.
Gradle은 이 디렉터리를 자동으로 인식해, 별도의 설정 없이도 하위 프로젝트에서 공통 코드를 사용할 수 있도록 지원합니다.

buildSrc 내부에 build.gradle.kts 파일이 존재하면, 해당 디렉터리는 하나의 독립된 빌드처럼 취급되어, 컴파일된 후 전체 빌드에 자동으로 포함됩니다.
덕분에 하위 프로젝트 간에 중복되는 빌드 스크립트나 버전 관리 로직을 중앙에서 관리할 수 있습니다.

보통 다음과 같은 경우 buildSrc를 사용합니다.

  • 하위 모듈 간에 공통으로 사용하는 Gradle Task, Plugin 등을 관리할 때
  • 간단한 Convention Plugin을 빠르게 적용하고자 할 때

Composite Builds

Composite Build 방식은 서로 독립적으로 관리되는 빌드 간에 로직이나 모듈을 공유하는데 사용하는 방법입니다.
settings.gradle.kts 파일에 includeBuild 명령어를 사용해 외부 빌드를 가져와 연결할 수 있으며, 연결된 빌드는 별도로도 독립적인 빌드가 가능합니다.

보통 다음과 같은 경우 Composite Build를 사용합니다.

  • 별도의 라이브러리 프로젝트를 외부 Maven 저장소에 배포하지 않고, 소스 코드를 직접 연결해서 개발하고자 하는 경우
  • 여러 서비스나 모듈을 독립적으로 관리하면서도 필요한 경우 통합 테스트를 진행하고자 할 때

멀티 모듈 관리하는 방법

멀티 모듈 프로젝트를 관리하는 방식은 다양합니다.
이번 섹션에서는 제가 실제로 사용하고 있는 방식인
buildSrc + gradle-multi-project-support plugin 조합을 소개드리고자 합니다.

gradle-multi-project-support 플러그인

LINE에서 개발한 이 플러그인은 공통 설정을 별도 모듈에 캡슐화하고,
각 하위 프로젝트에서는 해당 플러그인을 적용하기만 하면 공통 규칙이 자동으로 반영되도록 설계되어 있습니다.

자세한 내용은 아래 링크에서 확인하실 수 있습니다.
Line 공식 블로그 바로가기

아래와 같은 순서로 멀티 모듈 환경을 구성하는 방법을 소개하겠습니다.

글의 편의를 위해 프로젝트 최상단에 위치한 디렉토리는 root project,
하위 모듈은 sub project라고 지칭하겠습니다.
  1. buildSrc 설정
  2. settings.gradle.kts (root project)
  3. gradle.properties (sub project)
  4. build.gradle.kts (root project)
  5. build.gradle.kts (sub project)

아래 이미지에서 보이는 것처럼 java-module, kotlin-module 두가지 모듈을 기준으로 설명드리겠습니다.

buildSrc 설정

buildSrc에는 프로젝트에서 공통으로 사용하는 버전 상수들을 정의합니다.
Gradle은 buildSrc 내부 클래스들을 자동으로 인식해 build.gradle.kts에서 사용할 수 있으므로, 라이브러리 버전 관리를 중앙 집중화할 수 있습니다.

object Versions {
    const val kotlin = "1.9.25"
    const val springBoot = "3.4.4"
    const val springDependencyMgmt = "1.1.7"

    const val recipePlugin = "1.1.1"
}

root project settings.gradle.kts

루트 프로젝트에서는 하위 모듈들을 multi-project 빌드 대상으로 등록해주어야 합니다.

rootProject.name = "multi-module"

include(
    "java-module",
    "kotlin-module"
)
sub project gradle.properties

각 하위 프로젝트는 자신의 label을 gradle.properties 파일에 명시할 수 있습니다.
line에서 제공하는 플러그인은 이 label 값을 기준으로 어떤 설정을 적용할 지 판단합니다.

Kotlin-module의 gradle.properties

label=kotlin,library

Java-module의 gradle.properties

label=java

root project build.gradle.kts

루트 프로젝트의 build.gradle.kts에서는 다음과 같은 작업들을 수행합니다.

  1. 공통 플러그인 선언
  2. 하위 모듈 설정 적용
  3. label기반의 모듈 빌드 전략 정의

plugin { } 영역에서는 전체 프로젝트에서 사용할 플러그인을 선언하되, 루트프로젝트에서 직접 적용하지 않을 경우 apply false를 지정합니다.

subproject { } 블록을 통해 공통적인 설정을 지정합니다.

configureByLabels() 블록에서는 gradle.properties에 명시된 label값을 기준으로 설정을 분기합니다. 이 덕분에 label만으로 하위 모듈의 빌드 전략을 유연하게 관리할 수 있습니다.

만약 하위모듈에서 여러개의 label이 명시되어 있다면, 각 설정들을 덮어쓰기 하는 방식으로 빌드 전략을 구사할 수 있습니다.

sub project build.gradle.kts

각 하위 모듈에서는 해당 모듈에 필요한 의존성만 명시하면 됩니다.

예를 들어, 각 모듈에 필요한 의존성이 다음과 같다고 한다면

  • kotlin-module : spring-boot-starter
  • java-module : spring-boot-starter. spring-boot-web

각 모듈에서는 다음과 같이 설정할 수 있습니다.

kotlin-module의 build.gradle.kts

dependencies {
    implementation("org.springframework.boot:spring-boot-starter")
}

java-module의 build.gradle.kts

dependencies {
    implementation("org.springframework.boot:spring-boot-starter")
    implementation("org.springframework.boot:spring-boot-starter-web")
}

그 결과 다음과 같이 의존성들이 주입 된 것을 확인 할 수 있습니다.

java module

kotlin module

해당 글에서 사용한 예제 프로젝트는 github에서 살펴볼 수 있습니다.

Github 바로가기


Reference
  1. Gradle : Multi-Project Build Basics
  2. linecorp : Mono-repo, Multi-project를 Gradle 플러그인으로 손쉽게 관리하기
-->