Gradle(2) - 빌드 스크립팅 : 태스크(Task)에 대해 알아보자

지난 시간에는 Gradle을 설치하는 방법을 알아보았다. 이번 시간에는 태스크를 직접 정의하고 Gradle을 실행하는 방법을 알아보자.

Gradle은 항상 태스크를 기준으로 동작하기 때문에, 실행할 때에도 어떤 태스크를 실행할 것인지 지정해야 한다. Gradle과 관련해서 한번쯤은 봤을 gradle build 구문도 사실은 태스크 호출 구문이다.

Gradle을 실행하는 방법은 다음과 같다.

$ gradle [Task명]

gradle 커맨드가 실행되면 Gradle은 현재의 디렉토리 경로를 기준으로 build.gradle을 읽어들인 다음, 지정된 태스크를 찾아 실행한다. 빌드 스크립트라고 불리는 이 파일에 작성된 실행 명령에 따라 빌드가 진행되므로 빌드에 필요한 작업은 모두 이 스크립트 파일에 서술되어 있어야 한다. Gradle은 build.gradle 파일이 있는 디렉토리를 프로젝트 루트로 인식한다.

알아두어야 할 또 하나의 핵심 파일은 settings.gradle이다. build.gradle이 빌드 내용을 관리하는 스크립트 역할이라면 settings.gradle은 빌드 설정을 관리하는 역할이다. 프로젝트명이나 버전, 빌드 스크립트명 등 빌드에 필요한 설정 정보가 이 파일에 기록된다.

만약 gradle 커맨드가 실행된 위치에 build.gradlesettings.gradle이 없으면 오류가 발생한다.

빌드 스크립트로 사용되는 표준 파일명인 build.gradlesettings.gradle 파일의 설정 변경을 통해 커스터마이징 가능하다.

알아두자) 빌드란?

컴퓨터 소프트웨어 분야에서 빌드(Build)란, 소스 코드 파일을 서버나 개인 컴퓨터, 또는 휴대폰에서 실행할 수 있는 독립된 애플리케이션으로 변환하는 과정이나 그 결과물을 가리킨다.

빌드의 핵심은 개발자가 작성한 소스 코드를 실행 코드로 변경하는 컴파일 과정이지만, Gradle에서의 빌드 개념은 이를 더 확장하여 소프트웨어 개발을 위한 프로젝트 설정이나 디렉토리 구조화, 라이브러리 의존성 관리, 컴파일된 코드의 패키징, 배포를 위한 업로드, 배포에 이르기까지 전체 과정을 대상으로 한다. 개발자의 소스 코딩을 제외한 나머지 대부분의 작업 과정을 의미한다고 생각하면 된다.

gradle 외에 널리 알려진 빌드 도구로는 메이븐(Maven), 앤트(Ant) 등이 있다.

build.gradle 파일 작성하기

아직 Gradle 초기화 기능을 배우기 전이므로 실행에 필요한 파일은 직접 생성해야 한다. 디렉토리 하나를 만들고, 그 안에 build.gradlesettings.gradle을 추가하자. 앞에서도 설명했지만 이 디렉토리가 Gradle 프로젝트의 루트 디렉토리가 될 것이다.

$ mkdir GradlePractice && cd GradlePractice
$ echo "task hello { println 'Hello World!'}" >> build.gradle
$ echo "" >> settings.gradle

Gradle은 태스크 단위로 실행되므로, build.gradle에는 실행할 태스크가 정의되어 있어야 한다. 위 예제에서는 hello 태스크를 정의하고 있다. 태스크 작성 관련 문법이나 기능 설명은 일단 보류하고, 실행시 “Hello World!” 구문이 출력되도록 처리하였다. settings.gradle 파일은 내용에 상관없이 일단 생성되어 있기만 하면 문제되지 않으므로 우선 빈 파일로 생성한다.

디렉토리 구조는 tree 커맨드를 통해 확인할 수 있다. 디렉토리 구성을 확인해보자.

$ tree
.
├── build.gradle
└── settings.gradle

build.gradlesettings.gradle 파일이 정상적으로 생성되었음을 확인할 수 있다.

이제 작성된 스크립트를 실행해 볼 시간이다. 빌드 스크립트 실행을 위해서는 gradle 커맨드와 함께 실행할 태스크를 지정하면 된다. 앞서 정의한 hello 태스크를 지정하고 실행하자.

$ gradle hello 

> Task :hello
Hello World!

BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed

Gradle이 실행되면서 hello 태스크에 정의된 내용을 처리한다. 이에 따라 println 구문에 작성한 것처럼 “Hello World!” 메시지가 출력되는 것을 볼 수 있다.

이 과정에서 우리가 의도한 출력 메시지 외에도 Gradle 자체의 로그 메시지가 함께 출력되는데, 실행시 –quiet 옵션을 주면 로그 메시지가 필터링되어 작업 결과물만 출력되므로 간결하다. –quiet 옵션은 ‘-q’ 로도 사용 가능하다.

$ gradle hello -q
Hello World!

1. 커스텀 태스크 정의 문법

본격적으로 커스텀 태스크를 작성하는 방법에 대해 알아보자. 태스크를 정의하는 문법은 다음과 같다.

task <태스크명> {
    태스크 내용
}

기본적으로 Gradle은 Groovy와 Kotlin을 지원한다. 현재 포스팅에서 사용하는 언어는 Groovy인데, 자바와 비슷한 문법 덕분에 자바 경험이 있다면 다루기 크게 어렵지는 않다. Kotlin을 좋아하는 사용자라면 Gradle 공식 홈페이지 를 참고하기를 권한다. Goovy와 Kotlin 문법으로 나누어 잘 설명되어 있다.

다음은 hello 태스크를 정의하는 구문이다.

📂 build.gradle

task hello {
    doLast {
        println "Hello World!"
    }    
}

태스크 내부에 사용된 doLast는 가장 나중에 실행되는 코드 블록 문법이다. 이 블록 안에 작성된 코드는 태스크 내 작성 위치에 상관없이 무조건 맨 마지막에 실행된다. 잠시 후에 다시 살펴볼 예정이다.

다음은 모두 동일한 표현이다. Gradle은 이와 같은 문법을 모두 허용한다.

Case 1) 태스크 명에 문자열을 사용하여 정의

📂 build.gradle

task('hello') {
    doLast{
        println "Hello World!"
    }
}

Case 2) 특정 DSL 구문을 사용하여 정의

📂 build.gradle

task(hello) {
    doLast{
        println "Hello World!"
    }
}

Case 3) tasks 컨테이너를 사용하여 정의

📂 build.gradle

tasks.create('hello') {
    doLast{
        println "Hello World!"
    }
}

모든 코드는 build.gradle에 작성해야 함을 잊지 말자. 이 포스팅에서 각 구문 맨 위에 적힌 📂 build.gradle는 작성해야 할 파일명을 의미한다.

2. doFirst 구문과 doLast 구문

Gradle의 태스크는 doFirst 블록과 doLast 블록을 지원한다. 작성 순서에 상관없이 doFirst 블록은 가장 먼저 실행되고, doLast 블록은 그 반대로 가장 나중에 실행된다.

📂 build.gradle

task order {
    doLast {
        println "이것은 마지막에 실행될 구문입니다"
    }
    doFirst {
        println "이것은 맨 처음에 실행될 구문입니다"
    }
}

order 태스크를 실행한 결과는 다음과 같다.

$ gradle order -q 

이것은 맨 처음에 실행될 구문입니다
이것은 마지막에 실행될 구문입니다

분명 doLast 블록이 먼저, doFirst 블록이 그보다 나중에 작성되었음에도 불구하고 실제 실행시 반대 결과가 나왔다. 이처럼 doFirstdoLast 블록은 작성 위치와 무관하게 태스크 내에서 고정적인 실행 순서를 가진다.

태스크에 doFirst/doLast 블록이 모두 있어야 할 필요는 없다. 다만 둘 중 하나는 기본적으로 사용하는 것이 좋다.

3. 스크립트 방식으로 코드 작성하기

Gradle은 명령형 서술이 가능하기 때문에, 절차적으로 동작하는 간단한 스크립트를 태스크 내에 정의할 수 있다. 이는 선언적으로 작성되는 인프라스트럭처(Infrastructure) 빌드 도구인 테라폼(Terraform)과 구분되는 특징이다. 다음 예제에서 정의된 upper 태스크는 특정 변수의 모든 문자를 대문자로 변경한다.

📂 build.gradle

task upper {
    doLast {
        String mixedString = "gRaDLe"
        println "원본문자열: $mixedString"
        println "처리문자열: ${mixedString.toUpperCase()}"
    }
}

실행 결과는 다음과 같다.

$ gradle upper -q

원본문자열: gRaDLe
처리문자열: GRADLE

또다른 예를 보자.

📂 build.gradle

task loop {
    doLast {
        5.times { 
            print "$it " 
        }
    }
}

주어진 숫자만큼 코드를 반복하는 루프 구문이다. for 구문이나 while 구문처럼 복잡한 구조 없이도 지정된 크기만큼 순회가 가능하다(물론 for 구문도 지원한다). 이때 시스템 변수 $it는 순회 횟수, 또는 고유 실행 번호를 참조하기 위해 사용된다. 실행 결과는 다음과 같다.

$ gradle loop -q 

0 1 2 3 4

이처럼 태스크 내에서 변수를 선언하거나 값을 할당하는 것, 해당 변수를 객체 형태로 사용하는 것, 특정 횟수 만큼 순회하기 등 스크립트를 위한 다양한 문법이 허용된다. 따라서 간단한 스크립트 처리를 태스크 내에서 정의할 수 있다.

4. 외부 매개변수 사용

앞서 작성해 본 upper이나 loop 같은 태스크는 외부에서 매개변수 입력이 가능하도록 구현하면 훨씬 효율적이다. 이를 위해 Gradle에서는 매개변수 전달을 가능하게 해주는 -P 옵션을 제공한다. 해당 옵션을 통해 전달된 변수는 태스크 내에서 일반 변수처럼 사용할 수 있다. 이를 위해 별도 바인딩 과정은 필요하지 않다. 여러 개의 매개변수를 전달할 수 있으며, 갯수의 제한은 없다. 형식은 다음과 같다.

gradle <태스크명> -P <변수명>=<값> -P <변수명>=<값> ... 

매개변수 사용을 위해 조금 전 작성한 upper 태스크를 다음과 같이 수정하자.

📂 build.gradle

task upper {
    doLast {
        String mixedString = input
        println "원본문자열: $mixedString"
        println "처리문자열: ${mixedString.toUpperCase()}"
    }
}

바뀐 게 별로 없다. mixedString 변수에 값을 할당하는 대상이 치환되었으며, 이 과정에서 정의 안된 input 변수가 갑툭튀 했을 뿐이다. 이제 이 태스크를 호출해서 결과를 확인하자.

$ gradle upper -P input="mY naME IS wINNy lEE" -q 

원본문자열: mY naME IS wINNy lEE
처리문자열: MY NAME IS WINNY LEE

조금 전에 등장한 input이 -P 옵션과 함께 매개변수로 사용되었다. 실행 결과를 확인하면 매개변수로 치환한 부분이 잘 반영되었음을 알 수 있다.

이제 두 번째 태스크인 loop도 매개변수를 통해 순회 횟수를 제어할 수 있도록 수정해보자.

📂 build.gradle

task loop {
    doLast {
        int n = num.toInteger()
        n.times {
            print "$it "
        }
    }
}
$ gradle loop -P num=8 -q 

0 1 2 3 4 5 6 7

매개변수 num을 통해 정수 8을 전달했다. 이때 Gradle에서 외부 매개변수는 항상 String 타입으로 전달되기 때문에, toInteger() 메소드를 통해 정수로 변경하지 않으면 다소 황당한 결과가 벌어질 수도 있다. 전달한 값을 문자로 인식하고 그에 해당하는 아스키 코드값 만큼 순회를 하기 때문이다. 원하는 매개변수 타입이 문자열이 아니라면 원하는 형에 맞게 변경하는 타입 캐스팅이 필수적이다.

5. 동시에 여러 태스크 호출하기

gradle 커맨드 하나로 동시에 여러 개의 태스크를 호출할 수 있다. 방법은 다음과 같다.

$ gradle [Task명] [Task명] ...

직접 확인해보자.

📂 build.gradle

task taskX {
  doFirst {
    println "taskX를 실행합니다"
  }
}

task taskY {
  doFirst {
    println "taskY를 실행합니다"
  }
}

실행시에는 태스크 명을 나열해주면 된다. 태스크명 사이에 콤마나 구분자는 필요없다.

$ gradle taskX taskY -q

gradle taskX taskY -q
taskX를 실행합니다
taskY를 실행합니다

6. 의존성 태스크 정의하기

태스크를 정의하다 보면 다른 태스크의 실행이 전제되어야 할 경우가 있다. 서버에 배포를 하려면 소스 코드가 컴파일 및 패키징되어 있어야 하는 경우가 그러하다. 이를 프로그래밍 관점에서는 의존성을 가진다 혹은 종속적이다라고 표현한다. Gradle에서는 두 개의 태스크 사이에 의존성을 설정할 수 있다.

의존성 설정에는 dependsOn 키워드를 사용한다.

// 형식 #1 
task <태스크명> (dependsOn: <의존태스크명>) {
  ...
}

// 형식 #2 
task <태스크명> {
  dependsOn <의존태스크명>      
  ...
}

// 형식 #3
task <태스크명> {
  ...
}
<태스크명>.dependsOn(<의존태스크명>)

실제로 의존성 관계를 설정해보자. 배포를 담당하는 가상의 태스크 deploying과 패키징을 담당하는 packaging이 의존 관계에 있다고 가정한다.

📂 build.gradle

task packaging {
    doFirst {
        println("코드 패키징을 시작합니다...")
    }
}

task deploying(dependsOn: packaging) {
    doFirst {
        println("코드 배포를 시작합니다....")
    }
}
$ gradle deploy -q

코드 패키징을 시작합니다...
코드 배포를 시작합니다....

deploy 태스크만 실행했지만, 의존성 관계에 있는 packaging 태스크도 함께 실행된 것을 알 수 있다. 반대의 경우도 확인해보자.

$ gradle packaging -q

코드 패키징을 시작합니다...

packaging 태스크를 실행하면 deploy 태스크가 실행되지 않는다. 의존성 관계가 그 반대 방향으로만 설정되었기 때문이다. 참고로 Gradle에서 태스크간 상호 의존 관계는 지원되지 않는다.

6.1 아직 정의되지 않은 태스크를 의존 관계로 등록하기

build.gradle은 스크립트 파일이기 때문에, 작성 순서에 따라 결과가 달라질 수 있다. 다음을 보자.

📂 build.gradle

task deploy(dependsOn: packaging) {
    doFirst {
        println("코드 배포를 시작합니다....")
    }
}

task packaging {
    doFirst {
        println("코드 패키징을 시작합니다...")
    }
}

deploy와 packaing 태스크의 정의 순서를 바꾸었다. 다시 실행하여 결과가 어떻게 되는지 보자.

$ gradle deploy -q

FAILURE: Build failed with an exception.

* Where:
Build file '/../build.gradle' line: 16

* What went wrong:
A problem occurred evaluating root project 'GradlePractice'.
> Could not get unknown property 'packaging' for root project 'GradlePractice' of type org.gradle.api.Project.

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 777ms

순서만 바꾸었을 뿐인데, 실행시 오류가 발생한다. packaging 패키지를 찾지 못하기 때문이다. 이처럼 정의되지 않은 태스크를 먼저 의존성 관계로 지정하면 Gradle은 이를 인식하지 못한다.

해결 방법은 간단하다. 의존성을 지정할 때 태스크 이름에 쿼우팅 처리를 해주면 된다.

📂 build.gradle

task deploy(dependsOn: "packaging") {
    doFirst {
        println("코드 배포를 시작합니다....")
    }
}

task packaging {
    doFirst {
        println("코드 패키징을 시작합니다...")
    }
}
$ gradle deploy -q 

코드 패키징을 시작합니다...
코드 배포를 시작합니다....

이제 정상적으로 실행된다. 이처럼 태스크 지정시 쿼우팅을 하면 작성 순서 때문에 참조 오류가 발생하는 일을 없앨 수 있다.

6.2 의존성 태스크의 중복 실행 배제

여러 개의 태스크를 동시에 실행해도 의존성 관계에 있는 태스크는 최초 한 번만 실행된다. 다음을 보자.

📂 build.gradle

task baseTask {
	doFirst {
		println "baseTask를 실행합니다"
	}
}

task taskX {
	dependsOn baseTask
	doFirst {
		println "taskX를 실행합니다"
	}
}

task taskY {
	dependsOn baseTask
	doFirst {
		println "taskY를 실행합니다"
	}
}

taskX와 taskY는 baseTask에 의존성을 가진다. 따라서 이들 각각을 실행하면 매번 baseTask가 실행될 것이다. 하지만 두 태스크를 한꺼번에 실행하면 baseTask는 최초 한 번만 실행된다. 의존성 관계이니만큼 중복 실행될 필요가 없는 까닭이다.

$ gradle taskX -q 

baseTask를 실행합니다
taskX를 실행합니다 
$ gradle taskY -q 

baseTask를 실행합니다
taskY를 실행합니다
$ gradle taskX taskY -q 

baseTask를 실행합니다
taskX를 실행합니다
taskY를 실행합니다

7. 태스크 정보 추가하기

Gradle은 태스크 관리를 위해 메타 정보를 추가할 수 있는 옵션을 제공한다. 태스크 정의시에 포함할 수 있는 것들이다. 대표적인 것들로는 groupgroup 등이 있다.

📂 build.gradle

task deploy(dependsOn: "packaging") {
  	group "꼼꼼한 재은씨의 Custom Task"
  	description "코드를 배포하기 위한 태스크. packaging에 의존성이 있다."
    doFirst {
        println("코드 배포를 시작합니다....")
    }
}

task packaging {
  	group "꼼꼼한 재은씨의 Custom Task"
    description "코드를 패키징하는 태스크"
    doFirst {
        println("코드 패키징을 시작합니다...")
    }
}

작성된 정보는 태스크 목록에서 확인할 수 있다. 목록 확인은 gradle tasks --all 커맨드를 사용한다.

$ gradle tasks --all -q 

------------------------------------------------------------
Tasks runnable from root project
------------------------------------------------------------

Build Setup tasks
-----------------
init - Initializes a new Gradle build.
wrapper - Generates Gradle wrapper files.

...(중략)...

꼼꼼한 재은씨의 Custom Task tasks
--------------------------
deploy - 코드를 배포하기 위한 태스크. packaging에 의존성이 있다.
packaging - 코드를 패키징하는 태스크

Other tasks
-----------
prepareKotlinBuildScriptModel

태스크 목록 중에 조금 전 우리가 작성한 deploy와 packaging이 그룹을 이루고 있는 것을 볼 수 있다. 그룹명은 group 항목에 입력한 내용과 동일하다. 또한, description 항목에 입력한 내용이 태스크 설명으로 표시되어 있다.

이외에도 추가할 수 있는 메타 정보는 다양하다. 좀더 많은 내용을 알고 싶다면 Gradle 공식 페이지를 확인하기 바란다.