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 단계에서 tasksubset 을 생성하고 구성한다.

Execution
Configuration 단계에서 생성된 task graph 사용하여 실행할 task 를 결정한다.
모든 project 의 빌드관련 task(라이브러리 다운로드, 컴파일, 입출력 처리 등)를 실행한다.

아래와 같이 setting.gradlebuild.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 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

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 기초 문법

사용자 정의 클래스 정의 및 생성

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 문법

javaCollection 클래스들을 사용할 수 있다.

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()

hellomsg 처럼 이미 값이 할당되어 있다면 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

빌드하는 과정에서 별도의 라이브러리가 필요하다면 Projectbuildscript 블록을 추가하여 라이브러리를 의존성주입 받을 수 있다.

아래는 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가지 종류가 존재한다.

  1. 사전 정의 task
  2. 사용자 정의 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

간단한 사용자 정의 taskbuild.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 들이 순서대로 수행된다.

1

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'

Plugin

지금까지 gradle 을 보면 단순 쉘스크립트인데 언어가 groovy 인 것 같은 느낌이다.
Plugin 을 사용하면 유용한 기능들을 가져올 수 있다.

plugin 은 유용한 기능을 가진 task 의 집합이라 할 수 있다.
gradle 에선 JavaPlugin 과 같은 core plugins 들을 제공하며
Plugin 인터페이스를 구현해서 작성하거나 gradle DSL(groovy, kotlin) 을 사용해 작성할 수 있다.

Plugin 을 직접 구현한다면 2가지 방식으로 구현할 수 있다.

Script Plugin
build 스크립트에서 선언방식으로 Plugin 을 구성하고 빌드에 관여하는 방식.

Binary Plugin
jar 형태로 배포되고 build 스크립트에 사용되는 형식

일반적으로 초기에 script plugin 으로 구성되어 개발되다가 조직간 공유할 수 있는 binary plugin 으로 마이그레이션 된다.

Core Plugin, Community Plugin

Core Plugin 는 배포된 gradle 에 자체적으로 저장되어 있는 plugin 으로 별도의 version number 없이 사용한다, org.gradle 네임스페이스를 사용하며 생략 가능하다.

plugins {
    id 'java'
    // id "org.gradle.java"
}

아래 url 에 접속하면 gradle 에서 제공하는 Core Plugin 리스트를 확인할 수 있다.

https://docs.gradle.org/current/userguide/plugin_reference.html

반대로 Community Plugin 는 사용자 정의 플러그인으로 gradle portal 에 저장된 Plugin 을 다운받아 사용한다.
Spring Boot, Spring Dependency 와 같은 플러그인도 Community Plugin 이라 할 수 있다.

plugins {
    id 'org.springframework.boot' version '2.7.9'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

정규화된 id, version 을 명시해야한다.

아래 url 에 접속하면 gradle portal 에 등록한 사용자 정의 Community Plugin 리스트를 확인할 수 있다.

https://plugins.gradle.org/

Plugin 적용

Pluginencapsulated 하여 빌드구성에 적용시키고 싶으면 아래 2가지 과정을 거쳐야한다.

1. resolve the plugin
올바른 버전의 plugin 을 찾아 script classpath 에 적용시키는 것.
url, 특정 경로를 통해 plugin 을 가져와 resolve 하여 빌드에 참여시킬 수 있다.

2. apply the plugin to the target Plugin.apply(T) 메서드를 사용해 targetplugin 을 적용시키는 것.
대부분 targetProject 이다.

아래와 같이 plugins 블록을 사용해 Project 객체에 Plugin 을 적용할 수 있다.

plugins {
    id «plugin id» // core
    id «plugin id» version «plugin version» [apply «false»] // community
}

Projectplugins 블록을 만나면 위에서 설명한 2가지 과정을 거치는데
apply false 사용하면 Pluginapply 과정을 막을 수 있다.

선언만 해두고 Plugin 을 사용할 subproject 안에서만 사용하도록 설정가능하고,
한번 resolvePluginsubproject 에서 version 명시 없이 사용 가능하다.

// setting.gradle
rootProject.name = 'basic'

include "project-a"
// build.bradle [basic]
plugins {
    id 'org.springframework.boot' version '2.7.9' apply false
}
// build.gradle [project-a]
plugins {
    id 'org.springframework.boot'
}

만약 subprojects 와 같은 블럭 안에 Plugin 설정을 해야 한다면 apply plugin 함수를 사용할 수 있다.

plugins {
    id 'org.springframework.boot' version '2.7.9' apply false
}

subprojects {
    if (name.startsWith('app')) {
        apply plugin: 'org.springframework.boot'
    }
}

pluginManagement 블록

아래와 같이 setting.gradle 파일에 pluginManagement 블록을 정의하여 Plugin 의 관리를 할 수 있다.

// setting.gradle
pluginManagement {
    plugins {
        id 'org.springframework.boot' version "${springBootVersion}"
    }
}

rootProject.name = 'basic'

include "project-a"
// gradle.properties
springBootVersion=2.7.9
// build.gradle [basic]
plugins {
    id 'org.springframework.boot'
}

Plugin Repository

기본적으로 plugins 블록에 지정된 Plugin 들을 찾기 위해 gradle plugin potal 를 사용한다.
하지만 보안, 빌드구조상 커스텀 plugin repository 를 사용해야 할 경우 아래와 같이 pluginManagement 에서 repositories 블록을 사용한다.

pluginManagement {
    repositories {
        
        gradlePluginPortal() // 1순위
        google() // 2순위
        mavenCentral() // 3순위
        // 4순위
        maven { url 'https://xxx:8081/maven-repo' }
        ivy { url './ivy-repo' }
    }
}

카테고리: ,

업데이트: