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 를 실행하면 결과는 아래와 같다.
1 2 3 rootProject.name = 'basic' println 'initialization phase[setting.gradle]'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 println 'configuration phase[build.gradle]' tasks.register('configured' ) { println 'configuration phase[:configured]' } tasks.register('test' ) { doLast { println 'execution phase[test]' } } tasks.register('testBoth' ) { println 'configuration phase[testBoth]' doFirst { println 'execution phase[testBoth.doFirst]' } doLast { println 'execution phase[testBoth.doLast]' } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $ gradle --console verbose test testBoth
--console verbose 속성을 넣으면 [task 이름, 수명주기 로그] 를 출력한다.
gradle directory gradle 사용시 기본적으로 아래 2가지 디렉토리에 접근한다.
gradle 홈디렉토리: {HOME}/.gradle 위치
Project root 디렉토리: gradle 프로젝트 최상단 위치
gradle 홈디렉토리의 구조는 대략 아래와 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 디렉토리 구조는 대략 아래와 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 함수를 정의하지 않아도 자동 변환된다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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() } }
1 2 3 4 5 6 $ gradle custom-class
클로저 정의방법 groovy 에선 매개변수로 함수정의객체를 넣는 형식의 문법(일급객체)을 많이 사용하는데, 이런 매개변수를 클로저 (함수객체) 라 부르고 groovy 에서 Closure 클래스를 사용해 별도로 관리한다.
매개변수로 클로저 매개변수 하나만 받을 때 아래와 같이 다양한 구문사용이 가능하다.
아래와 같이 Closure 객체를 생성하고 task 메서드의 매개변수로 삽입한다.
1 2 3 4 5 6 7 8 9 task hi { println "in a closure" } task helo() { println "in a closure" } task hey({ println "in a closure" })
Collection 문법 java 의 Collection 클래스들을 사용할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 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 }
1 2 3 4 5 6 7 8 9 $ gradle testList > Configure project : org/gradle/api/** org/gradle/internal/** key1, value1 key2, value2 value1 value2
Closure delegate 객체 각 클로저 객체에는 delegate(대리자) 객체가 존재한다.delgate 를 통해 클로저에 대한 변수 및 메서드 참조가 가능하다.
외부에서 Closure 객체의 delegate 에 접근해서 값을 할당할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 def say = { println msg } say.delegate = [msg: "hello" ] say() def hello = { def msg = 'world' println msg } hello.delegate = [msg: "kouzie" ] hello()
hello 의 msg 처럼 이미 값이 할당되어 있다면 delegate 를 통해 값을 할당해도 무시된다.
1 2 3 > Configure project : hello world
Project gradle 프로젝트 구성시 groovy 문법을 사용해 각 클래스를 정의하고 사용하는데 제일 처음 접근하는 것은 Project 클래스이다.
그래서 project 접근 키워드는 생략 가능하다.
예로 task 를 추가하는 코드를 보면, 두 방식 모두 Project 내부 정의된 메서드를 통해 task 를 생성하는 것을 볼 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 println project.name println name tasks.register('hello' ) { doLast { println 'Hello world!' } } task hi { doLast { println "hi i'm kouzie" } }
buildscript 빌드하는 과정에서 별도의 라이브러리가 필요하다면 Project 에 buildscript 블록을 추가하여 라이브러리를 의존성주입 받을 수 있다.
아래는 Base64 인코딩 라이브러리를 사용하기 위해 commons-codec 라이브러리를 주입받는 예지이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import org.apache.commons.codec.binary.Base64buildscript { 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 등의 속성에서 바로 사용할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 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 생성
1 2 3 4 5 6 7 8 9 10 11 $ 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 들을 확인할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 $ 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 에 작성
1 2 3 4 5 6 7 8 9 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 가 출력된다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 $ 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 메서드를 통해 작업 의존성을 추가할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 tasks.register('hello' ) { doLast { println 'Hello world!' } } tasks.register('intro' ) { dependsOn tasks.hello doLast { println "I'm Gradle" } }
intro task 만 실행해도 hello task 도 같이 실행됨
1 2 3 4 5 6 7 8 $ 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 를 가져와 동적으로 메서드나 속성을 추가할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 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' } }
1 2 3 4 5 6 7 $ 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] 프로젝트를 계층으로 구성하고 테스트를 진행했다.
1 2 3 4 5 6 7 8 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" }
1 2 3 4 5 6 7 8 9 10 $ 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 를 추가정의하여 삽입하는 코드이다.
1 2 3 rootProject.name = 'basic' include 'project-a'
1 2 3 4 5 6 7 8 9 10 11 12 13 allprojects { afterEvaluate { project -> if (project.hasProperty("hasTests" ) && project.hasTests) { println "Adding test task to $project" project.task('test' ) { doLast { println "Running tests for $project" } } } } }
1 2 3 4 $ gradle -q test Adding test task to project ':project-a' Running tests for project ':project-a'
afterEvaluate 대신 afterProject 를 사용해도 무방하다.
tasks.whenTaskAdded 1 2 3 tasks.whenTaskAdded { task -> println "task added!! $task" } tasks.register('simple-task' )
1 2 3 4 gradle > Configure project : task added!! task ':simple-task'
Multi module api - 전이 의존성 멀티모듈로 프로젝트를 스타터 패키지와 같은 common module 을 구성할때 api() 함수를 사용해 전이 의존성 구성을 하면 편하다.
implementation 의 경우 캡슐화 처럼 동작하여 common 모듈에서 사용한다 하더라도 외부 프로젝트에서 접근이 불가능하지만 api 의 경우 의존성이 전이되어 외부 프로젝트에서도 접근 가능하다.
1 2 3 4 5 dependencies { implementation(kotlin("reflect" )) api("org.springframework.boot:spring-boot-starter-actuator" ) api("io.micrometer:micrometer-registry-prometheus" )
또한 api 를 사용하면 컴파일의 기준이 확장되어 빌드시간이 증가한다.