Gradle 초보를 위해 핵심만 추렸다

나는 무려 10년이 된 Java 프로젝트를 여러개 관리하고 있는데, Netbeans와 Ant 기반의 개발/빌드 환경을 사용한다.

Netbeans는 Sun이 Oracle로 넘어간 뒤에 Apache 재단으로 넘어가면서 개발 동력이 많이 떨어져 있다. 열심히 노력하기는 하지만, 경쟁 툴들이 워낙 많다보니... Ant 또한 마이크로 컨트롤에 치중한 느낌이어서, 의존성 관리를 자동화하는 Maven류에 밀린지 한참이다.

언제 한번 개발환경을 갈아쳐야지 벼르고 있다가, 큰 프로젝트가 끝나고 잠깐 1주일 느슨한 틈에 Gradle로 마이그레이션 작업을 했다. 그 와중에 경험한 것들을 추려서 정리하고자 한다.

왜 Gradle+Kotlin으로 정했나?

한때 Maven이 Java 빌드 툴로 인기를 끌었는데, XML로 된 거추장스러운 문법이 항상 문제였다. 그리고 자유도가 너무 낮다. 같은 XML 기반의 Ant도 그랬지만, XML은 데이터를 표현하는 방식이다 보니, 다양한 작업을 구현하기에는 한계가 존재했다. 그래서 스크립트 기반의 빌드툴이 나오기 시작했는데, 그 중에 가장 성공한 것이 Gradle이다.

Gradle은 초기에 Groovy를 사용하여 깔끔한 빌드 스크립트를 가능케 했다. 이 깔끔한 스타일을 전문용어로 DSL(Domain Specific Language)라고 한다. XML에 비하면 엄청 깔끔하다.


하지만 Groovy는 인터프리터 기반의 스크립트였기 때문에 사전 에러를 찾아내는데 한계가 있었다. 게다가 Kotlin이 갑자기 급부상하고, Gradle도 DSL로 Kotlin을 도입하면서 Gradle/Kotlin 조합이 유망해지고 있다. 어차피 Android 개발의 공식 도구가 된 Kotlin을 어차피 배워야 한다면, 굳이 Groovy라는 다른 언어를 배우느니 Kotlin으로 빌드 환경을 만드는 것이 낫다.

그래서 Gradle/Kotlin 조합으로 정했다. Groovy로 작성된 빌드 스크립트는 build.gradle 이지만, Kotlin으로 작성된 것은 build.gradle.kts 라고 .kts 확장자가 붙어야 한다. 나중에 Kotlin이 지배적이 되면 이 딱지도 사라질 것으로 기대한다.

Gradle 프로젝트 시작하기 


Gradle은 Java로 개발되었기에 거의 모든 OS에서 돌아간다. 여기서 다운받아 설치하면 된다. Gradle을 설치했다면 다음 명령으로 Gradle 프로젝트의 윤곽을 만들 수 있다. (DSL로 kotlin을 지정)

$ gradle init --dsl kotlin

하지만 굳이 Gradle을 설치하지 않아도 된다.

Gradle을 설치해도 되지만, 희안하게도 Gradle은 굳이 설치하지 않아도 된다. https://gradle-initializr.cleverapps.io/ 로 들어가면 gradle init을 실행한 것 같은 골격을 zip파일로 만들어준다. 일단 이 zip 파일을 풀어서 gradle wrapper를 실행하면 gradlew가 로컬 디렉토리에 생기기 때문에 Gradle을 굳이 설치할 필요가 없다.



Gradle Wrapper는 뭘까?


만일 Ant로 구성된 어떤 프로젝트를 git로 받아서 빌드하려면 Ant를 그 시스템에 설치해야 한다. 그런데 Ant를 설치하는 것도 귀찮고, 버전이 다른 것이 문제가 되기도 하고, 설치된 플러그인이 달라 골치를 썩힌다. Gradle은 이 문제를 깔끔하게 했는데, Gradle을 실행하는 스크립트를 소스 트리 안에 아예 포함시킨 Gradle Wrapper를 제공한다.

Gradle Wrapper는 gradlew 또는 gradlew.bat으로 실행하는데, 처음 실행한다면 아예 지정된 버전의 Gradle과 플러그인을 다운받아 설치까지 해 버린다. 그래서 Gradle Wrapper가 포함된 프로젝트는 다운받은 다음 gradlew만 실행하면 알아서 Gradle까지 설치해 버리니 여간 편리한 것이 아니다.

Gradle Wrapper는 gradle init을 실행하면 자동으로 만들어지지만, 따로 만들려면 다음 명령을 실행하면 된다.

$ gradle wrapper

그러면 gradle이라는 폴더 아래에 Wrapper가 만들어진다. 이 Wrapper까지 포함해서 소스 트리를 구성하면 된다.

만일 로컬에 설치된 Gradle 버전이 아닌, 임의의 버전으로 Wrapper를 만들고 싶다면 다음과 같이 하면 된다. (6.5를 깔 경우)

$ gradle wrapper --gradle-version 6.5

Gradle의 Task 

Gradle 빌드 스크립트를 작성한다는 것의 대부분은 Task를 작성하고 구성하는 것이다. Task는 서로 의존 관계를 가지는 하나의 작업인데, 고전적인 make나 ant를 사용해 봤다면 개념에 익숙할 것이다.

만일 Java Application으로 구성한 Gradle 프로젝트라면 다음과 같이 Task들이 구성된다. Java 소스를 컴파일해서 class 파일을 만들고, 이것을 jar로 묶는다. 그리고 배포용 tar와 zip을 만든 다음, test를 수행하는 식이다.


이런 기본 Task를 고쳐도 되고, 새로운 Task를 추가하여 위의 DAG(Directed Acyclic Graph)에 끼워 넣어도 된다. 위의 그림은 트리로 보이지만, 쉽게 표현하기 위해 사실 방향성 그래프를 풀어 헤친 것이다.

새로운 Task를 추가할 때는 tasks.register()를 사용하며, 기존에 있는 Task를 수정할 때는 tasks.named()를 사용하여 Task를 기술하면 된다. 이 두 함수에는 <>로 Task의 클래스를 지정할 수 있다. 이 클래스를 지정할 경우, 해당 클래스에 구현된 속성을 사용할 수 있다.

예를 들어 기본적인 Task는 description과 dependsOn과 같은 기본 속성이 제공된다. 하지만 Copy Task에는 from, into와 같은 복사하고자 하는 소스와 목적지를 정하는 속성이 제공되는 식이다.


tasks.register<Copy>("copyRoot") {
    description = "Copy logback.xml and etc to classes root"
    dependsOn("classes")

    from("conf") {
        include("*.xml")
    }
    into ("$buildDir/classes/java/main")    
}

tasks.named<Jar>("jar") {
    dependsOn("gitVersion")
    archiveName = "${rootProject.name}.jar"

    manifest {
        // attributes(mapOf("Main-Class" to "com.voce.protoss.ProtossMain"))
        attributes["Main-Class"] = "com.voce.protoss.ProtossMain"
        attributes["Gradle-Version"] = "Gradle " + getProject().getGradle().getGradleVersion()
        attributes["Created-By"] = "Java " + JavaVersion.current()
        attributes["Class-Path"] = configurations.runtimeClasspath.get().filter { 
            it.name.endsWith(".jar") 
        }.joinToString(separator=" ") { "lib/" + it.name }
    }
}


Gradle 빌드 스크립트의 문법은 왜 이상할까?

Kotlin은 Java와 매우 비슷한 문법을 가지고 있다. 그도 그럴듯이 Kotlin은 Java와 거의 빈틈없이 호환이 가능하다. 둘다 class 파일로 컴파일되어 JVM에서 실행될 수 있다. (물론 Kotlin은 Javascript나  Native로 컴파일될 수도 있다)

이런 상식을 가지고 build.gradle.kts 파일을 보면, 일반적인 Kotlin과 상당히 다른 문법을 사용한다는 것을 느낄 수 있다. 내가 처음 겪은 Gradle에서의 멘붕은 바로 이 때문이었다. 하지만 build.gradle.kts는 Kotlin의 연산자 오버로딩, 람다, 클로저 등의 수단을 이용해서 Kotlin 끼를 싹 뺀 DSL로 만든 것이다. 그래서 이 스크립트는 완벽하게 Kotlin에서 컴파일된다.

이런 마법이 어떻게 가능한지는 Fre Dumazy의 Writing DSLs in Kotlin 글을 보면 알 수 있다.

Gradle의 출력을 숨기지 않으려면?


Gradle로 프로젝트를 빌드하려면 gradlew build를 실행하면 된다. 하지만, 이렇게 하면 BUILD SUCCESSFUL만 딸랑 나오고 끝나 버린다. 중간 과정이 어떻게 나오는지 알 수가 없다.

일반적인 형태처럼 빌드 과정이 일렬로 출력되기 원하면 다음과 같이 실행하면 된다.

$ ./gradlew build --info

또는 아래와 같이 실행한다. (보다 함축적이고, 컬러로 출력된다)

$ ./gradlew build --console=verbose

build Task는 assemble과 test Task를 모두 실행한다. 테스트를 수행하지 않고 출력물만 얻으려면 ./gradlew assemble 을 실행하면 된다.

Gradle 빌드 스크립트의 Task 정보를 보려면?

Gradle 빌드 스크립트의 모든 Task를 보려면 다음과 같이 실행한다. 그러면 Task 이름과 description이 같이 출력된다.

$ ./gradlew tasks --all

하지만, 위의 출력 결과는 Task의 관계를 잘 보여주지 못한다.


Graddle의 task의 DAG 구성을 보려면 taskTree 라는 플러그인을 사용해야 한다. 별도로 설치할 필요는 없고, 다음과 같이 plugin 영역에 넣어주면 알아서 다운받아 설치한다.

plugins {
    // show DAG 
    id("com.dorongold.task-tree") version "1.4"
}

트리 형식으로 보려면 다음 명령을 실행한다.

$ ./gradlew build taskTree

Java 컴파일시 Encoding Error가 나면?

Windows에서 Java 컴파일시, 다음과 같이 인코딩 에러가 나는 경우가 있다. 소스코드는 UTF-8로 되어 있는데, OS는 EUC-KR로 되어 있어서 나는 에러이다.

C:\...\ModbusMapperTest.java:77: error: unmappable character for encoding MS949
    //bits = "4-3" ?쑝濡? ?븯?뜑?씪?룄 ?쐞?? ?삊媛숈씠 ?굹???빞 ?븿.

이 경우 javac 컴파일러의 옵션을 세팅해야 한다. Gradle의 Java 플러그인은 두개의 JavaCompile Task 클래스 인스턴스를 제공한다. compileJava와 compileTestJava이다. 같은 타입의 Task에 대해 한꺼번에 옵션을 주려면 다음과 같이 withType을 사용하면 된다.

tasks.withType<JavaCompile> {
    options.encoding = "UTF-8"
}

어떤 Task를 항상 실행시키고 싶으면?


Gradle의 Task는 항상 실행하는 것이 아니라, 입력의 변경 시간이 출력의 변경 시간보다 최신일 때 실행된다. 그래서 build를 실행하더라도 소스코드의 변경이 없으면 이후 과정은 모두 Up-To-Date되어 실행되지 않는다.

하지만 무조건 실행되어야 하는 케이스가 있다면 다음과 같이 outputs.upToDateWhen에 항상 false를 주면 된다.

tasks.register<Copy>("deploy") {
    description = "Deploy war to Tomcat"
    dependsOn("assemble")
    outputs.upToDateWhen { false }
    from("build/libs") {
        include("*.war")
    }
    into("${tomcatHome}/webapps")
}

의존성 문제 확인하기

Gradle은 Maven처럼 상위 라이브러리에 대한 Dependency만 정의해주면 알아서 필요한 부속 라이브러리까지 알아서 다운로드 받아 추려준다. 그 결과를 알고 싶다면 다음 명령으로 확인할 수 있다.

$ ./gradlew dependencies

만일 Gradle이 다운로드 받아둔 라이브러리 캐쉬를 모두 없애고, 새로 받고 싶다면 다음 명령을 실행하면 된다.

$ ./gradlew build --refresh-dependencies


그외 알아두면 좋을 사항들 



Fat Jar를 만들고 싶다면 Shadow Jar 플러그인을 사용하면 된다. Fat Jar는 의존하는 라이브러리 파일을 모두 풀어서, 메인코드와 함께 섞은 다음 하나의 Jar로 만드는 방법이다. 이렇게 하면 한 파일만 배포하면 되어서 편리하다.

VS Code를 사용한다면, Kotlin 확장이 아니라 Kotlin Language 확장을 설치해야 한다. 그래야 build.gradle.kts의 하이라이팅이 제대로 지원된다. 하지만 아직 VS Code에서 Kotlin이나 Gradle 지원이 완벽하지 않다. 당분간은 IntelliJ를 사용하는 것이 좋을 것이다.

댓글 없음:

댓글 쓰기

인기글