🍃SpringBoot

Spring Boot: Gradle(+ 멀티 모듈 고도화 전략 + 의존성 버전 관리)

limdaeil 2025. 10. 22. 23:42

🔖Contents

질문의 배경

최근 Spring Boot 프로젝트를 생성할 때 99%는 Gradle-Groovy 를 선택합니다. build.gradle 파일을 통해 필요한 라이브러리를 간편하게 관리하고, Github Action 혹은 AWS 인스턴스에서 배포할 때 gradlew 명령으로 쉽게 배포할 수 있었습니다. 하지만 실무와 근접한 프로젝트를 구성하거나 commons 처럼 라이브러리 모듈을 구현하기 위해서는 라이브러리 버전 관리가 필요합니다. 자연스럽게 Gradle을 좀 더 자세히 알아야 할 필요성 느끼게 되면서 정리하게 된 글 입니다. 

1. Gradle

Gradle은 자바, C++, 안드로이드 등 다양한 프로그래밍 언어로 작성된 소프트웨어의 빌드 및 관리를 자동화하는 오픈소스 도구입니다.(빌드 자동화 도구)
프로젝트의 의존성 관리, 컴파일, 테스트, 배포 등의 복잡한 작업을 자동화하여 개발 효율성을 높입니다.

Gradle 공식 홈페이지

 

기능 및 특징

  • 빌드 자동화: 소스 코드를 컴파일하고 실행 가능한 애플리케이션으로 만드는 과정을 자동화합니다.
  • 의존성 관리: 프로젝트에 필요한 외부 라이브러리나 의존성을 자동으로 관리하고 다운로드합니다.
  • 다국어 지원: 자바, 스칼라, C++, 파이썬 등 다양한 언어를 지원합니다.
  • 유연성과 확장성: Groovy를 기반으로 하여 유연하고 확장 가능한 빌드 스크립트를 작성할 수 있습니다.
  • 플러그인 시스템: 다양한 기능을 플러그인 형태로 제공하며, 이를 통해 빌드 로직을 재사용하고 확장할 수 있습니다.
  • 안드로이드 공식 빌드 도구: 특히 안드로이드 애플리케이션 개발에서 공식 빌드 시스템으로 널리 사용됩니다.
  • 이전 빌드 도구의 단점 보완: Ant의 자유도와 Maven의 관례를 결합하여 이전 빌드 도구들의 단점을 보완했습니다.

2. 스프링 부트에서 Gradle

스프링 부트는 Gradle을 통해 소스 코드를 컴파일하고, 의존성(라이브러리)를 관리하며, 애플리케이션을 실행 가능한 파일로 생성하는 등 개발 프로젝트의 전반적인 빌드 과정을 자동화하는 역할을 수행합니다. Gradle은 XML 기반의 Maven과 달리 Groovy 기반으로 build.gradle 파일에 스크립트를 작성하여 유연하고 간편한 설정을 관리할 수 있습니다.

스프링 부트에서 Gradle의 핵심 역할

  • 빌드 자동화: 소스 코드 컴파일, 테스트 실행, 패키징 등 빌드 프로세스를 자동화합니다.
  • 의존성 관리: 프로젝트에 필요한 외부 라이브러리(의존성)를 설정하고 자동으로 다운로드 및 관리합니다.
  • 간편한 설정: build.gradle 파일에 스크립트를 작성하므로, 복잡한 XML보다 관리가 용이하고 유연합니다.

2.1 gradlew, gradlew.bat

gradlewgradlew.bat은 스프링 부트 프로젝트에서 Gradle Wrapper의 실행 스크립트입니다. gradlew는 리눅스/macOS 용 셸 스크립트이고, gradlew.bat는 윈도우용 배치 스크립트입니다. 이 스크립트들을 통해 프로젝트를 빌드, 실행, 테스트하는 등의 Gradle 작업을 수행할 수 있으며, 이 방법은 특정 Gradle 버전에 의존할 필요 없이 일관된 빌드 환경을 제공합니다.

gradlewgradlew.bat의 역할

  1. 프로젝트 빌드 및 실행: gradlew 또는 gradlew.batbootRun, build와 같은 명령어를 붙여 애플리케이션을 실행하거나 빌드할 수 있습니다.
  2. 운영체제별 스크립트: 각 파일은 특정 운영체제에 맞게 작성되었습니다.
    • gradlew: Linux, macOS 등에서 사용됩니다. (Linux/macOS: ./gradlew bootRun)
    • gradlew.bat: Windows에서 사용됩니다. (Windows: gradlew.bat bootRun)
  3. Gradle Wrapper 사용: 프로젝트를 실행할 때 로컬에 Gradle이 설치되어 있지 않아도, 이 스크립트들이 자동으로 해당 Gradle 버전을 다운로드하고 사용하도록 합니다. 이처럼 Gradle Wrapper를 사용하면 개발 환경에 관계없이 동일한 버전의 Gradle을 사용할 수 있습니다.

2.2 Gradle Wrapper

Gradle Wrapper는 Gradle 빌드 도구를 사용하기 위해 프로젝트에 포함된 도구로, 사용자가 Gradle을 로컬 환경에 직접 설치하지 않아도 빌드 작업을 실행할 수 있게 해줍니다. gradle-wrapper.jar 파일과 설정(gradle/wrapper/gradle-wrapper.properties)을 포함하며, 프로젝트를 빌드하는 데 필요한 특정 버전의 Gradle을 자동으로 다운로드하여 실행해줍니다.

Gradle Wrapper의 역할 및 장점

  • 환경 종속성 제거: 개발자마다 Gradle을 설치하고 환경 변수를 설정하는 번거로움을 없애줍니다.
  • 일관된 빌드 환경: 프로젝트의 빌드 스크립트가 특정 버전의 Gradle을 실행하도록 강제하여 모든 개발 환경에서 동일한 방식으로 빌드되도록 보장합니다.
  • 자동화된 다운로드 및 실행: 사용자가 프로젝트를 처음 로드하거나 gradlew 명령어를 실행하면, gradle-wrapper.jargradle-wrapper.properties 파일에 명시된 버전의 Gradle을 자동으로 다운로드하고 캐싱하여 사용합니다.
  • 간편한 실행: 프로젝트 루트 디렉토리에서 ./gradlew 또는 gradlew.bat 와 같은 명령어를 사용하여 빌드, 테스트, 실행 등의 작업을 수행할 수 있습니다.

Gradle Wrapper 작동 방식

  1. 빌드 명령어 실행: 개발자가 프로젝트 디렉토리에서 ./gradlew와 같은 명령어를 입력합니다.
  2. Wrapper 실행: 시스템은 gradle-wrapper.jar 파일을 실행합니다.
  3. Gradle 버전 확인: gradle-wrapper.properties 파일에서 필요한 Gradle 버전을 확인합니다.
  4. Gradle 다운로드: 로컬 환경에 해당 버전의 Gradle이 설치되어 있지 않으면, Wrapper가 자동으로 Gradle을 다운로드하여 로컬 캐시에 저장합니다.
  5. Gradle 작업 실행:다운로드된 Gradle을 사용하여 사용자가 요청한 빌드 작업을 수행합니다.

gradle-wrapper.properties 파일 수정으로 Gradle 빌드 작업을 환경에 따라서 설정할 수 있습니다. 현재 내용에서는 생략하고 넘어가겠습니다.

2.3 build.gradle

build.gradle은 Gradle이라는 빌드 도구의 설정 파일로, 스프링 부트 프로젝트의 빌드 방식, 의존성(라이브러리), 환경 설정 등을 정의하는 데 사용됩니다. build.gradle 파일을 통해 프로젝트의 빌드 및 관리를 자동화하고, 필요한 라이브러리를 설정하며, 프로젝트의 전반적인 구성을 기술할 수 있습니다.

build.gradle 기능

  • 빌드 자동화: 소스 코드를 컴파일하고 테스트하며, 실행 가능한 애플리케이션으로 패키징하는 빌드 과정을 자동화합니다.
  • 의존성 관리: 프로젝트에 필요한 라이브러리나 프레임워크(예: Spring Boot Starter)를 추가하고 관리합니다.
  • 환경 설정: 프로젝트 빌드 시 필요한 다양한 환경 설정을 정의할 수 있습니다.
  • 스크립트 사용:빌드 스크립트는 Groovy나 Kotlin과 같은 도메인 특화 언어(DSL)로 작성되어 있어, 빌드 로직을 유연하게 제어할 수 있습니다.

build.gradle 파일 해심 구성

  • buildscript: 빌드에 필요한 플러그인 등 외부 라이브러리를 가져올 때 사용합니다.
  • repositories: 의존성을 다운로드할 저장소(Repository)를 지정합니다.
  • dependencies: 프로젝트가 의존하는 라이브러리들을 나열합니다.
  • plugins: 사용 중인 Gradle 플러그인을 명시합니다.

2.4 settings.gradle

settings.gradle은 스프링 부트 프로젝트에서 빌드에 포함될 모듈을 정의하고 프로젝트 구조를 설정하는 파일입니다. 이 파일은 루트 프로젝트 디렉터리에 위치하며, 여러 모듈로 구성된 멀티 프로젝트의 경우에는 어디서 플러그인을 가져올지, 라이브러리 저장소는 어디인지, 모듈은 어떤 것들이 있는지를 한 곳에서 관리합니다.

  • 프로젝트 설정: 프로젝트를 구성하고, 특히 멀티 프로젝트의 경우 포함해야 할 서브 프로젝트들을 정의합니다.
  • 빌드 구성: 프로젝트를 빌드할 때 어떤 모듈들을 포함해야 하는지 Gradle에게 알려주어 빌드 과정을 제어합니다.
  • 싱글 프로젝트와 멀티 프로젝트: 싱글 프로젝트에서는 특별히 설정할 내용이 적지만, 여러 서브 프로젝트로 구성된 복잡한 프로젝트에서는 각 프로젝트의 이름을 지정하는 데 사용됩니다.

3. Gradle.build 분석

멀티 모듈 프로젝트에서 루트 디렉토리의 build.gradle 입니다. 각 코드 줄마다 무엇을 의미하는지 분석하겠습니다.

plugins {
    id 'java'
    alias(libs.plugins.spring.boot) apply false
    alias(libs.plugins.depman)      apply false
}

// 모듈 역할 명시
def appModuleNames = ['commerce-catalog', 'commerce-order', 'commerce-payment', 'commerce-user']
def libModuleNames = ['commerce-commons']

allprojects {
    group = 'com.app'
    version = '0.0.1-SNAPSHOT'
    description = 'commerce'
    // repositories는 settings.gradle에서만 관리
}

subprojects {
    apply plugin: 'java'

    java {
        toolchain {
            languageVersion = JavaLanguageVersion.of(libs.versions.java.get() as String)
        }
    }

    // 전역 규약: 테스트 & 애너테이션 프로세서만
    dependencies {
        testImplementation platform(libs.junit.bom)
        testImplementation libs.junit.jupiter

        compileOnly libs.lombok
        annotationProcessor libs.lombok
    }

    // lombok annotationProcessor를 compileOnly에 자동 포함(편의)
    configurations {
        compileOnly { extendsFrom annotationProcessor }
    }

    tasks.named('test') {
        useJUnitPlatform()
    }

    // === 애플리케이션 모듈 전용 규약 ===
    if (name in appModuleNames) {
        apply plugin: libs.plugins.spring.boot.get().pluginId
        apply plugin: libs.plugins.depman.get().pluginId

        // 공통 실행 스타터(앱만)
        dependencies {
            implementation 'org.springframework.boot:spring-boot-starter-actuator'
            implementation 'org.springframework.boot:spring-boot-starter-validation'
        }

        // Boot 기본: bootJar=true, jar=false — 기본값 유지
    }

    // === 라이브러리 모듈 전용 규약 ===
    if (name in libModuleNames) {
        apply plugin: 'java-library'
        // 라이브러리는 JAR만 생성 (bootJar 적용 안 함)
    }
}

3.1 plugins {}

plugins {} 블록은 “이 빌드에서 어떤 플러그인(기능)을 쓸지” 선언합니다.

plugins {
    id 'java'
    alias(libs.plugins.spring.boot) apply false
    alias(libs.plugins.dependency-management) apply false
}
  • id 'java': 자바 프로젝트로 빌드를 의미(컴파일, 테스트, JAR 만들기 등 기본 작업을 수행합니다.)
  • alias(libs.plugins.spring.boot): 버전 카탈로그(libs.versions.toml)에서 정의한 스프링부트 플러그인을 별칭(alias) 으로 조회
  • apply false: "지금은 적용하지 않고 선언만 한 상태"를 의미
  • depman: io.spring.dependency-management 플러그인의 별칭. 스프링 생태계 BOM(버전 묶음)을 적용하도록 도움을 지원

💡BOM이란

스프링 부트 BOM(Bill of Materials)은 스프링 부트 프로젝트에서 사용되는 모든 의존성 라이브러리의 버전을 표준화하여 관리하는 '자재 명세서'입니다. 이를 통해 개발자는 각 라이브러리의 버전을 개별적으로 관리할 필요 없이, 스프링 부트가 권장하는 호환되는 버전으로 한 번에 설정하여 버전 충돌 없이 프로젝트를 구성할 수 있습니다.

  • 버전 관리 자동화: 프로젝트에 필요한 모든 라이브러리의 버전을 정의해 두어, 개발자가 개별적으로 버전을 찾고 지정하는 번거로움을 없애줍니다.
  • 의존성 충돌 방지: 스프링 부트 버전별로 최적화된 호환 가능한 라이브러리 버전이 묶여있기 때문에, 라이브러리 간의 버전 충돌을 방지합니다.
  • 일관된 개발 환경: 모든 개발자가 동일한 BOM을 사용함으로써, 프로젝트의 빌드와 실행 환경을 일관되게 유지할 수 있습니다.

💡io.spring.dependency-management 플러그인이란

io.spring.dependency-management 플러그인은 스프링 부트의 spring-boot-dependencies BOM(Bill of Materials)을 자동으로 가져와 의존성 버전을 일관되게 관리해 주는 Gradle 플러그인입니다. 스프링 부트 프로젝트의 빌드 과정에서 이 플러그인을 적용하면, 프로젝트에서 사용 중인 스프링 부트 버전에 맞는 spring-boot-dependencies BOM을 자동으로 가져옵니다.

3.2 def

def는 그루비에서 변수 선언입니다. 나중에 등장하는 if (name in appModuleNames) 처럼, 현재 처리 중인 모듈 이름이 어디에 속하는지로 규칙을 나눕니다.

def appModuleNames = ['commerce-catalog', 'commerce-order', 'commerce-payment', 'commerce-user']
def libModuleNames = ['commerce-commons']

3.3 allprojects {}

allprojects {}는 루트와 모든 서브프로젝트에 공통 속성을 지정합니다. 안의 group, version, description식별자/메타정보입니다.
산출물 좌표가 com.app:모듈명:0.0.1-SNAPSHOT처럼 됩니다.

allprojects {
    group = 'com.app'
    version = '0.0.1-SNAPSHOT'
    description = 'commerce'
    // repositories는 settings.gradle에서만 관리
}

💡라이브러리 버전 관리하는 경우

현재 제 코드는 버전 카탈로그(libs.versions.toml)에서 직접 정의한 버전을 사용하도록 관리하고 있습니다.
만약, BOM을 통해 의존성 버전을 자동으로 관리하는 경우에는 아래 코드처럼 apply plugin:으로 플러그인을 작성하면 됩니다.

// 모든 프로젝트에 적용되는 설정
allprojects {
    group = 'com.example'
    version = '1.0.0-SNAPSHOT'

    apply plugin: 'java'
    apply plugin: 'org.springframework.boot'
    apply plugin: 'io.spring.dependency-management'

    repositories {
        mavenCentral()
    }
}

3.4 subprojects {}

subproject {} 모든 서브 프로젝트의 공통적으로 적용되는 블록입니다.

1. apply plugin: 'java'

모든 서브프로젝트에 Java 플러그인을 적용합니다. (자바 소스 컴파일, 테스트 작업 생성)

subprojects {
    apply plugin: 'java'
}

2. toolchain {}

어떤 JDK 버전으로 빌드할지 정합니다. libs.versions.java는 버전 카탈로그에서 java = "21" 같은 값을 읽어옵니다.
팀원이 다른 JDK를 깔아놔도, Gradle이 알아서 JDK 21을 내려 쓰게 할 수 있어, 환경 차이를 줄입니다.

    java {
        toolchain {
            languageVersion = JavaLanguageVersion.of(libs.versions.java.get() as String)
        }
    }

3. dependencies {}

dependencies {}의존성(라이브러리) 선언 블록입니다.

    // 전역 규약: 테스트 & 애너테이션 프로세서만
    dependencies {
        testImplementation platform(libs.junit.bom)
        testImplementation libs.junit.jupiter

        compileOnly libs.lombok
        annotationProcessor libs.lombok
    }
  • testImplementation platform(libs.junit.bom): JUnit 의존성들의 버전을 BOM으로 한 번에 잠그는 문법
  • platform(...)은 “이 묶음의 버전 정리표를 적용해”라는 의미
  • testImplementation libs.junit.jupiter로 실제 JUnit Jupiter API를 조회. (버전 표기는 BOM이 담당)
  • compileOnly libs.lombok은 컴파일할 때만 lombok을 쓰겠다는 뜻(런타임엔 필요 없음)
  • annotationProcessor libs.lombok애너테이션 프로세서 경로에 lombok을 설정. (롬복이 소스에서 코드를 생성)

3.5 💡implementation, testImplementation, compileOnly, annotationProcessor

핵심 개념이자 선수 지식으로 의존성(Dependency)스코프(Scope)에 대해 먼저 이해해야만 합니다.

식재료와 도구로 요리하는 요리사

우선 의존성(Dependency)은 내 프로젝트(요리)를 만드는 데 필요한 외부 라이브러리(미리 만들어진 재료나 도구)라고 생각하시면 됩니다.
예를 들어, JSON 데이터를 다루기 위한 Gson 라이브러리나 테스트를 위한 JUnit 라이브러리가 핵심 재료 혹은 도구에요.

스코프(Scope)는 이 '재료'나 '도구'가 언제, 어디서 필요한지를 정해주는 규칙입니다. 모든 도구를 항상 부엌에 꺼내놓을 필요는 없잖아요? 요리할 때만 필요한 도구, 손님에게 나갈 때 필요한 도구, 맛을 테스트할 때만 필요한 도구가 각각 다른 것처럼요. implementation, testImplementation 등이 바로 이 스코프를 지정하는 키워드입니다.

정리 표

설정 역할 언제 필요? 최종 결과물(앱)에 포함? 대표적인 예시
implementation 내부 구현에 필요한 라이브러리 컴파일 시점 & 런타임 시점 O (포함됨) Gson, Retrofit
testImplementation 테스트 코드 작성
및 실행에 필요한 라이브러리
테스트 컴파일 시점
& 테스트 런타임 시점
X (포함 안 됨) JUnit, Mockito
compileOnly 컴파일 시점에만 필요한 라이브러리 오직 컴파일 시점 X (포함 안 됨) Lombok (어노테이션)
annotationProcessor 어노테이션을 처리하여 코드를 생성하는 도구 오직 컴파일 시점 (빌드 과정) X (포함 안 됨) Lombok (처리기)
  • 라이브러리 의존성: 프로젝트 실행에 필요한 외부 라이브러리(스프링 웹 스타터, 톰캣 등)를 의미하며, 스프링 부트 스타터를 통해 관리합니다.

4. configurations {}

configurations {}의존성 구역(스코프) 를 다루는 블록입니다.

compileOnlyannotationProcessor상속하게 해서, IDE에서 롬복 인식을 더 자연스럽게 합니다. (편의 세팅)

// lombok annotationProcessor를 compileOnly에 자동 포함(편의)
configurations {
    compileOnly { extendsFrom annotationProcessor }
}

5. tasks

tasks는 빌드 작업을 뜻합니다. test` 작업에 “JUnit Platform으로 실행해”라고 지정합니다. JUnit 5를 쓰려면 보통 이 설정이 필요합니다.

    tasks.named('test') {
        useJUnitPlatform()
    }

7. 애플리케이션 모듈 전용 규약: 부트 & 의존성은 앱에만

현재 모듈 이름이 앱 목록에 있으면, 그때만 스프링부트 플러그인의존성 관리 플러그인을 적용합니다.
아까 plugins { ... apply false }로 “선언만” 해둔 걸, 여기서 실제로 적용하는 구조입니다.

이렇게 하면 라이브러리 모듈(예: commons)에는 스프링부트가 적용되지 않습니다.

    if (name in appModuleNames) {
        apply plugin: libs.plugins.spring.boot.get().pluginId
        apply plugin: libs.plugins.depman.get().pluginId

8. 앱 모듈만 공통 주입

  • 앱 모듈에만 부트 스타터를 공통 주입합니다.
  • 버전을 적지 않는 이유는, 부트 BOM이 알아서 맞춰주기 때문입니다. (depman 플러그인이 그걸 가능하게 해 줌)
  • actuator는 헬스체크/메트릭, validationjakarta.validation(Bean 검증) 스타터입니다.
        // 공통 실행 스타터(앱만)
        dependencies {
            implementation 'org.springframework.boot:spring-boot-starter-actuator'
            implementation 'org.springframework.boot:spring-boot-starter-validation'
        }

9. JAR(bootJar)

스프링부트 플러그인이 적용되면, 기본적으로 실행 가능한 JAR(bootJar) 를 만들고, 일반 JAR는 비활성화됩니다.
앱 모듈은 이 기본값을 그대로 쓰면 됩니다.

        // Boot 기본: bootJar=true, jar=false — 기본값 유지
    }

💡bootJar와 JAR란?

bootJarSpring Boot 프로젝트를 위한 실행 가능한 JAR 파일(내장 서버 포함 JAR)을 생성하는 Gradle 작업입니다. 이는 개발된 애플리케이션을 다른 환경에 배포하여 실행할 때 사용하며, 별도의 웹 서버 설치 없이 애플리케이션을 바로 실행할 수 있도록 해줍니다.

jar 작업과의 차이점

  • jar 작업: 일반적인 Java 프로젝트의 경우, jar 작업을 통해 클래스 파일만 포함된 JAR 파일을 생성합니다. 이 경우 애플리케이션을 실행하려면 별도의 JRE나 웹 서버를 설치하고 설정해야 합니다.
  • bootJar 작업: bootJarjar 작업의 결과물에 내장 서버와 같은 실행에 필요한 모든 것을 추가하여, 실행 가능한 독립적인 파일로 만들어줍니다.

10. 라이브러리 모듈 전용 규약: 순수 JAR, 부트 금지

  • 라이브러리 모듈(commons 등)은 java-library 플러그인을 적용합니다.
  • java-libraryapi/implementation 의존성 구분을 지원해서, 재사용 모듈 작성에 유리합니다.
  • 스프링부트 플러그인은 적용하지 않습니다. 따라서 bootJar가 아닌 일반 JAR가 생성됩니다.
  • 여기엔 웹/JPA 같은 실행용 스타터를 넣지 않는 것이 원칙입니다. (의존 그래프 오염 방지)
    if (name in libModuleNames) {
        apply plugin: 'java-library'
        // 라이브러리는 JAR만 생성 (bootJar 적용 안 함)
    }
}

build.gradle 정리

  1. 자바/JDK 버전, 테스트(JUnit), 롬복 같은 공통 규약은 모든 모듈에 적용
  2. 스프링부트 + 실행 스타터앱 모듈에만 적용(실행 가능한 bootJar 생성)
  3. 라이브러리 모듈java-library만 적용(순수 JAR, 재사용성↑)
  4. 버전과 저장소는 각각 버전 카탈로그(TOML) / settings.gradle 에서 중앙관리

4. settings.gradle 분석

루트 디렉토리의 settings.gradle 입니다. 각 코드 줄마다 무엇을 의미하는지 분석하겠습니다.

import org.gradle.api.initialization.resolve.RepositoriesMode

pluginManagement {
    repositories {
        gradlePluginPortal()
        mavenCentral()
    }
}

dependencyResolutionManagement {
    // 저장소는 settings에서만 관리
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        mavenCentral()
    }
}

// ❗ versionCatalogs 블록 '전부' 제거 (자동 로드 사용)

rootProject.name = 'commerce'
include(
        ':commerce-commons',
        ':commerce-catalog',
        ':commerce-order',
        ':commerce-payment',
        ':commerce-user'
)

4.1 RepositoriesMode

RepositoriesMode라는 열거형(Enum)을 쓰기 위한 임포트입니다.
Groovy DSL에서는 이런 타입을 직접 참조할 때 가끔 임포트가 필요합니다. 뒤에서 저장소 정책을 정할 때 사용합니다.

import org.gradle.api.initialization.resolve.RepositoriesMode
pluginManagement {
    repositories {
        gradlePluginPortal()
        mavenCentral()
    }
}

플러그인을 어디서 내려받을지 정합니다. 플러그인은 “빌드에 기능을 끼우는 애드온”이고, 일반 라이브러리와 저장소 설정을 분리합니다. gradlePluginPortal()은 공식 플러그인 저장소, mavenCentral()은 우리가 익숙한 중앙 저장소입니다. 이렇게 두면 plugins { id "org.springframework.boot" version "…" } 같은 선언이 문제없이 동작합니다.

4.2 dependencyResolutionManagement {}

dependencyResolutionManagement 블록은 라이브러리 의존성을 어디서, 어떤 정책으로 받을지 전역으로 정하는 블록입니다.

repositoriesMode가 핵심입니다. FAIL_ON_PROJECT_REPOS는 “각 모듈의 build.gradle 어디에서도 repositories { … }를 쓰지 마라. 쓰면 빌드 실패로 처리하겠다”는 뜻입니다. 즉, 저장소 선언을 settings.gradle 한 곳에만 두어 팀 전체 설정을 일관되게 유지합니다.

저장소는 mavenCentral() 하나로 고정했습니다. 사내 Nexus/Artifactory가 있다면 여기에 추가하면 됩니다. 참고로 다른 모드로는 PREFER_SETTINGS(프로젝트에서도 선언은 허용하되 settings 우선), PREFER_PROJECT(프로젝트 우선) 등이 있지만, 멀티 모듈·팀 개발에서는 지금처럼 중앙집중이 유지보수에 가장 유리합니다.

dependencyResolutionManagement {
    // 저장소는 settings에서만 관리
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        mavenCentral()
    }
}

4.3 versionCatalogs {} 제거

// ❗ versionCatalogs 블록 '전부' 제거 (자동 로드 사용)

주석이지만 매우 중요합니다. Gradle은 gradle/libs.versions.toml 파일이 존재하면 자동으로 libs 카탈로그를 로드합니다. 따라서 versionCatalogs { create("libs") { from(files("gradle/libs.versions.toml")) } }를 또 쓰면 “한 카탈로그에서 from은 한 번만 호출 가능” 오류가 납니다. 자동 로드와 수동 선언을 둘 중 하나만 쓰는 게 원칙이고, 우리는 자동 로드를 선택했기 때문에 이 블록을 아예 적지 않습니다.

4.4 rootProject.name

루트 프로젝트의 이름입니다. IDE에서 보이는 프로젝트 표시, 기본 산출물 좌표, 보고서 제목 등 여러 곳에 쓰입니다.

팀에서 합의한 읽기 좋은 이름으로 두면 됩니다.

rootProject.name = 'commerce'

4.5 include

여기서 멀티 모듈을 선언합니다. 각 문자열은 루트 디렉터리 하위의 모듈 폴더를 가리킵니다. 예를 들어 :commerce-commonscommerce-commons/ 디렉터리의 서브프로젝트를 의미하고, 그 안의 build.gradle이 그 모듈의 규칙이 됩니다. 앞의 콜론(:)은 “루트 기준”을 뜻하는 네임스페이스 표시입니다.

새로운 모듈을 추가할 땐 폴더를 만들고 include(':새모듈')만 추가하면 Gradle이 그 모듈을 인식합니다.

include(
        ':commerce-commons',
        ':commerce-catalog',
        ':commerce-order',
        ':commerce-payment',
        ':commerce-user'
)

🔥settings.gradle 설정이 만들어 주는 개발 경험

settings.gradle 파일 하나로 플러그인 저장소라이브러리 저장소를 분리 관리하고, 저장소 선언을 전역 한 곳으로 모아서 팀 환경을 통일합니다. 버전 카탈로그는 자동 로드로 충돌을 원천 차단하고, 모듈 목록을 명시하여 IDE와 빌드가 동일한 구조를 바라보게 합니다. 결과적으로 각 모듈의 build.gradle은 비즈니스에 필요한 의존성만 깔끔히 적으면 되고, 버전과 저장소 같은 인프라 설정은 여기서 끝납니다.

🔥settings.gradle 설정에서 겪은 문제

가장 흔한 실수는 버전 카탈로그 중복 선언입니다. gradle/libs.versions.toml이 있는데 versionCatalogs { create("libs") { from(...) } }까지 쓰면 즉시 충돌합니다. 또 하나는 FAIL_ON_PROJECT_REPOS를 켰는데 서브프로젝트 어딘가에 repositories { ... }가 남아 있는 경우입니다. 이런 경우 빌드가 바로 실패하니, 저장소 선언은 반드시 settings에서만 관리하세요. 필요 시 정책을 임시로 PREFER_SETTINGS로 완화할 수 있지만, 장기적으로는 중앙집중이 안정적입니다.

5. 버전 카탈로그 toml

버전 카탈로그(Version Catalog)“의존성/플러그인 버전을 한 곳에서 이름으로 관리” 하는 Gradle 기능입니다. 파일 형식은 TOML(Tom’s Obvious, Minimal Language)이라는 키=값 기반 설정 포맷이고, Gradle은 기본 경로 gradle/libs.versions.toml을 자동으로 읽어 libs라는 카탈로그로 제공합니다. 그래서 build.gradle 안에서 libs.xxx 형태로 파일명을 사용합니다.

멀티 모듈 프로젝트에서 gradle/libs.versions.toml 입니다. 무엇을 의미하는지 분석하겠습니다.

[versions]
java = "21"
spring-boot = "3.5.6"
depman = "1.1.7"
lombok = "1.18.34"
mapstruct = "1.6.2"
jjwt = "0.12.6"
querydsl = "5.1.0"
redisson = "3.52.0"

junit = "5.11.4"

[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }
depman = { id = "io.spring.dependency-management", version.ref = "depman" }

[libraries]
# 테스트 공통
junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" }
redisson = { module = "org.redisson:redisson", version.ref = "redisson" }

# 애너테이션/매핑
lombok = { module = "org.projectlombok:lombok", version.ref = "lombok" }
mapstruct = { module = "org.mapstruct:mapstruct", version.ref = "mapstruct" }
mapstruct-processor = { module = "org.mapstruct:mapstruct-processor", version.ref = "mapstruct" }

# 선택: 보안/쿼리
jjwt-api = { module = "io.jsonwebtoken:jjwt-api", version.ref = "jjwt" }
jjwt-impl = { module = "io.jsonwebtoken:jjwt-impl", version.ref = "jjwt" }
jjwt-jackson = { module = "io.jsonwebtoken:jjwt-jackson", version.ref = "jjwt" }
querydsl-jpa = { module = "com.querydsl:querydsl-jpa", version.ref = "querydsl" }

5.1 toml 문법

  • 키=값: java = "21"
  • 섹션: [versions], [plugins], [libraries] 처럼 대괄호로 그룹을 나눕니다.
  • 객체(맵): { id = "...", version.ref = "..." } 이런 중괄호는 한 줄짜리 객체입니다.
  • 문자열은 큰따옴표 "...", 숫자는 따옴표 없이 씁니다.

5.2 [versions]

  • 각 키는 버전 별칭입니다. 아래 섹션에서 version.ref = "lombok"처럼 참조합니다.
  • 장점: 버전 업그레이드는 여기만 바꾸면 전 프로젝트에 반영됩니다.
[versions]
java = "21"
spring-boot = "3.5.6"
depman = "1.1.7"
lombok = "1.18.34"
mapstruct = "1.6.2"
jjwt = "0.12.6"
querydsl = "5.1.0"
redisson = "3.52.0"
junit = "5.11.4"

5.3 [plugins]

  • id는 플러그인 ID, version.ref는 위 [versions]에서 정의한 버전을 가리킵니다.
[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }
depman      = { id = "io.spring.dependency-management", version.ref = "depman" }

build.gradle에서 다음과 같이 사용할 수 있습니다.

// root: build.gradle  
plugins {
    alias(libs.plugins.spring.boot) apply false
    alias(libs.plugins.depman)      apply false
}
  • apply false: "지금은 적용하지 않고 선언만 한 상태"를 의미

5.4 [libraries]

  • module = "그룹:아티팩트"가 핵심. Maven 좌표를 이름 하나(libs.redisson)로 부를 수 있게 별칭을 만드는 것입니다.
  • version.ref = "..."[versions]에 적어둔 버전을 참조합니다.
  • junit-jupiter는 버전이 비어있죠? 이유는 옆에서 BOM으로 버전을 잠궈서 개별 버전이 불필요하기 때문이에요.
[libraries]
# 테스트 공통
junit-bom      = { module = "org.junit:junit-bom", version.ref = "junit" }
junit-jupiter  = { module = "org.junit.jupiter:junit-jupiter" }

redisson       = { module = "org.redisson:redisson", version.ref = "redisson" }

# 애너테이션/매핑
lombok                = { module = "org.projectlombok:lombok", version.ref = "lombok" }
mapstruct             = { module = "org.mapstruct:mapstruct", version.ref = "mapstruct" }
mapstruct-processor   = { module = "org.mapstruct:mapstruct-processor", version.ref = "mapstruct" }

# 선택: 보안/쿼리
jjwt-api      = { module = "io.jsonwebtoken:jjwt-api", version.ref = "jjwt" }
jjwt-impl     = { module = "io.jsonwebtoken:jjwt-impl", version.ref = "jjwt" }
jjwt-jackson  = { module = "io.jsonwebtoken:jjwt-jackson", version.ref = "jjwt" }
querydsl-jpa  = { module = "com.querydsl:querydsl-jpa", version.ref = "querydsl" }

💡build.gradle에서 라이브러리 의존성을 사용하는 방법

  • 스프링 부트 스타터(예: spring-boot-starter-web)는 버전 적지 마세요.
  • 부트 BOM이 자동으로 관리합니다. (dependency-management 플러그인 적용 시)
// product: build.gradle
plugins { id 'java' } // 부트 플러그인은 루트에서 주입됨

dependencies {
    implementation project(':commerce-commons')

    // 모듈별 필요한 스타터만 — 버전 기입 금지(BOM이 관리)
    implementation 'org.springframework.boot:spring-boot-starter-web'
    // 필요 시 추가
    // implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    // implementation 'org.springframework.boot:spring-boot-starter-security'

    // 선택: JJWT
    // implementation libs.jjwt.api
    // runtimeOnly libs.jjwt.impl
    // runtimeOnly libs.jjwt.jackson

    // 선택: QueryDSL (JPA 쓸 때)
    // implementation libs.querydsl.jpa
    // annotationProcessor "jakarta.persistence:jakarta.persistence-api"
    // annotationProcessor "jakarta.annotation:jakarta.annotation-api"

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

💡새로운 라이브러리 추가하는 방법

# 1. TOML에 버전과 라이브러리 별칭 추가
[versions]
guava = "33.2.1-jre"

[libraries]
guava = { module = "com.google.guava:guava", version.ref = "guava" }
// 모듈의 build.gradle에서 사용
dependencies {
  implementation libs.guava
}

“한 번 더 추가해야 하나요?” → 아니요. TOML + build.gradle 딱 두 군데면 끝.

// Redisson 쓰기
dependencies {
  implementation libs.redisson
}
// JJWT 쓰기
dependencies {
  implementation libs.jjwt.api
  runtimeOnly   libs.jjwt.impl
  runtimeOnly   libs.jjwt.jackson
}
# QueryDSL (JPA)
dependencies {
  implementation libs.querydsl.jpa
  annotationProcessor "jakarta.persistence:jakarta.persistence-api"
  annotationProcessor "jakarta.annotation:jakarta.annotation-api"
}

5.5💡 java-library는 java 플러그인의 상위 버전

java-library 플러그인은 “라이브러리 모듈”을 만들 때 쓰라고 나온 플러그인이고, API/구현 의존성 구분을 지원합니다.

예를 들어, 아래 코드처럼 commons 공통 모듈의 build.gradle 안에서 java-library 플러그인을 사용을 권장합니다.

 

  • java 플러그인: 자바 컴파일, 테스트, 패키징 지원(implementation, compileOnly, runtimeOnly)
  • java-library 플러그인: 위 기능 + api 구성이 추가 → 소비자에게 전이되는 컴파일 의존성을 선언할 수 있습니다.
# commons: build.gradle
plugins {
    id 'java-library'
    alias(libs.plugins.depman)
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.boot:spring-boot-dependencies:${libs.versions.spring.boot.get()}"
    }
}

dependencies {
    api(libs.jakarta.persistence) // JPA 애노테이션
    api(libs.spring.data.commons) // Auditing 메타 애노테이션
    api(libs.spring.data.jpa)     // AuditingEntityListener
    compileOnly(libs.hibernate.core) // 선택: 하이버네이트 애노테이션이 필요할 때만
    compileOnly(libs.lombok) // Lombok
    annotationProcessor(libs.lombok)
}

반드시 위의 코드처럼 작성해야만 라이브러리 모듈의 의존성을 최소화하는 것은 아니에요.
Gradle 문법과 Spring 생태계를 관리하는 방법을 충분히 알고 있다면 언제든지 원하는 방법으로 코드를 수정하실 수 있습니다.

항목 java java-library
기본 기능(컴파일, 테스트, JAR)
api 구성(공개 API 전이)
implementation(구현 전용)
라이브러리 제작에 최적화 보통 권장

 

💡 commons-domain에  java-library가 좋은 이유

commons-domain은 다른 모듈이 가져다 쓰는 라이브러리 역할이니까, 외부에 노출해야 하는 최소 의존성(예: jakarta.persistence-api)은 api 로, 내부 구현에만 쓰는 것(예: spring-data-jpa, spring-data-commons)은 implementation 으로 숨길 수 있어요. → 소비자 모듈의 클래스패스가 불필요하게 더러워지지 않습니다.

 

api 뭘까?

api는 java-library 플러그인이 추가해 주는 의존성 구분자입니다. 

api의 핵심 용도는 “이 모듈을 사용하는 쪽의 컴파일 클래스패스에도 같이 보낸다.”→ 즉, 전이(Transitive) 컴파일 의존성이에요.

  • implementation: “이 모듈 안에서만 사용한다."→ 전이가 안 됩니다.(소비자에겐 런타임까지만 보장될 수 있습니다).
  • compileOnly: 컴파일에만 필요, 런타임엔 없음(롬복, 애노테이션 등)
  • runtimeOnly: 런타임에만 필요(DB 드라이버 등)
  • annotationProcessor: 애노테이션 프로세서 전용(롬복, MapStruct 등)

예를 들어, 너의 라이브러리 public 메서드/필드/부모클래스/인터페이스에 외부 타입이 노출되면, 그 타입의 라이브러리를 소비자 컴파일러도 알아야 하니까 api를 사용하는 것이에요. 예를 들어 commons 모듈 안의 BaseEntity를 catalog 모듈 안의 엔티티에서 사용하는 것 입니다.

어떤 의존성이 어떤 범위로 선언했는지 궁금하다면 아래 명령어로 결과를 볼 수 있습니다. 아래 코드는 commerce-catalog가 commons를 implementation project(":commerce-commons")로 의존한다고 가정하고, 클래스패스에 commerce-commons가 올라왔는지 확인하기 위한 명령이에요.

# 전체 클래스패스 조회
./gradlew :commerce-catalog:dependencies --configuration compileClasspath

commons-catalog 클래스패스 목록

 

특정 라이브러리 의존성은 dependencyInsight 명령으로 확인을 할 수 있습니다. 유용하게 사용하는 방식에요.

# 예: jakarta.persistence-api가 어떻게 들어왔는지
./gradlew :commerce-catalog:dependencyInsight \
  --configuration compileClasspath \
  --dependency jakarta.persistence-api

# 예: spring-data-jpa가 전이되었는지
./gradlew :commerce-catalog:dependencyInsight \
  --configuration compileClasspath \
  --dependency spring-data-jpa

commons와 catalog에서 중복된 hibernate-core → spring-boot-starter-data-jpa

첨부된 그림을 보면 jakarta.persistence-api:3.1.0가 두 경로로 들어와요.
1. hibernate-core → spring-boot-starter-data-jpa (소비자 모듈이 직접 가지는 경로)
2. project :commerce-commons (← commons에서 전이되어 들어온 경로)

트리에서 \--- project :commerce-commons가 보이면, 그 라이브러리는 commons의 api로 공개되어 소비자(여기서는 commerce-catalog)의 compileClasspath로 전이된 거예요. 하지만 jakarta.persistence-api:3.1.0 compileClasspath 가 두 경로가 존재합니다.

 

아래 코드처럼 commons를 api  ➜ implementation 수정하면 catalog로 전이 의존성을 하지 않습니다.

수정된 내용을 반영하기 위해서는 다시 Gradle을 build 합니다.

plugins {
    id 'java-library'
    alias(libs.plugins.depman)
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.boot:spring-boot-dependencies:${libs.versions.spring.boot.get()}"
    }
}

dependencies {
    implementation(libs.jakarta.persistence)     // ← api  ➜ implementation
    implementation(libs.spring.data.commons)     // ← api  ➜ implementation
    implementation(libs.spring.data.jpa)         // ← api  ➜ implementation

    compileOnly(libs.hibernate.core)
    compileOnly(libs.lombok)
    annotationProcessor(libs.lombok)
}

jakarta.persistence-api가 오직 hibernate-core → spring-boot-starter-data-jpa 경로로만 들어오고, project :commerce-commons 경로가 사라졌습니다. → commons에서 전이 끊긴 상태입니다.

catalog의 compileClasspath에 jakarta.persistence-api가 starter 경로 하나만 존재하는지 확인하면 됩니다.

 

api?, implementation? 언제 무엇을 사용하는지 감으로 외우시면 됩니다.

  • 재사용 라이브러리/모듈: java-library
    • api: 공개 API에 드러나는 타입의 의존성 (진짜 최소만)
    • implementation: 내부에서만 쓰는 것들
  • 실행 앱 모듈(Spring Boot 앱 등): 보통 org.springframework.boot(내부적으로 java)만 있으면 충분

애플리케이션이면 java(또는 Spring Boot 플러그인), 공용 라이브러리면 java-library을 사용하고, java-library를 쓰면 api/implementation로 경계를 깨끗하게 나눌 수 있습니다.

 

 

💡BOM(플랫폼)과 카탈로그의 역할 분담

/gradle/libs.versions.toml 파일 내용

스프링/부트 생태계(starter-web, starter-data-jpa 등)는 부트 BOM이 버전을 통째로 관리합니다. 그래서 implementation 'org.springframework.boot:…'에서 버전을 명시하지 않습니다. 단, 외부 라이브러리(Redisson, JJWT, MapStruct, QueryDSL 등)는 TOML의 [versions]에서 버전을 고정하고 [libraries]로 alias를 만들어 사용합니다.