Gradle 완전 정복
개념부터 Spring Boot 실전 적용까지
Gradle 이란?
Gradle은 거의 모든 타입의 소프트웨어를 빌드할 수 있는 유연성을 목표로 설계된 오픈소스 빌드 자동화 도구입니다. JVM(자바 가상 머신) 위에서 동작하며, Groovy 또는 Kotlin DSL(Domain Specific Language)을 사용하여 빌드 스크립트를 작성합니다.
주요 특징은 다음과 같습니다.
높은 유연성과 확장성: Maven의 XML 기반 설정보다 훨씬 유연하고 가독성이 좋은 코드로 빌드 로직을 작성할 수 있습니다. 필요한 로직을 직접 프로그래밍하여 복잡한 빌드 시나리오에도 쉽게 대응할 수 있습니다.
성능: Gradle은 점진적 빌드(Incremental Build), 빌드 캐시(Build Cache), 데몬 프로세스(Gradle Daemon) 등의 기능을 통해 빌드 속도를 획기적으로 개선했습니다. 한번 빌드된 결과물은 캐싱하여 변경된 부분만 다시 빌드하므로 대규모 프로젝트에서 특히 빛을 발합니다.
DSL (Domain Specific Language): Groovy나 Kotlin을 사용하여 빌드 스크립트를 작성합니다. 이는 단순한 설정 파일이 아니라, 프로그래밍 코드로 빌드 과정을 제어할 수 있음을 의미합니다.
Gradle Wrapper
“어떤 버전의 Gradle을 사용할 것인가”를 보장하는 역할입니다. 프로젝트에 특정 버전의 Gradle을 내장시켜, 개발자나 빌드 서버에 해당 버전의 Gradle이 설치되어 있지 않아도 동일한 버전으로 빌드를 실행할 수 있게 해줍니다. 이는 모든 환경에서 동일한 빌드 결과를 보장하는 빌드 재현성(Reproducibility)의 핵심 요소입니다.
왜 Gradle을 선택해야 하는가?(maven과의 차이점)
Maven도 Gradle과 같은 역할을 하는 빌드 자동화 도구입니다. 다만 아래와 같은 차이점이 있습니다.
구분 | Gradle | Maven |
---|---|---|
스크립트 | Groovy 또는 Kotlin DSL (프로그래밍 방식) | XML (선언적 방식) |
유연성 | 매우 높음. 커스텀 로직 작성 용이 | 낮음. 정해진 라이프사이클과 구조 |
성능 | 점진적 빌드, 빌드 캐시 등으로 매우 빠름 | 상대적으로 느림 |
의존성 관리 | 동적인 의존성 버전 관리, implementation vs api | 정적인 의존성 관리 |
설정 | build.gradle 또는 build.gradle.kts | pom.xml |
위와같은 특징을 가진 Gradle은 마이크로서비스 아키텍처(MSA)나 복잡한 도메인 구조를 가진 프로젝트에서는 Gradle의 유연성과 성능이 큰 장점으로 다가옵니다. 빌드 스크립트 자체를 코드로 관리하며 재사용하고, 테스트하며, 비즈니스 요구사항에 맞게 커스터마이징할 수 있다는 점은 프로젝트의 유지보수성과 생산성을 크게 향상시킵니다.
Gradle의 핵심 개념
Project
빌드의 대상이 되는 단위입니다. 소스코드, 의존성, 빌드 결과물(jar, war 등)을 포함하는 하나의 모듈을 의미합니다. 단일 프로젝트일 수도 있고, 여러 하위 프로젝트(모듈)로 구성된 멀티 프로젝트일 수도 있습니다.
즉, 계층 구조를 가질 수 있습니다. 하나의 루트 프로젝트(Root Project) 아래에 여러 개의 하위 프로젝트(Sub-projects)를 두는 멀티 모듈 프로젝트 구성이 가능합니다. 그리고 각 프로젝트는 자신만의 소스 코드, 의존성, 그리고 실행할 태스크들을 가집니다.
그리고 groovy 문법에 의해 project
는 생략이 가능합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 원래 문법
project.plugins {
id 'application'
}
project.repositories {
mavenCentral()
}
project.dependencies {
implementation ~~~~
}
project.java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
// project 생략됨
plugins {
id 'application'
}
repositories {
mavenCentral()
}
dependencies {
implementation ~~~~
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
Task
빌드 과정에서 실행되는 가장 작은 작업 단위입니다. 컴파일(compileJava), 테스트(test), 패키징(jar 또는 bootJar) 등이 모두 Task에 해당합니다. 입력(Inputs)과 출력(Outputs)을 가지며, 다른 태스크에 의존할 수 있습니다. Gradle은 이 의존 관계를 분석하여 DAG(Directed Acyclic Graph)를 구성하고 순서에 맞게 Task를 실행합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
project.task("prepTask") {
println "configuring prepTask"
doLast() {
println "Doing preparation"
}
}
task publishDockerImage {
ext.version = "1.0.0"
def customVariable = "abc"
println "customVariable ${customVariable}"
doFirst() {
println "publishing DockerImage"
}
}
task deployingDockerImage(dependsOn: publishDockerImage) {
onlyIf { publishDockerImage.version == "1.0.0" }
doFirst() {
println "deploying DockerImage"
}
}
task cleanUp {
doFirst() {
println "cleaning up..."
}
}
task fileTask {
doLast {
def file = project.file('fileTaskSample.txt')
file.text = 'Hello, Gradle!'
println "Created file at ${file.absolutePath}"
}
}
Build Scripts
프로젝트를 어떻게 빌드할지 정의하는 파일입니다. Gradle은 이 스크립트를 실행하여 프로젝트와 태스크를 구성합니다.
- build.gradle 또는 build.gradle.kts: 각 프로젝트의 빌드 방법을 정의합니다. (의존성, 플러그인, 태스크 설정 등)
- settings.gradle 또는 settings.gradle.kts: 멀티 모듈 프로젝트에서 어떤 하위 프로젝트들을 빌드에 포함시킬지 정의합니다.
Plugin
특정 기능을 수행하는 Task들의 집합과 설정을 미리 정의해 놓은 것입니다. 예를 들어, id ‘java’ 플러그인을 적용하면 자바 소스를 컴파일하고 테스트하며 JAR 파일을 만드는 Task들이 자동으로 추가됩니다. Spring Boot 프로젝트에서는 id ‘org.springframework.boot’ 플러그인이 필수적입니다. 빌드 스크립트를 간결하게 유지하고, 재사용 가능한 빌드 로직을 쉽게 추가할 수 있게 해줍니다.
Gradle 빌드 생명주기(Build Lifecycle)와 3가지 단계(Phase)
Gradle 명령어를 실행하면, Gradle은 내부적으로 정해진 생명주기에 따라 작업을 수행합니다. 이 생명주기를 이해하는 것은 빌드 스크립트를 더 깊이 있게 이해하고, 빌드 시간을 최적화하며, 문제가 발생했을 때 효과적으로 디버깅하는 데 매우 중요합니다.
Gradle의 빌드 생명주기는 크게 3가지 단계로 나뉩니다.
1. 초기화 단계 (Initialization Phase)
빌드의 가장 첫 단계입니다. 이 단계의 핵심 목표는 빌드에 참여할 프로젝트를 결정하는 것입니다.
주요 역할
멀티 모듈 프로젝트의 경우, 어떤 하위 모듈(프로젝트)들을 이번 빌드에 포함시킬지 결정합니다.
Project 인스턴스 트리를 생성합니다.
- 핵심 스크립트
- settings.gradle.kts (또는 settings.gradle) 파일이 이 단계에서 실행됩니다. include(“sub-project-a”)와 같은 구문을 읽어 들여 빌드에 포함될 프로젝트 인스턴스(Project instances)를 생성합니다.
- 결과물
- 빌드에 참여하는 모든 Project 객체의 계층 구조가 메모리에 생성됩니다.
2. 구성 단계 (Configuration Phase)
초기화 단계에서 결정된 모든 프로젝트에 대해 빌드 스크립트를 실행하여 전체 Task 그래프(DAG)를 생성하는, 매우 중요한 단계입니다.
주요 역할
빌드에 참여하는 모든 프로젝트의 build.gradle.kts (또는 build.gradle) 파일을 실행합니다.
스크립트가 실행되면서 어떤 Task들이 있는지, Task들 간의 의존 관계(예: jar Task는 compileJava Task에 의존)는 무엇인지 파악합니다.
이 정보를 바탕으로 Task DAG(Directed Acyclic Graph, 방향성 비순환 그래프)를 생성합니다. 이 그래프는 실행해야 할 Task들과 그 실행 순서를 정의한 ‘작업 계획’입니다.
주의할 점: 이 단계에서는 실제 Task의 액션(doFirst, doLast 블록 등)은 실행되지 않습니다. 오직 Task 객체를 생성하고 Task 간의 관계만 설정합니다. 빌드 스크립트에 복잡한 로직이나 I/O 작업이 포함되어 있다면, 실제 Task 실행 전인 이 구성 단계에서 빌드 시간이 길어질 수 있습니다.
3. 실행 단계 (Execution Phase)
구성 단계에서 생성된 Task 그래프를 바탕으로 실제로 Task를 실행하는 마지막 단계입니다. doFirst와 doLast 블록은 모두 Gradle 생명주기의 마지막 단계인 ‘실행(Execution) 단계’에서 실제 Task가 동작할 때 실행됩니다.doFirst와 doLast 블록은 ‘구성(Configuration) 단계’에서는 등록만 될 뿐, 코드가 실행되지는 않습니다.
doFirst
: 해당 Task의 본래 동작(Action)이 실행되기 직전에 추가할 작업을 정의합니다. 가장 먼저 실행될 액션으로 등록됩니다.doLast
: 해당 Task의 본래 동작(Action)이 완료된 직후에 추가할 작업을 정의합니다. 가장 마지막에 실행될 액션으로 등록됩니다.실행 순서
- doFirst 블록에 등록된 액션들 (추가된 역순으로 실행)
- Task의 메인 액션
- doLast 블록에 등록된 액션들 (추가된 순서대로 실행)
예시
tasks.named("bootJar") {
doFirst {
println("## bootJar Task 시작! ##") // 1번으로 실행
}
doLast {
println("## bootJar Task 완료! ##") // 3번으로 실행
}
// bootJar의 원래 동작(JAR 파일 생성)이 2번으로 실행됨
}
주요 역할
사용자가 요청한 Task(예: build, bootJar)와 그에 의존하는 모든 Task를 실행합니다.
Up-to-Date Check (최신 상태 확인): Gradle의 핵심 성능 기능 중 하나로, 각 Task를 실행하기 전에 입력(소스 파일 등)과 출력(컴파일된 클래스 파일 등)이 이전 빌드와 비교하여 변경되었는지 확인합니다. 변경 사항이 없으면 해당 Task를 실행하지 않고 UP-TO-DATE로 표시하여 빌드 시간을 크게 단축시킵니다.
실제 코드 컴파일, 테스트 실행, 파일 복사, JAR 파일 생성 등의 작업이 이 단계에서 수행되어 빌드 결과물을 생성합니다.
Gradle 빌드 생명주기를 알아야하는 이유
빌드 성능 최적화: 빌드 속도가 느리다면 어느 단계에서 시간이 오래 걸리는지 파악해야 합니다. 만약 ‘Configuration’ 시간이 길다면, build.gradle.kts 스크립트 내에 불필요하게 무거운 연산이 없는지 확인하고 최적화할 수 있습니다.
정확한 디버깅: 에러가 발생했을 때, 에러 메시지가 “during configuration phase”와 같이 나온다면 build.gradle.kts 파일 자체의 로직 오류일 가능성이 높습니다. 반면 “execution failed for task ‘:test’“와 같이 나온다면 test Task가 실행되는 도중 실제 테스트 코드 등에서 문제가 발생한 것입니다.
고급 기능 활용: Gradle의 Configuration Caching과 같은 고급 성능 최적화 기능을 이해하고 활용하려면, 구성 단계와 실행 단계의 분리를 명확히 이해해야 합니다.
Gradle Configuration Caching이란?
Configuration Caching은 Gradle의 빌드 생명주기 중 ‘구성(Configuration) 단계’의 결과물(Task 그래프) 전체를 재사용하는 매우 강력한 성능 최적화 기능입니다.
일반적인 빌드에서는 코드를 변경하지 않아도 매번 build.gradle 스크립트를 실행하여 Task 그래프를 새로 생성하는 ‘구성 단계’를 거칩니다. 프로젝트가 커지고 빌드 로직이 복잡해질수록 이 구성 단계 자체에 소요되는 시간이 늘어납니다.
Configuration Caching을 활성화하면, 첫 빌드에서 생성된 Task 그래프의 스냅샷을 저장해 둡니다. 그리고 다음 빌드부터는 빌드 스크립트나 환경에 변경이 없을 경우, 이 ‘구성 단계’ 전체를 건너뛰고 저장된 Task 그래프를 즉시 로드하여 ‘실행(Execution) 단계’로 넘어갑니다.
gradle.properties 파일에 한 줄을 추가하여 활성화할 수 있습니다.(org.gradle.configuration-cache=true
)
Maven과 Gradle의 Build Lifecycle의 차이
Maven과 Gradle의 Lifecycle 철학 차이는 ‘규칙(Convention)’과 ‘유연성(Flexibility)’ 사이의 접근 방식 차이로 요약할 수 있습니다.
구분 | Maven | Gradle |
철학 | 엄격한 규칙 기반 (Convention over Configuration) | 유연한 작업 기반 (Task-based Flexibility) |
구조 | 정해진 순서의 선형적인 단계(Phase)들의 집합. (validate → compile → test → package…) 각 단계는 미리 정의된 목표(goal)들을 실행합니다. | Task들의 의존 관계 그래프(DAG). 정해진 순서가 없으며, Task 간의 의존성에 따라 동적으로 실행 순서가 결정됩니다. |
핵심 개념 | Build Phase (빌드 단계) | Task (독립적인 작업 단위) |
장점 | 구조가 명확하고 예측 가능하여 배우기 쉽고, 어떤 Maven 프로젝트든 구조가 거의 동일합니다. | 빌드 로직을 코드로 자유롭게 작성할 수 있어 복잡하고 특수한 빌드 요구사항에 쉽게 대응할 수 있으며, 성능이 우수합니다. |
단점 | 정해진 틀을 벗어나는 커스텀 로직을 추가하기 복잡하고 장황합니다. | 높은 자유도만큼 빌드 스크립트의 복잡도가 높아질 수 있으며, 초기 학습 곡선이 가파를 수 있습니다. |
Gradle Daemon
Gradle Daemon은 빌드 성능을 향상시키기 위해 백그라운드에서 실행되는 장기 실행 프로세스(long-lived process)입니다.
기본적으로 Gradle은 자바(JVM) 위에서 동작합니다. Daemon이 없다면, gradle build와 같은 명령어를 실행할 때마다 새로운 JVM을 시작하고, Gradle 클래스를 로드하고, 프로젝트 정보를 분석하는 과정을 매번 반복해야 합니다. 이 과정은 특히 작은 빌드에서도 수 초의 오버헤드를 발생시킵니다.
Daemon은 이러한 반복적인 시작 비용을 없애기 위해 한 번 실행된 후 메모리에 상주하며 다음 빌드 요청을 대기합니다.
Daemon이 빌드를 빠르게 하는 이유
Gradle Daemon은 크게 두 가지 방식으로 빌드 속도를 향상시킵니다.
- JVM 부팅 비용 절감
- 가장 큰 이점입니다. JVM을 시작하고 필요한 클래스들을 로드하는 과정은 생각보다 많은 리소스를 소모합니다. Daemon은 이 JVM 프로세스를 계속 살아있는 상태로 유지함으로써, 매 빌드마다 발생하는 JVM 시작 오버헤드를 최초 한 번으로 줄여줍니다.
- 빌드 정보 메모리 캐싱
- Daemon은 단순한 JVM 재활용을 넘어, 빌드에 대한 정보를 메모리에 캐싱합니다.
- 프로젝트 구조: 어떤 하위 모듈들이 있는지, 각 모듈의 소스 파일은 어디에 있는지 등의 프로젝트 구조 정보를 메모리에 유지합니다.
- 컴파일된 빌드 스크립트: build.gradle.kts와 같은 빌드 스크립트의 컴파일 결과물을 캐싱하여 다음 빌드 시 재사용합니다.
- 그 외 데이터: 파일 시스템 스냅샷 등 빌드에 필요한 다양한 데이터를 메모리에 올려두어 디스크 I/O를 줄입니다.
이 덕분에 연속적인 빌드는 이전 빌드의 정보를 최대한 재활용하여 훨씬 빠르게 실행될 수 있습니다.
Gradle Daemon의 장점
Gradle Daemon은 특히 반복적인 빌드 작업(코드 수정 -> 빌드 -> 테스트)이 잦은 개발 환경에서 체감 성능이 크게 향상됩니다. 그리고 매번 프로세스를 띄우고 내리는 비용을 절감합니다.
Gradle 3.0 이상부터는 기본적으로 활성화되어 있어 별도 설정 없이 사용 가능합니다.
Daemon 관리하기
일반적으로 Daemon은 자동으로 관리되지만, 필요에 따라 직접 제어할 수 있습니다.
프로젝트 루트 또는 Gradle 홈 디렉터리의 gradle.properties 파일에서 Daemon 사용 여부를 명시적으로 제어할 수 있습니다. (true/false)
1
org.gradle.daemon=true
CI/CD 환경에서 이 옵션을 고려해야합니다.
과거에는 CI/CD 환경에서 Daemon을 비활성화(org.gradle.daemon=false)하는 것이 일반적인 권장 사항이었습니다.
그 이유는 CI 빌드는 보통 깨끗한 환경(예: 새로운 Docker 컨테이너)에서 실행되므로, Daemon이 다음 빌드를 위해 살아남아 캐시를 재사용할 이점이 적은 일회성 환경입니다.
그리고, 빌드가 끝난 후 Daemon 프로세스가 좀비처럼 남아 시스템 리소스를 불필요하게 점유하는 것을 방지하기 위함(리소스 정리)입니다.
하지만 최신 CI/CD 도구와 Gradle의 발전으로 인해 Daemon을 활성화하는 것이 더 유리한 경우도 많아졌습니다.
하나의 빌드 서버(에이전트)가 여러 빌드를 순차적으로 처리하는 경우, Daemon을 활성화하면 후속 빌드에서 JVM 부팅 및 캐싱의 이점을 누릴 수 있어 전체적인 빌드 처리량이 향상됩니다. 그리고,Gradle의 빌드 캐시(org.gradle.caching=true)와 함께 사용될 때, Daemon은 캐시된 데이터를 메모리에 유지하여 더 빠른 접근을 제공하므로 빨라집니다.
컨테이너 기반의 일회성 빌드 환경 (예: Jenkins on Kubernetes, GitHub Actions의 기본 러너): 비활성화(–no-daemon) 하는 것이 리소스 관리 측면에서 더 깔끔하고 예측 가능할 수 있으나, 여러개의 task가 묶여 있거나 고정된 빌드 서버/에이전트 환경 (예: 고정된 EC2 인스턴스에서 Jenkins 에이전트 실행)에서는 Daemon을 활성화하여 빌드 속도 향상의 이점을 취하는 것을 고려해볼 만합니다.
Gradle Daemon 메모리 설정
Gradle Daemon이 사용하는 메모리(정확히는 JVM 힙 메모리)는 gradle.properties 파일에서 JVM 옵션을 통해 직접 설정할 수 있습니다. 이는 애플리케이션의 규모나 개발 환경의 사양에 맞춰 빌드 성능을 최적화하는 데 매우 중요합니다.
프로젝트 루트 또는 Gradle 홈 디렉터리(~/.gradle/)의 gradle.properties 파일에 다음 속성을 추가합니다.
1
2
3
4
# Gradle Daemon의 JVM 인수를 설정합니다.
# -Xmx: 최대 힙 크기 (Max Heap Size)
# -Xms: 시작 힙 크기 (Initial Heap Size)
org.gradle.jvmargs=-Xmx2g -Xms512m -XX:MaxMetaspaceSize=512m
CI 서버와 같이 여러 빌드가 동시에 실행될 수 있는 환경에서는 시스템 전체 리소스를 고려하여 적절한 값으로 제한하는 것이 좋습니다.
Gradle Daemon 명령어
./gradlew build --no-daemon
: 임시 비활성화. gradle.properties 설정과 관계없이 현재 빌드에서만 Daemon을 사용하지 않습니다../gradlew --status
: Daemon 상태 확인. 현재 실행 중인 Daemon 프로세스의 상태(PID, 상태, 마지막 사용 시간 등)를 확인할 수 있습니다../gradlew --stop
: 모든 Daemon 중지. 실행 중인 모든 Gradle Daemon 프로세스를 강제로 종료시킵니다.
실행과정 요약 : ./gradlew build 명령어를 실행하면
위에서 정리한 내용을 토대로 ./gradlew build
를 실행하면 어떤 동작이 이뤄지는지 정리해보았습니다.
./gradlew build
명령어를 실행하면, Wrapper가 먼저 동작합니다.Wrapper는 gradle/wrapper/gradle-wrapper.properties 파일을 확인하여 프로젝트에 지정된 버전의 Gradle이 로컬 캐시에 있는지 확인하고, 없으면 다운로드합니다.
지정된 버전의 Gradle 실행을 준비한 후, Daemon 설정을 확인합니다.
Daemon이 활성화되어 있다면, Wrapper는 해당 버전의 Gradle을 실행할 Daemon 프로세스가 있는지 확인하고, 있으면 그 Daemon에게 빌드 작업을 위임합니다. 만약 없다면 새로운 Daemon 프로세스를 시작합니다.
Spring Boot 프로젝트에 Gradle 적용하기
이제 실제 Spring Boot 프로젝트에서 Gradle을 어떻게 사용하는지 build.gradle 파일을 중심으로 살펴보겠습니다.
build.gradle (Groovy vs Kotlin DSL) 파일 완벽 분석 최신 Spring Initializr에서는 Kotlin DSL(build.gradle.kts)을 기본으로 권장하는 추세입니다. IDE의 자동완성, 리팩토링 등 강력한 지원을 받을 수 있어 Groovy보다 생산성이 높습니다.
아래는 일반적인 Spring Boot 프로젝트의 build.gradle.kts 예시입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// build.gradle.kts
// 1. Plugins: 프로젝트에 적용할 플러그인들을 선언합니다.
plugins {
// Java 17 버전을 사용합니다.
java
// Spring Boot 플러그인을 적용합니다. 의존성 관리, 실행 가능한 JAR 패키징 등을 지원합니다.
id("org.springframework.boot") version "3.3.1"
// Spring 의존성 관리 플러그인입니다. Spring Boot 버전에 맞는 라이브러리 버전을 자동으로 관리해줍니다.
id("io.spring.dependency-management") version "1.1.5"
// Kotlin을 JVM 환경에서 사용하기 위한 플러그인입니다.
kotlin("jvm") version "1.9.24"
// JPA 사용을 위한 플러그인입니다.
kotlin("plugin.jpa") version "1.9.24"
// Spring 관련 Kotlin 지원 플러그인입니다.
kotlin("plugin.spring") version "1.9.24"
}
// 2. Group & Version: 프로젝트의 그룹 ID와 버전을 지정합니다.
group = "com.example"
version = "0.0.1-SNAPSHOT"
// 3. Java Version: 사용할 Java 버전을 설정합니다.
java {
sourceCompatibility = JavaVersion.VERSION_17
}
// 4. Repositories: 의존성을 다운로드할 원격 저장소를 지정합니다.
repositories {
mavenCentral() // Maven 중앙 저장소를 사용합니다.
}
// 5. Dependencies: 프로젝트에서 사용할 라이브러리 의존성을 선언합니다.
dependencies {
// Spring Boot
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-validation")
// Database
runtimeOnly("com.mysql:mysql-connector-j")
runtimeOnly("org.postgresql:postgresql")
// Redis, Kafka, Elasticsearch 등...
implementation("org.springframework.boot:spring-boot-starter-data-redis")
implementation("org.springframework.kafka:spring-kafka")
// implementation("org.springframework.boot:spring-boot-starter-data-elasticsearch")
// Test
testImplementation("org.springframework.boot:spring-boot-starter-test")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
// 6. Tasks: 특정 Task에 대한 설정을 커스터마이징합니다.
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict"
jvmTarget = "17"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
plugins
: 프로젝트에 필요한 기능(Java, Spring Boot, Kotlin 등)을 플러그인 형태로 추가합니다.repositories
: dependencies에 선언된 라이브러리들을 어디서 다운로드할지 지정합니다. 보통 mavenCentral()을 사용합니다.dependencies
: 이 프로젝트가 의존하는 라이브러리 목록입니다.implementation
: 현재 모듈 내부에서만 사용하는 의존성입니다.(다른 곳에서 가져다 쓸때 내가 뭘 썻는지 볼 수 있다.) 이 의존성이 변경되어도 이 모듈을 의존하는 다른 모듈은 재컴파일되지 않아 빌드 속도에 유리합니다. 가장 기본적으로 사용하는 방식입니다.api
: 현재 모듈의 API(public 메서드 등)에 노출되는 의존성입니다.(다른 곳에서 가져다 쓸때 내가 뭘 썻는지 볼 수 있다.)이 의존성이 변경되면 이 모듈을 의존하는 다른 모듈까지 모두 재컴파일됩니다. 멀티 모듈 환경에서 공통 라이브러리를 외부에 노출해야 할 때 사용합니다.runtimeOnly
: 런타임에만 필요한 의존성입니다. JDBC 드라이버가 대표적인 예입니다.testImplementation
: 테스트 코드에서만 사용하는 의존성입니다.
자주 사용하는 Gradle Task 명령어
터미널이나 IDE의 Gradle 탭에서 다음 명령어들을 실행할 수 있습니다. (./gradlew는 Gradle Wrapper를 사용하는 명령어입니다.)
./gradlew build
: 프로젝트를 빌드합니다. (컴파일, 테스트, 패키징 모두 수행)./gradlew build -x {task}
: 특정 task를 제외하고 빌드를 실행합니다.(ex. ./gradlew build -x test : test를 제외하고 빌드 실)./gradlew bootJar
: 실행 가능한 Spring Boot JAR 파일을 생성합니다../gradlew bootRun
: 애플리케이션을 실행합니다../gradlew test
: 테스트를 실행합니다../gradlew clean
: build 디렉터리를 삭제하여 빌드 결과물을 초기화합니다../gradlew tasks --all
: 실행 가능한 모든 Task 목록을 보여줍니다.(어떤 task를 사용할 수 있는지 확인하거나 커스텀 task가 잘 등록되었는지 볼 때)./gradlew dependencies
: 프로젝트의 의존성 트리를 출력합니다../gradlew --refresh-dependencies
: 의존성을 강제로 다시 다운로드합니다.(원격 저장소의 SNAPSHOT 버전이 갱신되었거나 캐시 문제를 해결하고 싶을 때)./gradlew build --scan
: 빌드를 실행하고 결과를 웹 기반 리포트로 생성합니다.(빌드가 느린 원인을 분석하거나 빌드 실패 원인을 상세히 파악하고 공유할 때)
멀티 모듈 프로젝트 구성 및 관리
DDD(도메인 주도 설계)나 클린 아키텍처를 적용하다 보면 자연스럽게 프로젝트를 여러 모듈로 분리하게 됩니다. 예를 들어 api, core, infrastructure 와 같이 역할을 기준으로 모듈을 나눌 수 있습니다.
1. settings.gradle.kts
루트 프로젝트의 settings.gradle.kts 파일에 포함할 하위 모듈들을 정의합니다.
1
2
3
4
5
6
// settings.gradle.kts
rootProject.name = "my-project"
include("my-project-api")
include("my-project-core")
include("my-project-infrastructure")
2. 루트 build.gradle.kts
모든 하위 모듈에 공통으로 적용될 설정을 정의합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// root build.gradle.kts
plugins {
// ... 공통 플러그인
}
// 모든 하위 프로젝트에 적용될 설정
// allprojects 써도 가능.
// 다만 subprojects는 루트 build.gradle이 excution되지않음. allprojects는 루트 build.gradle이 excution됨.
subprojects {
// 반복되는 부분이라 루트 build.gradle에서 하위 모듈로 내려줄 수 있다.
apply(plugin = "java")
apply(plugin = "org.springframework.boot")
apply(plugin = "io.spring.dependency-management")
apply(plugin = "kotlin.jvm")
group = "com.example"
version = "0.0.1-SNAPSHOT"
java {
sourceCompatibility = JavaVersion.VERSION_17
}
repositories {
mavenCentral()
}
// ... 공통 의존성 등
}
3. 하위 모듈 build.gradle.kts
각 모듈은 필요한 의존성을 개별적으로 추가합니다.
1
2
3
4
5
6
7
// my-project-api/build.gradle.kts
dependencies {
// core 모듈에 의존
implementation(project(":my-project-core"))
implementation("org.springframework.boot:spring-boot-starter-web")
}
커스텀 Task 작성하기
Gradle의 강력한 기능 중 하나는 필요한 작업을 Task로 직접 만들어 자동화할 수 있다는 것입니다. 예를 들어, 빌드 시 프론트엔드 리소스를 특정 디렉토리로 복사하는 Task를 만들 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 커스텀 Task 정의
abstract class CopyFrontendTask : DefaultTask() {
@get:InputDirectory
abstract val from: DirectoryProperty
@get:OutputDirectory
abstract val to: DirectoryProperty
@TaskAction
fun copy() {
project.copy {
from(from)
into(to)
}
}
}
// Task 등록 및 설정
tasks.register<CopyFrontendTask>("copyFrontendFiles") {
from.set(layout.projectDirectory.dir("../frontend/build"))
to.set(layout.buildDirectory.dir("resources/main/static"))
}
// bootJar Task가 실행되기 전에 copyFrontendFiles Task가 먼저 실행되도록 의존성 설정
tasks.named("bootJar") {
dependsOn("copyFrontendFiles")
}
Gradle Version Catalogs: 버전관리
멀티 모듈 프로젝트가 커질수록 각 모듈의 build.gradle.kts에 흩어져 있는 라이브러리 버전들을 관리하기가 까다로워집니다. 버전 충돌이 발생할 수도 있고, 전체 라이브러리 버전을 한 번에 올리는 것도 번거로운 작업이 됩니다. Version Catalogs는 이러한 문제를 우아하게 해결합니다.
중앙 관리: 모든 의존성(라이브러리, 플러그인)과 버전 정보를 libs.versions.toml 파일 한 곳에서 관리합니다.
타입 안정성: IDE의 자동완성 기능을 통해 오타를 방지하고, 정의된 의존성에 안전하게 접근할 수 있습니다.
가독성 및 유지보수성 향상: build.gradle.kts 파일이 훨씬 간결해지고, 버전 업데이트가 용이해집니다.
번들링: 여러 의존성을 하나의 ‘번들’로 묶어 한 번에 추가할 수 있습니다.
적용 방법
- gradle/libs.versions.toml 파일 생성
- 프로젝트 루트의 gradle 디렉터리 아래에 libs.versions.toml 파일을 생성합니다.
- libs.versions.toml 파일 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 버전 변수를 정의합니다.
[versions]
springBoot = "3.3.1"
springDependencyManagement = "1.1.5"
kotlin = "1.9.24"
querydsl = "5.1.0"
# 라이브러리 정보를 정의합니다. (alias = "group:artifact:version")
[libraries]
spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "springBoot" }
spring-boot-starter-data-jpa = { module = "org.springframework.boot:spring-boot-starter-data-jpa", version.ref = "springBoot" }
querydsl-jpa = { module = "com.querydsl:querydsl-jpa", version.ref = "querydsl" }
querydsl-apt = { module = "com.querydsl:querydsl-apt", version.ref = "querydsl" }
# 플러그인 정보를 정의합니다.
[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "springBoot" }
spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "springDependencyManagement" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
# 여러 라이브러리를 묶어서 관리합니다.
[bundles]
querydsl = ["querydsl-jpa", "querydsl-apt"]
- build.gradle.kts에서 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// plugins 블록에서 사용
plugins {
alias(libs.plugins.spring.boot)
alias(libs.plugins.kotlin.jvm)
}
dependencies {
// 단일 라이브러리 사용
implementation(libs.spring.boot.starter.web)
implementation(libs.spring.boot.starter.data.jpa)
// 번들로 묶은 라이브러리 사용
implementation(libs.bundles.querydsl)
}
Test Fixtures: 재사용 가능한 테스트 코드의 분리
여러 모듈에서 공통으로 사용해야 하는 테스트용 헬퍼 클래스, DTO, 테스트 데이터 생성 로직 등이 있을 때 Test Fixtures 기능을 사용하면 매우 유용합니다. 이는 테스트 코드를 위한 일종의 ‘공유 라이브러리’를 만드는 기능입니다.
특징
중복 제거: 여러 모듈에 흩어져 있는 공통 테스트 코드를 한 곳으로 모아 관리합니다.
관심사 분리: 프로덕션 코드(src/main)에 테스트용 코드가 섞이는 것을 방지하고, 순수 테스트 코드(src/test)와도 분리하여 관리할 수 있습니다.
적용 방법
java-test-fixtures 플러그인 적용
- 공유할 테스트 코드를 담을 모듈(예: my-project-core)의 build.gradle.kts에 플러그인을 추가합니다.
1 2 3 4
// my-project-core/build.gradle.kts plugins { `java-test-fixtures` }
공유 테스트 코드 작성
- 해당 모듈의 src/testFixtures/java 디렉터리에 공유할 테스트 클래스를 작성합니다.
1 2 3 4 5 6 7 8
// my-project-core/src/testFixtures/java/com/example/TestObjectFactory.java package com.example; public class TestObjectFactory { public static Member createMember(String name) { return new Member(name, 20); } }
다른 모듈에서 사용
- 공유 코드가 필요한 다른 모듈(예: my-project-api)의 build.gradle.kts에 의존성을 추가합니다.
1 2 3 4 5
// my-project-api/build.gradle.kts dependencies { // core 모듈의 testFixtures를 참조합니다. testImplementation(testFixtures(project(":my-project-core"))) }
Composite Builds: 독립적인 프로젝트를 함께 빌드하기
라이브러리 프로젝트와 해당 라이브러리를 사용하는 애플리케이션 프로젝트를 동시에 개발하고 테스트해야 하는 상황이 있습니다. 보통은 라이브러리를 빌드하여 로컬 저장소(mavenLocal())에 publish하고 애플리케이션에서 그 버전을 가져와야 해서 번거롭습니다. Composite Builds는 이 과정을 획기적으로 개선합니다.
장점
개발 편의성: 라이브러리를 로컬에 배포하는 과정 없이, 라이브러리 코드 변경 사항을 애플리케이션에서 즉시 확인하고 디버깅할 수 있습니다.
독립성 유지: 각 프로젝트는 독립적인 Gradle 프로젝트로 유지됩니다.
적용 방법
애플리케이션 프로젝트의 settings.gradle.kts 파일에 함께 빌드할 라이브러리 프로젝트의 경로를 추가하기만 하면 됩니다.
1
2
3
4
5
6
// my-application/settings.gradle.kts
rootProject.name = "my-application"
// 함께 빌드할 라이브러리 프로젝트의 상대 경로를 지정합니다.
includeBuild("../my-library")
이제 애플리케이션의 build.gradle.kts에 라이브러리 의존성이 선언되어 있다면, Gradle은 로컬 저장소 대신 includeBuild로 지정된 프로젝트를 소스로 사용하여 의존성을 자동으로 대체합니다.
빌드 성능 최적화 팁
Gradle Daemon 활성화
: org.gradle.daemon=true를 gradle.properties에 추가하여 데몬을 항상 실행 상태로 둡니다. (기본값)병렬 실행 활성화
: org.gradle.parallel=true를 gradle.properties에 추가하여 멀티 모듈 프로젝트에서 병렬로 Task를 실행합니다.빌드 캐시 사용
: org.gradle.caching=true를 gradle.properties에 추가하여 빌드 결과물을 캐싱하고 재사용합니다.implementation 사용
: 불필요한 api 사용을 줄이고 implementation을 사용하여 모듈 간의 결합도를 낮추고 불필요한 재컴파일을 방지합니다.--build-scan 옵션 활용
: ./gradlew build –scan 명령어를 사용하면 빌드 과정에 대한 상세한 리포트를 웹에서 확인할 수 있어 병목 지점을 쉽게 찾을 수 있습니다.
마치며
지금까지 Gradle의 기본 개념부터 Spring Boot 프로젝트에서의 실전 활용, 그리고 심화 내용까지 깊이 있게 다뤄보았습니다. Gradle은 단순히 의존성을 관리하고 빌드를 실행하는 도구를 넘어, 프로젝트의 구조를 정의하고 개발 프로세스를 자동화하며 생산성을 극대화하는 강력한 플랫폼입니다.