운영 자동화#1 — 빌드 자동화 by Gradle

개발자가 비지니스 로직 구현에 집중하기 위한 운영 자동화의 첫번째 단계로 Gradle을 활용한 빌드 자동화에 대해서 설명한다. 개발에 대한 아주 기본적인 내용과 직접 Gradle 플러그인을 만드는 고급과정은 생략하고 gradle을 활용하기 위한 핵심적인 내용만 다루니 참고하기 바란다.

빌드 자동화란?

프로젝트에서 개발을 하기전에 보통 로컬 개발 환경 셋팅을 한다. IDE 또는 편집기를 설정하는 일을 제외하면 보통 가장 먼저하는 일이 빌드 환경 설정일 것이다. 정적 타이핑 언어는 보통 코드를 작성하면 컴파일을 해서 오브젝트 파일을 생성하고, 링킹이라는 작업을 통해서 실행 파일 또는 java의 jar와 같은 라이브러리 파일을 생성한다. 언어마다 차이가 있지만 이런 작업들을 자동화하는 것이 빌드 자동화이다.

빌드 자동화를 위한 툴은 언어마다 매우 다양한데, C나 C++ 등을 사용할때는 보통 Makefile, Java는 Ant, Maven, Scala는 sbt를 주로 사용하고, 최근에는 여러가지 언어의 빌드 환경을 구성할 수 있는 오픈소스인 Gradle도 등장했다.

Gradle이 나온지는 꽤 오래됐지만, 구글이 안드로이드의 기본 빌드 시스템으로 Gradle을 선택하면서 사용자가 급증하였다. 과거에 안드로이드 스튜디오가 이클립스로 되어 있을때는 프로젝트 빌드를 이클립스가 담당했지만, 인텔리제이를 기반으로 바뀌면서 빌드 시스템이 IDE와 독립적으로 분리되었다. 여기서는 안드로이드 등 특정 IDE와는 독립적으로 Gradle 자체를활용한 빌드 자동화를 설명한다.

Gradle의 특장점

간결함

Gradle은 기본적으로 Groovy라는 언어를 활용한 DSL을 스크립트로 사용한다. 사실상 플러그인을 직접 작성할 것이 아니라면 Groovy라는 언어를 몰라도 Gradle 스크립트에 익숙해 질 수 있다. Gradle은 다양한 기능과 장점이 있지만, 개인적으로 가장 큰 장점으로 간결함을 꼽고 싶다.

일단 xml을 사용하지 않기때문에 장황하지 않다. Groovy라는 언어를 기반으로 하기 때문에 변수 선언, if, else, for 등의 로직 구현이 가능하다.

문서화

필자는 처음 Gradle를 공부할 때, 주로 공식 홈페이지를 활용했다. 문서화가 매우 잘되어있기 때문에 이것만으로도 충분하다고 생각한다. Gradle에는 플러그인이 굉장히 많기 때문에 모든 문서를 정독하는 것을 불필요하다. 하지만 플러그인들을 제외한 문서만 읽고 따라하는데는 하루 2~3시간씩 5일 정도면 충분하다. 공식 홈페이지 외에도 많은 좋은 자료들이 있지만, 버전 업이 매우 빠르게 되고 있기때문에 가급적이면 공식 홈페이지를 먼저 보자!

속도

Gradle은 성능 향상을 위해서 다양한 기능을 제공한다. 빌드 시스템에서 빌드 속도는 매우 중요한 부분이다. 개발을 할때 빌드를 얼마나 자주하는지 생각해 보자. 빌드 속도는 개발 생상성에 지대한 영향을 미친다.

Gradle은 성능 향상을 위해서 증분 빌드, 작업 결과 캐싱, 증분 하위 작업, 데몬 프로세스 재사용, 병렬 실행, 병렬 다운로드 등의 기능을 지원한다. 여기서는 각 기능에 대한 자세한 내용은 다루지 않겠다. https://gradle.org/features/에서 확인하자.

멀티 프로젝트

Gradle은 멀티 프로젝트 구성이 가능하다. 하나의 repository내에 여러개의 하위 프로젝트를 구성할 수 있다. 하나의 아티팩트(ex: jar)를 만들기 위해서 별도의 프로젝트를 만들고 Repository를 구성할 필요가 없다.

또한 상위 프로젝트의 의존성 및 설정을 하위 프로젝트에서 상속받아 사용할 수 있기 때문에 중복 설정이 불필요하다.

유연성 + 확장성

Groovy 기반 스크립팅을 통해서 다양한 기능을 스크립트안에 직접 구현할 수 있다. 예를들어 시스템 프로퍼티에서 현재 빌드를 하고 있는 사용자의 아이디나 OS 버전 정보를 가져와서 아티팩트의 버전 정보로 사용할 수 있다.

직접 Task를 구현하고, 플러그인을 만들어서 기능을 추가할 수도 있다. 이런 뛰어난 확장성과 유연함으로 수많은 오픈소스 플러그인 생태계가 만들어 졌다.

플러그인 생태계

Gradle에는 플러그인을 관리하기 위한 허브가 있다. 그리고 허브안에는 수많은 Gradle 플러그인들이 존재한다. 여기서는 몇가지 유용한 플러그인만 소개한다.

checkstyle, pmd, findBugs, Sonar, Lint 등 소스코드 정적 분석 툴들을 적용할 수 있다. 플러그인을 적용하면 빌드시, 룰에 어긋나는 코드를 경고한다. 또한 웹 기반의 리포트도 만들어준다.

jacoco, cobertura, clover, sonarqube 등 테스트 커버리지 툴들을 적용할 수 있다. 기본적으로 테스트 커버리지에 대한 리포트가 만들어지고, 경고를 띄우는 등의 설정도 가능하다.

license 플러그인을 사용하면 자동으로 모든 java 파일의 상단에 원하는 형식의 license를 넣어주도록 할 수 있다.

안드로이드 뿐만 아니라 C++, Swift, Objective C, Haskell, Scala 등의 언어도 플러그인을 설정하면 빌드가 가능하다.


지금부터는 필자와 같이 Gradle 프로젝트를 만들어보면서 개념과 사용법을 익혀보자. 여기서 다룰 예제는 폴리글랏 언어를 지원하는 멀티 프로젝트 구성이다. 추가로 위에서 언급했던 몇가지 플러그인들을 적용해 본다. 완성된 예제는 Github를 참고하자.

기본 설정

먼저 Gradle을 설치해보자. 최신 버전(현재 4.2.1)의 Gradle은 Java JDK 또는 JRE 7 또는 이상의 버전이 필요하다. Java가 설치되어 있지 않다면 설치하자. 그리고 공식 사이트를 참고해서 최신 버전의 gradle을 설치하자.

설치 및 확인이 완료되었다면 프로젝트 폴더를 하나 생성하고 터미널에서 아래 명령을 실행하자.

> gradle init

아래 폴더 및 파일들이 생성될 것이다.

project/
gradlew
gradlew.bat
gradle/wrapper/
gradle-wrapper.jar
gradle-wrapper.properties
build.gradle
settings.gradle

각각에 대해서 살펴보기 전에 Gradle Wrapper에 대해서 알아보자. 일단 Gradle Wrapper를 사용하는 목적은 이미 존재하는 프로젝트를 새로운 환경에 설치할때 별도의 설치나 설정과정없이 곧 바로 빌드할 수 있게 하기 위함이다. Java나 Gradle도 설치할 필요가 없다. 또한 로컬에 설치된 Gradle 또는 Java의 버전도 신경쓸 필요가 없다. 따라서 항상 Wrapper를 사용할 것을 권장한다.

gradlew 파일은 유닉스용 실행 스크립트다. Gradle로 컴파일이나 빌드 등을 할때, 아래와 같이 하면 로컬에 설치된 gradle을 사용한다.

> gradle build

이 경우 Java나 Gradle이 설치되어 있어야 하고, 새로받은 프로젝트의 Gradle 버전과 로컬에 설치된 Gradle 버전이 호환되지 않으면 문제가 발생할 수 있다. 따라서 Wrapper를 사용하면 아래와 같이 실행한다.

> ./gradlew build

gradlew.bat 파일은 원도우용 실행 배치 스크립트다. 원도우에서 실행 가능하다는 점만 제외하면 gradlew와 동일하다.

gradle/wrapper/gradle-wrapper.jar 파일은 Wrapper 파일이다. gradlew나 gradlew.bat 파일이 프로젝트 내에 설치하는 이 파일을 사용하여 gradle task를 실행하기 때문에 로컬 환경의 영향을 받지 않는다. (실제로는 Wrapper 버전에 맞는 구성들을 로컬 캐시에 다운로드 받음)

gradle/wrapper/gradle-wrapper.properties 파일은 Gradle Wrapper 설정 파일이다. 이 파일의 wrapper 버전 등을 변경하면 task 실행시, 자동으로 새로운 Wrapper 파일을 로컬 캐시에 다운로드 받는다.

build.gradle 파일은 의존성이나 플러그인 설정 등을 위한 스크립트 파일이다. 뒷부분에서 많이 다룰 것 이다.

settings.gradle 파일은 프로젝트의 구성 정보를 기록하는 파일이다. 어떤 하위프로젝트들이 어떤 관계로 구성되어 있는지를 기술한다. Gradle은 이 파일에 기술된대로 프로젝트를 구성한다.

멀티 프로젝트 구성

이제 기본적인 설정이 끝났다. 이제 폴리글랏 언어를 지원하는 멀티 프로젝트로 구성해 보자. 먼저 settings.gradle 파일을 열고 아래와 같이 셋팅하자.

rootProject.name = 'algorithm'

include 'java'
include 'kotlin'

rootProject.name은 최상위 프로젝트의 이름이다. 기본적으로는 프로젝트 폴더명으로 만들어진다.

그리고 algorithm 프로젝트의 하위프로젝트로 java, kotlin를 포함시켰다. 여기서 만약 하위 프로젝트의 하위 프로젝트를 만드려면 include 'java::sub' 와 같이 할 수 있다.

이제 하위 프로젝트 폴더를 만들자. IDE를 활용하면 간단하게 하위 프로젝트를 생성할 수 있지만, 여기서는 수동으로 만들어 볼 것이다.

> mkdir java
> mkdir kotlin

그리고 각각의 하위 프로젝트의 기본 폴더 트리를 생성한다.

> mkdir -p java/src/main/java
> mkdir -p kotlin/src/main/kotlin

여기서는 각 언어의 기본 폴더 구조가 비슷하지만, 언어에 따라서 다를 수 있다. 여기서 각 하위 프로젝트의 폴더명은 java, kotlin으로 생성되었지만, 모듈명은 기본으로 algorithm-java, algorithm-kotlin이 된다. IDE로 프로젝트를 열어보면 이러한 폴더명과 모듈명을 확인할 수 있다. 또한 모듈명을 직접 변경할 수도 있다.

이제 각 하위 폴더를 Gradle 프로젝트로 만들 차례다. 최상위 폴더에 build.gradle 파일이 존재하는 것 처럼, 각 하위 프로젝트의 상위에도 build.gradle 파일을 만들자.

> touch java/build.gradle
> touch kotlin/build.gradle

마지막으로 최상위 프로젝트의 build.gradle 파일에 모든 하위 프로젝트에 적용되는 공통 설정을 적용하자.

subprojects {
group = "funfunstudy" // 생성될 아티팩트의 그룹명

repositories {
mavenCentral()
}

dependencies {
}
}

subprojects내의 설정값들은 모든 하위 프로젝트에 적용될 것이다. 만약 최상위 프로젝트를 포함한 모든 하위 프로젝트에 공통으로 적용하고 싶다면, allprojects를 사용할 수 있다.

Java용 프로젝트 설정

java/build.gradle 파일을 아래와 같이 설정하자

apply plugin: 'java'        // 'java'라는 Gradle 플러그인 적용
sourceCompatibility = 1.8   // Java 호환 버전을 1.8로 설정
dependencies {
compile 'ch.qos.logback:logback-classic:1.2.3'
compile 'ch.qos.logback:logback-core:1.2.3'
    testCompile 'junit:junit:4.12'
}

이 설정들은 java 폴더 하위의 프로젝트에만 적용된다. 이제 컴파일할 HelloWorld.java 파일을 java/src/main/java에 추가하고, ./gradlew build로 빌드해보자.

import java.util.Scanner;
class HelloWorld {
    public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int cases = sc.nextInt();
        for (int i = 0; i < cases; i++) {
String name = sc.next();
System.out.println("Hello, " + name + "!");
}
}
}

정상적으로 빌드가 되는 것을 확인할 수 있을 것이다.

의존성 관리

이전 예제에서 dependencies로 사용될 외부 라이브러리에 대한 의존성을 설정하였다. 여기서는 Gradle의 의존성 설정 방법에 대해서 자세히 알아보자.

아래와 같이 repositories를 사용해서 의존성을 가져올 주소를 설정한다. dependencies를 사용해서 설정된 Repository에서 가져올 아티팩트를 설정한다.

allprojects {
repositories {
mavenCentral()
jcenter()
        maven {
url "http://repo.mycompany.com/maven2"
}
        ivy {
url "../local-repo"
}
}

dependencies {
// 로컬 jar 파일의 의존성 설정
compile fileTree(dir: 'libs', include: '*.jar')
// 로컬 프로젝트간 의존성 설정
compile project(':shared')
// 컴파일 타임에 의존성을 받아옴
compile 'com.google.guava', name: 'guava:23.0'
// 테스트시만 의존성을 받아옴
// 마이너 버전을 '+'로 설정해서 항상 4점대 최신 버전을 사용
testCompile group: 'junit', name: 'junit', version: '4.+'
// 컴파일할때는 사용하고, 아티팩트를 만들때는 포함하지 않음
compileOnly 'org.projectlombok:lombok:1.16.18'
// 실행할때 의존성을 받아옴(기본적으로 컴파일을 모두 포함)
runtime('org.hibernate:hibernate:3.0.5')
}
}

uploadArchives를 사용해서 생성된 아티팩트를 배포하기 위한 주소를 설정한다.

uploadArchives {
repositories {
ivy {
credentials {
username "username"
password "pw"
}
url "http://repo.mycompany.com"
}
}
}

Gradle의 의존성 설정에 대해서 자세히 알아보았다. 이외에도 Gradle은 의존성 설정 관련하여 여러가지 유용한 기능을 제공한다. 자세한 내용은 여기를 참고하자.

Kotlin용 프로젝트 설정

kotlin/build.gradle 파일을 아래와 같이 설정하자.

buildscript {
ext.kotlin_version = '1.1.2-4' // ext로 전역변수를 설정하여 사용

repositories {
mavenCentral()
}

dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // $를 사용하여 변수를 참조할 수 있음
}
}

apply plugin: 'kotlin'

dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"

testCompile "org.jetbrains.kotlin:kotlin-test"
testCompile "org.jetbrains.kotlin:kotlin-test-junit"
}

buildscript classpath를 설정하여 해당 클래스 경로내의 클래스들을 사용할 수 있다. 아래 예를보자.

import org.apache.commons.codec.binary.Base64

buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'commons-codec:commons-codec:1.2'
}
}

task encode {
doLast {
def byte[] encodedString = new Base64().encode('hello world\n'.getBytes())
println new String(encodedString)
}
}

Base64 클래스는 Gradle에 기본으로 import된 클래스가 아니기 때문에 buildscript에 해당 클래스 경로를 등록해주었다. 이제 빌드 스크립트 내에서 해당 클래스를 import하고 사용할 수 있게된 것을 확인할 수 있다.

멀티 프로젝트를 구성할때는 buildscript에 설정되 의존성을 모든 서브 프로젝트의 빌드 스크립트에서 사용할 수 있다.

kotlin/build.gradle에서는 apply plugin: 'kotlin' 을 사용하여 Kotlin 플러그인을 적용하였다. 플러그인 적용으로 간단하게 Kotlin 빌드를 위한 준비가 끝났다. 아래와 같이 kotlin/src/main/kotlin/HelloWorld.kt 파일을 추가하여 빌드해보자.

import java.util.Scanner

fun main(args: Array<String>) {
val sc = Scanner(System.`in`)
val cases = sc.nextInt()

for (i in 0..cases - 1) {
val name = sc.next()
println("Hello, $name!")
}
}

Gradle 플러그인

지금까지 예제에서 간단하게 Java와 Kotlin을 빌드하기 위한 플러그인만 적용해 보았다. 위에서 언급한 것 처럼 Gradle의 플러그인들은 이보다 훨씬 많고, 오픈소스 플러그인 생태계도 활성화 되어 있다. 여기서는 필자가 자주 사용하는 플러그인들을 살펴보자.

checkstyle

checkstyle은 Gradle과는 별개로 프로젝트로 소스코드 정적분석 툴이다. Gradle에서는 빌드시 정적분석을 포함할 수 있도록 checkstyle 플러그인을 내장하고 있다.

apply plugin: 'checkstyle'
checkstyle {
toolVersion = "7.6"
configFile = rootProject.file('codequality/checkstyle.xml') as File
ignoreFailures = true
showViolations = true
}

apply plugin: 'checkstyle'을 하면 해당 플러그인에 포함된 기능들을 사용할 수 가 있다. 이 플러그인에는 checkstyle이라는 함수가 있고, 함수에 {} 클로저를 넘겨서 설정한다.

toolVersion이라는 속성으로 checkstyle의 버전을 설정하고, configFile로 적용할 룰을 설정할 수 있는 파일을 연결한다. ignoreFailures를 true로 설정하면 빌드할때 소스코드가 룰에 어긋나도 빌드 에러를 발생 시키지 않는다. Warning만 보여준다. showViolations은 빌드시, 위반사항에 대한 내용을 콘솔에 보여줄지를 결정한다.

기본적으로 룰 위반시, build/reports/checkstyle에 상세 보고서도 만들어 준다. 기타 속성 및 상세 설명은 여기를 참고하자.

cobertura

covertura는 테스트 커버리지 도구이다.

apply plugin: 'cobertura'
cobertura {
coverageSourceDirs = sourceSets.main.java.srcDirs
coverageFormats = ['html']
coverageIncludes = ['**/*.java', '**/*.groovy']
coverageExcludes = []
}

coverageSourceDirs는 측정 대상 소스코드를 설정한다. coverageFormats는 커버리지 측정 결과 보고서 포멧을 설정한다. coverageIncludes는 측정에 포함시킬 소스코드 파일 및 패턴을 설정할 수 있다. coverageExcludes는 커버리지 측정에 제외시킬 소스코드를 설정한다. 여기를 참고하면 더 많은 속성들을 알아볼 수 있다.

cobertura는 checkstyle과 달리 Gradle이 내장하고 있는 플러그인이 아니다. 따라서 위와같이 설정하면 플러그인을 찾지 못해서 에러가 난다. 따라서 외부 라이브러리를 포함할 수 있는 classpath를 설정한다.

buildscript {
repositories {
mavenLocal()
}
    dependencies {
classpath 'net.saliman:gradle-cobertura-plugin:2.4.0'
}
}

license-gradle-plugin

마지막으로 한가지만 더 적용해보자. 몇가지를 해보면 플러그인을 적용하는 방식이 비슷하기 때문에 새로운 플러그인에 쉽게 적응할 수 있다. 정말 처음부터 해보자.

프로젝트의 모든 소스코드에는 라이선스가 표기되어 있어야 한다는 요구사항이 있다고 가정하자. IDEA나 이클립스같은 IDE의 기능을 활용할 수도 있지만, 왠지 Gradle 플러그인도 있을 법하다.

구글에 gradle license로 검색해보니 여러가지가 나온다. 그중에서 가장 상단에 있고, Star가 많은 license-gradle-plugin을 적용해 보겠다.

buildscript {
repositories {
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath "gradle.plugin.nl.javadude.gradle.plugins:license-gradle-plugin:0.14.0"
}
}

apply plugin: "com.github.hierynomus.license"

지금까지와 비슷한 방식으로 classpath를 설정하고 플러그인을 적용하였다. 여기서 https://plugins.gradle.org/m2/ 는 Gradle 플러그인 허브같은 곳이다. 대부분의 오픈소스 플러그인들은 이곳에 올리고 있다. 그런데 메뉴얼을 보면 Gradle 2.1 이상부터는 아래와 같은 방식으로 플러그인 적용이 가능하다.

plugins {
id "com.github.hierynomus.license" version "0.14.0"
}

2.1 이상 버전부터 plugins이 등장하여 플러그인 적용이 좀 더 간단해졌다.

이제 라이선스 파일을 등록하고 연결해야한다.

license {
header rootProject.file('codequality/HEADER')
strictCheck true
ext.year = Calendar.getInstance().get(Calendar.YEAR)
ext.name = 'Company'
ext.email = 'support@company.com'
}

최상위 폴더나 지정된 위치에 라이선스 파일을 만들고 header로 연결해준다. strictCheck를 true로 하면, 소스코드의 헤더의 라이선스 내용을 엄격하게 체크한다. license내에서 ext 변수를 선언하면, 라이선스 파일 내에서 해당 변수를 참조할 수도 있다.

이제 헤더에 라이선스가 기록되지 않은 소스코드를 만들고 빌드하면 빌드 에러가 발생할 것이다. ./gradlew licenseFormat 를 실행해보면 자동으로 라이선스가 빠진 소스코드에 헤더를 넣어주는 것을 확인할 수 있을 것이다. 여기를 보면 이외에도 많은 기능들을 포함하고 있다.

이외에도 Gradle에는 굉장히 많은 내장 플러그인과 오픈소스 플러그인들이 있다. 이런 플러그인들을 잘 활용하면 개발자가 좀 더 중요한 일에 시간을 쓸 수 있다.

Gradle 스크립트의 이해

Gradle 스크립트는 groovy를 사용해서 만든 DSL이다. 모든 Gradle 스크립트는 두가지 개념으로 구성되어 있는데 projects와 tasks 이다.

모든 Gradle 빌드는 하나 이상의 projects로 구성된다. 그리고 각 project는 하나 이상의 task들로 구성되어 있다. 이 task는 어떤 클래스를 컴파일하거나 JAR를 생성하거나 javadoc을 만드는 작업들을 의미한다.

task hello {
doLast {
println 'Hello world!'
}
}

간단한 hello task를 위와 같이 직접 만들 수 있다. 그리고 gradle -q hello 로 해당 task만 실행해 볼 수도 있다.

Gradle 스크립트는 DSL이지만, 몇가지 약속을 제외하면 groovy라는 언어가 지닌 강점을 모두 이용할 수 있다. 따라서 DSL이 가지는 몇가지 특징과 groovy라는 언어만 알면, 직접 유용한 플러그인을 개발하는 것도 어려운 일이 아니다.

task hello {
doLast {
println 'Hello world!'
}
}
task intro(dependsOn: hello) {
doLast {
println "I'm Gradle"
}
}

dependsOn을 사용해서 task간의 의존성을 만들 수 있다. 위 예제의 경우, intro task를 실행하면 먼저 hello가 실행되고 intro가 실행된다.

Gradle 스크립트에 대한 자세한 내용은 groovy 언어에 대한 이해와 비슷하기 때문에 이 정도로 줄이겠다. Gradle 플러그인을 직접 개발하기 위해서는 Gradle API 문서와 DSL Reference 문서를 참고해야한다.

Reference

http://kwonnam.pe.kr/wiki/gradle#gradle

https://gradle.org