도입
Gradle을 build.gradle 하나로 돌아가는 빌드 도구처럼 보이지만 실제로는 넓은 범위를 다루고 있습니다.
어떤 프로젝트가 존재하는지 정의하고, 어떤 플러그인을 적용할지 결정하며, 어떤 의존성을 어떤 범위로 해석할지 관리하고, 최종적으로 어떤 태스크를 어떤 순서와 조건으로 실행할지 계산합니다.
그래서 Gradle을 이해하면 빌드 시스템 전체를 구조적으로 이해한다고 할 수 있습니다.
필요성
실무에서 빌드는 단순히 결과물 하나를 만들고 끝나는 작업이 아닙니다.
컴파일, 테스트, 패키징, 문서 생성, 코드 품질 검사, 아티팩트 발행, 배포 전 검증까지 모두 빌드 파이프라인 안에서 연결됩니다.
이때 Gradle은 각 작업을 독립된 태스크로 다루고, 의존성 및 입력값을 기준으로 필요한 일만 실행하려고 합니다.
그래서 규모가 커질수록 “빌드 도구를 쓴다”보다 “빌드 시스템을 설계한다”는 관점이 중요해집니다.
- 단일 프로젝트부터 멀티 프로젝트까지 같은 모델로 확장 가능
- 플러그인 기반이라 언어·플랫폼별 기능을 조합하기 쉬움
- 의존성 해석, 태스크 실행, 캐시, 데몬, 툴체인 같은 운영 요소를 함께 다룸
- 로컬 개발과 CI 자동화를 같은 Wrapper 기준으로 맞추기 좋음
정의
Gradle에는 의존성 관리, 태스크 실행, 프로젝트 설정 모델 등의 강력한 핵심 시스템이 들어 있습니다.
하지만 언어별 빌드나 프레임워크별 빌드, 배포 규칙, 코드 생성, 퍼블리싱 같은 구체 기능은 대부분 플러그인으로 확장됩니다.
그래서 core engine + plugins 구조로 확인합니다.
"플러그인이 프로젝트 모델과 태스크를 등록하고
Gradle 엔진이 그 관계를 해석해 실행하는 시스템에 가깝습니다."
핵심 원리
Gradle에서 태스크는 빌드가 수행하는 독립 작업 단위입니다.
예를 들어 컴파일, JAR 생성, 테스트, 퍼블리싱, 문서 생성 같은 작업이 모두 태스크로 표현됩니다.
중요한 점은 태스크가 단독으로 존재하지 않는다는 것입니다.
어떤 태스크는 다른 태스크의 결과를 필요로 하고, 어떤 태스크는 플러그인이 자동으로 추가하며, 어떤 태스크는 사용자가 직접 등록합니다.
결국 빌드는 “무조건 전부 실행”이 아니라 “이번 호출에 필요한 그래프만 계산해서 실행”하는 흐름이 됩니다.
Gradle에서는 빌드 작업도 코드로 모델링되며, 플러그인과 스크립트가 함께 태스크 생태계를 만듭니다.
tasks.register("printBuildInfo") {
group = "help"
description = "현재 빌드 정보를 출력한다"
doLast {
println("Gradle build is running")
}
}
빌드 생명주기
| 단계 | 역할 | 실무 해석 |
|---|---|---|
| Initialization | 어떤 프로젝트들이 이번 빌드에 포함되는지 결정 | settings.gradle(.kts)가 중심이 되는 단계 |
| Configuration | 각 프로젝트의 build script를 평가하고 어떤 작업이 필요한지 계산 | 플러그인 적용, 태스크 등록, 의존성 설정이 여기서 이뤄짐 |
| Execution | 선택된 태스크를 실제로 실행 | 컴파일, 테스트, 패키징, 퍼블리싱 등이 여기서 수행됨 |
이 구조를 모르면 설정 단계에서 이미 무거운 로직이 돌고 있는데, 실행 단계가 느리다고 오해하기 쉽습니다. 반대로 Configuration Cache를 이해하면 왜 어떤 빌드는 설정을 통째로 건너뛰는지 자연스럽게 이해할 수 있습니다.
Gradle Build Lifecycle
1) Initialization
2) Configuration
3) Execution
기본 구조
my-project/
├── settings.gradle.kts
├── build.gradle.kts
├── gradle.properties
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── app/
│ └── build.gradle.kts
└── core/
└── build.gradle.kts
settings.gradle(.kts): 어떤 프로젝트들이 빌드에 포함되는지 정의build.gradle(.kts): 플러그인, 의존성, 태스크, 퍼블리싱 같은 빌드 로직 정의gradle.properties: Gradle 속성, 시스템 속성, 프로젝트 속성 관리gradlew,gradlew.bat: Wrapper 실행 스크립트gradle/wrapper/gradle-wrapper.properties: Wrapper가 사용할 Gradle 배포판 정보
특히 초보자가 가장 많이 헷갈리는 부분은 settings.gradle과 build.gradle의 역할 차이입니다. 전자는 “프로젝트 구조”, 후자는 “프로젝트를 어떻게 빌드할지”에 더 가깝습니다.
기본 구현
// settings.gradle.kts
rootProject.name = "demo-app"
// build.gradle.kts
plugins {
application
}
repositories {
mavenCentral()
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
application {
mainClass.set("com.example.Main")
}
tasks.test {
useJUnitPlatform()
}
./gradlew run, ./gradlew build, ./gradlew tasks 같은 기본 흐름은 이해할 수 있습니다. 중요한 것은 문법보다도 plugin → project model → task 흐름을 보는 습관입니다.패턴 1. 단일 프로젝트 JVM 빌드
단일 프로젝트에서는 보통 프로젝트 성격에 맞는 플러그인을 고르는 것이 먼저입니다. 실행 가능한 프로그램이면 application, 다른 모듈이 소비하는 라이브러리면 java-library가 더 자연스럽습니다.
특히 라이브러리에서는 api와 implementation 차이를 아는 것이 중요합니다. 공개 API에 노출되는 의존성은 api, 내부 구현에서만 쓰는 의존성은 implementation으로 나누는 편이 소비자 compile classpath를 줄이고 불필요한 전파를 막는 데 유리합니다.
plugins {
`java-library`
}
dependencies {
api(project(":api-model")) // 공개 API에 노출되는 경우
implementation(project(":core")) // 내부 구현에만 필요한 경우
}
패턴 2. 멀티 프로젝트 빌드
Gradle은 멀티 프로젝트 빌드를 매우 강하게 지원합니다. 루트 프로젝트 아래에 여러 서브프로젝트를 두고, settings.gradle(.kts)에서 이를 포함시키는 구조가 대표적입니다.
이 방식의 장점은 단순한 폴더 분리 수준이 아닙니다. 프로젝트 간 의존성을 명시적으로 모델링할 수 있고, 어떤 모듈이 바뀌었을 때 어떤 태스크가 다시 실행돼야 하는지도 훨씬 명확해집니다.
// settings.gradle.kts
rootProject.name = "shop-platform"
include("app", "core", "api")
// app/build.gradle.kts
plugins {
application
}
dependencies {
implementation(project(":core"))
implementation(project(":api"))
}
패턴 3. 성능과 운영
| 요소 | 역할 | 실무 해석 |
|---|---|---|
| Wrapper | 프로젝트가 선언한 Gradle 버전으로 실행 | 반드시 ./gradlew 기준으로 통일하는 편이 좋음 |
| Daemon | 백그라운드 JVM을 재사용해 반복 빌드 속도 향상 | 반복 빌드에서 체감 성능에 직접 영향 |
| Build Cache | 이미 만든 task output을 재사용 | 실행 단계의 중복 일을 줄이는 기능 |
| Configuration Cache | 설정 단계 결과를 캐시해 configuration 자체를 건너뜀 | 호환성 확인 후 도입해야 효과가 큼 |
| Toolchains | 컴파일/테스트/실행에 사용할 JDK 도구 집합 지정 | 로컬 JAVA_HOME 차이에서 오는 흔들림을 줄임 |
# gradle.properties
org.gradle.caching=true
org.gradle.parallel=true
# 호환성 검토 후 활성화
# org.gradle.configuration-cache=true
이 다섯 가지는 전부 “빌드가 된다 / 안 된다”보다 “빌드가 얼마나 재현 가능하고 빠르며 운영 가능한가”에 가까운 요소입니다. 프로젝트가 커질수록 이 차이는 체감상 매우 커집니다.
한계와 주의점
Gradle의 가장 큰 장점은 확장성이고, 동시에 가장 큰 함정도 확장성입니다. 공통 설정을 각 모듈의 build.gradle에 복붙하기 시작하면, 작은 수정 하나가 여러 파일에 흩어지고 빌드 구조도 빠르게 흐려집니다.
그래서 규모가 커질수록 공통 build logic은 convention plugin으로 빼고, 가능하면 포함 빌드(build-logic) 쪽으로 분리하는 편이 유지보수에 유리합니다. 또한 Configuration Cache는 성능상 매력적이지만, 기존 플러그인과 빌드 로직이 모두 자동으로 호환되는 것은 아닙니다.
- 공통 로직을 서브프로젝트마다 복붙하면 장기적으로 유지보수가 급격히 나빠짐
- 루트 프로젝트 build script에 모든 것을 몰아넣으면 구조가 불명확해짐
- Configuration Cache는 플러그인 및 빌드 로직 호환성 확인이 선행돼야 함
- Toolchain 없이 로컬 JDK에만 의존하면 개발자 환경 차이가 그대로 빌드 차이로 이어짐
자주 하는 실수
- 설치된
gradle로 실행하고 프로젝트의 Wrapper를 무시함 settings.gradle과build.gradle의 책임을 혼동함- 라이브러리 프로젝트에서
api와implementation을 구분하지 않아 의존성이 과하게 전파됨 - 여러 모듈에 같은 설정을 반복해 적고 convention plugin으로 정리하지 않음
- Configuration Cache를 호환성 점검 없이 바로 전역 활성화함
- JDK 차이를 toolchain이 아니라 개발자 로컬 설치 상태에만 맡김
실무 루틴
- 항상 Wrapper 기준으로 실행한다.
- JDK는 가능하면 toolchain으로 고정한다.
settings.gradle(.kts)는 구조 정의에 집중시킨다.- 각 모듈
build.gradle(.kts)는 해당 모듈 전용 설정만 남기고 작게 유지한다. - 공통 설정은 convention plugin 또는
build-logic로 모은다. - 캐시 기능은 무조건 켜기보다 호환성을 보면서 단계적으로 도입한다.
디버깅
./gradlew --version으로 실제 사용 중인 Gradle과 JVM 조합을 확인한다../gradlew tasks, ./gradlew projects로 구조와 태스크 목록을 본다../gradlew dependencies, dependencyInsight로 실제 의존성 해석 결과를 확인한다.javaToolchains 태스크와 toolchain 설정을 함께 확인한다.help --warning-mode=all 같은 경로로 deprecation 경고를 먼저 수집한다.자주 쓰는 명령어
./gradlew --version
./gradlew tasks
./gradlew projects
./gradlew dependencies
./gradlew dependencyInsight --dependency guava --configuration runtimeClasspath
./gradlew help --warning-mode=all
요약
- ✅ Gradle은 task execution, dependency management, project configuration을 중심으로 동작한다.
- ✅ 실제 기능의 상당수는 plugin이 공급한다.
- ✅ 빌드는 initialization → configuration → execution 순으로 진행된다.
- ✅
settings.gradle(.kts)는 구조,build.gradle(.kts)는 빌드 로직에 가깝다. - ✅ Wrapper는 실행 환경을 통일하는 가장 중요한 기본 장치다.
- ✅ Daemon, Build Cache, Configuration Cache, Toolchain은 성능과 재현성을 좌우한다.
- ✅ 라이브러리 프로젝트에서는
api와implementation구분이 중요하다. - ✅ 규모가 커질수록 공통 build logic은 convention plugin이나
build-logic로 분리하는 편이 좋다.