IT_Programming/Dev Tools

[Gradle] Gradle 사용자 가이드 번역!

JJun ™ 2015. 4. 10. 18:37



 출처: http://kwonnam.pe.kr/wiki/gradle
 참고: 
http://www.gliderwiki.org/wiki/197   






Gradle

설치

  • 설치 파일 압축을 풀고 설치 경로를 GRADLE_HOME 환경변수로 설정
  • GRADLE_OPTS에 Gradle 전용 JVM 옵션 설정
  • GRADLE_USER_HOME : 없으면 $HOME/.gradle. 여기에 의존 *.jar 파일등이 저장된다.
  • JAVA_OPTS에 자바 애플리케이션 공용 JVM 옵션 설정
  • *.build 파일의 인코딩
    • 기본적으로 Java의 file.encoding 시스템 프라퍼티를 따른다.
    • 윈도우 환경에서 UTF-8로 빌드 파일을 만들려면 GRADLE_OPTS=-Dfile.encoding=UTF-8 형태로 강제 지정
export GRADLE_OPTS="-Dfile.encoding=UTF-8 -Xmx512m -XX:PermSize=64m -XX:MaxPermSize=256m"

실행속도 높이기

  • ${HOME}/.gradle/gradle.properties
    org.gradle.daemon=true
  • 이제부터 Gradle이 데몬으로 떠서 실행되기 때문에 초기 로딩 시간이 줄어든다.
  • 3시간동안 Gradle 작업이 없으면 데몬이 자동 종료된다.

스크립트에서 사용할 인증 정보 분리해두기

build setup

  • 프로젝트를 시작할 때 gradle init를 실행하면 현재 디렉토리에 Gradle 관련 기본 파일들을 생성해준다.(구버전은 setupBuild)
  • Maven 프로젝트 디렉토리에서 실행하면 자동 컨버팅을 실행한다.
  • 실행시 프로젝트 구조를 Java library로 지정하기(1.7 이후)
    gradle init --type java-library
     
    # --type 을 생략하면 basic 으로 지정된다.
  • Project Types
    • pom
    • basic
    • java-library
    • groovy-library
    • scala-library
  • Project 디렉토리 구조 생성 task
    // 적용된 프로그래밍 언어 플러그인에 따라 소스 트리 구조 자동 생성
    task initSrc << {
        project.sourceSets*.allSource.srcDirTrees.flatten().dir.each { dir ->
            dir.mkdirs()
        }
    }

명령 실행과 옵션

  • 기본적으로 gradle을 통해 실행되는 단위를 “Task 태스크”라고 한다.(Ant의 target, Maven의 phase와 유사한 개념)
  • 태스크는 의존 관계에 따라 단 한 번만 실행된다.
  • -q : quiet. 로그 안 찍음. Gradle Logging
  • -x 태스크 : 해당 테스크는 실행하지 않음.
  • --continue : 빌드 실패시 즉시 종료하지 않고, 독립적인 태스크들은 모두 수행하고 종료한다.
  • -d|--debug : Debug 정보 및 stacktrace 출력
  • 태스크 축약
    • dist → di 형태로 최소한 알아볼 수 있는 만큼만 적어도 됨
    • 낙타 표기 compileTest → cT
  • -b 빌드파일 : build.gradle 이 아닌 다른 빌드 파일을 선택해 실행한다. 이 경우 settings.gradle 은 무시된다.
  • -p 프로젝트명 : 멀티 프로젝트에서 어떤 서브 프로젝트를 선택해서 실행 할 경우. -b 대신 -p를 사용할 것.
  • -P프라퍼티이름=값 : 프라퍼티 지정. 값 없이 프라퍼티만 지정해도 된다.
    • 이 값은 빌드 스크립트에서 프라퍼티이름으로 바로 접근 가능하다.
    • 프라퍼티 이름에 “my.property” 형태로 돼 있다면 project.get('my.property') 형태로 접근 가능하다.
  • --gui : GUI 환경에서 태스크를 실행하고 관리한다.
  • --recompile-scripts : build.gradle들 다시 컴파일한다.
  • 환경변수 TERM=dumb으로 하면 Gradle의 진행 상황 로그가 안나오게 된다.
  • Task 상세 도움말은 help --task [태스크이름] 으로 볼 수 있다.
    gradle help --task wrapper  

빌드 정보 확인

  • projects : 프로젝트 목록
  • tasks : 태스크 목록
    • 기본적으로 태스크 그룹에 속한 것만 보여준다.
      dist {
          description = '태스크 설명'
          group = '태스크의 그룹'
      }
      // 혹은
      dist.description = '태스크 설명'
      dist.group = '태스크의 그룹'
    • --all : 태스크 그룹에 상관없이 다 보여줌
  • [자식프로젝트명:]dependencies : Root 혹은 지정 프로젝트의 의존성 트리를 보여준다.
    • –configuration runtime : runtime 의존성만 보여준다.
  • [자식프로젝트명:]properties : Root 혹은 지정 프로젝트의 속성 값들을 모두 보여준다.
  • --profile : 빌드 수행을 프로파일링하여 성능 정보를 ./build/reports/profile 디렉토리에 저장한다.
  • -m 태스크들 : 태스크를 실제 수행은 하지 않고, 해당 태스크와 함께 실행되는 모든 태스크 목록을 순서대로 보여준다.

태스크 튜토리얼

  • 의존성 : task name(depdendsOn: 다른태스크 | [task1, task2, …]) … 형태로 만든다.
  • task “태스크이름” … : 태스크 이름이 문자열 GString이 될 수 있기 때문에 동적으로 태스크를 생성하는 것이 가능하다.
  • 태스트의 시작과 끝 액션
    task hello << {
        println 'Hello Earth'
    }
    hello.doFirst {
        println 'Hello Venus'
    }
    hello.doLast {
        println 'Hello Mars'
    }
    hello << {
        println 'Hello Jupiter'
    }
    • < <는 doLast와 같은 의미이다. doFirst/doLast는 여러개 선언될 수 있으며 doFirst가 선언된 순서로 먼저 실행되고, 그 뒤에 doLast가 선언된 순서대로 실행된다.
  • Task Properties : 태스크 안에서 ext.프라퍼티명 = 값 형태로 선언하면 다른 위치에서 태스크명.프라퍼티명 으로 해당 값을 읽을 수 있다.
  • 기본 태스크
    defaultTasks 'clean', 'run'
     
    task clean << {
    ...
    }
    task run << {
    ...
    }
    • 멀티 프로젝트에서 각 하위 프로젝트는 자신만의 기본 태스크를 선언할 수 있다. 기본 태스크가 없으면 부모의 기본 태스크를 수행한다.
  • 수행할 태스크에 따라 조건 주기
    gradle.taskGraph.whenReady { taskGraph ->
        if (taskGraph.hasTask(release)) {
            // release 라는 태스크를 수행할 예정일때 미리 실행할 코드
        } else {
            // release 라는 태스크를 수행할 예정이 아닐 때 미리 실행할 코드
        }
    }

Java 개발하기

다음 java 개발 관련 항목들을 읽어본다.

DSL

  • Groovy 빌드 파일은 기본적으로 Project 클래스의 인스턴스이다.
  • 빌드 파일에 등장하는 변수나 메소드 중에서 미리 선언된 것이 아니라면 Project 클래스에서 해당 속성을 찾는다.
  • Project 객체는 project로도 접근 가능하다.

Project 객체의 기본 프라퍼티들

  • project Project : 자기 자신의 인스턴스
  • name String : 프로젝트 디렉토리명
  • path String : 프로젝트의 Fully Qualified Name
  • description String : 프로젝트 설명
  • projectDir File : 빌드 스크립트가 있는 프로젝트 디렉토리
  • buildDir File : projectDir/build 이 값을 바꾸면 빌드 디렉토리를 바꿀 수 있게 되는 것이다.
  • group Object : unspecified - 직접 지정
  • version Object : unspecified - 직접 지정
  • ant AntBuilder : AntBuilder 인스턴스

변수 선언

  • 로컬 변수 : def 변수명으로 선언. 해당 스크립트 로컬에서만 접근 가능하다.
  • ext 변수 : 프로젝트 전체와 서브 프로젝트에서도 접근 가능하다.
  • ext 변수 선언과 사용
    ext.javaVersion = '1.7' // 한개씩 선언
    ext {
        // 여러개 한꺼번에 선언
        springVersion = '3.1.0.RELEASE'
        emailNotification = 'build@master.org'
    }
     
    // 가변 Key, 가변 값 형태로 코드를 통해 프라퍼티를 추가할 때는 아래 방식을 사용한다.
    project.ext['keyname'] = 'value'
     
    task hello << {
        println "javaVersion : ${javaVersion}"
        println "springVersion : ${springVersion}"
        println "emailNotification : ${emailNotification}"
    }

스크립트 컴파일

  • 모든 빌드 스크립트는 컴파일하여 .gradle 에 저장하고 캐시된다.
  • 빌드 파일이 변경되면 그 때 재컴파일 한다.
  • --recompile-scripts 옵션을 주면 강제 재컴파일 한다.

이것 저것

디렉토리 생성

  • 여러 태스크에서 특정 디렉토리를 필요로 할 경우에는, 디렉토리 생성 태스크를 만들고 다른 태스크가 그 태스크에 의존하도록 만든다.
classesDir = new File('build/classes')
task resources << {
    classesDir.mkdirs()
    // do something
}
task compile(dependsOn: 'resources') << {
    if (classesDir.isDirectory()) {
        println '필요한 디렉토리가 존재하네요.'
    }
    // do something
}

Gradle 프라퍼티와 시스템 프라퍼티

  • -D프라퍼티명=값으로 시스템 프라퍼티를 추가할 수 있다.
  • gradle.properties를 통해 프라퍼티를 추가할 수 있다.
    • $USER_HOME/.gradle/gradle.properties 혹은
    • 프로젝트홈/gradle.properties
    • $USER_HOME에 있는 것이 우선한다.
    • 여기 지정된 값을 project 객체를 통해 접근할 수 있다.
  • -P프라퍼티명=값으로 project 객체에 프라퍼티를 추가한다.
  • 환경변수 ORG_GRADLE_PROJECT_프라퍼티이름=값으로 project 객체에 프라퍼티를 추가한다.
  • 시스템 프라퍼티 org.gradle.project.프라퍼티이름=값으로 project 객체에 프라퍼티를 추가한다.
  • gradle.properties의 프라퍼티 중에 systemProp.으로 시작하는 프라퍼티는 시스템 프라퍼티로 변환된다.
    • gradle.properties
      gradlePropertiesProp=gradlePropertiesValue
      systemPropertiesProp=shouldBeOverWrittenBySystemProp
      envPropertiesProp=shouldBeOverWrittenByEnvProp
      systemProp.system=systemValue
    • build.gradle
      task printProps << {
          println commandLineProjectProp
          println gradlePropertiesProp
          println systemProjectProp
          println envProjectProp
          println System.properties['system']
      }
    • 실행하면
      > gradle -q -PcommandLineProjectProp=commandLineProjectPropValue -Dorg.gradle.project.systemProjectProp=systemPropertyValue printProps
      commandLineProjectPropValue
      gradlePropertiesValue
      systemPropertyValue
      envPropertyValue
      systemValue

프로젝트 프라퍼티 검사

  • 프로젝트 프라퍼티는 빌드 스크립트에서 프라퍼티 이름으로 바로 접근 가능하다. 하지만 프라퍼티가 존재하지 않으면 예외가 발생한다.
  • hasProperty('propertyName')으로 프라퍼티의 존재 여부를 검사할 수 있다.

외부 빌드 스크립트로 프로젝트 구성하기

외부 *.gradle 빌드 스크립트를 만들어서 불러올 수 있다.

  • build.gradle에서 other.gradle을 불러온다.
    apply from: 'other.gradle'
  • other.gradle
    println "configuring $project"
    task hello << {
        println 'hello from other script'
    }
  • 실행하면
    > gradle -q hello
    configuring root project 'configureProjectUsingScript'
    hello from other script

임의의 객체 구성하기

configure 메소드로 임의의 객체를 구성할 수 있다.

  • build.gradle
    task configure << {
        pos = configure(new java.text.FieldPosition(10)) {
            beginIndex = 1
            endIndex = 5
        }
        println pos.beginIndex
        println pos.endIndex
    }
  • 실행하면
    > gradle -q configure
    1
    5

외부 스크립트로 임의의 객체 구성하기

  • build.gradle
    task configure << {
        pos = new java.text.FieldPosition(10)
        // 외부 스크립트 적용
        apply from: 'other.gradle', to: pos
        println pos.beginIndex
        println pos.endIndex
    }
  • other.gradle
    beginIndex = 1;
    endIndex = 5;
  • 실행하면
    > gradle -q configure
    1
    5

캐싱 cache

Gradle은 컴파일한 스크립트를 캐싱한다. 프로젝트에서 처음으로 빌드를 실행하면 .gradle 디렉토리가 만들어지고 거기에 컴파일된 스크립트가 들어간다. 다음에 다시 빌드를 실행하면 스크립트에 변경이 없다면 컴파일 해뒀던 것을 실행한다. 그렇지 않으면 재 컴파일을 하고 캐시에 새로운 버전이 들어간다. --recompile-scripts옵션으로 실행하면 캐시를 삭제하고 모두 다시 컴파일해서 캐시에 새로 저장한다.

자세히 살펴보기

Plugins


읽을꺼리







Gradle Task

선언

task hello << {
    println "hello"
}
 
// 괄호하고 이름
task(hello) << {
    println "hello"
}
 
task(copy, type: Copy) {
    from(file('srcDir'))
    into(buildDir)
}
 
// 이름을 문자열로
task('hello') <<
{
    println "hello"
}
 
// tasks 에 추가
tasks.add(name: 'taskName', type: org.something.GradleTask, dependsOn: 'anotherTask') {
    // task 설정
    println "hello"
}
 
// with create
 
tasks.create(name: 'taskName', type: org.something.GradleTask, dependsOn: 'anotherTask') {
   // task 설정
}

태스크 정보 설정

  • Task의 description과 group을 지정하면 gradle tasks 시에 정보를 표시해 준다.
  • group은 마음대로 정할 수 있지만, 기본적으로 build 등이 있다.
    dist {
        description = '태스크 설명'
        group = '태스크의 그룹'
    }
    // 혹은
    dist.description = '태스크 설명'
    dist.group = '태스크의 그룹'
  • 동적 프라퍼티 설정
    task something {
        ext.prop1 = 'xxx'
        ext.prop2 = 'yyy'
    }
    // 외부에서 something.prop1 으로 접근 가능

task에 접근하기

  • task hello가 있을 때,
    • hello.name
    • project.hello.name
    • tasks.hello.name
    • tasks['hello'].name
  • tasks.getByPath()로 접근
    project(':projectA') {
        task hello
    }
     
    task hello
     
    println tasks.getByPath('hello').path // :hello
    println tasks.getByPath(':hello').path // :hello
    println tasks.getByPath('projectA:hello').path // :projectA:hello
    println tasks.getByPath(':projectA:hello').path // :projectA:hello

task 설정

  • 설정하기
    // 단일 선언
    task myCopy(type: Copy)
     
    // 태스크의 메소드 호출 등으로 설정하기 1
    Copy myCopy = task(myCopy, type: Copy)
    myCopy.from 'resources'
    myCopy.into 'target'
    myCopy.include('**/*.txt', '**/*.xml', '**/*.properties')
     
    // 설정 2
    task(myCopy, type: Copy)
        .from('resources')
        .into('target')
        .include('**/*.txt', '**/*.xml', '**/*.properties')
     
    // 설정 3
    task myCopy(type: Copy)
     
    myCopy {
       from 'resources'
       into 'target'
       include('**/*.txt', '**/*.xml', '**/*.properties')
    }
     
    // 설정 4, configure() 메소드
    task myCopy(type: Copy)
     
    myCopy.configure {
       from('source')
       into('target')
       include('**/*.txt', '**/*.xml', '**/*.properties')
    }
     
    // 설정 5. 선언시
    task copy(type: Copy) {
       from 'resources'
       into 'target'
       include('**/*.txt', '**/*.xml', '**/*.properties')
    }
  • 의존성 설정
    // 다른 프로젝트의 태스크
    project('projectA') {
        task taskX(dependsOn: ':projectB:taskY') << {
            println 'taskX'
        }
    }
     
    // 나중에 설정
    taskX.dependsOn taskY
     
    // 여러 태스크에 의존
    taskX.dependsOn ['taskY', 'taskZ']
     
    // 이름을 리턴하는 클로저를 통한 다중 설정
    taskX.dependsOn {
        tasks.findAll { task -> task.name.startsWith('lib') }
    }
  • 설명(description) 추가 : 태스크에 description 프라퍼티를 설정하면 gradle tasks에서 볼 수 있게 된다.
  • 태스크 대체 : 플러그인에서 생성한 태스크를 개발자가 원하는 것으로 대체하길 원하거나 할 때 사용 overwrite: true
    task copy(type: Copy)
     
    task copy(overwrite: true) << {
        println('I am the new one.')
    }

태스크 건너 뛰기

  • onlyIf
    // hello task에 대해
    hello.onlyIf { !project.hasProperty('skipHello') }
     
    // 실행시 skipHello 프라퍼티 지정
    gradle hello -PskipHello 
  • StopExecutionException 예외를 던지면, 해당 지점부터 그 태스크는 실행이 안되고 건너뛴다. 그 이후 실행할 태스크는 계속 실행된다.
    task compile << {
        println 'We are doing the compile.'
    }
     
    compile.doFirst {
        // if문에 원하는 조건을 지정한다.
        if (true) { throw new StopExecutionException() }
    }
    task myTask(dependsOn: 'compile') << {
       println 'I am not affected'
    }
  • task.enabled=true|false 이 값이 true여야만 해당 태스크가 실행된다.

이미 최신으로 갱신된 태스크 건너뛰기

  • 자바 컴파일 태스크 같은 경우 이미 모든 최신 java 파일이 컴파일 돼 있다면 건너뛰는 기능이 있다. 이 같은 것을 구현하는 방법.
  • 모든 태스크에는 TaskInputs inputs와 TaskOutputs outputs 프라퍼티가 있다. 이 값을 설정해주면 자동으로 UP-TO-DATE인지 검사하여 실행 여부를 결정한다.
    task transform {
        ext.srcFile = file('mountains.xml')
        ext.destDir = new File(buildDir, 'generated')
        inputs.file srcFile
        outputs.dir destDir
        doLast {
            println "Transforming source file."
            destDir.mkdirs()
            // outputs.dir 영역에 파일을 생성하는 코드..
        }
    }
  • 작동방식
    • 태스크 실행시작시 inputs에 있는 파일의 스냅샷을 찍는다.
    • 태스크 실행후 outputs에 있는 파일의 스냅샷을 찍는다.
    • 태스크를 재실행할 때 이전 inputs, outputs의 스냅샷과 현재 inputs와 outputs의 스냅샷을 비교하여 변경 사항이 없으면 해당 태스크를 건너뛴다. 아니면 태스크를 실행하고 모든 스냅샷을 다시 찍는다.

멀티 프로젝트에서 각 프로젝트 별 최신 갱신 여부 검사

inputs/outputs를 사용하여 VCS에서 받은 멀티 프로젝트의 프로젝트별 갱신 여부를 검사할 수 있다.

task checkUpToDate {
    description = '프로젝트 최신 갱신 여부 검사'
 
    def checkFile = file(new File(tmpDir, "gradle_${project.name}_check_up_to_date").absoluteFile)
 
    FileTree projectFileTree = fileTree(dir: project.projectDir)
    projectFileTree.exclude "${builDir}/**/*"
 
    inputs.files projectFileTree
    outputs.file checkFile
 
    doLast {
        println "[${project.name}] needs refresh."
        if (checkFile.exists()) {
            checkFile.delete()
        }
 
        checkFile.createNewFile()
    }
}

Task Rules

  • tasks.addRule 태스크 생성 규칙을 통해 동적으로 태스크를 만들어낼 수 있다.
    tasks.addRule("Pattern: ping<ID>") { String taskName ->
        if (taskName.startsWith("ping")) {
            task(taskName) << {
                println "Pinging: " + (taskName - 'ping')
            }
        }
    }
     
    // Rule에 대해 의존성을 지정하는 것도 가능하다.
    task groupPing {
        dependsOn pingServer1, pingServer2
    }

태스크의 실행 순서

  • 태스크에 dependsOn [a, b] 형태로는 실행 순서를 지정할 수 없다. dependsOn 은 의존 대상을 명시할 뿐 의존 대상의 실행순서는 명시하지 않는다.
  • 기존방법 : 다음과 같은 방식으로 순서를 명확히 하는 것도 가능하다.
    task somethingLastTask << {
        tasks.somethingFirstTask.execute()
        tasks.somethingSecondTask.execute()
        ....
        // do somthing last
    }
  • 새로운 방법(2015년 1월 현재 incubating) :
    // 보통은 task1 -> task2 순서로 실행하지만 특정 상황에서는 이를 무시한다.
    task2.shouldRunAfter task1
     
    // 무조건 task1 -> task2 순서를 지킨다.
    task2.mustRunAfter task1

tasks

  • tasks는 TaksContainer의 인스턴스이며 이는 TaskCollection 인터페이스를 상속한다.
  • TaskCollection.withType tasks.withType(TaskType) { … } 을 사용하여 특정 태스크 타입에 대한 공통 설정을 수행할 수 있다.

UP-TO-Date upToDate 조건

  • TaskOutputs.upToDateWhen을 통해 upToDate 검사 조건을 변경할 수 있다.
  • 태스크를 무조건 실행하게 만들고자 한다면 Up to date 검사를 안하게 만들면 된다.
    // 태스크 선언부에서..
    outputs.upToDateWhen { false }

--rerun-tasks

  • –rerun-tasks 옵션을 주면 up-to-date 상태와 무관하게 무조건 태스크를 실행한다.








Gradle 파일 다루기



파일 객체 확보

  • Project.file() 메소드로 프로젝트에 상대적인 경로에 있는 파일 객체를 얻을 수 있다.
    // 프로젝트 디렉토리에 대해 상대 경로
    File configFile = file('src/config.xml')
     
    // 절대 경로
    configFile = file(configFile.absoluteFile)
     
    // 현재 명령이 실행된 위치의 상대 경로로 된 파일 객체 사용
    configFile = file(new File('src/config.xml'))
  • file() 메소드에 인자로 아무것이나 넘겨도 되며, 자동으로 이를 File 객체로 바꾸는 시도를 한다. 보통은 String, File 객체를 사용한다.
  • 보통은 프로젝트경로에 상대 경로로 지정되며, 절대 경로 문자열이 넘어올 경우이면 절대 경로를 사용한다.
  • file() 메소드는 file:/some/path.xml같은 URL도 인식한다.
  • file('상대경로') : 현재 디렉토리와 무관하게 프로젝트 디렉토리에 상대 경로로 간주.
  • new File('경로') : 현재 디렉토리에 상대 경로
  • file() 메소드로 절대 경로를 가리키려면 file(new File('경로').absoluteFile) 형태를 사용해야 한다.

File Collections

  • FileCollection Interface. 말 그대로 파일들의 컬렉션이다.
  • Project.files() 메소드로 파일 컬렉션 인스턴스를 생성할 수 있다.
  • file()처럼 프로젝트에 상대적인 경로 우선이다.
  • 일반 용법
    FileCollection collection = files('src/file1.txt', new File('src/file2.txt'), ['src/file3.txt', 'src/file4.txt'])
  • 다양한 용법
    // 일반 컬렉션 처럼 반복 가능
    collection.each {File file ->
        println file.name
    }
     
    // 다른 컬렉션으로 변경
    Set set = collection.files
    Set set2 = collection as Set
    List list = collection as List
    String path = collection.asPath
    File file = collection.singleFile
    File file2 = collection as File
     
    // 컬렉션 합치기, 빼기
    def union = collection + files('src/file3.txt')
    def different = collection - files('src/file3.txt')
  • files()에 클로저 혹은 Callable 인스턴스를 인자로 주기. 클로저에서 files() 메소드의 인자가 될 수 있는 값을 리턴해주면 된다.
    task list << {
        File srcDir
     
        // 클로저로 파일 컬렉션 생성하기. 현재 시점에 srcDir == null 이지만, 늦은 초기화를 하기 때문에 문제가 없다.
        def collection = files { println ">> ${srcDir}"; srcDir.listFiles() }
     
        srcDir = file('src')
        println "Contents of $srcDir.name"
     
        // 여기서 collection 초기화 하면서 위에서 정의한 클로저 실행.
        collection.collect { relativePath(it) }.sort().each { println it }
     
        srcDir = file('src2')
        println "Contents of $srcDir.name"
        // collection을 다시 초기화 하면서 위에서 정의한 클로저 재실행.
        collection.collect { relativePath(it) }.sort().each { println it }
    }
  • files()의 인자로 가능한 다른 타입들
    • FileCollection : 펼쳐진 상태로 추가된다.
    • Task : 태스크의 oputput 파일이 파일 컬렉션이 추가된다.
    • TaskOutputs : TaskOutputs의 출력 파일들이 추가된다.
  • 파일 컬렉션은 늦은 초기화로 수행된다. 즉, 미래에 만들어질 파일을 파일 컬렉션으로 만들어도 된다.

파일 Tree

  • FileTree Interface. 계층구조로 된 파일의 컬렉션. FileCollection을 구현하고 있다.
  • Project.fileTree() 메소드로 객체 생성.
  • Ant 스타일의 include/exclude가 가능하다.
  • FileTree 생성하는 법
    task list << {
        // 기준 디렉토리를 지정하여 FileTree 생성
        FileTree tree = fileTree(dir: 'src/main')
     
        // 패턴 추가/제외
        tree.include '**/*.java'
        tree.exclude '**/Abstract*'
     
        // 경로를 바로 줘서 생성
        tree = fileTree('src').include('**/*.java')
     
        // 클로저로 생성하기, 여러개씩 include, exclude
        tree = fileTree('src') {
            include '**/*.java'
            include '**/*.xml'
            exclude(['**/Abstract*', '**/*Test.java'])
        }
     
        tree.each { File file ->
            println file.absolutePath
        }
     
        // map으로 생성하기
        tree = fileTree(dir: 'src', include: '**/*.java')
        tree = fileTree(dir: 'src', includes: ['**/*.java', '**/*.xml'])
        tree = fileTree(dir: 'src', include: '**/*.java', exclude: '**/*test*/**')
    }
  • FileTree 용법 : FileCollection의 모든 용법을 포함한다.
    task list << {
        def tree = fileTree('src') {
            include '**/*.java'
            include '**/*.xml'
            exclude(['**/Abstract*', '**/*Test.java'])
        }
     
        // include에 매칭되는 것만 필터링
        FileTree filtered = tree.matching {
            include '**/*properties.xml'
        }
     
        filtered.each { File file ->
            println "Properties : ${file}"
        }
     
        FileTree sum = tree + fileTree(dir: 'src/test')
     
        tree.visit { element ->
            println "$element.relativePath => $element.file"
        }
    }
  • FileTree.matching(Closure)에서 PatternFilterable의 메소드들을 사용해 필터링 규칙을 정한다.
  • zipTree()와 tarTree()로 압축 파일에 대해 FileTree 객체 생성 가능.
    FileTree zip = zipTree('someFile.zip')
     
    FileTree tar = tarTree('someFile.tar')
     
    //tar tree는 압축형식을 파일 확장자로 스스로 판단하지만 (보통 *.tar.gz 형태)
    //압축 형태를 파일명으로 알 수 없을 때는 다음과 같이 명시한다.
    FileTree someTar = tarTree(resources.gzip('someTar.ext'))

입력 파일 묶음 지정

  • 다양한 곳에서 입력 파일 묶음을 지정해야 한다.(예: 자바 클래스 컴파일시 소스 파일 목록 등).
  • 이 입력 파일 묶은음 files() 로 생성가능한 모든 값을 지정할 수 있다. 즉, 다시말해 문자열, file, 컬렉션, FileCollection, FileTree 게다가 클로저를 이용할 수도 있다.
    // Use a File object to specify the source directory
    compile {
        source = file('src/main/java')
    }
     
    // Use a String path to specify the source directory
    compile {
        source = 'src/main/java'
    }
     
    // Use a collection to specify multiple source directories
    compile {
        source = ['src/main/java', '../shared/java']
    }
     
    // Use a FileCollection (or FileTree in this case) to specify the source files
    compile {
        source = fileTree(dir: 'src/main/java').matching { include 'org/gradle/api/**' }
    }
     
    // Using a closure to specify the source files.
    compile {
        source = {
            // Use the contents of each zip file in the src dir
            file('src').listFiles().findAll {it.name.endsWith('.zip')}.collect { zipTree(it) }
        }
    }
  • 대부분의 경우 입력 파일지정 프라퍼티는 동일한 이름의 메소드로도 호출 가능하다.
    compile {
        // Add some source directories use String paths
        source 'src/main/java', 'src/main/groovy'
     
        // Add a source directory using a File object
        source file('../shared/java')
     
        // Add some source directories using a closure
        source { file('src/test/').listFiles() }
    }

파일/디렉토리 관리

파일/디렉토리 관리는 Project 객체에 내장된 각종 메소드를 직접 호출하거나, Copy 등의 상위 태스크를 상속받아 태스크로 만드는 방식으로 처리할 수도 있다.

파일 복사

  • Copy 태스크로 파일을 복사한다.
  • 복사시 입력 소스와 출력 대상은 CopySpec 인터페이스의 메소드로 표현한다. Copy 태스크는 이 인터페이스를 구현하고 있다.
  • 기본 동작
    task copyTask(type: Copy) {
        from 'src/main/webapp'
        into 'build/explodedWar'
    }
  • from() 메소드는 files()에서 받을 수 있는 모든 인자를 받는다.
    • 인자가 디렉토리이면 그 디렉토리 이하 모든 파일(디렉토리 자체는 제외)을 복사
    • 인자파 파일이면 해당 파일만 복사
    • 인자가 존재하지 않는 파일이면 무시.
    • into() 메소드는 file()에서 받을 수 있는 모든 인자를 받는다.
  • from 예
    task anotherCopyTask(type: Copy) {
        // Copy everything under src/main/webapp
        from 'src/main/webapp'
        // Copy a single file
        from 'src/staging/index.html'
        // copyTask의 outputs
        from copyTask
        // copyTaskWithPatterns 태스크의 outputs 명시
        from copyTaskWithPatterns.outputs
        // Zip 파일 내용
        from zipTree('src/main/assets.zip')
        // Determine the destination directory later
        into { getDestDir() }
    }
  • 복사 대상을 상세히 명시
    task copyTaskWithPatterns(type: Copy) {
        from 'src/main/webapp'
        into 'build/explodedWar'
        include '**/*.html'
        include '**/*.jsp'
        exclude { details -> details.file.name.endsWith('.html') && details.file.text.contains('staging') }
    }
  • Project.copy() 메소드도 동일하게 사용할 수 있다.
    task copyMethod << {
        copy {
            from 'src/main/webapp'
            into 'build/explodedWar'
            include '**/*.html'
            include '**/*.jsp'
        }
    }

복사시 이름 변경

task rename(type: Copy) {
    from 'src/main/webapp'
    into 'build/explodedWar'
    // Use a closure to map the file name
    rename { String fileName ->
        fileName.replace('-staging-', '')
    }
    // Use a regular expression to map the file name
    rename '(.+)-staging-(.+)', '$1$2'
    rename(/(.+)-staging-(.+)/, '$1$2')
}

복사시 파일 내용 필터링(내용 변형)

import org.apache.tools.ant.filters.FixCrLfFilter
import org.apache.tools.ant.filters.ReplaceTokens
 
task filter(type: Copy) {
    from 'src/main/java'
    into 'build/filtered'
    // Substitute property references in files
    expand(copyright: '2009', version: '2.3.1')
    expand(project.properties)
 
    // Ant이용.
    filter(FixCrLfFilter)
    filter(ReplaceTokens, tokens: [copyright: '2009', version: '2.3.1'])
    // 모든 줄을 대괄호로 감싸기
    filter { String line ->
        "[$line]"
    }
}

CopySpec

  • CopySpec은 계층구조이다. 목표 경로, include/exclude 패턴, 복사 행위, 이름 매핑, 필터 등을 모두 상속한다.
  • nested copy specs
    apply plugin: 'java'
     
    repositories {
        mavenCentral()
    }
     
    dependencies {
        compile 'org.hibernate:hibernate-core:3.6.7.Final'
    }
     
    task nestedSpecs(type: Copy) {
        into 'build/explodedWar'
        exclude '**/*Test.java'
        from('src/main') {
            include '**/*.java'
        }
     
        // 아래 into는 위에서 선언한 build/explodedWar를 상속하여 그에 상대적인 경로이다.
        // 즉 buld/explodedWar/libs 를 뜻한다.
        into('libs') {
            // 모든 의존성 *.jar들을 libs 에 복사한다.
            from configurations.runtime
        }
    }

Sync

  • Copy를 상속한 태스크.
  • 두 폴더간의 싱크를 수행한다.
    apply plugin: 'java'
     
    repositories {
        mavenCentral()
    }
     
    dependencies {
        compile 'org.hibernate:hibernate-core:3.6.7.Final'
    }
     
    // 모든 의존성 *.jar들을 build/libs 에 복사한다.
    task libs(type: Sync) {
        from configurations.runtime
        into "$buildDir/libs"
    }

파일 압축

  • JarWarZipTar 등을 지원한다.
  • 기본 용법
    apply plugin: 'java'
     
    task zip(type: Zip) {
        from 'src/dist'
        into('libs') { // 압축 파일 안의 libs/ 디렉토리로 파일 넣음
            from configurations.runtime
        }
    }








Gradle Ant 호출

Ant 태스크와 타입

  • 항상 ant라는 AntBuilder 인스턴스를 사용해 앤트 태스크를 호출할 수 있다.
  • ant.태스크이름으로 호출한다.
  • 앤트 태스크 속성은 Map 형태의 파라미터로 전달한다.
  • 각종 Ant 태스크를 AntBuilder 형태로 변환한 것은 Gant Tasks 를 참조한다.
  • 메시지 출력
    task hello << {
        String greeting = 'hello from Ant'
        ant.echo(message: greeting)
        ant.echo('hello from Ant') // 이것도 가능.
    }
  • Ant 태스크의 중첩 요소들을 클로저로 표현한다.
    task zip << {
        ant.zip(destfile: 'archive.zip') {
            fileset(dir: 'src') {
                include(name: '**/*.xml')
                exclude(name: '**/*.java')
            }
        }
    }
  • Ant의 타입도 태스크 처럼 메소드 이름으로 접근한다. 메소드를 호출하면 Ant의 데이터 타입을 리턴한다.
  • Ant path 객체를 생성하고 보여주기
    task list << {
        def path = ant.path {
            fileset(dir: 'src', includes: '**/*.xml')
        }
     
        path.list().each {
            println it
        }
    }

커스텀 Ant 태스크

  • ant.taskdef
    task check << {
        ant.taskdef(resource: 'checkstyletask.properties') {
            classpath {
                fileset(dir: 'libs', includes: '*.jar')
            }
        }
        ant.checkstyle(config: 'checkstyle.xml') {
            fileset(dir: 'src')
        }
    }
  • 커스텀 Ant 태스크에 의존성 지정하기
    • 먼저 의존성 태스크 이름으로 의존성을 지정한다.
      configurations {
          pmd // pmd 라는 설정이 생기고
      }
       
      dependencies {
          pmd group: 'pmd', name: 'pmd', version: '4.2.5' // pmd 설정에 의존성을 지정한다.
      }
    • configurations.이름.asPath로 의존성 사용
      task check << {
          ant.taskdef(name: 'pmd', classname: 'net.sourceforge.pmd.ant.PMDTask', classpath: configurations.pmd.asPath)
          ant.pmd(shortFilenames: 'true', failonruleviolation: 'true', rulesetfiles: file('pmd-rules.xml').toURI().toString()) {
              formatter(type: 'text', toConsole: 'true')
              fileset(dir: 'src')
          }
      }

Ant 빌드 가져오기

  • ant.importBuild()로 build.xml을 직접 읽어올 수 있다.
  • 각 Ant 태스크는 Gradle 태스크로 변환된다.
  • 간단한 build.xml 로딩
    • build.gradle
      ant.importBuild 'build.xml'
    • build.xml
      <project>
          <target name="hello">
              <echo>Hello, from Ant</echo>
          </target>
      </project>
    • 이제 gradle hello로 Ant 타겟을 Gradle 태스크인 것 처럼 실행할 수 있다.
    • Ant 타겟에 다른 액션 추가, 의존하기 등도 가능하면 Ant 태스크가 Gradle 태스크에 의존하는 것도 가능하다.
      <project>
          <target name="hello" depends="intro"> <!-- intro는 Gradle 태스크 -->
              <echo>Hello, from Ant</echo>
          </target>
      </project>

Ant Properties

  • 다음과 같이 Ant 프라퍼티를 설정할 수 있다.
    ant.buildDir = buildDir
    ant.properties.buildDir = buildDir
    ant.properties['buildDir'] = buildDir
    ant.property(name: 'buildDir', location: buildDir)
  • Ant 프라퍼티 읽기
    // build.xml에서 프라퍼티를 설정했을 때
    <property name="antProp" value="a property defined in an Ant build"/>
    // 다음처럼 읽는다.
    println ant.antProp
    println ant.properties.antProp
    println ant.properties['antProp']
  • Ant 레퍼런스 설정과 읽기
    • 설정
      // 레퍼런스 설정
      ant.path(id: 'classpath', location: 'libs') // id 지정 방식
      ant.references.classpath = ant.path(location: 'libs')  // 명시적으로 references에 넣기
      ant.references['classpath'] = ant.path(location: 'libs') // [] 연산자 이용
      // build.xml에서 참조 가능
      <path refid="classpath"/>
    • 읽어오기
      // build.xml에 설정된 값을
      <path id="antPath" location="libs"/>
      // 다음처럼 읽는다.
      println ant.references.antPath
      println ant.references['antPath']

java task

Ant Java 태스크는 Java 클래스를 실행한다. Gradle Java Plugin에서 JavaExec를 사용하는 방법도 있다.

ant.java(classname: '실행할JavaClass', fork: true, classpath: configurations.임의의Config.asPath) {
  arg(value: '파라미터1')
  arg(value: '파라미터2')
  ...
}

zip task

  • 일반적인 zip 압축
    task zip << {
        ant.zip(destfile: 'archive.zip') {
            fileset(dir: 'src') {
                include(name: '**/*.xml')
                exclude(name: '**/*.java')
            }
        }
    }
  • 파일들을 압축파일의 특정 디렉토리에 넣기 ZipFileSet
    // resources.zip 파일의 resources/ 디렉토리로 압축
     
    ant.zip(destfile: "${buildDir}/resources.zip") {
        zipfileset(dir: "${webAppDirName}/WEB-INF/resources", prefix: "resources")
    }









Gradle Dependencies

의존성 소개

  • Gradle은 이행적(transitive) 의존성 관리를 지원한다.
  • Maven과 Ivy를 지원한다.
  • 이행적 의존성이 아닌 일반 파일로 저장된 외부 라이브러리도 지원한다.
  • 빌드 스크립트에서 직접 의존성을 지정한다.

의존성 설정 기본

group, name, version 순서로 써줄 수 있다.

dependencies {
    compile group: 'org.hibernate', name: 'hibernate-core', version: '3.6.7.Final'
}
// 짧게 쓰면 "group:name:version"
dependencies {
    compile 'org.hibernate:hibernate-core:3.6.7.Final'
}

리포지토리의 기본

Maven과 Ivy 리포지토리를 지원한다.

  • 가장 기본 Maven 리포지토리
    repositories {
        mavenCentral()
    }
  • 기타 Maven 리포지토리 URL 지정
    repositories {
        maven {
            url "http://repo.mycompany.com/maven2"
        }
    }
  • maven {}을 여러번 사용하는 것이 가능하다.
  • 사용자 정의 리포지토리에 접속 계정정보 추가
    repositories {
        maven {
            credentials {
                username 'user'
                password 'password'
            }
            url "http://repo.mycompany.com/maven2"
        }
    }
  • jCenter() Bintray jCenter를 리포지토리로 추가(1.7)
    repositories {
        jcenter()
    }

의존성 관리 Best Practices

jar 이름에 버전 붙이기

  • Manifest 파일에 버전을 넣어 두기도 한다. 그래도..
  • jar 파일 자체에 그 일부로 버전번호를 명시하는 것이 좋다.
  • 이행적 의존성 관리를 사용할 경우 jar 파일에 버전 명시는 필수이다.
  • 어느 라이브러리 버전을 사용하는지 모르면, 찾기 어려운 버그를 만나기가 쉽다.

이행적 의존성 관리를 사용하라

  • 이행적 의존성 관리를 사용하지 않으면 최상위 의존성을 삭제할 경우 그것이 의존하는 다른 라이브러리가 무엇인지 몰라서 불필요한 jar가 계속 쌓이게 된다.
  • Gradle은 Maven/Ivy 아니라도 일반 jar 파일에 대한 의존성도 지원한다.

버전 충돌

  • 동일한 jar의 서로 다른 버전 충돌은 정확히 찾아내어 해결해야 한다.
  • 이행적 의존성 관리를 하지 않으면 버전 충돌을 알아내기 힘들다.
  • 서로 다른 의존성은 서로 다른 버전의 다른 라이브러리에 의존하기 마련이고, 이 경우 버전 충돌이 일어난다.
  • Gradle이 제공하는 충돌 방지 전략
    • 최신 우선 : 가장 최신 의존성이 기본적으로 사용된다.
    • 빠른 실패 : 버전 충돌이 일어나면 빠른 시간안에 실패한다. 이렇게 되면 개발자 스스로 충돌을 제어할 수 있게 된다. ResolutionStrategy 참조.
    • 버전 충돌을 커스터마이징할 수 있게 Gradle을 만들어가고 있다.
  • 버전 충돌 해결 방법
    • 충돌이 발생하는 라이브러리를 최상위 의존성으로 버전을 명시하여 강제(forced) 지정한다. DependencyHandler 참조.
    • 아무 의존성(이행적이든 아니든)이든 강제로 지정한다.

동적 버전(Dynamic Version) 결정과 변하는 모듈(Changing Module)

  • 때로는 특정 의존 라이브러리에 대해 항상 최신 버전을 사용하거나, 혹은 특전 버전대(2.x 버전대중에서) 최신을 사용하고 싶을 경우가 있다. 동적 버전을 통해 가능하다.
  • 특정 버전대 : 2.+
  • 사용 가능한 최신 버전 : latest.integration
  • 변하는 모듈 : 어떤 경우에는 동일 버전이라도 모듈이 변경되는 때가 있다(Maven의 SNAPSHOT, 특히 사내 프로젝트의 경우 이런게 많음).
  • 동적 버전은 실제 버전이 변경되고, 변하는 모듈은 버전은 그대로이지만 그 내용물(jar)이 계속해서 변경될 수 있다.
  • 기본적으로 동적 버전과 변하는 모듈은 24시간 캐시된다. 설정을 통해 바꿀 수 있다.
  • 특정 라이브러리의 변경을 매번 검사해야 한다면 changing = true 옵션 추가
    compile ('com.some:library:0.1') { changing = true }
    • 단, Maven Repository 일 때만 그렇다(Maven 자체의 기본정책).
    • Ivy Repository는 SNAPSHOT이라도 changing = true 설정이 필요하다.

ResolutionStrategy

ResolutionStrategy

경험상으로 볼 때 failOnVersionConflict()를 설정하고 의존성을 관리하는 것이 좋아보인다. 나도 모르는 사이에 이행성에 의해 버전이 변하는 것을 방지할 수 있다.

configurations.all {
  resolutionStrategy {
    // 동일 모듈에 대한 버전 충돌시 즉시 오류 발생하고 실패.
    failOnVersionConflict()
 
    // 특정 모듈의 버전을 강제 지정(최상위건 이행적 의존성이건 무관함)
    force 'asm:asm-all:3.3.1', 'commons-io:commons-io:1.4'
    //  이미 강제 지정된 모듈 버전을 대체함
    forcedModules = ['asm:asm-all:3.3.1']
 
    // 동적 버전 탐색을 10분 캐시
    cacheDynamicVersionsFor 10, 'minutes'
    // 변하는 모듈(Changing Module)을 캐시하지 않음
    cacheChangingModulesFor 0, 'seconds'
  }
}

Java와 의존성 관리

  • 원칙적으로 자바에는 의존성 관리 기법이 존재하지 않기 때문에 Maven, Ivy 같은 비표준 솔루션이 만들어지게 되었다.
  • Maven은 완전한 빌드 시스템이며, Ivy는 의존성 관리만 한다.
  • Maven과 Ivy 모두 특정 jar에 대한 의존성 정보를 기술하는 XML 파일 기술자(descriptor)를 통해 의존성을 관리한다.
  • Gradle 의존성 분석 엔진은 pom(Maven)과 ivy를 기술자를 모두 지원한다.

의존성 구성(dependency configurations)

  • Java에서 의존성은 ConfigurationContainer configurations로 그룹화 된다. 구성의 각 그룹은 클래스패스를 의미한다.
  • 많은 Gradle 플러그인들이 구성를 미리 정의해 두고 있다. 사용자가 스스로 자신만의 구성을 추가할 수 있다. 예를 들면 빌드시에는 필요없지만 배포는 같이 되야하는 추가 JDBC 드라이브 같은 것들.
  • 프로젝트 구성은 ConfigurationContainer configurations 객체로 관리한다. configurations 객체에 클로저를 전달하면 이 클래스의 API가 호출된다.
  • configurations 의 각 항목은 Configuration 객체이다.

Java에서는 기본적으로 네 가지 configuration이 존재한다

  • compile : 프로젝트를 컴파일할 때 필요한 의존 라이브러리들
  • runtime : 프로젝트를 실행할 때 필요한 의존 라이브러리들. 기본적으로 compile을 모두 포함한다.
  • testCompile : 프로젝트의 테스트를 컴파일할 때 필요한 라이브러리들. 기본적으로 프로젝트의 컴파일된 클래스들과 compile 의존성을 포함한다.
  • testRuntime : 프로젝트의 테스트를 실행할 때 필요한 라이브러리들. 기본적으로 compile, runtime, testCompile 의존성을 포함한다.

configuration 선언

configurations {
    compile
}

configuration 접근

println configurations.compile.name
println configurations['compile'].name

구성하기

configurations {
    compile {
        description = 'compile classpath'
        transitive = true
    }
    runtime {
        extendsFrom compile
    }
}
configurations.compile {
    description = 'compile classpath'
}

의존성 설정하기

  • 의존성에는 여러가지 타입이 있다.
  • DependencyHandler에 거의 모든 의존성 설정 방법의 예가 나온다..

외부 모듈 의존성

리포지토리에 있는 외부 모듈에 대한 의존성. 가장 일반적인 방식이다.

dependencies {
    runtime group: 'org.springframework', name: 'spring-core', version: '2.5'
    runtime 'org.springframework:spring-core:2.5', 'org.springframework:spring-aop:2.5'
    runtime(
        [group: 'org.springframework', name: 'spring-core', version: '2.5'],
        [group: 'org.springframework', name: 'spring-aop', version: '2.5']
    )
    runtime('org.hibernate:hibernate:3.0.5') {
        transitive = true
    }
    runtime group: 'org.hibernate', name: 'hibernate', version: '3.0.5', transitive: true
    runtime(group: 'org.hibernate', name: 'hibernate', version: '3.0.5') {
        transitive = true
    }
}
  • 외부 의존성 설정 방식은 두가지가 존재한다. 문자열 지정방식과 맵 지정 방식.
  • ExternalModuleDependency에서 더 많은 프라퍼티와 메소드를 볼 수 있다.
  • 문자열 지정 방식은 “그룹:아티팩트이름:버전”을 지정할 수 있다.
  • 맵 지정방식은 ExternalModuleDependency의 모든 프라퍼티를 지정할 수 있다.
  • Dependency의 모든 API에 접근하려면 의존성 선언문에 클로저를 인자로 넘겨주고 그 안에서 호출하면 된다.
  • 의존성이 선언되면 그에 맞는 기술자 파일(pom.xml, ivy.xml)을 찾고 그에 따라 해당 모듈의 artifact jar 파일과 그 의존하는 파일들을 다운로드 한다.
  • 의존성 기술자 파일이 존재하지 않으면 바로 적합한 파일명을 구성하여(hibernate-3.0.5.jar 형태) 다운로드 한다.
  • Maven에서는 모듈이 하나의 artifact만 존재하지만 Gradle과 Ivy는 하나의 모듈이 여러개의 artifact를 가질 수 있다. 각 artifact는 서로 다른 의존성을 가질 수 있다.

다중 artifact 모듈에 의존한다면

  • Maven에서는 다중 artifact 모듈이 있을 수 없다.
  • Gradle이 Ivy 리포지토리의 모듈에 의존할 경우 이런 상황이 발생할 수 있다.
  • Gradle에서 Ivy 모듈에 의존성을 지정할 경우, 정확히는 해당 모듈의 default 구성에 의존성을 지정한 것이다.
  • 따라서 실제 의존하게 되는 artifact(보통은 *.jar)는 해당 모듈의 default 구성에 첨부된 모든 artifact들이다.
    • default 구성에 원치않는 artifact가 있을 경우에 해당 모듈을 빼도록 의존성을 구성해야 한다.
    • 필요한 artifact가 default가 아닌 다른 구성에 들어있을 경우 어떤 구성의 artifact를 가져올지 명시해줘야 한다.
  • 자세한 사항은 DependencyHandler를 참조한다.

해당 Artifact만 가져오기 지정

  • 특정 모듈을 가져올 때 원칙적으로는 해당 모듈이 의존하는 다른 모듈까지 모두 가져오게 된다.
  • 이 때 해당 모듈의 지정된 artifact(jar)만 가져오고 의존성은 무시하게 만들 수 있다.
  • 문자열 지정법에서는 끝에 @확장자를 지정한다. 보통은 @jar가 된다. (artifact 파일이 zip 일수도 있다)
  • 맵 지정법에서는 ext 키에 확장자를 지정한다.
  • 이런일이 가능하게 되는 이유는 의존성을 탐색할 때 지정된 확장자의 파일만 다운로드하고 pom.xml이나 ivy.xml같은 모듈 기술자 파일은 다운로드하지 말라는 뜻이기 때문이다. 모듈 기술자 파일을 받지 않았으므로 의존성을 분석할 수 없기 때문에 해당 모듈의 해당 확장자 파일만 받게 된다. 이미 다운로드 받은 모듈 기술자는 무시한다.
dependencies {
    runtime "org.groovy:groovy:1.8.7@jar"
    runtime group: 'org.groovy', name: 'groovy', version: '1.8.7', ext: 'jar'
}

분류자 Classifier

  • Maven에는 분류자(classifer)가 존재한다.
  • 분류자는 @ 확장자 지정자와 함께 사용할 수 있다.
compile "org.gradle.test.classifiers:service:1.0:jdk15@jar"
otherConf group: 'org.gradle.test.classifiers', name: 'service', version: '1.0', classifier: 'jdk15'

특정 구성의 외부 의존성 목록 보기

다음 태스크를 만들고 gradle -q listJars로 실행한다. 태스크 없이 gradle dependencies만 해도 된다.

task listJars << {
    configurations.compile.each { File file -> println file.name }
}

configuration 다루기

  • Configuration (configurations.compile 객체 같은 것)
  • 특정 configuration의 전체 의존성 정보를 보려면 configurations.[configurationName].resolvedConfiguration.resolvedArtifacts 를 이터레이션 돌면 된다. ResolvedArtifact
    • resolvedArtifact.getModuleVersion().getId()는 ModuleVersionIdentifier 객체이며 여기에 모듈에 관한 group, name, version 정보가 들어있다.
  • 따라서 아래와 같이 이터레이션을 돌면 의존성 정보를 알 수 있다.
    configurations.runtime.resolvedConfiguration.resolvedArtifacts.each { raf ->
        def cid = raf.getModuleVersion().getId()
        println "Dependency for runtime ${cid.group}:${cid.name}:${cid.version}"
    }

클라이언트 모듈 의존성

  • 클라이언트 모듈 의존성은 빌드 스크립트에서 직접 이행적 의존성을 선언할 수 있게 해준다. 즉, pom.xml 같은 모듈 기술자를 대체하는 기법이다.
  • 아래 설정에서는 현재 프로젝트가 groovy에 의존하지만, groovy 자체의 의존성은 무시하고 빌드 파일에서 직접 지정한 의존성을 따르도록 한다.
    dependencies {
        runtime module("org.codehaus.groovy:groovy-all:1.8.7") {
            dependency("commons-cli:commons-cli:1.0") {
                transitive = false
            }
            module(group: 'org.apache.ant', name: 'ant', version: '1.8.4') {
                dependencies "org.apache.ant:ant-launcher:1.8.4@jar", "org.apache.ant:ant-junit:1.8.4"
            }
        }
    }
  • ClientModule 참조.
  • 현시점(gradle 1.2)에서는 클라이언트 모듈 의존성을 사용한 프로젝트를 Maven 등의 리포지토리에 올릴 경우 클라이언트 모듈 의존성은 무시된다.

프로젝트 의존성

  • 멀티 프로젝트에서 다른 프로젝트에 대한 의존성을 설정할 수 있다.
    dependencies {
        compile project(':shared') // shared 프로젝트에 의존
    }

파일 의존성

  • 파일 의존성을 사용하면 jar 파일을 리포지토리에 넣지 않고도 의존성에 추가하는 것이 가능하다.
  • FileCollection 을 인자로 넘기면 된다.
    dependencies {
        runtime files('libs/a.jar', 'libs/b.jar')
        runtime files('dir/to/classes') // 디렉토리 자체를 클래스패스에 추가
        runtime fileTree(dir: 'libs', include: '*.jar')
    }
  • 파일 의존성은 프로젝트를 리포지토리에 배포할 경우에는 의존성으로 추가되지 않는다.
  • 동일 빌드 내에서는 파일 의존성이 이행적으로 추가된다. 즉, 현재 프로젝트를 다른 서브 프로젝트에서 의존하면 현재 프로젝트에 대한 파일 의존성도 함께 추가된다.
  • 파일이 원래 존재하는 것이 아니라 태스크를 통해 생성되는 경우 파일 의존성에 해당하는 파일들을 어느 태스크에서 생성하는지도 명시할 수 있다.
    dependencies {
        compile files("$buildDir/classes") {
            builtBy 'compile' // 'compile' 태스크에 의해 클래스 파일들 생성됨.
        }
    }
     
    task compile << {
        println 'compiling classes'
    }
     
    task list(dependsOn: configurations.compile) << {
        println "classpath = ${configurations.compile.collect {File file -> file.name}}"
    }

Gradle API 의존성

Gradle 태스크나 플러그인을 만들 경우에 현재 Gradle API(DependencyHandler.gradleApi()에 대해 의존성을 지정할 수 있다.

dependencies {
    compile gradleApi()
}

로컬 Groovy 의존성

Gradle 과 함께 배포되는 Groovy에 대한 의존성(DependencyHandler.localGroovy())을 지정할 수 있다. 마찬가지로 Gradle 태스크나 플러그인을 만들때 사용할 수 있다.

dependencies {
    groovy localGroovy()
}

이행적 의존성 제외하기

이행적 의존성 중에서 일부는 제외하도록 설정할 수 있다.

// 구성 단위 제외
configurations {
    compile.exclude module: 'commons' // compile configuration에서 특정 모듈 제외
    all*.exclude group: 'org.gradle.test.excludes', module: 'reports' // 모든 configuration에서 특정 모듈 제외
}
 
// 의존성 단위 제외
dependencies {
    compile("org.gradle.test.excludes:api:1.0") { 
        exclude module: 'shared' // 특정 의존성에 대해선만 모듈 제외.
        exclude group: 'groupId', module: 'artifactId'
    }
}
  • 특정 구성(configuration)에서 이행적 의존성을 제거하면 의존성 구성을 분석하거나 해당 구성을 상속할 때 그 의존성은 제외된다.
  • 모든 구성에서 제외시킬 때는 all*으로 Groovy의 spread-dot 연산자를 사용한다.
  • 제외할 의존성 지정시에는 의존성의 이름만 지정(module: '이름')하거나 그룹 이름(group: '그룹이름')만 지정하거나 혹은 둘다 함께 지정할 수 있다.
  • 자세한 것은 Dependency와 Configuration을 참조한다.
  • 모든 이행적 의존성이 제외 가능한 것은 아니다. 없어도 되는 의존성인지 주의깊게 살펴보고 지정해야 한다.
  • 의존성 제외가 필요한 경우
    • 의존성 제외가 아닌 다른 방식으로 해결 가능한지 항상 고려한다.
    • 라이센스 때문에 해당 모듈을 빼야한다.
    • 어떠한 원격 리포지토리에서도 받아 올 수 없는 모듈이다.
    • 실행 시점에는 필요 없는 모듈이다.
    • 의존성에 지정된 버전이 다른 버전과 충돌한다. 이때는 버전 충돌 부분으로 해결하도록 한다.
  • 대부분의 경우 의존성 제외는 구성 단위로 해야 한다. 그래야 더 명시적이다.
  • 의존성 단위 제외의 경우 구성의 다른 의존성에서 제외했던 모듈을 다시 의존할 경우 무용지물이 된다.
  • 의존성 제외는 ModuleDependency를 참고한다.

선택적 속성들

  • 의존성의 name을 제외한 모든 속성은 선택적이다. 리토지토리 타입에 따라 어떤 속성은 꼭 필요하기도 하고 그렇지 않기도 한다.
    • Maven : group, name, version 필수
    • 파일 시스템 리포지토리 : name 혹은 name과 version
  • 선택적 속성 예
    dependencies {
        runtime ":junit:4.10", ":testng"
        runtime name: 'testng' 
    }
  • 구성에 의존성의 배열을 지정할 수도 있다.
    List groovy = ["org.codehaus.groovy:groovy-all:1.8.7@jar",
                   "commons-cli:commons-cli:1.0@jar",
                   "org.apache.ant:ant:1.8.4@jar"]
    List hibernate = ['org.hibernate:hibernate:3.0.5@jar', 'somegroup:someorg:1.0@jar']
    dependencies {
        runtime groovy, hibernate // 컬렉션을 의존성에 추가함
    }

의존성의 구성

  • Gradle의 의존성은 여러가지 구성(configurations)을 가질 수 있다.
  • 지정하지 않으면 기본 구성을 사용한다.
  • Maven에는 기본 구성밖에 없다.
  • Ivy에는 의존성에 여러 구성을 둘 수 있다.
    dependencies {
        runtime group: 'org.somegroup', name: 'somedependency', version: '1.0', configuration: 'someConfiguration'
    }
  • Gradle의 서브 프로젝트에 대한 의존성을 지정할 때는 다음과 같이한다.
    dependencies {
        compile project(path: ':api', configuration: 'spi')
    }

의존성 보고서

모든 이행적 의존성에서 제외시키기

configurations {
    all*.exclude group: 'xml-apis', module: 'xmlParserAPIs'
}
 
// Equivalent to:
configurations {
    all.collect { configuration ->
        configuration.exclude group: 'xml-apis', module: 'xmlParserAPIs'
    }
}

의존성 사용하여 작업하기

  1. 다음과 같은 의존성 설정이 있다고 할 때
    configurations {
        sealife
        alllife
    }
     
    dependencies {
        sealife "sea.mammals:orca:1.0", "sea.fish:shark:1.0", "sea.fish:tuna:1.0"
        alllife configurations.sealife // sealife에서 상속 받음
        alllife "air.birds:albatros:1.0"
    }
    1. shark-1.0 → seal-2.0, tuna-1.0
    2. orca-1.0 → seal-1.0
    3. tuna-1.0 → herring-1.0
  2. 각 구성의 의존성에 다음과 같이 접근할 수 있다.
    task dependencies << {
        configurations.alllife.dependencies.each { dep -> println dep.name }
        println()
        configurations.alllife.allDependencies.each { dep -> println dep.name }
        println()
        configurations.alllife.allDependencies.findAll { dep -> dep.name != 'orca' }.each { dep -> println dep.name }
    }
    1. allDependencies는 해당 구성의 부모에 있는 의존성까지 포함한 모든 의존성이다.
    2. dependencies는 해당 구성에만 속한(부모에 속한 것 제외) 의존성들이다.
  3. 구성에 속한 의존성의 모든 파일 객체는 files() 메소드로 접근할 수 있다.
    task allFiles << {
        configurations.sealife.files.each { file ->
            println file.name
        }
    }
    1. Configuration.files 메소드는 해당 구성의 모든 artifact를 가져온다.
  4. 구성을 복사할 수 있다. 클로저로 복사될 대상을 지정할 수 있다.
    task copy << {
        configurations.alllife.copyRecursive { dep -> dep.name != 'orca' }.allDependencies.each { dep ->
            println dep.name
        }
        println()
        configurations.alllife.copy().allDependencies.each { dep ->
            println dep.name
        }
    }
    1. copy() 메소드는 해당 구성에 속한 의존성만 복사한다.
    2. copyRecursive() 메소드는 상속한 부모의 구성의 의존성까지 복사한다.
  5. 복사된 구성은 원본과 항상 같지는 않다. 복사 대상 서브셋의 의존성간에 버전 충돌이 있을 경우 최종 복사된 의존성과 원본 의존상간에 차이가 발생할 수 있다.
    task copyVsFiles << {
        configurations.sealife.copyRecursive { dep -> dep.name == 'orca' }.each { file ->
            println file.name
        }
        println()
        // 위와는 다른 내용 출력
        configurations.sealife.files { dep -> dep.name == 'orca' }.each { file -> 
            println file.name
        }
    }
    // 결과
    > gradle -q copyVsFiles
    orca-1.0.jar
    seal-1.0.jar
     
    orca-1.0.jar
    seal-2.0.jar
    1. orca는 seal-1.0에 의존하고 있는 반면, shark는 seal-2.0에 의존하고 있는 상태.
    2. 원본 구성은 버전 충돌이 있고, 기본 행동법칙에 따라 최신 버전인 seal-2.0을 사용한다. 따라서 원본을 사용하는 files()메소드는 orca에 대한 이행적 의존성 판단 결과로 seal-2.0을 선택한다.
    3. 복제된 구성에서는 orca의 의존성만 복제했으며 따라서 버전 충돌이 없고 seal-1.0이 선택된다.
  6. 구성이 한번 결정되면 변경할 수 없다. 이 상태를 변경하려고 하면 예외가 발생한다.
  7. 변경하고자 할경우 구성 복사를 사용한다. 복사된 구성은 결정되기 전 상태이며 따라서 변경 후 결정이 가능하다.
  8. 더 자세히 알고자 한다면 Configuration 참조.

리포지토리(Repositories) 자세히

  • Gradle의 리포지토리 관리는 Apache Ivy에 기반하고 있다.
  • Gradle은 여러 종류의 리포지토리를 지원하며 각각 독립적으로 다뤄진다.
  • Gradle은 특정 리포지토리에서 모듈 기술자를 만나면 동일 리포지토리에서 해당 모듈의 artifact를 다운로드한다.
  • 모듈의 메타 정보와 artifact는 동일 리포지토리의 동일 위치에 있어야 하지만, 여러 URL을 가진 단일 리포지토리를 만드는 것은 가능하다.

Maven 중앙 리포지토리

로컬 Maven 리포지토리

  • 로컬 Maven 캐시를 리포지토리로 사용한다.
  • 개발시 SNAPSHOT 버전 등을 Local에서 바로 받아오거나 할 때 편리하다. Deploying an Artifact to the Local Cache in Gradle
    repositories {
        mavenLocal()
    }
  • settings.xml이 존재하면 이에 따라 로컬 리포지토리를 판단한다.
    • $USER_HOME/.m2/repository 기본값
    • $USER_HOME/.m2/settings.xml 우선
    • $M2_HOME/conf/settings.xml 차선

Maven 사용자 정의 리포지토리

  • 사용자가 직접 리포지토리를 만들어서 지정할 수 있다.
    repositories {
        maven {
            url "http://repo.mycompany.com/maven2"
        }
    }
  • 때로는 POM 위치와 JAR위치가 다를 수 있는데 그럴 때는 artifactUrls를 지정해 JAR를 찾을 수 있게 해준다.
    repositories {
        maven {
            // POM과 jar등의 artifact 탐색
            url "http://repo2.mycompany.com/maven2"
     
            // 위에서 artifact가 없으면 다음에서 검색
            artifactUrls "http://repo.mycompany.com/jars"
            artifactUrls "http://repo.mycompany.com/jars2"
        }
    }

인증 필요한 Maven 리포지토리 접근

repositories {
    maven {
        credentials {
            username 'user'
            password 'password'
        }
        url "http://repo.mycompany.com/maven2"
    }
}

단일 디렉토리 리포지토리

  • 파일시스템의 단일 디렉토리를 리포지토리로 사용할 수 있다.
    repositories {
        flatDir {
            dirs 'lib'
        }
        flatDir {
            dirs 'lib1', 'lib2'
        }
    }
  • 일반적으로는 name만 지정해주면 된다.
    repositories {
        mavenCentral()
        flatDir {
            dirs 'libs'
        }
    }
     
    /* libs 디렉토리에 imap.jar, smtp.jar, hibernate-jpa-2.0-api-1.0.0.Final.jar 가 있을 때 */
     
    dependencies {
        compile name: 'imap' // 혹은 compile ':imap'
        compile name: 'smtp' // 혹은 compile ':smtp'
     
        // 혹은 compile ':hibernate-jpa-2.0-api:1.0.0.Final'
        compile name: 'hibernate-jpa-2.0-api', version: '1.0.0.Final' 
    }
     
    task listJars << {
        configurations.compile.each { File file -> println file.name}
    }
  • gradle -q listJars 실행 결과
    imap.jar
    smtp.jar
    hibernate-jpa-2.0-api-1.0.0.Final.jar

Ivy 리포지토리

  • 표준 레이아웃으로 Ivy 리포지토리를 사용할 수 있다. IvyArtifactRepository 참조.
    repositories {
        ivy {
            url "http://repo.mycompany.com/repo"
            layout "maven"
        }
    }

Ivy 리포지토리 사용자 정의 패턴

  • 비표준 레이아웃을 사용할 경우 해당 리포지토리의 패턴 레이아웃을 지정할 수 있다.
    repositories {
        ivy {
            url "http://repo.mycompany.com/repo"
            layout 'pattern', {
                artifact "[module]/[revision]/[artifact].[ext]"
                ivy "[module]/[revision]/ivy.xml"
            }
        }
    }

Ivy 리포지토리에 서로 다른 artfact와 ivy 파일 위치를 지정하기

  • Ivy 리포지토리에서 ivy 파일과 artifact를 서로 다른 위치에서 가져오도록 하고자 할 경우에는 ivy와 artifact의 완전한 URL 패턴을 명시해주면 된다.
    repositories {
        ivy {
            artifactPattern "http://repo.mycompany.com/3rd-party-artifacts/[organisation]/[module]/[revision]/[artifact]-[revision].[ext]"
            artifactPattern "http://repo.mycompany.com/company-artifacts/[organisation]/[module]/[revision]/[artifact]-[revision].[ext]"
            ivyPattern "http://repo.mycompany.com/ivy-files/[organisation]/[module]/[revision]/ivy.xml"
        }
    }
  • artifactPattern과 ivyPattern의 값은 완전한 URL이어야 한다. 상대 URL불가.
  • 불완전한 URL은 프로젝트에 상대적인 파일 경로로 간주된다.

인증이 필요한 Ivy 리포지토리

repositories {
    ivy {
        credentials {
            username 'user'
            password 'password'
        }
        artifactPattern "http://repo.mycompany.com/[organisation]/[module]/[revision]/[artifact]-[revision].[ext]"
    }
}

리포지토리 다루기

  1. 리포지토리 객체에 접근 하려면
    println repositories.localRepository.name
    println repositories['localRepository'].name
  2. 리포지토리 구성하기
    repositories {
        flatDir {
            name 'localRepository'
        }
    }
    repositories {
        localRepository {
            dirs 'lib'
        }
    }
    repositories.localRepository {
        dirs 'lib'
    }

의존성 결정은 어떻게 이뤄지는가?

Gradle은 의존성과 선언과 리포지토리 정의를 읽어서 의존성 결정 과정(dependency resolution)을 통해 모든 의존하는 파일을 다운로드한다. 그 과정은 다음과 같다.

  1. 지정된 의존성에 대해 Gradle은 처음에 의존성의 모듈을 결정한다. 각 저장소를 순서대로 탐색하여 처음에는 모듈 기술자 파일(pom 혹은 ivy)을 찾는다. 파일이 존재하면 모듈이 존재한다는 의미이다. 모듈 기술자 파일이 없다면 모듈의 아티팩트 파일(jar)이 존재하는지 찾을 것이다.
    1. 의존성이 동적 버전(1.+ 형태)으로 지정돼 있다면, Gradle이 가장 최신의 정적 버전을 리토지토리에서 찾아서 정한다. Maven에서는 maven-metadata.xml 파일로, Ivy에서는 디렉토리의 파일 목록을 확인해서 결정한다.
    2. 모듈 기술자가 pom 파일이고 부모 pom이 선언돼 있다면, Gradle은 해당 pom의 부모까지 재귀적으로 탐색하여 정하게 된다.
  2. 일단 모든 리포지토리에서 모듈을 다 탐색했으면 Gradle은 그 중에서 가장 사용에 적합한 것을 선택한다. 이 작업은 다음과 같은 조건에 따라 이뤄진다.
    1. 동적 버전에서는 낮은 버전보다는 더 높은 버전이 선택된다.
    2. 모듈 기술자 파일(pom/ivy)가 있는 모듈이 artifact 파일만 있는 모듈보다 우선 선택된다.
    3. 상단에 선언된 리포지토리에 있는 모듈이 하단에 선언된 리포지토리에 있는 것보다 우선 선택된다.
    4. 의존성이 정적 버전으로 선언 돼 있고 모듈 기술자 파일이 리포지토리에 존재할 경우에는 그 이후의 리포지토리로는 탐색을 하지 않는다.
  3. 모듈의 모든 artifact들은 위의 프로세스를 거쳐 선택된것과 동일한 리포지토리에서 받는다.

의존성 캐시

Gradle의 의존성 캐시는 두가지 키 타입의 스토리지로 이뤄져 있다.

  • jar,pom,ivy 파일등을 포함한 다운로드한 artifact 파일을 저장. 다운로드한 artifact의 저장소 경로는 SHA1 체크섬을 포함하고 있으며 이 때문에 이름은 같지만 내용이 다른 2개의 artifact가 저장될 수도 있다.
  • 결정된 모듈의 메타 정보(결정된 동적 버전, 모듈 기술자, artifact 포함)를 바이너리로 저장.

캐시 관련 명령행 옵션

오프라인

--offline 스위치는 재검사할 필요가 있는지 여부와 무관하게 항상 캐시를 사용해 의존성을 결정하도록 한다. Gradle이 의존성 결정때문에 네트워크에 접근하는 일은 생기지 않는다. 필요한 모듈이 캐시에 없으면 빌드가 실패한다.

캐시 갱신

리포지토리 설정 상태에 따라 캐시의 싱크가 안 맞을 수도 있다. 리포지토리를 잘못 설정했거나, 변하지 않는 모듈을 잘못 배포했을 수 있다.

--refresh-dependencies 옵션을 사용하면 모든 의존성 캐시를 갱신한다. 이 옵션을 사용하면 결정된 모듈과 artifact의 모든 캐시된 항목들을 무시한다. 모든 리포지토리에 대해 의존성 결정을 다시 수행하고 다운로드한다.

의존성 캐시 세부 설정

ResolutionStrategy를 통해 캐시 세부 설정이 가능하다.

  • 기본적으로 동적 버전은 24시간동안 캐싱한다. 이를 변경하려면
    configurations.all {
        resolutionStrategy.cacheDynamicVersionsFor 10, 'minutes' // 10분
    }
  • 변하는 모듈도 24시간 캐시한다. 이를 변경하려면
    configurations.all {
        resolutionStrategy.cacheChangingModulesFor 30, 'days' // 30일
    }

이행적(transitive) 의존성 관리 전략

  • Maven Central이 아닌 사설 리포지토리를 운영하라. Maven Central은 느리고 가끔 다운될 수 있다.
  • 리포지토리가 존재하지 않는 외부 라이브러리는 파일 시스템이 저장하여 버전 컨트롤 시스템으로 관리한다.

provided

  • Gradle 2.x 에서 최종적으로 compileOnly Scope가 생겼다.
  • 따라서 아래 방법들은 Gradle 구버전에서만 사용하고 최신 버전에서는 compileOnly를 사용한다.

아래 모든 방법들을 사용하기 보다는 Gradle Web(War) Plugin 에 나오는 exclude 방식을 추천.

컴파일시에는 클래스패스에 넣지만, 실행/배포시에는 빠져야 하는 의존성이 있을 수 있다. 예를 들면 Servlet API 같은 것들이 그렇다. Servlet API는 Tomcat등의 WAS에 내장되어 있으므로 배포는 할 필요가 없다.

Gradle 1.2는 현재 Gradle Web(War) Plugin이 아닐경우에 provided를 제공해주고 있지 않다. 하지만 이를 흉내낼 수 있다. [#GRADLE-784] Provide a 'provided' configuration 참조.

configurations {
    provided
}
 
/* 배포시에는 빠지고, 컴파일시에는 들어가는 의존성을 provided에 지정한다. */
sourceSets {
    main {
        compileClasspath += configurations.provided
    }
    test {
        compileClasspath += configurations.provided
    }
}
 
// war 프로젝트일 경우
war {
    classpath -= configurations.provided
}

Gradle Ecplise Plugin 사용시 조정이 필요하다.

eclipse {
    classpath {
        // 클래스패스에는 넣지만...
        plusConfigurations += configurations.provided
 
        // .classpath 파일에서 해당 jar의 classpathentry@export를 false로 변경
        noExportConfigurations += configurations.provided 
 
        // 현재 Gradle 1.2는 noExportConfigurations를 설정해도 WTP에서 export되는 문제가 있다.
        // 이 문제는 멀티 프로젝트일 경우에만 발생한다. 멀티 프로젝트 아니라며 다음은 불필요함.
        // .classpath XML에서 @exported == false|null인 classpathentry의
        // "org.eclipse.jst.component.dependency" <attribute> 삭제해야 한다
        file.withXml { provider ->
            provider.asNode().classpathentry.findAll {
                it.@kind == 'lib' && (it.@exported == null || it.@exported == 'false')
            }.each { cpe ->
                def attributes = cpe.children()[0];
                if (attributes == null) { return }
 
                def componentDependency = attributes.attribute.find { it.@name == 'org.eclipse.jst.component.dependency'}
                if (componentDependency == null) { return }
 
                attributes.remove(componentDependency)
            }
        }
 
    }
    wtp {
        component {
            // WTP Deployment Assembly component 에서는 뺀다. 'war' 플러그인이 적용되지 않은 상태에서는 사용 할 수 없다.
            minusConfigurations += configurations.provided
        }
    }
}

의존하고 있는 라이브러리를 모두 모으기

Gradle Community Forums - How can I gather all my project's dependencies into a folder?

task copyToLib(type: Copy) {
    into "$buildDir/output/lib"
    from configurations.runtime
}

dependencyInsight

CompositeBuild

  • Gralde Composite Build 여러 프로젝트 연관관계가 있을 때 다른 의존 프로젝트를 리포지토리에 올리지 않고 로컬에 있는 상태로 의존할 수 있게 해준다.

참조할 DSL

Dependency Management Plugin





Gradle Build Lifecycle

Gradle은 의존성 기반의 프로그래밍용 언어이다. 이 말은 태스크를 정의하고 또한 태스크들 사이의 의존성도 정의 할 수 있다는 뜻이다.

Gradle은 태스크들이 의존성의 순서에 따라 실행되고, 오직 한 번만 실행될 것임을 보장한다.

Gradle은 태스크를 실행하기 전에 완전한 의존성 그래프를 구축한다.

빌드 단계

Gradle 빌드는 3 단계로 구분된다.

  • 초기화 : 단일/멀티 프로젝트 빌드 지원. 초기화 단계에서는 어느 프로젝트를 빌드하는지 결정하고 각각에 대해 Project 객체를 생성한다.
  • 구성 : 빌드에 속하는 모든 프로젝트의 빌드 스크립트를 실행한다. 이를 통해 프로젝트 객체를 구성한다.
  • 실행 : 구성 단계에서 생성하고 설정된 태스크 중에 실행할 것을 결정한다. 이 때 gradle 명령행에 인자로 지정한 태스크 이름과 현재 디렉토리를 기반으로 태스크를 결정하여 선택된 것들을 실행한다.

설정 파일

빌드 파일 말고도 설정 파일도 있다. 설정 파일은 명명규칙에 따라 Gradle이 자동 인식한다. 기본 파일명은 settings.gradle이다.

설정 파일은 초기화 단계에서 실행된다. 멀티 프로젝트는 무조건 최상위 프로젝트에 settings.gradle이 있어야 한다. 어느 프로젝트가 멀티 프로젝트 빌드에 속하는지를 여기서 정한다. Gradle Multi Project 참조. 단일 프로젝트 빌드에서는 설정 파일이 없어도 된다.

  • 단일 프로젝트에서의 settings.gradle
    println 'This is executed during the initialization phase.'
  • build.gradle
    println 'This is executed during the configuration phase.'
     
    task configured {
        println 'This is also executed during the configuration phase.'
    }
     
    task test << {
        println 'This is executed during the execution phase.'
    }
  • 실행하면
    > gradle test
    This is executed during the initialization phase.
    This is executed during the configuration phase.
    This is also executed during the configuration phase.
    :test
    This is executed during the execution phase.
    BUILD SUCCESSFUL

빌드 스크립트에서는 프라퍼티 접근과 메소드 호출이 project 객체로 위임 된다. 유사하게 설정 파일에서는 Settings settings 객체로 위임 된다.

멀티 프로젝트 빌드

멀티 프로젝트 빌드는 Gradle을 한 번실행하는 동안 하나 이상의 프로젝트를 빌드한다. 설정 파일에 소속 프로젝트를 지정해준다. Gradle Multi Project 참조.

프로젝트 위치

멀티 프로젝트는 항상 단일 최상위 아래에 트리 구조로 표현된다. 트리의 각 요소는 프로젝트를 나타낸다. 프로젝트는 경로로 표현된다. 보통 경로는 프로젝트의 파일 시스템에서의 물리적 위치로 구성된다. 하지만 이것도 설정 가능하다. 프로젝트 트리는 settings.gradle 파일에서 생성되며 기본적으로 최상위 프로젝트 아래에 있다고 가정한다. 하지만 최상위 프로젝트의 위치도 설정 파일에서 바꿀 수 있다.

트리구조 구축

계층적 레이아웃

  • settings.gradle
    include 'project1', 'project2', 'project2:child1'

include 메소드는 인자로 프로젝트 경로를 받는다. 이 경로는 파일 시스템에서의 상대 경로로 간주된다. 예를들어 경로가 'services:api'이면 최상위 프로젝트에 상대 경로로 'services/api' 디렉토리로 간주된다. 트리의 잎(leaf)만 지정한다. 이 말은, 'services:hotels:api'를 지정하면 세개의 프로젝트인 'services', 'services:hotels', 'services:hotels:api'가 생성된다는 뜻이다.

단층(flat) 레이아웃

  • settings.gradle
    includeFlat 'project3', 'project4'

includeFlat 메소드는 디렉토리 이름을 인자로 받는다. 이 디렉토리들은 최상위 디렉토리와 같은 레벨에 존재해야 한다. 이 디렉토리들의 위치는 멀티 프로젝트 트리에서 최상위 프로젝트의 자식 프로젝트로 간주된다.

프로젝트 트리의 요소 수정

설정 파일에서 만들어진 멀티 프로젝트 트리는 프로젝트 기술자(project descriptors)라는 것으로 만들어진다. 이 기술자를 변경할 수 있다.

  • 프로젝트 트리의 요소 수정 전 settings.gradle
    println rootProject.name
    println project(':projectA').name
  • 수정해보자 settings.gradle
    rootProject.name = 'main'
    project(':projectA').projectDir = new File(settingsDir, '../my-project-a')
    project(':projectA').buildFileName = 'projectA.gradle'

ProjectDescriptor를 참조한다.

초기화

Gradle이 단일 혹은 멀티 프로젝트 빌드를 실행할지 판단하는 기준이 있다. 설정 파일이 있는 디렉토리에서 멀티 프로젝트를 빌드를 실행하면 판단이 쉽다. 하지만 서브 프로젝트 아무데서나 빌드를 실행할 수도 있다. settings.gradle이 없는 곳에서 빌드를 실행하면

  • 현재 디렉토리와 동일한 계층 단계의 master라는 디렉토리에서 settings.gradle을 찾는다.
  • 없으면, 부모 디렉토리에서 settings.gradle을 찾는다.
  • 없으면, 단일 프로젝트로 빌드를 실행한다.
  • settings.gradle가 존재하면 현재 프로젝트가 멀티 프로젝트 계층에 속하는지 판단한다. 아니라면 단일 프로젝트로 실행하고 맞다면 멀티 프로젝트로 빌드를 실행한다.

이런 식으로 작동하는 이유는 멀티 프로젝트 일 경우 모든 멀티프로젝트 빌드 구성을 생성해야하기 때문이다. -u 명령행 옵션을 주면 부모 디렉토리에서 설정파일 찾는 것을 막는다. 이 경우에는 항상 단일 프로젝트로 실행한다. settings.gradle 파일이 있는 곳에서 -u는 아무 기능도 없다.

설정 파일 자동 탐색은 물리적으로 계층/단층 레이아웃인 멀티 프로젝트에서만 작동한다. 단층 레이아웃에서는 위에서 기술한 명명규칙을 지켜야 한다. Gradle은 멀티 프로젝트에 임의의 물리적 레이아웃을 지원하지만 이 때는 항상 설정 파일이 있는 곳에서 빌드를 실행해야 한다.

Gradle은 빌드에 참여하는 모든 프로젝트에 대해 Project 객체를 생성한다. 각 프로젝트는 기본적으로 탑레벨 디렉토리를 name으로 갖는다. 최상위를 제외한 모든 프로젝트는 부모 프로젝트가 있고, 자식 프로젝트를 가질 수 있다.

빌드 스크립트 라이프사이클에 반응하기

라이프싸이클을 진행하는 동안 빌드 스크립트에서 알림을 받을 수 있다. 알림은 특별한 리스너 인터페이스를 구현하거나 혹은 알림이 발생했을 때 실행할 클로저를 제공해 주는 두가지 방식으로 구현한다.

프로젝트 평가

프로젝트를 평가하기 직전과 직후에 알림을 받을 수 있다. 빌드 스크립트에서 모든 정의가 적용된 이후에 추가적인 구성을 수행할 때나 로깅 혹은 프로파일링을 하고자 할 때 사용한다.

  • hasTest 프라퍼티가 true인 프로젝트들에 test 태스크 추가하기 build.gradle
    allprojects {
        afterEvaluate { project ->
            if (project.hasTests) {
                println "Adding test task to $project"
                project.task('test') << {
                    println "Running tests for $project"
                }
            }
        }
    }
  • projectA.gradle
    hasTests = true
  • 실행하면
    > gradle -q test
    Adding test task to project ':projectA'
    Running tests for project ':projectA'

Project.afterEvaluate()를 사용하여 프로젝트 평가 뒤에 실행할 클로저를 추가 하였다.

아무 프로젝트든 평가한 뒤에 알림을 받는 것도 가능하다. afterProject는 프로젝트 평가의 성공 여부와 무관하게 호출된다.

  • 프로젝트의 성공 여부를 보여주는 build.gradle
    gradle.afterProject {project, projectState ->
        if (projectState.failure) {
            println "Evaluation of $project FAILED"
        } else {
            println "Evaluation of $project succeeded"
        }
    }
  • 실행하면
    > gradle -q test
    Evaluation of root project 'buildProjectEvaluateEvents' succeeded
    Evaluation of project ':projectA' succeeded
    Evaluation of project ':projectB' FAILED

Gradle 객체에 ProjectEvaluationListener 를 추가해도 된다.

  • 최상위 build.gradle에서 특정 하위 프로젝트를 콕 집어서 지정
    project(':sub-project').afterEvaluate {
       // ....
    }

태스크 생성

  • 프로젝트에 태스크가 추가된 직후에 알림을 받을 수 있다. 기본값을 설정하거나 태스크가 빌드에 노출되기전에 행위를 추가하고자 할 때 사용한다.
  • 각 태스크가 생성된 뒤에 srcDir을 설정하는 build.gradle
    tasks.whenTaskAdded { task ->
        task.srcDir = 'src/main/java'
    }
     
    task a
     
    println "source dir is $a.srcDir"
  • 실행하면
    > gradle -q a
    source dir is src/main/java

TaskContainer에 Action을 추가해도 된다.

태스크 실행 그래프가 정해진 뒤에

  • 실행할 태스크에 release가 있는지 여부에 따라 버전 변경하는 build.gradle
    task distribution << {
        println "We build the zip with version=$version"
    }
     
    task release(dependsOn: 'distribution') << {
        println 'We release now'
    }
     
    gradle.taskGraph.whenReady {taskGraph ->
        if (taskGraph.hasTask(release)) {
            version = '1.0'
        } else {
            version = '1.0-SNAPSHOT'
        }
    }

release 태스크의 실행여부에 따라 버전값이 달라진다. TaskExecutionGraph에 TaskExecutionGraphListener를 추가해도 된다.

태스크 실행

어떤 태스크이든 실행 직전과 직후에 알림을 받을 수 있다.

  • 태스크 실행과 종료를 로그로 남기는 build.gradle
    task ok
     
    task broken(dependsOn: ok) << {
        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 -q broken
    executing task ':ok' ...
    done
    executing task ':broken' ...
    FAILED

TaskExecutionGraph에 TaskExecutionListener를 추가해도 된다.









Gradle Multi Project

최상위 프로젝트의 이름

settings.gradle 파일에서 다음과 같이 최상위 프로젝트 이름을 지정한다. 이는 해당 프로젝트 디렉토리 이름과 무관하게 설정된다.

rootProject.name = '프로젝트이름'

멀티 프로젝트의 기본

  • 최상위 프로젝트에 settings.gradle이 필요하다. 여기서 하위 프로젝트를 include해준다.
    include "shared", "api", "services:webservice", "services:shared"
  • 최상위 프로젝트의 build.gradle에 모든 서브 프로젝트에 공통된 설정을 넣는다.
    subprojects {
        apply plugin: 'java'
        apply plugin: 'eclipse-wtp'
     
        repositories {
           mavenCentral()
        }
     
        dependencies {
            testCompile 'junit:junit:4.8.2'
        }
     
        version = '1.0'
     
        jar {
            manifest.attributes provider: 'gradle'
        }
    }
    • 모든 서브 프로젝트에 java, eclipse-wtp 플러그인이 적용되고, 지정된 리포지토리와 의존성이 무조건 추가된다.

서브 프로젝트간 의존성

// 어느 서브 프로젝트의 build.gradle
dependencies {
    compile project(':shared')
}
// shared 서브 프로젝트에 의존하고 있다.

교차 프로젝트 구성 Cross Project Configuration

공통 행위 정의하기

  • 다음과 같은 구조의 프로젝트가 있다고 하자. water가 부모 프로젝트이다.
    water/
      build.gradle
      settings.gradle
      bluewhale/
  • settings.gradle
    include 'bluewhale'
  • 서브 프로젝트의 빌드 파일은 없어도 상관없다. 부모 프로젝트에서 서브 프로젝트의 행위를 정의하는 것도 가능하다.
    Closure cl = { task -> println "I'm $task.project.name" }
    task hello << cl
    project(':bluewhale') {
        task hello << cl
    }
  • 위를 gradle -q hello로 실행하면
    > gradle -q hello
    I'm water
    I'm bluewhale
  • Gradle에서는 어떠한 빌드 스크립트에서라도 멀티 프로젝트의 아무 프로젝트에나 접근할 수 있다. Project API에는 project()라는 메소드가 있으며, 이는 프로젝트의 경로를 인자로 받아서 해당 경로의 Project 객체를 리턴한다. 이러한 방법을 교차 프로젝트 구성 cross project configuration이라고 부른다.
  • krill 서브 디렉토리를 만들어서 krill 서브 프로젝트를 선언한다. settings.gradle
    include 'bluewhale', 'krill'
  • 모든 프로젝트에 적용되는 태스크를 선언한다.
    allprojects {
        task hello << { task -> println "I'm $task.project.name" }
    }
  • 실행하면
    > gradle -q hello
    I'm water
    I'm bluewhale
    I'm krill
  • Project API의 allprojects 프라퍼티는 현재 프로젝트와 그것의 모든 서브 프로젝트를 리턴한다. allprojects에 클로저를 인자로 주면 클로저의 구문이 allprojects의 프로젝트들로 위임된다. allprojects.each로 이터레이션을 도는 것도 가능하다.
  • Gradle은 기본적으로 구성 주입(configuration injection)을 사용한다.
  • 또한 다른 빌드 툴 처럼 프로젝트 상속 구조도 가능하다.

서브 프로젝트 구성

Project.subprojects로 서브 프로젝트들만 접근하는 것도 가능하다. allprojects는 부모 프로젝트까지 포함한 것이다.

공통 행위 정의

  • 서브 프로젝트에만 적용되는 공통 행위 정의하기
    allprojects {
        task hello << {task -> println "I'm $task.project.name" }
    }
    // 아래는 서브프로젝트에만 적용된다.
    subprojects {
        hello << {println "- I depend on water"}
    }
  • 실행하면
    > gradle -q hello
    I'm water
    I'm bluewhale
    - I depend on water
    I'm krill
    - I depend on water

특정 서브 프로젝트에만 행위 추가

  • 일반적으로는 서브 프로젝트의 빌드 파일에 해당 프로젝트에 국한된 행위를 기술한다. 하지만 특정 프로젝트에 국한된 행위를 부모 프로젝트 빌드 파일에 정의할 수도 있다.
    allprojects {
        task hello << {task -> println "I'm $task.project.name" }
    }
    subprojects {
        hello << {println "- I depend on water"}
    }
    project(':bluewhale').hello << {
        println "- I'm the largest animal that has ever lived on this planet."
    }
  • 실행하면
    > gradle -q hello
    I'm water
    I'm bluewhale
    - I depend on water
    - I'm the largest animal that has ever lived on this planet.
    I'm krill
    - I depend on water
  • 서브 프로젝트의 디렉토리 최상단에 build.gradle을 두고 거기에 행위를 추가할 수 있다.
    water/
      build.gradle
      settings.gradle
      bluewhale/
        build.gradle
      krill/
        build.gradle
  • bluewhale/build.gradle
    hello.doLast { println "- I'm the largest animal that has ever lived on this planet." }
  • krill/build.gradle
    hello.doLast {
        println "- The weight of my species in summer is twice as heavy as all human beings."
    }
  • build.gradle
    allprojects {
        task hello << {task -> println "I'm $task.project.name" }
    }
    subprojects {
        hello << {println "- I depend on water"}
    }
  • 실행하면
    > gradle -q hello
    I'm water
    I'm bluewhale
    - I depend on water
    - I'm the largest animal that has ever lived on this planet.
    I'm krill
    - I depend on water
    - The weight of my species in summer is twice as heavy as all human beings.

프로젝트 필터링 Filter Projects

tropicalFish라는 프로젝트를 추가하고 water 프로젝트 빌드 파일에 행위를 더 추가해보자.

이름으로 필터링

  • 변경된 프로젝트 레이아웃
    water/
      build.gradle
      settings.gradle
      bluewhale/
        build.gradle
      krill/
        build.gradle
      tropicalFish/
  • settings.gradle
    include 'bluewhale', 'krill', 'tropicalFish'
  • build.gradle
    allprojects {
        task hello << {task -> println "I'm $task.project.name" }
    }
    subprojects {
        hello << {println "- I depend on water"}
    }
    // 이름이 tropicalFish가 아닌 프로젝트만 찾아서 설정
    configure(subprojects.findAll { it.name != 'torpicalFish' }) {
        hello << {println '- I love to spend time in the arctic waters.'}
    }
  • 실행하면
    > gradle -q hello
    I'm water
    I'm bluewhale
    - I depend on water
    - I love to spend time in the arctic waters.
    - I'm the largest animal that has ever lived on this planet.
    I'm krill
    - I depend on water
    - I love to spend time in the arctic waters.
    - The weight of my species in summer is twice as heavy as all human beings.
    I'm tropicalFish
    - I depend on water
  • Project.configure() 메소드는 리스트를 인자로 받아서 리스트 안의 프로젝트에 구성을 추가한다.

프라퍼티로 필터링하기

ext 프라퍼티를 통해 필터링이 가능하다.

  • 프로젝트 레이아웃
    water/
      build.gradle
      settings.gradle
      bluewhale/
        build.gradle
      krill/
        build.gradle
      tropicalFish/
        build.gradle
  • bluewhale/build.gradle
    ext.arctic = true
    hello.doLast { println "- I'm the largest animal that has ever lived on this planet." }
  • krill/build.gradle
    ext.arctic = true
    hello.doLast {
        println "- The weight of my species in summer is twice as heavy as all human beings."
    }
  • tropicalFish/build.gradle
    ext.arctic = false
  • build.gradle
    allprojects {
        task hello << { task -> println "I'm $task.project.name" }
    }
     
    subprojects {
        hello {
            doLast { println "- I depend on water"}
            afterEvaluate { Project project ->
                if (project.arctic) {
                    doLast {
                        println '- I love to spend time in the arctic waters.'
                    }
                }
            }
        }
    }
  • 실행하면
    > gradle -q hello
    I'm water
    I'm bluewhale
    - I depend on water
    - I'm the largest animal that has ever lived on this planet.
    - I love to spend time in the arctic waters.
    I'm krill
    - I depend on water
    - The weight of my species in summer is twice as heavy as all human beings.
    - I love to spend time in the arctic waters.
    I'm tropicalFish
    - I depend on water
  • afterEvaluate는 서브 프로젝트의 빌드 스크립트를 모두 수행한 뒤에 인자로 넘어온 클로저를 실행하라는 의미이다. arctic 프라퍼티가 서브 프로젝트 빌드 스크립트에 선언 돼 있기 때문이다.

멀티 프로젝트 빌드 실행 규칙

  • 최상위 프로젝트에서 hello 태스크를 실행하면 최상위와 그 아래 모든 서브 프로젝트의 hello 태스크가 실행 된다.
  • bluewhale 디렉토리로 이동해서 hello 태스크를 실행하면 bluewhale 프로젝트의 태스크만 실행된다.
  • Gradle의 태스크 실행
    • 현재 디렉토리에서 시작하여 계층구조를 탐색하여 hello라는 이름의 태스크를 찾고 실행한다.
    • Gradle은 항상 모든 프로젝트를 평가하고, 존재하는 모든 태스크 객체를 생성한다.
    • 그리고서 태스크의 이름과 현재 디렉토리를 기반으로 실행해야할 태스크를 결정한다.
    • Gradle의 교차 프로젝트 구성 때문에 모든 프로젝트는 어떠한 태스크를 실행할 때는 그 전에 먼저 평가 돼야 한다.
  • bluewhale/build.gradle
    ext.arctic = true
    hello << { println "- I'm the largest animal that has ever lived on this planet." }
     
    task distanceToIceberg << {
        println '20 nautical miles'
    }
  • krill/build.gradle
    ext.arctic = true
    hello << { println "- The weight of my species in summer is twice as heavy as all human beings." }
     
    task distanceToIceberg << {
        println '5 nautical miles'
    }
  • 최상위 프로젝트에서 실행하면
    > gradle distanceToIceberg
    :bluewhale:distanceToIceberg
    20 nautical miles
    :krill:distanceToIceberg
    5 nautical miles
    BUILD SUCCESSFUL
    Total time: 1 secs
  • 최상위 water 프로젝트에서 실행한다. water와 tropicalFish는 distanceToIceberg 태스크가 없지만 상관없다. 왜냐면 계층 구조를 따라 내려가면서 해당 명칭의 태스크를 실행한다라는 규칙 때문이다.

절대 경로로 태스크 실행하기

  • tropicalFish에서 실행한 gradle -q :hello :krill:hello hello
    > gradle -q :hello :krill:hello hello
    I'm water
    I'm krill
    - I depend on water
    - The weight of my species in summer is twice as heavy as all human beings.
    - I love to spend time in the arctic waters.
    I'm tropicalFish
    - I depend on water
  • water의 :hello, krill의 hello, tropicalFish의 hello 순서로 실행된다.

프로젝트와 태스크의 경로

  • 프로젝트 경로의 패턴은 다음과 같다.
    • 항상 콜론(:)으로 시작한다. 이는 최상위 프로젝트를 의미한다.
    • 최상위 프로젝트만 이름 없이 사용된다.
    • :bluewhale 은 파일 시스템상에서 water/bluewhale을 뜻한다.
  • 태스크의 경로는 프로젝트 경로에 태스크 이름을 붙인 것이다.
    • :bluewhale:hello는 bluewhale 프로젝트의 hello 태스크
    • 프로젝트 안에서는 태스크 이름만 사용하면 해당 프로젝트의 태스크로 간주한다. 상대 경로로 해석하기 때문이다.

의존성 - 어느 의존성을 선택하지?

의존성과 실행 순서에 대해서 확인해보자.

실행 의존성

의존성과 실행 순서

  • 프로젝트 레이아웃
    messages/
      settings.gradle
      consumer/
        build.gradle
      producer/
        build.gradle
  • settings.gradle
    include 'consumer', 'producer'
  • consumer/build.gradle
    task action << {
        println("Consuming message: " +
                (rootProject.hasProperty('producerMessage') ? rootProject.producerMessage : 'null'))
    }
  • producer/build.gradle
    task action << {
        println "Producing message:"
        rootProject.producerMessage = 'Watch the order of execution.'
    }
  • 실행하면
    > gradle -q action
    Consuming message: null
    Producing message:
  • 이것은 작동하지 않는다. 왜냐면 명시적으로 정의하지 않으면 Gradle은 알파벳 순서에 따라 태스크를 실행하기 때문이다.
  • 따라서 :consumer:action이 :producer:action 보다 먼저 실행된다.
  • producer 프로젝트를 aProducer로 바꾸면 원하는 대로 작동한다.
  • aProducer로 바뀐 상태에서 consumer 디렉토리에서 action 태스크를 실행하면 규칙에 따라 :aProducer:action은 실행이 안되므로 null이 찍힌다.

태스크 실행 의존성 선언하기

  • consumer/build.gradle
    task action(dependsOn: ':producer:action') << {
        println("Consuming message: " +
            (rootProject.hasProperty('producerMessage') ? rootProject.producerMessage : 'null'))
    }
  • 최상위와 consumer 디렉토리 어디에서든 실행하면
    > gradle -q action
    Producing message:
    Consuming message: Watch the order of execution.
  • :consumer:action이 :producer:action에 실행시 의존성을 걸고 있기 때문에 항상 :producer:action이 먼저 실행된다.

교차 프로젝트 태스크 의존성의 특징

  • 의존성을 지정할 때 태스크 이름은 아무 상관이 없다.

구성 시(Configuration Time) 의존성 설정하기

  • 태스크에 의존성을 거는 것이 아니라 프로젝트 구성에 의존해야 할 경우가 있다.
  • consumer/build.gradle
    message = rootProject.hasProperty('producerMessage') ? rootProject.producerMessage : 'null'
     
    task consume << {
        println("Consuming message: " + message)
    }
  • producer/build.gradle
    rootProject.producerMessage = 'Watch the order of evaluation.'
  • 실행하면
    > gradle -q consume
    Consuming message: null
  • 기본 빌드 파일 평가 순서가 알파벳 순서이기 때문에 consumer가 producer보다 먼저 평가된다.
  • 해결하려면 consumer/build.gradle
    evaluationDependsOn(':producer')
     
    message = rootProject.hasProperty('producerMessage') ? rootProject.producerMessage : 'null'
     
    task consume << {
        println("Consuming message: " + message)
    }
  • 실행하면
    > gradle -q consume
    Consuming message: Watch the order of evaluation.
  • evaluationDependsOn은 producer를 consumer보다 먼저 평가하게 만든다.
  • 사실 위의 경우는 억지스럽다. 사실은 그냥 rootProject.producerMessage 값을 바로 읽게 만들기만 해도 된다. consumer/build.gradle
    task consume << {
        println("Consuming message: " +
                (rootProject.hasProperty('producerMessage') ? rootProject.producerMessage : 'null'))
    }
  • 구성시 의존성은 실행시 의존성과는 매우 다르다.
  • 구성시 의존성은 프로젝트간에 맺는 것이고, 실행시 의존성은 태스크간의 의존성으로 결정된다.
  • 다른 다른 점은 서브 프로젝트에서 빌드 명령을 내려도 항상 모든 프로젝트의 구성을 수행한다는 점이다.
  • 기본 구성 순서는 위에서 아래로 내려간다.
  • 기본 구성 순서를 아래에서 위로 방향으로 바꾸려면 부모 프로젝트가 자신의 자식 프로젝트에 의존한다는 뜻이 되는데 이 때는 evaluationDependsOnChildren() 메소드를 사용한다.
  • 동일 단계의 프로젝트간 구성 순서는 알파벳 순서에 따른다. 가장 일반적인 경우로 모든 서브 프로젝트가 Java 플러그인을 사용하는 것 처럼 공통의 라이프사이클을 공유하는 때가 있다.
  • dependsOn을 사용해 서로 다른 두 프로젝트의 실행시 의존성을 지정할 경우 이 메소드는 기본적으로 두 프로젝트간에 구성 의존성을 생성하는 것이다. 따라서 이 때는 구성 의존성을 명시적으로 지정하지 않아도 된다.

실전 예제

두 개의 웹 애플리케이션 서브 프로젝트를 가진 최상위 프로젝트가 웹 애플리케이션 배포본을 생성하는 예를 본다. 예제에서는 단 하나의 교차 프로젝트 구성을 사용한다.

  • 프로젝트 레이아웃
    webDist/
      settings.gradle
      build.gradle
      date/
        src/main/java/
          org/gradle/sample/
            DateServlet.java
      hello/
        src/main/java/
          org/gradle/sample/
            HelloServlet.java
  • settings.gradle
    include 'date', 'hello'
  • build.gradle
    allprojects {
        apply plugin: 'java'
        group 'org.gradle.sample'
        version = '1.0'
    }
     
    subprojects {
        apply plugin: 'war'
        repositories {
            mavenCentral()
        }
     
        dependencies {
            compile "javax.servlet:servlet-api:2.5"
        }
    }
     
    task explodedDist(dependsOn: assemble) << {
        File explodedDist = mkdir("$buildDir/explodedDist")
     
        subprojects.each { project ->
            project.tasks.withType(Jar).each { archiveTask ->
                copy {
                    from archiveTask.archivePath
                    into explodedDist
                }
            }
        }
    }
  • 최상위 프로젝트에서 gradle -q explodedDist를 실행하면 “$buildDir/explodedDist”에 hello-1.0.jar와 date-1.0.jar 가 생성된다.
  • date와 hello 프로젝트는 webDist 프로젝트의 구성시 의존성을 가진 상태이다. 그리고 빌드 로직도 webDist에서 주입되었다.
  • 하지만 실행시 의존성은 webDist가 date와 hello의 빌드된 아티팩트에 의존한다.
  • 세번째 의존성으로 webDist가 자식인 date와 hello에 구성시 의존성도 있는데, 이는 arhivePath를 알아야만 하기 때문이다. 하지만 태스크를 실행하는 시점에 요청한다. 따라서 순환 의존성은 아니다.
  • withType() 메소드. 컬렉션에서 특정 타입인 것만 골라서 새로운 컬렉션으로 만든다.

프로젝트 lib 의존성

한 프로젝트가 다른 프로젝트의 컴파일 결과와 그 의존하는 라이브러리들 모두에 의존하는 경우가 발생한다. 이 때 프로젝트간 의존성을 설정한다.

  • 프로젝트 레이아웃
    java/
      settings.gradle
      build.gradle
      api/
        src/main/java/
          org/gradle/sample/
            api/
              Person.java
            apiImpl/
              PersonImpl.java
      services/personService/
        src/
          main/java/
            org/gradle/sample/services/
              PersonService.java
          test/java/
            org/gradle/sample/services/
              PersonServiceTest.java
      shared/
        src/main/java/
          org/gradle/sample/shared/
            Helper.java
  • shared, api, personService 프로젝트가 있다. personService는 다른 두 프로젝트에 의존하고, api는 shared에 의존한다.
  • settinga.gradle
    include 'api', 'shared', 'services:personService'
  • build.gradle
    subprojects {
        apply plugin: 'java'
        group = 'org.gradle.sample'
        version = '1.0'
        repositories {
            mavenCentral()
        }
        dependencies {
            testCompile "junit:junit:4.8.2"
        }
    }
     
    project(':api') {
        dependencies {
            compile project(':shared')
        }
    }
     
    project(':services:personService') {
        dependencies {
            compile project(':shared'), project(':api')
        }
    }
  • lib 의존성은 실행시 의존성의 특별한 형태이다. 의존성이 걸리게 되면 다른 프로젝트가 먼저 빌드하여 jar를 생성하고 그것을 현재 프로젝트의 클래스패스에 추가한다.
  • 따라서 api 디렉토리에서 gradle compile을 실행하면 shared가 먼저 빌드 되고 그 뒤에 api가 빌드 된다. 프로젝트 의존성은 부분적인 멀티 프로젝트 빌드를 가능케 한다.
  • Ivy 방식의 매우 상세한 의존성 설정도 가능하다.
  • build.gradle
    subprojects {
        apply plugin: 'java'
        group = 'org.gradle.sample'
        version = '1.0'
    }
     
    project(':api') {
        configurations {
            spi
        }
        dependencies {
            compile project(':shared')
        }
        task spiJar(type: Jar) {
            baseName = 'api-spi'
            dependsOn classes
            from sourceSets.main.output
            include('org/gradle/sample/api/**')
        }
        artifacts {
            spi spiJar
        }
    }
     
    project(':services:personService') {
        dependencies {
            compile project(':shared')
            compile project(path: ':api', configuration: 'spi')
            testCompile "junit:junit:4.8.2", project(':api')
        }
    }
  • Java 플러그인은 기본적으로 프로젝트당 모든 클래스를 포함한 하나의 jar를 생성한다. 위 예제에서는 api 프로젝트의 인터페이스만 포함하는 추가적인 jar를 생성하였다.

의존하는 프로젝트의 빌드 금지하기

  • 때로는 부분 빌드를 할 때 의존하고 있는 프로젝트를 빌드하지 않기를 바랄 때도 있다. -a 옵션으로 gradle을 실행하면 된다.

분리된(decoupled) 프로젝트

  • 두 프로젝트간의 프로젝트 모델에 접근하지 않는 것을 서로 분리된(decoupled) 프로젝트라고 부른다.
  • 분리된 프로젝트는 프로젝트 의존성이나 태스크 의존성으로만 연결되어 있다.
  • 그 외의 어떠한 형태의 프로젝트간 소통행위( 다른 프로젝트의 값을 읽거나 수정하는 등)은 두 프로젝트를 엮인(coupled) 프로젝트로 만든다.
  • 엮인 프로젝트가 되는 가장 일반적인 상황은 교차 프로젝트 설정에서 구성 주입을 사용할 경우이다.
  • allprojects 혹은 subprojects 키워드를 사용한 순간 프로젝트들은 엮인 것이다.

멀티 프로젝트 빌드와 테스트

  • Java 플러그인의 build 태스크를 사용하여 컴파일, 테스트, 코드 스타일 검사(CodeQuality 플러그인 사용시)등을 할 수 있다.
  • 다중 프로젝트에서 여러 범위의 프로젝트에 대해 빌드를 할 경우가 있는데 이 때 buildNeeded 와 buildDependents 태스크를 사용한다.
  • “프로젝트 lib 의존성”의 프로젝트로 테스트 해본다.
  • gradle :api:build : api와 api가 의존하는 모든 프로젝트에 대해 컴파일과 jar를 수행하고 api 프로젝트의 build를 수행한다.
  • gradle -a :api:build : api 프로젝트의 build만 수행한다.
  • gradle :api:buildNeeded : api와 api가 의존하는 모든 프로젝트의 build를 수행한다.
  • gradle :api:buildDependents : api와 api에 의존하는 모든 프로젝트에 대해 build를 수행한다.
  • gradle build : 모든 프로젝트에 대해 build한다.

프라퍼티와 메소드 상속

  • 프로젝트에 정의된 프라퍼티와 메소드는 모든 서브 프로젝트로 상속된다.
  • 이 때문에 gradle 태스크이름 -P프라퍼티이름=값으로 실행할 경우 모든 project 객체에서 해당 프라퍼티를 사용할 수 있게 된다.

멀티 프로젝트 단위 테스트간의 의존성

개인적으로 아래 방법보다는 공통 단위 테스트용 프로젝트를 만들고(예: xxx-test-support) 해당 프로젝트에 각종 테스트용 의존성과 테스트용 유틸리티 클래스를 일반 코드로 작성한 뒤에 다른 프로젝트들이 testCompile project(':xxx-test-support') 형태로 의존성을 추가하는 것이 더 일관성 있고 깔끔한 방법으로 보인다.


ProjectA와 ProjectB의 단위테스트가 존재하는데, ProjectB의 단위테스트가 ProjectA의 단위테스트 클래스 중 일부에 의존하고 있다면, 기본적으로는 클래스를 찾지 못해 예외가 발생한다. 단위 테스트는 프로젝트간 의존성에서 제외되기 때문이다. build - Multi-project test dependencies with gradle에 해결책이 있으나 classes가 write-only로 바뀌고 읽을 때는 output을 하도록 바뀌었다.

  • ProjectB의 build.gradle
    dependencies {
        testCompile project(':projectA').sourceSets.test.output // projectA의 단위 테스트 클래스에 의존함.
        // 이 방법은 eclipse에서 projecA의 단위 테스트 디렉토리를 라이브러리로 등록하는 문제가 있음.
    }
  • 위 방법보다는 configuration을 사용하는 다른 방법이 더 유용하다. Gradle Ecplise Plugin 사용시 설정 필요 build.gradle
    configurations {
        crosstest
        testCompile.extendsFrom crosstest  // testCompile이 crosstest에 의존하게 변경
    }
     
    dependencies {
        crosstest project(':projectA').sourceSets.test.output
    }
     
    eclipse {
        classpath {
            minusConfigurations += configurations.crosstest // 불필요한 classpath 등록 방지
        }
    }







Gradle Logging

  • 기본 로그 레벨
    1. ERROR Error messages
    2. QUIET Important information messages
    3. WARNING Warning messages
    4. LIFECYCLE Progress information messages
    5. INFO Information messages
    6. DEBUG Debug messages
  • 명령행 지정
    • 기본 : LIFECYCLE
    • -q or --quiet : QUIET
    • -i or --info : INFO
    • -d or --debug : DEBUG

빌드 스크립트에서 로그 남기기

logger 객체가 존재하므로 바로 사용하면 된다.

logger.quiet('An info log message which is always logged.')
logger.error('An error log message.')
logger.warn('A warning log message.')
logger.lifecycle('A lifecycle info log message.')
logger.info('An info log message.')
logger.debug('A debug log message.')
logger.trace('A trace log message.')

외부 툴과 라이브러리 로그

  • Gradle은 Ant, Ivy 등의 로그를 Gradle 로깅으로 변환한다.
  • System.out은 QUIET로 변환된다.
  • System.err은 ERROR로 변환된다.
  • 표준 아웃의 로그 레벨 변경
    logging.captureStandardOutput LogLevel.INFO
    println 'A message which is logged at INFO level'







Gradle Daemon

  • Gradle을 대몬으로 띄워둔 상태에서 고속으로 빌드를 수행할 수 있다.

대몬 실행

  • --daemon 옵션을 주면서 태스크를 실행하면 대몬이 없으면 대몬을 생성하고 존재하면 그것을 사용한다.
  • gradle.properties를 통해 대몬을 항상 사용하게 할 수 있다.
  • –no-daemon 으로 대몬을 사용금지할 수 있다.
  • –stop 대몬 종료







Gradle 빌드 환경 설정

  • gradle.properties 로 기본 빌드 환경을 설정할 수 있다.
    • $HOME/.gradle/gradle.properties
    • 프로젝트홈/gradle.properties
    • 명령행에서 시스템 프라퍼티로 명시 : -Dsome.property

buildEnvironment

  • buildEnvironment Task로 build 의존성을 확인할 수 있다. (Gradle 2.10 or later)
gradlew buildEnvironment

프라퍼티들

  • org.gradle.daemon=true/false : Gradle 대몬으로 실행할지 여부
  • org.gradle.java.home
  • org.gradle.jvmargs

시스템 프라퍼티

Http Proxy

  • gradle.properties에 다음 추가. Proxy 서버는 알아서 변경.
  • user 이하 생략 가능.
systemProp.http.proxyHost=www.somehost.org
systemProp.http.proxyPort=8080
systemProp.http.proxyUser=userid
systemProp.http.proxyPassword=password
systemProp.http.nonProxyHosts=*.nonproxyrepos.com|localhost    
 
systemProp.https.proxyHost=www.somehost.org
systemProp.https.proxyPort=8080
systemProp.https.proxyUser=userid
systemProp.https.proxyPassword=password
systemProp.https.nonProxyHosts=*.nonproxyrepos.com|localhost








Gradle Organizing Build Logic

  • Gradle에는 빌드 스크립트 로직을 구성하는 여러 가지 방법이 있다.
  • 태스크 클로저에 직접 넣기.
  • 여러 태스크가 동일 로직을 사용한다면 메소드로 빼서 호출한다. 멀티 프로젝트에서 이 로직을 사용한다면 부모 프로젝트에 메소드를 선언한다.
  • 메소드로 빼 빌드로직이 너무 복잡하다면 OO 모델로 바꿀 수 있다. 특정 디렉토리에 클래스를 넣어두면 Gradle이 자동으로 컴파일하여 빌드 스크립트 클래스패스에 추가한다.

프라퍼티와 메소드 상속

부모 프로젝트에 정의된 모든 메소드와 프라퍼티는 서브 프로젝트에서도 접근 가능하다. 이를 통해 공통 구성을 정의하고 빌드 로직을 메소드로 만들어서 서브 프로젝트에서 사용하도록 하면 된다.

  • build.gradle
    srcDirName = 'src/java'
     
    def getSrcDir(project) {
        return project.file(srcDirName)
    }
  • child/build.gradle
    task show << {
        // 상속받은 프라퍼티 사용
        println 'srcDirName: ' + srcDirName
     
        File srcDir = getSrcDir(project)
        println 'srcDir: ' + rootProject.relativePath(srcDir)
    }
  • 실행하면
    > gradle -q show
    srcDirName: src/java
    srcDir: child/src/java

구성 주입(Injected configuration)

상속보다는 Gradle Multi Project에 나온 교차 프로젝트 구성방식과 서브프로젝트 구성 방식이 더 좋다. 이유는 주입방식은 빌드 스크립트에서 명백하게 일어나고 서로 다른 프로젝트에 다른 로직을 주입할 수 있다. 그리고 리포지토리, 플러그인, 태스크와 기타 등등 모든 종류의 구성을 주입할 수 있다.

  • build.gradle
    subprojects {
        // 프라퍼티와 메소드를 주입한다.
        srcDirName = 'src/java' // 프라퍼티
        srcDir = { file(srcDirName) } // 메소드
     
        // Inject a task
        task show << {
            println 'project: ' + project.path
            println 'srcDirName: ' + srcDirName
            File srcDir = srcDir()
            println 'srcDir: ' + rootProject.relativePath(srcDir)
        }
    }
     
    //  특정 프로젝트에는 특수한 경우의 구성을 주입한다.
    project(':child2') {
        srcDirName = "$srcDirName/legacy"
    }
  • child1/build.gradle
    // 주입된 프라퍼티와 메소드를 사용하지만, 값은 덮어 쓴다.
    srcDirName = 'java'
    def dir = srcDir()
  • 실행하면
    > gradle -q show
    project: :child1
    srcDirName: java
    srcDir: child1/java
    project: :child2
    srcDirName: src/java/legacy
    srcDir: child2/src/java/legacy

buildSrc 프로젝트의 소스 빌드하기

Gradle을 실행하면 buildSrc라는 디렉토리가 존재하는지 검사한다. 존재한다면 자동으로 이 코드를 컴파일하고 테스트한 뒤에 빌드 스크립트의 클래스패스에 집어 넣는다. 그 외에 다른 것을 할 필요가 없다. 여기에 Custom Task와 Gradle Custom Plugins을 넣으면 된다.

멀티 프로젝트에서는 최상위 프로젝트에만 buildSrc를 둘 수 있다.

  • buildSrc의 기본 빌드 스크립트
    // --build.gralde이 없어도 아래 빌드 스크립트가 존재하는 것으로 간주한다.--
    // 1.6 버전에서는 명시적으로 필요한 것으로 보인다.
    apply plugin: 'groovy'
    dependencies {
        compile gradleApi()
        groovy localGroovy()
    }

이 말은 buildSrc에 기본 Java/Groovy 프로젝트를 구성할 수 있다는 뜻이다. Gradle Java PluginGradle Groovy Plugin

좀 더 복잡한 것이 필요하다면 자신만의 build.gradle을 만들면 된다. Gradle은 기본 빌드 스크립트를 build.gradle이 있건 없건 무조건 적용한다. 즉, 빌드 스크립트에는 기본값 외에 다른 것만 정의하면 된다. 1.6 버전에서는 기본 설정도 함께 넣어야 하게 바뀐 것 같다.

  • buildSrc/build.gradle
    apply plugin: 'groovy'
    repositories {
        mavenCentral()
    }
     
    // gradle과 groovy에 관한 의존성은 추가할 필요가 없다.
    dependencies {
        compile gradleApi()
        groovy localGroovy()
        testCompile group: 'junit', name: 'junit', version: '4.8.2'
    }

buildSrc 프로젝트도 멀티 프로젝트 빌드가 될 수 있다. 하지만 실제 빌드의 클래스패스에 넣고자 하는 buildSrc 서브 프로젝트는 최상위 buildSrc 프로젝트의 runtime 의존성에 설정해야 한다.

  • 서브프로젝트를 최상위 buildSrc프로젝트에 넣기 build.gradle
    rootProject.dependencies {
      runtime project(path)
    }

JDBC Driver 로딩 못하는 문제

  • Gradle 1.6에서 buildSrc의 커스텀 태스크/플러그인 프로젝트에 지정된 JDBC Driver 클래스를 못 찾는 문제가 있다.(No suitable driver found for jdbc:…)
  • http://stackoverflow.com/questions/6329872/how-to-add-external-jar-files-to-gradle-build-scriptbuildSrc/build.gradle
    repositories {
        mavenCentral()
    }
    configurations {
        driver
    }
    dependencies {
        driver group: 'mysql', name: 'mysql-connector-java', version: '5.1.16'
    }
     
    URLClassLoader loader = GroovyObject.class.classLoader
    configurations.driver.each {File file ->
        loader.addURL(file.toURL())
    }
     
    // JDBC Driver 사용하는 코드 혹은 custom task/plugin 제작

공유 스크립트

gradle에서 “외부 빌드 스크립트로 프로젝트 구성하기” 참조.

커스텀 태스크

커스텀 플러그인

외부 빌드 실행

GradleBuild 태스크를 를 사용하여 다른 빌드의 태스크를 실행할 수 있다. dir 혹은 buildFile 프라퍼티로 실행할 빌드를 지정하고 tasks 프라퍼티로 실행할 태스크를 지정할 수 있다.

  • 다른 빌드를 실행하기 build.gradle
    task build(type: GradleBuild) {
        buildFile = 'other.gradle'
        tasks = ['hello']
    }
  • other.gradle
    task hello << {
        println "hello from the other build."
    }
  • 실행하면
    > gradle -q build
    hello from the other build.

빌드 스크립트 전용 외부 라이브러리

빌드 스크립트가 외부 라이브러리를 필요로 한다면 buildScript() 메소드를 사용하여 빌드 스크립트 자체의 스크립트 클래스패스에 추가하면 된다. 빌드 스크립트 클래스패스를 지정하는 클로저를 인자로 전달한다.

  • 빌드 스크립트의 외부 의존성 지정하기 build.gradle
    buildscript {
        repositories {
            mavenCentral()
        }
        dependencies {
            classpath group: 'commons-codec', name: 'commons-codec', version: '1.2' // classpath 구성 사용
        }
    }

buildScript()메소드에 전달된 클로저는 ScriptHandler의 인스턴스를 구성한다.

빌드 스크립트의 클래스패스는 classpath 구성을 사용하여 지정한다. 여기서는 프로젝트 의존성을 제외한 모든 의존성을 지정할 수 있다.

빌드 스크립트 클래스패스를 지정한 위에는 빌드스크립트에서 해당 클래스를 마음대로 사용할 수 있다.

  • 외부 의존 클래스 사용하는 빌드 스크립트 build.gradle
    import org.apache.commons.codec.binary.Base64
     
    buildscript {
        repositories {
            mavenCentral()
        }
        dependencies {
            classpath group: 'commons-codec', name: 'commons-codec', version: '1.2'
        }
    }
     
    task encode << {
        def byte[] encodedString = new Base64().encode('hello world\n'.getBytes())
        println new String(encodedString)
    }
  • 실행하면
    > gradle -q encode
    aGVsbG8gd29ybGQK
  • 멀티 프로젝트에서는 프로젝트 빌드 스크립트의 의존성이 모든 서브프로젝트에도 적용된다.
  • 멀티 프로젝트에서 최상위 프로젝트의 buildscript에 repository를 추가해도, 하위 프로젝트에서 다시 buildscript 구문을 넣고 의존성을 지정하면 하위 프로젝트의 buildscriptt 구문에 repository를 재지정해야 한다.

Ant 의존성 추가

빌드 스크립트의 외부 의존성 추가 방식으로는 Ant에 의존성을 추가할 수 없다.

  • Ant에 의존성 추가 build.gradle
    configurations {
        ftpAntTask
    }
     
    dependencies {
        // ant-commons-net의 maven pom.xml이 잘못돼 있어서 ant-commons-net의 추가 의존성을 직접 지정해줬다.
        ftpAntTask("org.apache.ant:ant-commons-net:1.8.4") {
            module("commons-net:commons-net:1.4.1") {
                dependencies "oro:oro:2.0.8:jar"
            }
        }
    }
     
    task ftp << {
        ant {
            taskdef(name: 'ftp',
                    classname: 'org.apache.tools.ant.taskdefs.optional.net.FTP',
                    classpath: configurations.ftpAntTask.asPath) // 여기서 추가된 클래스패스 사용!
            ftp(server: "ftp.apache.org", userid: "anonymous", password: "me@myorg.com") {
                fileset(dir: "htdocs/manual")
            }
        }
    }

apply

apply from: 'some.gradle'로 외부 Gradle 스크립트를 적용할 수 있다. 이때 파일 대신 URL을 적어도 된다. 인트라넷 URL을 통해 전사 공통 자바 소스 스타일 검사기능 추가하는 방법

apply from: 'http://intranet/source/quality.gradle'

이 방식을 사용할 경우 Jenkins등의 CI에서 동시 빌드시에 외부 리소스에 대한 락이 걸리는 상황이 발생한다.[GRADLE-2795] Gradle locks the global script cache during the entire build, causing subsequent builds to fail if scripts change 아래와 유사한 오류가 발생할 것이다.

A problem occurred evaluating script.
Could not open buildscript class cache for script 'http://.../?p=build-core;a=blob_plain;f=repository-utils.gradle;hb=HEAD' (/home/build/.gradle/caches/1.6/scripts/_p_build_core_a_blob_plain_f_r_4n9gdhqrjd4inp4c6jive7ql9c/DefaultScript/buildscript).
Timeout waiting to lock buildscript class cache for script 'http://.../?p=build-core;a=blob_plain;f=repository-utils.gradle;hb=HEAD' (/home/build/.gradle/caches/1.6/scripts/_p_build_core_a_blob_plain_f_r_4n9gdhqrjd4inp4c6jive7ql9c/DefaultScript/buildscript). 
It is currently in use by another Gradle instance.
Owner PID: unknown
Our PID: 15314
Owner Operation: unknown
Our operation:
Lock file: /home/build/.gradle/caches/1.6/scripts/_p_build_core_a_blob_plain_f_r_4n9gdhqrjd4inp4c6jive7ql9c/DefaultScript/buildscript/cache.properties.lock

이 때 해결책은 빌드 스크립트 URL의 맨 뒤에 Jenkins Job의 이름을 넣어주는 것이다. 마지막의 ${java.net.URLEncoder.encode(System.getenv()['JOB_NAME'] ?: 'NOJOB', 'UTF-8')} Job 마다 서로 다른 build script Cache를 생성하여 Lock 충돌이 방지된다.

apply from: "http://server/epository-utils.gradle?jn=${java.net.URLEncoder.encode(System.getenv()['JOB_NAME'] ?: 'NOJOB', 'UTF-8')}"

JOB_NAME 대신 Math.random()사용시 계속해서 빌드 스크립트 캐시가 서로 다른이름으로 생성되어 파일 갯수가 증가하게 된다. /home/[username]/.gradle/caches/[version]/scripts/* 디렉토리를 cron 등으로 주기적으로 정리해줘야한다. 아래는 하루에 한 번씩 어제날짜의 캐시 디렉토리를 삭제하는 Unix script.

find /home/[user]/.gradle/caches/*/scripts -maxdepth 1 -mindepth 1 -mtime +1 -type d -exec rm -rf {} \;








Gradle Custom Task

Gradle은 두가지 타입의 태스크를 지원한다. 하나는 간단한 태스크로 액션 클로저를 사용해 정의한다. 가장 기본적인 형태이다. 이 타입의 태스크는 클로저에 태스크의 행위를 기술한다. 빌드 스크립트에서 단 한 번만 쓸법한 태스크를 구현할 때 좋다.

다른 타입은 “확장(enhanced) 태스크”로 행위를 태스크에 모두 내장시키고 그 태스크를 설정할 수 있는 프라퍼티들을 제공해준다. Gradle Task 에서 살펴보았다. 대부분의 Gradle 플러그인은 확장 태스크를 사용한다. 확장 태스크는 간단한 태스크에서 처럼 태스크의 행위를 구현할 필요가 없다. 단지 태스크를 선언하고 태스크 프라퍼티를 설정해주면 된다. 이 방법은 여러 서로 다른 곳에서 서로 다를 빌드에서 행위를 재사용할 수 있게 해준다.

확장 태스크의 행위와 프라퍼티는 태스크 클래스로 정의된다. 확장 태스크를 선언할 때는 타입 혹은 태스크의 클래스를 명시한다.

사용자 정의 태스크를 구현하는 것은 쉽다. 원하는 어떤 언어로든지 사용자 정의 태스크를 구현하고 바이트코드로 컴파일해서 제공하기만 하면 된다. 그래도 Gradle API가 Groovy에 맞춰져 있으므로 Groovy로 만드는 것이 제일 편할 것이다.

태스크 클래스 패키징

빌드 스크립트

빌드 스크립트 안에서 클래스를 만들어 구현한다. 자동으로 컴파일되어 클래스패스에 추가된다. 태스크 클래스를 정의한 빌드 스크립트에서만 사용가능하다.

buildSrc 프로젝트

추천하는 방식.

Gradle Organizing Build Logic에 나온 buildSrc 프로젝트에 넣는다. rootProjectDir/buildSrc/src/main/groovy 디렉토리 아래에 넣는 것이다. Gradle이 자동으로 컴파일, 테스트, 클래스패스 추가를 해준다.

이렇게 할 경우 모든 프로젝트의 빌드스크립트에서 태스크 클래스를 사용할 수 있다. 빌드 자체의 외부에는 노출되지 않는다.

독립 프로젝트

태스크 클래스용 독립 프로젝트를 만들고 jar로 묶어서 배포한다.

간단한 사용자 정의 태스크 클래스 만들기

Task의 구현체인 DefaultTask를 상속하여 구현한다.

태스크에 어떤 행위를 추가하려면 TaskAction 어노테이션을 지정한 메소드를 추가하면 된다. 태스크가 실행 될 때 해당 메소드가 호출된다. 하지만 꼭 메소드를 정의할 필요는 없다. 태스크 클래스 생성자에서 doFirst()혹은 doLast()를 행위를 지정한 클로저를 인자로 줘서 호출해도 된다.

  • build.gradle
    task hello(type: GreetingTask)
     
    class GreetingTask extends DefaultTask {
        @TaskAction
        def greet() {
            println 'hello from GreetingTask'
        }
    }
     
    // 혹은 생성자를 이용한 다음도 가능하다.
    class GreetingTask extends DefaultTask {
        GreetingTask() {
            doFirst {
                println "hello from Greeting Task constructor"
            }
        }
    }
  • 실행하면
    > gradle -q hello
    hello from GreetingTask

태스크에 프라퍼티를 추가하여 커스터마이징을 할 수 있게 하자. 태스크는 POGO이므로 태스크 객체에 프라퍼티를 지정하거나 메소드를 호출 할 수 있다.

  • 프라퍼티 추가된 태스크 build.gradle
    // 기본값사용
    task hello(type: GreetingTask)
     
    // greeting 변경
    task greeting(type: GreetingTask) {
        greeting = 'greetings from GreetingTask'
    }
     
    class GreetingTask extends DefaultTask {
        def String greeting = 'hello from Greeting Task'
        @TaskAction
        def greet() {
            println greeting
        }
    }
  • 실행하면
    > gradle -q hello greeting
    hello from GreetingTask
    greetings from GreetingTask

project로 현재 프로젝트의 인스턴스에 접근할 수 있다. project.rootProject로 최상위 프로젝트에 접근할 수 있다.

독립 프로젝트

이제 태스크를 독립 프로젝트로 분리하여 배포하고 공유할 수 있도록 해보자. 이는 간단한 Groovy 프로젝트로 태스크 클래스를 포함하는 JAR를 생성하기만 하면 된다. Groovy 프로젝트로 만들고 Gradle API를 컴파일시 의존성에 추가한다.

package를 org.gradle로 지정해야 실제 사용시 import를 안하고 사용할 수 있다.

  • build.gradle
    apply plugin: 'groovy'
     
    dependencies {
        compile gradleApi()
        groovy localGroovy()
    }
  • src/main/groovy/org/gradle/GreetingTask.groovy
    package org.gradle
     
    // build.gradle에서는 자동으로 추가되지만 여기서는 명시적으로 import 해야한다.
    import org.gradle.api.DefaultTask
    import org.gradle.api.tasks.TaskAction
     
    class GreetingTask extends DefaultTask {
        String greeting = 'hello from GreetingTask'
     
        @TaskAction
        def greet() {
            println greeting
        }
    }

다른 프로젝트에서 태스크 클래스 사용하기

태스크 클래스를 빌드 스크립트의 클래스패스에 추가해야 한다. buildscript { } 블럭을 사용하면 된다. Gradle Organizing Build Logic에서 빌드 스크립트에 외부 의존성 추가하는 부분을 참조한다. 아래 예제는 로칼 리포지토리에 태스크 클래스를 배포한 상황을 예로 들고 있다.

  • 다른 프로젝트에서 사용자 정의 태스크 사용하기
    buildscript {
        // 빌드 스크립트 리포지토리에 추가하고
        repositories {
            maven {
                url uri('../repo')
            }
        }
     
        // classpath 구성에 의존성을 추가한다.
        dependencies {
            classpath group: 'org.gradle', name: 'customPlugin', version: '1.0-SNAPSHOT'
        }
    }
     
    task greeting(type: org.gradle.GreetingTask) {
        greeting = 'howdy!'
    }

태스크 클래스의 단위 테스트 작성하기

ProjectBuilder 클래스를 사용하여 Project 인스턴스를 만들어낼 수 있다. 이를 가지고 태스크 클래스를 테스트하면 된다.

  • src/test/groovy/org/gradle/GreetingTaskTest.groovy
    class GreetingTaskTest {
        @Test
        public void canAddTaskToProject() {
            Project project = ProjectBuilder.builder().build()
            def task = project.task('greeting', type: GreetingTask)
            assertTrue(task instanceof GreetingTask)
        }
    }







Gradle Custom Plugins

Gradle 플러그인은 재사용 가능한 빌드 로직을 패키지화하여 여러 서로다른 프로젝트와 빌드에서 사용할 수 있도록 한 것이다.

플러그인은 아무 언어로 작성해도 되며 Java 바이트코드로 컴파일해서 제공하기만 하면 된다.

플러그인 패키징

빌드 스크립트

빌드 스크립트에 직접 플러그인 소스를 작성해도 된다. 자동으로 컴파일되어 빌드 스크립트의 클래스패스에 추가된다. 플러그인을 선언한 빌드 스크립트 외부에서는 접근할 수 없다.

buildSrc 프로젝트

추천하는 방식.

Gradle Organizing Build Logic에 나온 buildSrc 프로젝트에 넣는다. rootProjectDir/buildSrc/src/main/groovy 디렉토리 아래에 넣는 것이다. Gradle이 자동으로 컴파일, 테스트, 클래스패스 추가를 해준다.

이렇게 할 경우 모든 프로젝트의 빌드 스크립트에서 이 플러그인에 접근 가능하다.

독립 프로젝트

플러그인용 독립 프로젝트를 만들고 Jar로 묶어서 배포한다.

간단한 플러그인 작성

Plugin을 구현하여 사용자 정의 플러그인을 만든다. Gradle은 플러그인 객체를 생성하고 프로젝트 객체를 인자로 하여 Plugin.apply() 메소드를 호출한다.

  • 사용자 정의 플러그인 build.gradle
    apply plugin: GreetingPlugin
     
    class GreetingPlugin implements Plugin<Project> {
        void apply(Project project) {
            project.task('hello') << {
                println "Hello from the GreetingPlugin"
            }
        }
    }
  • 실행하면
    > gradle -q hello
    Hello from the GreetingPlugin

플러그인이 적용되는 모든 프로젝트에 대해서 플러그인 인스턴스가 각각 하나씩 생성된다.

빌드에서 입력 받기

플러그인들은 대부분 빌드 스크립트에서 설정을 해야할 필요가 있다. 이때 확장 객체(extension object)를 사용한다. Gradle의 Project는 ExtensionContainer 객체를 통해 플러그인에 전달되는 설정과 프라퍼티들을 추적할 수 있도록 해준다. 확장 컨테이너를 통해 사용자 입력을 받을 수 있다. 입력을 받으려면 확장 컨테이너의 확장 목록에 자바 빈에 호환되는 클래스를 추가하면 된다.

  • greeting 확장 객체를 프로젝트에 추가하기 build.gradle
    apply plugin: GreetingPlugin
     
    // 확장 객체 값 사용
    greeting.message = 'Hi from Gradle'
     
    class GreetingPlugin implements Plugin<Project> {
        void apply(Project project) {
            // 'greeting' 확장 객체 추가
            project.extensions.create("greeting", GreetingPluginExtension)
     
            // 관련 설정을 사용하는 태스크 추가
            project.task('hello') << {
                println project.greeting.message
            }
        }
    }
     
    class GreetingPluginExtension {
        def String message = 'Hello form GreetingPlugin'
    }
  • 실행하면
    > gradle -q hello
    Hi from Gradle

이 예제에서 GreetingPluginExtension은 message 필드를 가지고 있는 POGO이다. greeting 이라늠 이름으로 확장 객체가 플러그인에 추가 되었다. 이 객체는 동일한 이름으로 프로젝트의 프라퍼티로서 접근할 수 있다.

종종 지정할 프라퍼티가 많은 경우 확장 객체에 구성 클로저 블록을 추가하여 한번에 설정할 수 있도록 해준다.

  • 구성 클로저를 사용하는 플러그인 build.gradle
    apply plugin: GreetingPlugin
     
    // 확장 객체의 프라퍼티들을  한 번에 설정한다.
    greeting {
        message = 'Hi'
        greeter = 'Gradle'
    }
     
    class GreetingPlugin implements Plugin<Project> {
        void apply(Project project) {
            project.extensions.create("greeting", GreetingPluginExtension)
            project.task('hello') << {
                println "${project.greeting.message} from ${project.greeting.greeter}"
            }
        }
    }
     
    class GreetingPluginExtension {
        String message
        String greeter
    }
  • 실행하면
    > gradle -q hello
    Hi from Gradle

클로저 블럭의 이름(greeting)은 확장 객체의 이름과 같다. Groovy 클로저 위임 기능에 따라 클로저가 실행될 때 확장 객체의 필드는 클로저에 있는 변수와 매핑된다.

사용자 정의 태스크와 플러그인에서 파일 다루기

Project.file() 메소드로 파일들의 값을 최대한 늦게 결정하도록 하는 것이 좋다.

  • 파일 프라퍼티를 늦게 평가하기 build.gradle
    class GreetingToFileTask extends DefaultTask {
     
        def destination
     
        File getDestination() {
            project.file(destination)
        }
     
        @TaskAction
        def greet() {
            def file = getDestination()
            file.parentFile.mkdirs()
            file.write "Hello!"
        }
    }
     
    task greet(type: GreetingToFileTask) {
        destination = { project.greetingFile }
    }
     
    task sayGreeting(dependsOn: greet) << {
        println file(greetingFile).text
    }
     
    greetingFile = "$buildDir/hello.txt"
  • 실행하면
    > gradle -q sayGreeting
    Hello!

위 예제에서는 태스크의 destination 프라퍼티를 Project.file() 메소드를 사용하여 최대한 늦게 평가하도록 하였다. 빌드 스크립트에서 태스크 선언 보다 더 뒤에 greetingFile 프라퍼티를 추가한 것을 볼 수 있다. 이 늦은 초기화 기법을 적극적으로 사용하라.

독립 프로젝트

독립 프로젝트로 만들면 jar로 배포하여 다른 사람들과 공유할 수 있다. 보통 다음과 같은 최소한의 빌드 스크립트로 시작한다.

  • 사용자 정의 플러그인을 위한 build.gradle
    apply plugin: 'groovy'
     
    dependencies {
        compile gradleApi()
        groovy localGroovy()
    }

Gradle은 Plugin 구현체를 META-INF/gradle-plugins에서 플러그인 이름과 같은 프라퍼티 파일을 통해 찾는다.

  • src/main/resources/META-INF/gradle-plugins/greeting.properties
    implementation-class=org.gradle.GreetingPlugin

프라퍼티 파일의 이름이 플러그인의 이름이 된다. implementation-class 프라퍼티는 Plugin 구현 클래스를 가리킨다.

다른 프로젝트에서 플러그인 사용하기

buildscript { } 블럭을 통해 클래스패스에 사용자 정의 플래그인 클래스를 추가한다. Gradle Organizing Build Logic 참조. 다음 예제는 로컬 리포지토리에 플러그인을 저장해서 사용하는 것을 보여준다.

  • 다른 프로젝트에서 사용자 정의 플러그인을 사용하는 build.gradle
    buildscript {
        repositories {
            maven {
                url uri('../repo')
            }
        }
        dependencies {
            classpath group: 'org.gradle', name: 'customPlugin', version: '1.0-SNAPSHOT'
        }
    }
    apply plugin: 'greeting'

플러그인의 테스트 작성하기

ProjectBuilder 클래스를 사용하여 Project 인스턴스를 만들어낼 수 있다. 이를 통해 플러그인 구현체를 테스트한다.

  • src/test/groovy/org/gradle/GreetingPluginTest.groovy
    class GreetingPluginTest {
        @Test
        public void greeterPluginAddsGreetingTaskToProject() {
            Project project = ProjectBuilder.builder().build()
            project.apply plugin: 'greeting'
     
            assertTrue(project.tasks.hello instanceof GreetingTask)
        }
    }

다중 도메인 객체 처리하기

Gradle은 빌드 언어와 잘 작동하는 객체의 컬렉션을 다루는 도우미 클래스를 제공해주고 있다.

  • 도메인 객체를 다루는 build.gradle
    apply plugin: DocumentationPlugin // DocumentaionPlugin.apply()가 실행된다.
     
    // books NamedDomainObjectContainer 값을 구성한다. apply보다 나중에 실행되지만 'books.all { }'의 행위가 다 적용된다.
    books {
        quickStart {
            sourceFile = file('src/docs/quick-start')
        }
        userGuide {
        }
        developerGuide {
        }
    }
     
    task books << {
        books.each { book ->
            println "$book.name -> $book.sourceFile"
        }
    }
     
    class DocumentationPlugin implements Plugin<Project> {
        void apply(Project project) {
            def books = project.container(Book)
            books.all { // 현재 있는, 그리고 앞으로 컬렉션에 추가될 객체를 모두 돌면서 아래 수행
                sourceFile = project.file("src/docs/$name")
            }
            project.extensions.books = books
        }
    }
     
    class Book {
        final String name // 'name' 필드는 필수이며 상수이고 유일한 값이어야 한다.
        File sourceFile
     
        Book(String name) {
            this.name = name
        }
    }
  • 실행하면
    developerGuide -> /home/user/gradle/samples/userguide/organizeBuildLogic/customPluginWithDomainObjectContainer/src/docs/developerGuide
    quickStart -> /home/user/gradle/samples/userguide/organizeBuildLogic/customPluginWithDomainObjectContainer/src/docs/quick-start
    userGuide -> /home/user/gradle/samples/userguide/organizeBuildLogic/customPluginWithDomainObjectContainer/src/docs/userGuide

Project.container()는 NamedDomainObjectContainer의 인스턴스를 생성한다. 이 클래스에는 객체를 관리하고 구성하는 편리한 메소드들이 들어있다.

project.container 메소드를 통해 사용할 객체의 타입은 항상 name 이라는 프라퍼티를 가지고 있어야 하며, 이 필드는 객체의 이름으로써 유일한 값이면서 상수여야 한다.

project.container(Class) 메소드는 객체의 새로운 인스턴스를 생성하면서 하나의 문자열을 인자로 받아 객체의 이름(name)으로 지정하려고 시도한다.

NamedDomainObjectContainer는 DomainObjectCollection을 상속하고 있다. DomainObjectCollection.all() 메소드는 컬렉션에 현재 있는 항목들과 그 이후 추가될 항목들까지 돌면서 클로저의 내용을 실행한다. 따라서 여기서 books 선언이 플러그인 적용보다 늦게 발생했으나 books.all {} 블럭의 내용이 모두 자동으로 실행되게 된다.

태스크간의 의존성

플러그인 태스크 간의 의존성은 다음과 같은 형태로 정의할 수 있다.

project.task('sometask') {
    dependsOn(project.tasks.a, project.tasks.b)
}

플러그인에 의존성 주입






Gradle Initialization Scripts - 초기화 스크립트

기본 사용법

초기화 스크립트는 Gradle의 다른 스크립트와 유사하다. 하지만 빌드가 시작되기 전에 실행된다.

  • 시스템 전역 설정. 예를들면 커스텀 플러그인 찾을 위치 지정.
  • 현재 환경에 따른 프라퍼티 설정. 개발 장비, 지속적 통합 장비 등에 따른 프라퍼티 값 바꿔지기 같은 경우.
  • 빌드에 필요한 개발자의 개인적 설정 정보.
  • 장비 전용 설정. JDK 위치 등.
  • 빌드 리스너(build listener)를 등록하여 Gradle 빌드 이벤트를 받는다.
  • 빌드 로거를 등록한다.
  • buildSrc 프로젝트의 클래스에는 접근할 수 없다.

초기화 스크립트 실행

  1. -I 혹은 --init-script 뒤에 스크립트 경로를 줘서 실행. 명령행 인자를 여러번 사용하면 여러 초기화 스크립트가 등록된다.
  2. $USER_HOME/.gradle/init.gradle
  3. $USER_HOME/.gradle/init.d/*.gradle
  4. $GRADLE_HOME/init.d/*.gradle

여러개의 초기화 스크립트가 발견되면 그 모두를 위에 나온 순서대로 다 실행한다. 동일 디렉토리의 스크립트는 알파벳 순서로 실행된다.

초기화 스크립트 작성하기

단순 groovy 스크립트이다. 모든 초기화 스크립트는 Gradle 인스턴스를 받는다. 모든 프라퍼티 참조와 메소드 호출은 Gradle 인스턴스로 위임된다.

프로젝트 구성하기

초기화 스크립트에서 프로젝트 구성을 할 수도 있다. 이는 멀티 프로젝트 빌드에서 구성하는 것과 유사하다. 이는 프로젝트 자체가 평가되기 전에 실행된다.

  • 프로젝트 평가전에 먼저 구성 수행하기 build.gradle
    repositories {
        mavenCentral()
    }
     
    task showRepos << {
        println "All repos:"
        println repositories.collect { it.name }
    }
  • init.gradle
    allprojects {
        repositories {
            mavenLocal()
        }
    }
  • 실행하면
    > gradle --init-script init.gradle -q showRepos
    All repos:
    [MavenLocal, MavenRepo]

초기화 스크립트의 외부 의존성

initscript() 메소드로 초기화 스크립트 전용 외부 의존성을 지정할 수 있다.

  • 외부 의존성을 선언하는 init.gradle
    initscript {
        repositories {
            mavenCentral()
        }
        dependencies {
            classpath group: 'org.apache.commons', name: 'commons-math', version: '2.0'
        }
    }

initscript() 메소드의 인자로 넘어온 클로저는 ScriptHandler 인스턴스를 구성한다.
 classpath 구성에 의존성을 추가하면 된다. Gradle Dependencies 참조.







Gradle Wrapper

명령행을 통한 Wrapper 기본 설정

Gradle 3.1 버전에 all 타입으로 wrapper 설정 생성.

gradle wrapper --gradle-version 3.1 --distribution-type all

distribution-type은 Gradle 3.x 부터 가능.

기본 설정

  • 더이상 불필요.
  • build.gradle
    task wrapper(type: Wrapper) {
        gradleVersion = '1.6' // 원하는 Gradle 버전 명시
    }
  • gradle wrapper를 한 번실행해 주면 gradle/wrapper 디렉토리가 생성되고 래퍼 jar 들이 복사된다.
  • 이 상태 그대로를 VCS에 올려서 공유하면 다른 개발자들은 gradlew 혹은 gradlew.bat 명령으로 Gradle 작업을 수행하면 된다.
  • *Nix 계열에서는 gradlew 파일에 실행 권한이 없으므로 항상 sh gradlew 옵션들 형태로 실행해야 한다.

세부 설정

  • gradlew[.bat] 파일에서 DEFAULT_JVM_OPTS를 편집하여 기본 JVM 옵션을 설정할 수 있다.
  • gradle/wrapper/gradle-wrapper.properties 파일에서 distributionUrl 을 프라퍼티 파일이 있는 디렉토리에 대해 상대 경로로 지정하면 URL에서 다운로드 받지 않고 저장된 파일을 사용할 수 있다.

gradlew 자동 찾아 실행하는 스크립트

  • Linux에서 현재 디렉토리를 포함하여 자기 위의 디렉토리를 자동으로 탐색하여 gradlew 명령을 찾고 이를 실행해주는 스크립트.
  • 파일명 : gw
    #!/bin/bash
    current_dir=`pwd`
    while [ "$current_dir" != "/" ]
    do
        if [ -f "$current_dir/gradlew" ]
        then
            break;
        fi
        current_dir=`dirname "$current_dir"`
    done
     
    if [ "$current_dir" != "/" ]
    then
        bash "$current_dir/gradlew" "$@"
    else
        echo "No gradlew file exists."
    fi
  • 실행예
    # 현재 디렉토리가 ~/project/module_1 이고, gradlew 는 project에 있을 때
    gw dependencies
     
    # 위 명령은 자동으로 project/gradlew dependencies를 현재 디렉토리에서 실행한다.
    # 따라서 실제 실행 결과는 다음과 같다.
    ~/project/gradlew :module_1:dependencies






Maven에서 Gradle로

Gradle이 Maven보다 좋았던 점

  • 프로젝트 구성과 빌드는 근본적으로 “구성”이라는 정적인 요소와 “빌드”라는 동적인 요소의 집합이다. 이를 Maven은 정적인 데이터를 저장하는 XML로 만들어서 동적인 행위 정의를 매우 어렵게 만들었다.
    • Maven의 가장 큰 문제이며 이로인한 복잡한 프로젝트에서 설정이 거의 불가능한 상황이 자주 발생한다.
    • Gradle은 DSL로 설정 정보를 구성하고, 그 자체가 Groovy 스크립트 언어이므로 동적인 작업은 그냥 Groovy 코드로 즉석에서 작성하면 된다.
  • Maven은 상속 구조를 사용해 멀티 모듈을 구현한다. Gradle은 구성 주입(Configuration Injection)을 사용한다.
    • Maven에서 특정 설정을 몇몇 모듈에서만 공통으로 사용하려면 불필요하게 부모 프로젝트를 생성하여 설정하고 그것을 자식들이 상속하게 해야 한다. 게다가 다른 모든게 같더라도 약간이라도 설정이 다른 프로젝트가 하나라도 있다면 그 프로젝트는 상속을 할 수 없고, 거의 모든 설정을 중복해서 해당 프로젝트에 넣어줘야 한다.
    • Gradle은 공통 설정을 조건에 따라 특정 프로젝트에만 주입 가능하다. 불필요한 상속 전용 프로젝트는 필요없다.
  • 프로젝트에 상대적인 파일 경로로 작업을 할 때 Gradle은 rootProject.file() 로 쉽게 구성 가능하다.(특히 Eclipse linkedResources 적용할 때)
  • Maven은 자신만의 플러그인을 만들기가 힘들다. 하지만 Gradle은 build.gradle 혹은 buildSrc를 통해 자신만의 플러그인과 태스크를 매우 쉽게 정의할 수 있다.
  • Gradle은 Ant 태스크를 바로 가져다가 사용할 수 있기 때문에 수많은 Java Ant 태스크들을 이미 내장하고 있는 것이나 다름 없다.
  • Gradle은 Task간의 작동 순서 정의가 매우 쉽다. Maven은 정적인 특성 때문에 특정 태스크를 반복 수행하거나 하는 등의 작업이 힘들고, 다른 Phase에 태스크를 끼워넣는 것도 직관적이지 못하다.
  • Gradle은 Maven플러그인으로는 있으나 Gradle 혹은 Ant 플러그인이 없을 경우 그냥 외부 프로그램을 실행해버리거나 Groovy로 Maven 플러그인의 Java 코드를 호출해서 실행하면 된다.

현재(1.2) Gradle의 문제점

  • 의존성에서 provided를 기본으로 제공하지 않고 있다. 하지만 방법은 있다. configurations를 직접 구성해야한다.
  • Maven보다 프로젝트 컴파일/ 빌드 속도가 느리다.
  • 이행적 의존성 충돌이 발생할 때 모르는 사이에 지정한 것보다 높은 버전의 라이브러리를 받아오는 현상이 생긴다. 이것은 문제라기 보다는 Gradle의 의도인데 이것을 이해하지 못하면 의도치 않은 일이 생길 수 있다. Gradle Dependencies 에서 의존성 충돌에 관해 잘 참고할 것.
  • IDE 지원이 다소 미흡함. 그러나 Eclipse는 대부분 문제가 해결 가능하다.

pom.xml에서 의존성 문자열 모두 뽑아내기

  • dependencies.groovy
    def pom = new XmlParser().parse(new File(args[0]))
     
    def dependencies = pom.dependencies.dependency.each {
         // GString을 사용할 수도 있어서 일부러 쌍따옴표로 했음
        println "\"${it.groupId.text()}:${it.artifactId.text()}:${it.version.text()}\","
    }
  • 실행한 뒤에 맨 끝에 쉼표는 잘 조정해서 쓸 것
    > groovy dependencies pom.xml
    "com.google.guava:guava:10.0.1",
    "spy:spymemcached:2.7",
    "org.codehaus.jettison:jettison:1.3",
    .....

provided

기본적으로 Gradle은 provided를 제공하지 않고 있다. 하지만 Gradle Web(War) Plugin 프로젝트는 providedCompile을 지원하고 있으며, 유사 기능을 흉내낼 수 있다.

Gradle Dependencies - provided 참조.

이행적 의존성으로 인한 라이브러리 버전 변경 대비

현재 프로젝트에서 spring-data-jpa를 사용하는데, 이 모듈이 Spring Core/Beans 등에 대해 가변 버전으로 의존하고 있다. 이에 따라 현재 프로젝트의 공식 Spring 버전은 3.1인데, 이행성과 가변 버전 변경에 따라 Core/Beans 등이 3.2.0.M2로 지정되는 현상이 발생하였다.

Gradle Dependencies를 참고하여 의존성 버전 관리 전략에 failOnVersionConflict()을 넣어 주고 항상 명시하는 것이 좋아보인다.

Profile 흉내내기

  • -Pprofile=값 형태의 옵션으로 Maven의 Profile을 흉내낼 수 있다.
    final String DEFAULT_PROFILE = 'development'
    allprojects {
        if (!project.hasProperty('profile') || !profile) {
            ext.profile = DEFAULT_PROFILE
        }
     
        // 리소스에 각 프로필별 리소스 디렉토리 추가
        sourceSets {
            main {
                resources {
                    srcDir "src/main/resources-${profile}"
                }
            }
        }
    }
  • 이제 gradle -Pprofile=production형태로 호출하면 모든 프로젝트에서 profile 속성에 production이라는 값이 지정된 상태가 된다. 이에 따라 가변적인 행동을 정의해 준다.
  • -Pprofile을 생략하면 기본인 DEFAULT_PROFILE 상수의 값 development로 작동하게 된다.

Apache CXF

Apache CXF로 SOAP Client Class 생성하는 것은 JavaExec 태스크로 하면 된다. CXF 참조

Annotation Processing

Annotation Processing은 원칙적으로는 컴파일 과정에서 자동으로 수행된다. 하지만 JPA 2 MetaModel 생성같이 소스를 생성해야 할 경우가 있는데 그럴 때는 -proc:only 옵션으로 독립적으로 소스 생성만 하는 컴파일러를 돌려주고 그 뒤에 실제 컴파일을 수행하도록 실행 계획을 짜면 된다.

Lombok의 경우에도 아무 설정도 할 필요없다. 클래스패스에만 있으면 자동으로 수행된다.

Gradle에서 JPA2 MetaModel 생성 참조.

projectA의 단위테스트가 projectB의 단위테스트에 의존

projectA의 단위테스트가 projectB의 단위테스트에 의존하는 경우가 있다. 원칙적으로는 이러면 안된다. 근본적으로 projectA와 B간의 의존관계는 메인 자바 소스에 대한 것이어야지 테스트에 대한 것이면 안된다. 하지만 어쨌든 이런 상태로 컴파일과 실행이 가능하게 만들 수는 있다.

Multi Project 단위 테스트간의 의존성 참조






Gradle에서 JPA 2 MetaModel 생성

Hibernate MetamodelGen을 이용하여 JPA2 MetaModel을 생성하는 예를 보여준다. 실제로는 compile 태스크에 들어가는게 좋으나, 현재 지원을 안 해서, 독릭적으로 JavaCompile 태스크를 만들고, 거기서 Annotation Processor만 호출하도록 변경한 것이다.

  • -proc:only 옵션 때문에 실제 컴파일을 하지 않는다.
  • Java 6 이상에서만 작동한다.

Cannot find symbol 오류

  • QueryDSL과 Hibernate/Eclipse Metamodel Generator를 함께 사용할 때 아직 생성되지 않은 메타 모델 클래스를 사용하는 코드들 때문에 cannot find symbol 에러가 발생할 수 있는데, 이는 이 둘을 서로 따로 생성했을 때 발생하는 현상이다.
  • 정황상 Lombok을 함께 사용할 경우 각 AP가 실행된 뒤에 다시 lombok AP가 돌면서 발생하는 것으로 보인다.
  • 이 둘을 함께 지정해서 APT 를 수행해야 에러가 나지 않는다.
"-processor",
  "com.mysema.query.apt.jpa.JPAAnnotationProcessor,org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor"

Gradle Annotation Process 예

http://alvinalexander.com/java/jwarehouse/hibernate/build.gradle.shtml 에 있는 것을 옮겼다.

Gradle 기반으로 정적 분석 도구 사용시에 여기서 자동 생성된 클래스는 정적 분석에서 예외처리해줘야 한다.(/Q[A-Z].*\.class//.*\_.class/Java Static Analysis 를 참조한다.

ext.jpaMetamodelGeneratedDir = "$buildDir/생성된 메타 모델 클래스를 저장할 디렉토리"
 
configurations {
    jpaMetamodelGen {
        extendsFrom compile
    }
}
 
dependencies {
    jpaMetamodelGen "org.hibernate:hibernate-jpamodelgen:1.2.0.Final"
}
 
sourceSets {
    main {
        java {
            srcDir jpaMetamodelGeneratedDir
        }
    }
}
 
task generateJpaMetamodel(type: JavaCompile) {
 
    def targetDir = file(jpaMetamodelGeneratedDir)
    def compiledDestinationDir = "${buildDir}/tmp/apt-jpa"
    doFirst {
        // 항상 대상 디렉토리를 먼저 비우고 시작해야 한다.
        delete(targetDir)
        targetDir.mkdirs()
    }
 
    doLast {
        delete(compiledDestinationDir) // UP-TO-DATE 방지
    }
 
    // -proc:only 는 Annotation Processor로 소스 생성만 한다 컴파일은 하지 않음
    // -s 경로 는 생성된 소스가 들어갈 디렉토리를 뜻한다.
    classpath = configurations.jpaMetamodelGen
    source = sourceSets.main.java
    destinationDir = file(compiledDestinationDir)
    options.define(
        compilerArgs: [
            "-nowarn",
            "-proc:only",
            "-encoding", "UTF-8",
            "-s", targetDir.absolutePath,
            // processor 지정은 안해도 된다. 안하면 모든 어노테이션 프로세서 실행
            "-processor", "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor"         ]
    )
}
 
// 필요하면 compileJava가 generateJpaMetamodel 에 의존하도록 변경한다.