November 19, 2023

Gradle 8.4 멀티 프로젝트

멀티 프로젝트 구성할 때 필요한 내용들 몇 가지를 정리해 보았습니다.





멀티 프로젝트? 멀티 모듈 프로젝트?

Gradle에서는 멀티 프로젝트라고 부르고, IntelliJ 에서는 멀티 모듈 프로젝트라고 부릅니다. 뉘앙스를 따지자면 약간의 차이가 있겠으나, 상위 프로젝트가 하위 프로젝트 여럿을 포함하는 형태를 뜻하는 것은 같습니다. Gradle 가이드에서는 Multi-project 라고 소개하고 있으므로, 여기서도 그렇게 칭하도록 하겠습니다.


서브 프로젝트 네이밍

  1. 프로젝트 이름을 디렉토리 이름으로 쓰시라.

    이게 무슨 말인가 하면, 예를 들어 my-app 이라는 루트 프로젝트에 foo 라는 이름의 서브 프로젝트가 있는데, 굳이 foo 프로젝트의 디렉토리 이름을 my-foo 등으로 바꾸지 말라는 말인 듯 싶습니다. 물론 이름 중복 등을 피하기 위해서 디렉토리명을 바꾸는 것은 어쩔 수 없지만, 특별한 이유가 없다면 프로젝트 이름을 디렉토리 이름으로 하라는 얘기입니다.

  2. 프로젝트명은 kebab-case를 사용하라.

  3. 루트 프로젝트 이름을 settings file 에 정의하라.

    rootProject.name 에 루트 프로젝트 이름을 적도록 합니다. 그렇지 않으면 문제가 생길 수 있다고 합니다.


Groovy DSL vs. Kotlin DSL

스크립트에 무슨 언어를 쓰라고는 매뉴얼에서 찾지 못했습니다.

어떤 언어를 쓸 지 고민이라면 IntelliJ를 쓸 수 있는 환경에서는 Kotlin DSL을, 그렇지 않다면 Groovy DSL을 사용하길 추천드립니다. Kotlin DSL은 Groovy DSL에 비해 빌드 속도가 조금 느리지만, IntelliJ에서 Typing 관련한 지원이 Groovy보다 좋습니다. Groovy는 동적 타이핑이라 IDE에서의 지원이 한계가 있습니다. 하지만 VSCode 등 그외의 환경에서는 Kotlin에 대한 지원이 좋지 않아 많이 불편합니다. 이런 것을 보면 Kotlin은 유료라고 봐야할 것 같기도 합니다.

Cross Project Configuration

루트 프로젝트의 빌드 스크립트에서 allprojects {}subprojects {} 를 사용하여 서브 프로젝트의 설정을 할 수 있습니다. allprojects {} 는 루트 프로젝트를 포함한 모든 프로젝트에 설정을 적용하고, subprojects {} 는 서브 프로젝트에만 적용되게 합니다.

이런 방식을 Cross Project Configuration이라고 합니다. 이 방법은 단점이 있는데, 루트의 빌드 스크립트를 뒤져야하므로 서브 프로젝트의 빌드 스크립트만 봐서는 로직을 알아보기 힘들 수 있습니다. Gradle 매뉴얼에서는 그 대신, Convention Plugin을 사용하길 권장하고 있습니다.

  • 서브 프로젝트의 타입이 X라면, Y로 설정하라 패턴의 cross-configuration 이 있다면, X-conventions 플러그인을 적용하는 것과 같습니다.

  • 특정 타입의 서브 프로젝트에서 정보를 추출하는 것 은 프로젝트 간에 아티팩트를 공유하는 방법으로 접근할 수 있습니다.


루트 프로젝트에서 플러그인 버전 정하기

Cross Project Configuration을 사용했을 때, 플러그인의 버전을 맞추기 위한 방법입니다.

루트 프로젝트의 build.gradle 에서 plugins {} 블록에 외부 플러그인을 작성하고, apply false 를 추가합니다. 이렇게 하면 플러그인을 resolve 한 뒤 apply 하지는 않은 상태가 됩니다.

PROJECT_ROOT/build.gradle
plugins {
    id 'org.springframework.boot' version '3.1.5' apply false
    id 'io.spring.dependency-management' version '1.1.3' apply false
    id 'org.jetbrains.kotlin.jvm' version '1.9.20' apply false
    id 'org.jetbrains.kotlin.plugin.spring' version '1.9.20' apply false
}

이렇게 하면 서브 프로젝트의 build.gradle 에서 id 만 기재해주면 됩니다.

PROJECT_ROOT/subproject/build.gradle
plugins {
	id 'org.springframework.boot'
	id 'io.spring.dependency-management'
	id 'org.jetbrains.kotlin.jvm'
	id 'org.jetbrains.kotlin.plugin.spring'
}


Convention Plugins

Gradle에서 추천하는 방법인, 플러그인 시스템을 이용하는 것입니다. 빌드에 관한 설정을 플러그인에 담아서 직접 만들 수 있습니다. 플러그인을 위한 서브 프로젝트를 만들면 되는데, buildSrc 라는 디렉토리명으로 만들면 따로 include 하지 않아도 Gradle에서 알아서 만들어 줍니다.

위 링크의 예제처럼 구조를 만듭니다.

├── buildSrc
│   ├── build.gradle
│   ├── settings.gradle
│   ├── src
│   │   ├── main
│   │   │   └── groovy
│   │   │       ├── myproject.java-conventions.gradle
│   │   │       └── myproject.library-conventions.gradle
...

그렇게 하면, 서브 프로젝트의 빌드 스크립트에서 myproject.java-conventions 같은 이름으로 플러그인을 사용할 수 있습니다.

subproject/build.gradle
plugins {
    id 'myproject.java-conventions'
}

단, buildSrc 에서 외부 플러그인을 사용하고 싶다면, buildSrc/build.gradleplugins {} 가 아닌, dependencies 에 플러그인을 추가하여야 합니다. (See below)

buildSrc/build.gradle
plugins {
    id 'groovy-gradle-plugin'
}

repositories {
    gradlePluginPortal()
}

dependencies {
    implementation 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20'
    implementation 'org.springframework.boot:spring-boot-gradle-plugin:3.1.5'
}


Version Catalog 를 이용한 Dependency 버전 관리

Gradle은 의존하는 라이브러리나 버전을 중앙에서 관리할 수 있게끔 Version Catalog 기능을 제공합니다. 이렇게 하면 모듈별 version을 한 곳에서 관리하는 것이 가능해집니다. 하지만 스크립트의 복잡도가 더 올라갈 수도 있다고 생각되어서 플러그인이 많지 않거나 다양한 버전관리가 필요한게 아니라면 굳이 쓰지 않아도 될 것 같습니다.

PROJECT_ROOT/gradle/libs.versions.toml
[versions]
kotlin = "1.9.20"
springBoot = "3.1.5"
buildSrc/settings.gradle
dependencyResolutionManagement {
    versionCatalogs {
        libs {
            from(files("../gradle/libs.versions.toml"))
        }
    }
}

buildSrc 내부의 빌드 스크립트나 Convention Plugin을 의존하는 서브 프로젝트의 빌드 스크립트에서 lib.versions.springBoot.get() 등으로 접근할 수 있습니다.


어떤 방법을 사용해야 하나?

구글링을 해보면, 대부분 Cross Project Configuration을 사용한다는 것을 알 수 있습니다. 사용하기도 간단합니다. 루트 프로젝트의 빌드 스크립트에 설정을 작성해주기만 하면 됩니다. 작은 프로젝트에서는 괜찮지만, 프로젝트 규모가 커지면 서브 프로젝트에 해당하는 내용을 찾는 것이 힘들어질 수 있습니다.

Convention Plugin은 buildSrc 나 플러그인을 위한 서브 프로젝트를 추가하여야하는 수고가 필요합니다. 하지만 사용에 따라 플러그인을 모델링하여 배포하거나 재사용할 수 있습니다. 문제는 개발자들에게 생소할 수 있다는 것입니다.

제가 보기엔 Convention Plugin을 만드는 방법이 더 깔끔해 보입니다. 동료가 모르면 README에 가이드를 작성해두면 좋겠죠. 크지 않은 프로젝트라면 Cross Project Configuration을 적용해도 나쁘지 않아 보입니다.

다음 포스트에서는 Kotlin DSL로 Convention Plugin에서 의존성을 관리하는 방법을 알아보겠습니다.

Tags: multi-project  gradle  gradle-8.4