gradle - 개요!
gradle
https://gradle.org/
https://github.com/gradle/gradle
https://docs.gradle.org/current/userguide/build_lifecycle.html
Groovy
또는 Kotlin DSL
을 사용한 오픈 소스 빌드 자동화 도구
maven
과 비교해 속도 뿐만 아니라 자유로운 스크립트성 언어를 통해 빌드설정이 가능하다.
JVM 기반으로 동작하기 때문에 PC 에 JDK 8 이상이 필요하다.
gradle
은 이 빌드하는 과정을 위한 클래스가 정의되어 있고 인스턴스화 시켜 빌드하는 과정에 사용한다.
아래 3개 클래스가 중점적으로 사용된다.
- Project: 각 프로젝트 표현 객체
- Task: 각종 작업 표현 객체
- Gradle:
Gradle
전역객체
gradle
을 사용한다는 것은 요약하면 계층형태의 프로젝트별로 Project
객체를 생성하고, Project
객체에 빌드를 위한 Task
객체들을 설정하고,
[Gradle, Project]
객체에 각종 콜백함수들을 삽입해 빌드 과정에서 부가적은 프로세스를 진행시키는 것이다.
Build Lifecycle
Gradle
프로젝트에 매핑되는 Project
객체를 생성하고 내부에 각종 task
들을 정의하고 빌드 작업을 수행한다.
아래와 같이 3 단계로 나눌 수 있다.
- Initialization
- Configuration
- Execution
Initialization
빌드에 참여할 프로젝트들을 결정하고 Project
객체의 인스턴스를 생성한다.
- 상위 디렉토리에서
settings.gradle
탐색. - 없는 경우 빌드는 단일 프로젝트로 실행 빌드.
- 있는 경우 현재 프로젝트가 계층내부에 속해있는지 확인.
- 속해있을 경우 다중 프로젝트, 그렇지 않을경우 단일 프로젝트로 빌드.
Configuration
Project
객체 내부 properties 나 변수 등을 configure
한다.
Configuration
단계에서 task
의 subset
을 생성하고 구성한다.
Execution
Configuration
단계에서 생성된 task graph
사용하여 실행할 task
를 결정한다.
모든 project
의 빌드관련 task
(라이브러리 다운로드, 컴파일, 입출력 처리 등)를 실행한다.
아래와 같이 setting.gradle
과 build.gradle
를 설정하고 실행시키면 대략 어떤 phase
에 코드가 실행되는지 알 수 있다.
[test, testBoth]
task
를 실행하면 결과는 아래와 같다.
// setting.gradle
rootProject.name = 'basic'
println 'initialization phase[setting.gradle]'
// build.gradle
println 'configuration phase[build.gradle]'
tasks.register('configured') {
println 'configuration phase[:configured]'
}
// test task 정의
tasks.register('test') {
doLast {
println 'execution phase[test]'
}
}
// testBoth task 정의
tasks.register('testBoth') {
println 'configuration phase[testBoth]'
doFirst {
println 'execution phase[testBoth.doFirst]'
}
doLast {
println 'execution phase[testBoth.doLast]'
}
}
$ gradle --console verbose test testBoth
# initialization phase[setting.gradle]
# > Configure project :
# configuration phase[build.gradle]
# configuration phase[testBoth]
# > Task :test
# execution phase[test]
# > Task :testBoth
# execution phase[testBoth.doFirst]
# execution phase[testBoth.doLast]
# BUILD SUCCESSFUL in 669ms
# 2 actionable tasks: 2 executed
--console verbose
속성을 넣으면[task 이름, 수명주기 로그]
를 출력한다.
gradle directory
gradle 사용시 기본적으로 아래 2가지 디렉토리에 접근한다.
- gradle 홈디렉토리:
{HOME}/.gradle
위치 - Project root 디렉토리: gradle 프로젝트 최상단 위치
gradle 홈디렉토리의 구조는 대략 아래와 같다.
GRADLE HOME DIRECTORY
├── caches Global cache directory
│ ├── 7.4 Version-specific caches
│ ├── 7.5
│ ├── ...
│ ├── jars-3 Shared caches(Library Dependency)
│ └── modules-2 Shared caches
├── daemon Registry and logs of the Gradle Daemon
├── jdks JDKs downloaded by the toolchain support
├── wrapper Downloaded by the Gradle Wrapper
│ └── dists
│ ├── ⋮
│ ├── gradle-7.4-bin
│ └── gradle-7.5-bin
└── gradle.properties Global Gradle configuration properties
Project root 디렉토리 구조는 대략 아래와 같다.
ROOT GRADLE DIRECTORY
├── .gradle Project-specific cache directory
│ ├── 7.4 Version-specific caches
│ ├── 7.5
│ └── ⋮
├── build Gradle generates all build artifacts
├── gradle.properties Project-specific Gradle configuration properties
├── settings.gradle The project’s settings file(subprojects is defined)
├── subproject-one multiple subprojects
| └── build.gradle subproject's build script
├── subproject-two
| └── build.gradle
⋮
├── gradlew Scripts for Gradle Wrapper
├── gradlew.bat Scripts for Gradle Wrapper
└── gradle Downloaded by the Gradle Wrapper
└── wrapper
Groovy 기초 문법
사용자 정의 클래스 정의 및 생성
groovy
에선 getter
, setter
함수를 정의하지 않아도 자동 변환된다.
class UserInfo {
String name
String email
}
tasks.register('custom-class') {
def user = configure(new UserInfo()) {
name = "Isaac Newton"
email = "isaac@newton.me"
}
doLast {
println user.name
println user.email
println user.getName() + ", " + user.getEmail()
}
}
$ gradle custom-class
# > Task :custom-class
# Isaac Newton
# isaac@newton.me
# Isaac Newton, isaac@newton.me
클로저 정의방법
groovy
에선 매개변수로 함수정의객체를 넣는 형식의 문법(일급객체)을 많이 사용하는데,
이런 매개변수를 클로저(함수객체) 라 부르고 groovy
에서 Closure
클래스를 사용해 별도로 관리한다.
매개변수로 클로저 매개변수 하나만 받을 때 아래와 같이 다양한 구문사용이 가능하다.
아래와 같이 Closure
객체를 생성하고 task
메서드의 매개변수로 삽입한다.
task hi {
println "in a closure"
}
task helo() {
println "in a closure"
}
task hey({
println "in a closure"
})
Collection 문법
java
의 Collection
클래스들을 사용할 수 있다.
task testList {
List<String> list = new ArrayList<String>()
list.add('org/gradle/api/**')
list.add('org/gradle/internal/**')
list.each { str -> println str }
Map<String, String> map = [key1: "value1", key2: "value2"]
map.entrySet().each { entry -> println "$entry.key, $entry.value" }
println map.key1
println map.key2
}
$ gradle testList
> Configure project :
org/gradle/api/**
org/gradle/internal/**
key1, value1
key2, value2
value1
value2
Closure delegate 객체
각 클로저 객체에는 delegate
(대리자) 객체가 존재한다.
delgate
를 통해 클로저에 대한 변수 및 메서드 참조가 가능하다.
외부에서 Closure
객체의 delegate
에 접근해서 값을 할당할 수 있다.
def say = {
println msg
}
say.delegate = [msg: "hello"]
say()
def hello = {
def msg = 'world'
println msg
}
hello.delegate = [msg: "kouzie"]
hello()
hello
의 msg
처럼 이미 값이 할당되어 있다면 delegate
를 통해 값을 할당해도 무시된다.
> Configure project :
hello
world
Project
gradle 프로젝트 구성시 groovy 문법을 사용해 각 클래스를 정의하고 사용하는데
제일 처음 접근하는 것은 Project
클래스이다.
그래서 project 접근 키워드는 생략 가능하다.
예로 task
를 추가하는 코드를 보면, 두 방식 모두 Project
내부 정의된 메서드를 통해 task
를 생성하는 것을 볼 수 있다.
println project.name
println name // project 키워드 생략
// Project.TaskContainer getTasks();
tasks.register('hello') {
doLast {
println 'Hello world!'
}
}
// Project.task
task hi {
doLast {
println "hi i'm kouzie"
}
}
buildscript
빌드하는 과정에서 별도의 라이브러리가 필요하다면 Project
에 buildscript
블록을 추가하여 라이브러리를 의존성주입 받을 수 있다.
아래는 Base64
인코딩 라이브러리를 사용하기 위해 commons-codec
라이브러리를 주입받는 예지이다.
import org.apache.commons.codec.binary.Base64
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath group: 'commons-codec', name: 'commons-codec', version: '1.2'
}
}
tasks.register('encode') {
doLast {
byte[] encodedString = new Base64().encode('hello world\n'.getBytes())
println new String(encodedString)
}
}
ext
Extra properties
(추가속성) 이라 부르는 사용자 정의 속성을 ext
메서드를 통해 정의할 수 있다.
사용자 정의 속성은 Project
, Task
등의 속성에서 바로 사용할 수 있다.
ext {
springVersion = "3.1.0.RELEASE"
emailNotification = "build@master.org"
}
tasks.register('printProperties') {
doLast {
println springVersion
println emailNotification
}
}
Task
task
는 작업의 최소단위, gradle
에는 2가지 종류가 존재한다.
- 사전 정의
task
- 사용자 정의
task
아무것도 미리 정의되어 있지 않은 basic gradle project
생성
$ gradle init
Starting a Gradle Daemon (subsequent builds will be faster)
Select type of project to generate:
1: basic
2: application
3: library
4: Gradle plugin
Enter selection (default: basic) [1..4] 1
...
BUILD SUCCESSFUL in 19s
tasks
옵션으로 정의되어 있는 tasks
들을 확인 가능하다.
아무것도 정의하지 않았지만 task 그룹 [Build Setup tasks, Help tasks]
에 저장된
사전 정의 task
들을 확인할 수 있다.
$ gradle tasks
> Task :tasks
------------------------------------------------------------
Tasks runnable from root project 'basic'
------------------------------------------------------------
Build Setup tasks
-----------------
init - Initializes a new Gradle build.
wrapper - Generates Gradle wrapper files.
Help tasks
----------
buildEnvironment - Displays all buildscript dependencies declared in root project 'basic'.
dependencies - Displays all dependencies declared in root project 'basic'.
dependencyInsight - Displays the insight into a specific dependency in root project 'basic'.
help - Displays a help message.
tasks - Displays the tasks runnable from root project 'basic'.
...
BUILD SUCCESSFUL in 595ms
1 actionable task: 1 executed
간단한 사용자 정의 task
를 build.gradle
에 작성
// build.gradle
task copy(type: Copy, group: "Custom", description: "Copies sources to the dest directory") {
from "src"
into "dest"
}
task zip(type: Zip, group: "Archive", description: "Archives sources in a zip file") {
from "src"
archiveFileName = "basic-demo-1.0.zip"
}
tasks
옵션으로 다시 정의된 tasks
들을 조회하면
Archive
, Custom
그룹과 사용자가 정의한 task
가 출력된다.
$ gradle tasks
> Task :tasks
------------------------------------------------------------
Tasks runnable from root project
------------------------------------------------------------
Archive tasks
-------------
zip - Archives sources in a zip file
Custom tasks
------------
copy - Copies sources to the dest directory
...
BUILD SUCCESSFUL in 680ms
1 actionable task: 1 executed
Task dependencies
Gradle is an example of dependency based programming: you define tasks and dependencies between tasks
gradle
은 여러 task
간의 의존성을 정의하는 dependency based programming
이다.
Gradle은 task
실행 전 task graph
를 구성한다.
만약 build task
를 수행할 경우 그림과 같이 연결된 task
들이 순서대로 수행된다.
dependsOn
메서드를 통해 작업 의존성을 추가할 수 있다.
tasks.register('hello') {
doLast {
println 'Hello world!'
}
}
tasks.register('intro') {
dependsOn tasks.hello
doLast {
println "I'm Gradle"
}
}
intro task
만 실행해도 hello task
도 같이 실행됨
$ gradle intro
Starting a Gradle Daemon (subsequent builds will be faster)
> Task :hello
Hello world!
> Task :intro
I'm Gradle
task 관련 메서드 모음
위에서 말했듯이 [Gradle, Project, Task]
클래스를 통해 빌드가 이루어진다.
이 클래스 내부에 task 를 다루는 각종 메서드를 알아본다.
tasks.named
tasks.named
메서드를 사용해 기존에 정의된 task 를 가져와 동적으로 메서드나 속성을 추가할 수 있다.
tasks.register('hello') {
doLast { println 'Hello Earth' }
}
tasks.named('hello') {
doFirst { println 'Hello Venus' }
}
tasks.named('hello') {
doLast { println 'Hello Mars' }
}
tasks.named('hello') {
doLast { println 'Hello Jupiter' }
}
$ gradle hello
> Task :hello
Hello Venus
Hello Earth
Hello Mars
Hello Jupiter
beforeEvaluate, afterEvaluate, beforeProject, afterProject
4가지 함수 모두 configuration
단계에서 실행되는 메서드,
이벤트 발생시 동작하는 클로저 함수(콜백함수)를 등록할 수 있다.
configuration
시작 전 호출하는beforeEvaluate
,beforeProject
configuration
완료 후 호출하는afterEvaluate
,afterProject
[basic]
[project-a]
프로젝트를 계층으로 구성하고 테스트를 진행했다.
// build.gradle [basic]
allprojects {
project.beforeEvaluate { project -> println "beforeEvaluate $project" }
project.afterEvaluate { project -> println "afterEvaluate $project" }
}
gradle.beforeProject { project -> println "beforeProject $project" }
gradle.afterProject { project -> println "afterProject $project" }
$ gradle
> Configure project :
afterProject root project 'basic'
afterEvaluate root project 'basic'
> Configure project :project-a
beforeProject project ':project-a'
beforeEvaluate project ':project-a'
afterProject project ':project-a'
afterEvaluate project ':project-a'
4 가지 메서드 역할은 동일하며 굳이 차이를 찾자면 함수 정의 위치가 Gradle
, Project
객체인 점이다.
그래서 Evaluate
함수를 project-a
에 적용하려면 allprojects
와 같은 함수를 사용해야한다.
이런 클로저 함수를 사용하면 configuration
단계에서 각 Project
인스턴스의 설정을 다이나믹하게 진행할 수 있다.
아래 예제는 hasTests
속성을 가지는 프로젝트의 경우 test task
를 추가정의하여 삽입하는 코드이다.
// setting.gradle
rootProject.name = 'basic'
include 'project-a'
//build.gradle [root]
allprojects {
afterEvaluate { project ->
if (project.hasProperty("hasTests") && project.hasTests) {
println "Adding test task to $project"
project.task('test') {
doLast {
println "Running tests for $project"
}
}
}
}
}
//build.gradle [project-a dir]
ext.hasTests = true
$ gradle -q test
Adding test task to project ':project-a'
Running tests for project ':project-a'
afterEvaluate
대신afterProject
를 사용해도 무방하다.
tasks.whenTaskAdded
tasks.whenTaskAdded { task -> println "task added!! $task" }
tasks.register('simple-task')
gradle
> Configure project :
task added!! task ':simple-task'