모종닷컴

Gradle의 빌드 라이프사이클 본문

Programming/Gradle

Gradle의 빌드 라이프사이클

모종 2022. 8. 28. 17:38
반응형

Gradle에 대해 알아보자 글에서 빌드 단계에 해당하는 내용이겠네요. 이번 글에서 그레이들 빌드 라이프사이클에 대해 알아보도록 하겠습니다.

그레이들의 핵심은 의존성 기반의 프로그래밍을 위한 언어입니다. Gradle 용어로 이것은 작업과 작업 간의 종속성을 정의할 수 있음을 의미합니다. Gradle은 종속성 순서대로 작업을 실행하고 각 작업이 한 번만 실행될수 있도록 보장합니다. 작업을 실행할 때 이러한 종속성 그래프를 작성하는 빌드 도구가 있어서 실행 전 종속성 그래프가 빌드됩니다.

빌드 스크립트가 이러한 종속성 그래프를 구성합니다. 따라서 빌드 스크립트는 엄밀히 말하면 빌드를 구성시키는 스크립트입니다. 

빌드 단계

초기화

그레이들은 단일 및 멀티 프로젝트 빌드를 지원합니다. 초기화 단계에서 그레이들은 참여할 프로젝트를 결정하고 이러한 각 프로젝트들에 대한 Project 인스턴스를 생성합니다.

구성

이 단계에서 초기화 단계에서 생성된 Project 인스턴스를 구성시킵니다. 빌드에 참여하게 된 모든 프로젝트의 빌드 스크립트가 실행됩니다.

실행

그레이들은 구성 단계에서 생성 및 구성된 태스크의 하위 집합을 결정합니다. 하위 집합은 현재 디렉토리와 gradle 명령어에 넘겨진 인수에 의해 결정됩니다. 그리곤 선택된 태스크들을 실행합니다.

 

설정 파일

빌드 스크립트 파일 외에 Gradle은 설정 파일을 정의하고 있습니다. 설정 파일은 그레이들 명명 규칙에 의해 결정됩니다. 기본적으로 파일의 이름은 settings.gradle 입니다.

설정 파일은 빌드의 초기화 단계에서 실행됩니다. 멀티 프로젝트의 경우 빌드에 참여할 프로젝트를 정의해야하기 때문에 root 프로젝트에 settings.gradle 파일이 반드시 있어야 합니다. 반면 단일 프로젝트의 경우 이 설정 파일은 선택사항입니다. 참여할 프로젝트들을 정의하는 것 외에도 빌드 스크립트 클래스패스 경로에 라이브러리를 추가하는 것이 필요할 수 있습니다. 몇 가지 단일 프로젝트 빌드에 대해 알아보죠.

그레이들 프로젝트 하나를 만들고 시작하면 되겠습니다. 저는 Intellij IDEA 에서 그레이들 프로젝트를 하나 생성했습니다.

settings.gradle

rootProject.name = 'gradle-practice'
println 'This is executed during the initialization phase.'

build.gradle

인텔리제이로 생성하면 기본적으로 적혀있는 코드들이 존재할텐데 저는 다 지우고 원문 에시에 있는 코드를 붙였습니다.

println 'This is executed during the configuration phase.'

tasks.register('configured') {
    println 'This is also executed during the configuration phase, because :configured is used in the build.'
}

tasks.register('test') {
    doLast {
        println 'This is executed during the execution phase.'
    }
}

tasks.register('testBoth') {
    doFirst {
        println 'This is executed first during the execution phase.'
    }
    doLast {
        println 'This is executed last during the execution phase.'
    }
    println 'This is executed during the configuration phase as well, because :testBoth is used in the build.'
}

출력 결과 보기

gradle 명령어를 실행하기 전에 터미널에서 gradle --version 명령어 실행시 gradle command 를 찾을 수 없다는 출력을 본 경우에는 환경변수 설정이 되어있지 않은 것이므로 아래 명령어로 환경변수를 설정하도록 합니다. 버전은 설치되어 있는 버전을 명시해주면 되는데 저는 5.6.4 버전이라 아래와 같이 되겠네요.

export PATH=$PATH:/opt/gradle/gradle-5.6.4/bin

루트 프로젝트 위에서 터미널을 열고 gradle test testBoth 명령어를 실행해보도록 하겠습니다. 출력 결과는 아래와 같습니다.

정의한 build.gradle 파일을 보면서 출력물을 이해해봅시다. 저희가 gradle 명령어로 넘긴 인자는 test, testBoth 태스크 이름입니다. 따라서 저의가 build.gradle에 정의한 태스크 중 test, testBoth가 대상이겠죠. 따라서 configured 태스크의 경우에는 실행이 되지 않는게 맞습니다. 따라서 아래 문구가 출력이 되지 않은 것이겠죠. 

This is also executed during the configuration phase, because :configured is used in the build.

빌드 스크립트의 경우 속성 엑세스 및 메서드 호출이 Project 인스턴스에 위임됩니다. 마찬가지로 설정 파일 내의 속성 엑세스 및 메서드 호출은 Settings 클래스라는 곳에 위임됩니다.

뜬금 없이 Settings 클래스 얘기가 나와서 뭔가 싶습니다. 간단하게 Settings에 대해 알아보면 빌드에 참여할 Project 인스턴의 계층을 인스턴스화하고 구성하는 데 필요한 구성에 대한 내용이 Settings 클래스에 선언된다고 하네요. settings.gradle에 일대일 대응되며 프로젝트를 assemble 하기 전에 이 인스턴스를 만들고 설정 파일을 실행한다고 합니다. Settings 클래스에 대한 상세한 설명은 여기에 있습니다.

settings.gradle 파일 자동 찾기 

그레이들은 빌드시 어떻게 단일 프로젝트인지 멀티 프로젝트인지 알 수 있는걸까요? 만약 settings.gradle 파일과 함께 멀티 프로젝트를 빌드하면 그레이들은 빌드를 구성할 때 settings.gradle 파일을 사용합니다. 또한 그레이들은 빌드를 구성하고 있는 서브프로젝트 안에서 빌드를 실행할수도 있습니다. settings.gradle 이 없는 프로젝트 내에서 그레이들을 실행하는 경우 그레이들은 다음과 같은 방법으로 settings.gradle 파일을 찾게 되어있습니다.

  • 부모 디렉토리에 settings.gradle 파일이 있는지 찾습니다.
  • 만약 찾을 수 없다면 빌드는 단일 프로젝트 빌드로 실행됩니다.
  • 만약 부모 디렉토리에서 settings.gradle 파일을 찾았다면 현재 프로젝트가 settings.file에 멀티 프로젝트 세팅이 되어있는지 체크합니다. 만약 멀티 프로젝트에 참여하는 프로젝트가 아니라면 단일 프로젝트 빌드로 실행합니다.

이런 짓(?)을 하는 목적이 뭘까요 ㅋㅋ? 그레이들은 현재 프로젝트가 멀티 프로젝트의 하위 프로젝트인지 여부를 확인해야 합니다. 물론 하위 프로젝트라면 하위 프로젝트와 종속 프로젝트만 빌드되지만 그레이들은 전체 멀티 프로젝트 빌드에 대한 빌드 구성을 생성해야 합니다. 현재 프로젝트에 settings.gradle 파일이 포함된 경우 빌드는 항상 다음과 같이 실행합니다.

  • settings.gradle 파일이 멀티 프로젝트 계층 구조를 정의하지 않는 경우 단일 프로젝트 빌드
  • settings.gradle 파일이 다중 프로젝트 계층 구조를 정의하는 경우 다중 프로젝트 빌드.

단일 프로젝트 빌드의 구성 및 실행

단일 프로젝트 빌드의 경우 초기화 단계 이후 워크플로우는 간단합니다. 초기화 단계에서 생성된 Project 객체애 대해 빌드 스크립트를 실행합니다. 그 후 그레이들은 명령어 인자로 넘긴 태스크의 이름과 동일한 태스크를 찾습니다. 만약 태스크 이름들이 존재한다면 인자의 전달된 순서대로 별도의 빌드로 실행됩니다. 

멀티 프로젝트 빌드의 구성 및 실행

멀티 프로젝트 빌드의 구성과 실행은 이곳에서 설명됩니다. (포스팅에 담기에는 너무 기네요. 나중에 별도의 포스팅을 할까 생각중입니다)

빌드 스크립트의 라이프사이클

빌드 스크립트는 빌드가 라이프사이클을 통해 진행됨에 따라 알림을 받을 수 있습니다. 이러한 알림은 일반적으로 두 가지 형식으로 취할 수 있습니다. 특정 리스너 인터페이스를 구현하거나 혹은 알림이 실행될 때 실행할 클로저를 제공하는 것입니다. 아래의 예제가 클로저를 이용하여 알림을 받는 방법입니다. 리스너 인터페이스를 구현하는 방법은 API 문서를 참조하라고 되어있네요.

프로젝트 해석 (Project evaluation)

프로젝트 해석 전후에 바로 알림을 받을 수 있습니다. 이것은 빌드 스크립트의 모든 정의가 적용된 후 추가 구성을 수행하거나 일부 사용자 정의 로깅 또는 프로파일링을 수행하는 것과 같은 작업을 수행하는 데 사용할 수 있습니다.

아래는 hasTests 속성 값이 true인 각 프로젝트에 test 태스크를 추가하는 예입니다. 원문에서는 프로젝트 구조에 대해서는 쓰여있지 않아서 제가 임의로 프로젝트를 재구성하였습니다.

프로젝트 구성

먼저 루트프로젝트에 project-a 모듈(디렉토리)를 하나 만들었습니다.

프로퍼티 설정

그 다음 project-a 아래 gradle.properties를 아래의 내용으로 추가하였습니다.

hasTests=true

allproejct를 사용할 것임으로 루트 프로젝트(사진의 gradle-practice) 또한 해당 프로퍼티가 필요하므로 루프 프로젝트 밑에도 gradle.properties를 아래의 내용으로 추가하였습니다.

hasTests=false

 

build.gradle 수정

위에서 작성한 build.gradle 최하단에 아래 코드를 추가하였습니다.

allprojects {
    afterEvaluate { project ->
        if (project.hasTests == "true") {
            println "Adding test task to $project"
            project.task('test') {
                doLast {
                    println "Running tests for $project"
                }
            }
        } else {
            println "Do not adding test task to $project"
        }
    }
}

출력 결과 보기

루트 프로젝트에서 터미널을 열고 gradle test 명령어를 실행했고 아래와 같은 출력 결과를 받았습니다.

루트 프로젝트에서는 이전의 태스크의 test 태스크가 실행되었고 하위 프로젝트인 project-a 에서는 저희가 추가한 코드에서 test 태스크가 추가되어서 실행되었습니다.

이 예제가 보여주는 것은 Project.afterEvaluate()가 프로젝트 해석이후 실행된다는 것입니다. 루트프로젝트가 해석된 이후 Do not adding task ~ 가 출력되었고 project-a 에 대하여 해석할 때 Adding test task~가 출력되었습니다.

태스크 생성

프로젝트에 태스크가 추가된 이후 즉각적으로 알림을 받을 수 있습니다. 빌드 파일에서 작업을 사용할 수 있게 되기 전에 일부 기본값을 설정하거나 동작을 추가하는 데 사용할 수 있습니다. 이 때는 tasks.whenTaskAdded를 이용해서 클로저를 넘겨주면 됩니다.

settings.gradle에 project-a 선언을 주석처리 하고

build.gradle에 아래의 내용으로 대체합니다.

tasks.whenTaskAdded { task ->
    task.ext.srcDir = 'src/main/java'
}

tasks.register('a')

println "source dir is $a.srcDir"

gradle a  명령어를 실행하면서 출력 결과를 보도록 합시다.

작업 실행

작업이 실행되기 직전과 직후에 알림을 받을 수 있습니다.

build.gradle 내용을 아래로 대체합니다.

tasks.register('ok')

tasks.register('broken') {
    dependsOn ok
    doLast {
        throw new RuntimeException('broken')
    }
}

gradle.taskGraph.beforeTask { Task task ->
    println "executing $task ..."
}

gradle.taskGraph.afterTask { Task task, TaskState state ->
    if (state.failure) {
        println "FAILED"
    }
    else {
        println "done"
    }
}

gradle broken 명령어를 실행합니다.

 

원문

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

반응형