05. Gradle ( 2012 - )
Gradle은 Ant, Maven과 같은 이전 세대 빌드 도구의 단점을 보완하고 장점을 취합해 만든 오픈 소스 빌드 도구 프로젝트다. 기존 도구들이 xml과 같은 마크업 언어로 프로젝트를 관리했던 것과는 달리 Groovy기반의 DSL(Domain Specific Language)를 이용해 프로젝트를 관리한다. 최근에는 Kotlin DSL을 사용할 수 있게 되어서 Groovy기반 DSL의 아쉬운 부분(예를 들어 자동 완성 안됨)도 보완되었다. 또한 Gradle Wrapper를 이용해 Gradle이 설치되어 있지 않은 시스템에서도 프로젝트 빌드가 가능하다.
뒤늦게 개발된 빌드 도구라서 그런지 기존 도구를 대체할 많은 장점을 가지고 있다. Maven, Ivy같은 기존 저장소 인프라를 그대로 사용할 수 있으며, pom.xml, ivy.xml 파일에 대한 마이그레이션이 가능하므로 기존에 작성된 레거시 프로젝트를 쉽게 Gradle 프로젝트로 전환할 수 있다. Gradle은 Java뿐만 아니라 C++, Groovy, Kotlin, Android, Scala, Javascript 등의 언어로 만들어진 프로젝트에 대해서도 빌드를 지원하며, 특히 Google은 Gradle을 Android용 공식 빌드 도구로 채택했다.
Gradle이 빠른 이유
Gradle 공식 홈페이지에 가보면 Maven과의 성능 비교를 보여주는 아티클이 있다. Gradle에 따르면 Maven과 빌드 시간을 비교했을 때 최소 2배에서 크게는 100배까지 더 빠르다고 한다.
그래프를 보면 총 세 가지 방법으로 테스트를 진행했다. (1) Test단계를 포함한 클린 빌드 (2) 앞단계에서 캐싱 기능을 포함했을 경우 (3) 하나의 파일만 교체된 경우. 대부분의 경우에서 Gradle이 빠른 것을 볼 수 있다. 특히 캐싱 기능을 포함하거나 하나의 파일만을 교체한 뒤 컴파일을 진행할 경우에는 속도 차이가 크게 나는 것을 알 수 있다. 이유가 뭘까?
Gradle이 설명하는 Maven보다 빠른 이유는 Work avoidance와 Incrementality 메커니즘이다. 크게 세 가지 특징이 Maven과의 차별점이다.
(1) Incrementality (증분 빌드)
- Gradle은 기존에 빌드되어 있는 파일들을 다시 빌드할 때, 모두 새롭게 빌드하는 것이 아니라 기존과 비교해 달라진 점이 있는 파일들만 따로 빌드한다. 예를 들어 1000개의 파일로 된 프로젝트를 빌드해야 할 때 만약 2개의 파일만 수정됐다면 2개의 파일만 새롭게 빌드한다.
- Gradle은 빌드가 처음 실행될 때 입력 파일(소스 코드)의 지문(fingerprint)를 추출한다. 이 '지문'은 입력 파일의 경로와 파일 컨텐츠의 해시를 담고 있다. 빌드가 성공적으로 마무리되면 Gradle은 출력 파일(컴파일된 파일)의 지문을 추출한다. 이 지문에도 출력 파일 세트와 각 파일 컨텐츠의 해시를 담고 있다. 그리고 다음 빌드가 실행될 때까지 두 지문을 모두 가지고 있다가 다음 빌드의 입력 파일 지문이 이전 지문과 동일하다면 Gradle은 출력에 변경사항이 없다고 판단하고 작업을 건너 뛴다.
(2) Build Cache (빌드 캐시)
- 위의 증분 빌드에서 설명한 것처럼 Gradle은 입력 파일과 출력 파일의 지문을 캐시에 유지해 다음 빌드에서 재사용한다
(3) Gradle Daemon (Gradle 데몬)
- Gradle은 위의 메커니즘들을 관리하는 백그라운드 프로세스를 사용한다. Gradle은 Groovy로 개발된 프로젝트로 Groovy 또한 JVM환경에서 실행된다. 따라서 초기 Gradle 실행 시 적지 않은 시간이 소요된다. 이 문제를 해결하기 위해서 초기 실행 시 Gradle Daemon을 백그라운드에서 계속 유지시키는 것이다.
- Gradle Daemon은 빌드 전반에 걸쳐서 명령줄 인수(Command Line Arguments), 프로젝트 디렉토리, 환경 변수와 같은 프로젝트 정보들을 캐싱하고 어떤 파일을 다시 빌드해야 하는지 계산하는 등의 작업을 담당한다.
gradle --status
위 명령어를 입력하면 현재 백그라운드 프로세스에서 활성화된 Gradle Daemon을 확인할 수 있다.
시작하기
위에서 언급한 것처럼 Gradle은 Gradle Wrapper를 이용해 Gradle 바이너리를 직접 설치하지 않고도 해당 프로젝트를 빌드할 수 있다. 우선 Gradle 프로젝트를 만들어보자. Gradle을 설치하고 다음과 같은 명령어를 입력하면 Gradle 프로젝트 생성 과정이 진행된다.
gradle init
프로젝트 구조
Gradle 빌드 스크립트 DSL(Domain Specific Language)는 기본적으로 Groovy지만 Gradle 3.0부터 Kotlin이 도입되었고 2023년 4월에 공식 DSL로 채택되었으므로 Kotlin으로 진행한다. 프로젝트 최외부에 gradlew와 gradlew.bat 실행파일이 있고, gradle/wrapper 아래에 gradle-wrapper.jar이 있다. gradlew는 Linux와 MacOS용 쉘 스크립트이며, gradlew.bat은 Windows 배치 스크립트다. 두 스크립트 모두 먼저 JDK 경로를 찾아 JVM을 실행하고 gradle-wrapper.jar을 실행한다. gradle-wrapper.jar은 gradle-wrapper.properties를 참고해 Gradle 배포판을 로컬에 다운로드한다. gradle-wrapper.properties를 보자
distributionUrl에 특정 버전이 명시되어 있다. 따라서 이 정보가 있으면 Gradle 바이너리 파일이 굳이 로컬에 설치되어 있지 않더라도 Gradle Wrapper에 의해 프로젝트에 종속된 특정 Gradle 버전을 사용할 수 있다. Gradle에서도 안정적이고 표준화된 빌드를 위해 로컬에 설치한 Gradle이 아닌 각 프로젝트에 있는 gradlew를 이용할 것을 권장한다.
그 밖에 프로젝트의 하위 프로젝트 리스트를 정의할 수 있는 settings.gradle.kts, 프로젝트 전용 캐시 디렉토리인 .gradle 등이 있다.
빌드 및 라이프 사이클
Gradle의 빌드 방법은 아주 간단하다.
./gradlew build
./gradlew.bat build
빌드 결과를 보면 Gradle 또한 일련의 빌드 과정인 라이프 사이클이 있다. Gradle은 작업을 실행하기 전에 우선 작업 그래프(Task Graph)를 만들어 현재 빌드에 필요한 작업(Task)만을 포함하도록 구성한다. 또 빌드에 참여하는 각 프로젝트마다 DAG(Directed Acyclic Graph)를 만들어 작업간의 종속성을 평가하고 빌드를 순서에 따라 실행할 수 있도록 한다.
Gradle의 빌드는 초기화(Initialization), 구성(Configuration), 실행(Execution) 세 단계로 실행된다.
초기화 단계에서는 최상위 폴더에 있는 settings.gradle.kts를 확인해 빌드에 참여할 프로젝트를 결정하고 Project 인스턴스를 생성한다. 구성 단계에서는 빌드에 참여할 각 프로젝트의 빌드 스크립트를 모두 평가해 작업 그래프를 생성한다. 이 과정에서 각 작업(Task) 안에 작성한 코드들이 실행된다. 그 중 doFirst나 doLast 같이 작업(Task)내에 다시 작성하는 코드 블록은 수행단계에서 실행되도록 작업이 예약된다. 실행 단계에서는 종속성 순서에 따라 선택한 작업을 예약하고 실제로 실행한다. Gradle 공식 문서에 있는 예제를 좀 더 보기 쉽게 한글로 수정해서 확인해보자
// settings.gradle.kts
rootProject.name = "basic"
println("이 구문은 초기화 단계에서 실행됩니다.")
// build.gradle.kts
println("이 구문은 구성 단계에서 실행됩니다. (최외부 블록)");
tasks.register("configured") {
println("이 구문은 원래 구성 단계에서 실행됩니다. 하지만, 커맨드에 포함되어 있지 않아 실행되지 않습니다. (configured 태스크 블록)")
}
tasks.register("test") {
doLast {
println("이 구문은 실행 단계에서 실행됩니다. (test 태스크의 doLast 블록)")
}
}
tasks.register("testBoth") {
doFirst {
println("이 구문은 실행 단계에서 가장 먼저 실행됩니다. (testBoth 태스크의 doFirst 블록)")
}
doLast {
println("이 구문은 실행 단계에서 가장 마지막에 실행됩니다. (testBoth 태스크의 doLast 블록)")
}
println("이 구문은 구성 단계에서 실행됩니다. 왜냐하면 :testBoth 태스크를 실행하기 위해 구성 단계에서 태스크를 구성해야 하기 때문입니다. (testBoth 태스크 블록)");
}
$ ./gradlew.bat test testBoth
이 구문은 초기화 단계에서 실행됩니다.
> Configure project : 이 구문은 구성 단계에서 실행됩니다. (최외부 블록)
이 구문은 구성 단계에서 실행됩니다. 왜냐하면 :testBoth 태스크를 실행하기 위해 구성 단계에서 태스크를 구성해야 하기 때문입니다. (testBoth 태스크 블록)
> Task :test
이 구문은 실행 단계에서 실행됩니다. (test 태스크의 doLast 블록)
> Task :testBoth
이 구문은 실행 단계에서 가장 먼저 실행됩니다. (testBoth 태스크의 doFirst 블록)
이 구문은 실행 단계에서 가장 마지막에 실행됩니다. (testBoth 태스크의 doLast 블록)
위의 코드와 같이 settings.gradle.kts와 build.gradle.kts를 구성하고 test와 testBoth 태스크만을 실행하면 다음과 같은 결과를 얻을 수 있다. 위에서 설명한 것처럼 초기화 단계에서는 settings.gradle.kts를 평가해서 빌드에 참여할 프로젝트를 확인한다. 위의 코드에는 없지만 실제로는 settings.gradle.kts에는 include("app")과 같이 빌드에 참여할 프로젝트를 결정할 수 있다.
다음으로 빌드에 참여하는 각각의 프로젝트의 build.gradle.kts를 평가하는데, 이 단계가 구성 단계다. 이 단계에서 각각의 build.gradle.kts가 실행되고, 실행 단계에서 수행되어야 할 작업들을 구성해 작업 그래프를 만들게 된다. 이때 configured 태스크는 요청된 작업에 없으므로 제외된다. 또 build.gradle.kts를 실행하는 과정에서 작업으로 평가되지 않는 부분은 위의 결과처럼 구성 단계에서 실행된다.
마지막으로 작업을 실행하는 실행 단계에서는 각 작업 그래프를 사용해 실제로 태스크들이 실행된다.
'프로그래밍 > Java' 카테고리의 다른 글
[Java] JDK 부수기 - (1) java.lang.Object - 1. clone (0) | 2023.11.20 |
---|---|
[Java] Java기초 - (3) 배열 (0) | 2023.11.01 |
[Java] Java Build Tools 발전과정 - (2) Maven (0) | 2023.10.24 |
[Java] Java Build Tools 발전과정 - (1) Make vs Ant (0) | 2023.10.20 |
[Java] Java기초 - (2) 기초 문법 - 연산자 (0) | 2023.10.18 |