출처: http://kwonnam.pe.kr/wiki/gradle
참고: http://www.gliderwiki.org/wiki/197
Gradle
- Maven을 대체할 수 있는 프로젝트 구성/관리 및 Build tool.
- Maven을 넘어 Gradle로 가자 - Maven 대신 Gradle을 써야하는 이유
- 사실은 Maven + Ant + 스크립트 언어라고 보는 것이 맞다. 프로젝트 구성 뿐만아니라 AntBuilder와 그루비 스크립트를 통해 Ant의 역할, 배포 스크립트의 역할까지도 가능하다.
- Maven을 대체할 수 있는 프로젝트 구성/관리 및 Build tool.
- Maven을 넘어 Gradle로 가자 - Maven 대신 Gradle을 써야하는 이유
- 사실은 Maven + Ant + 스크립트 언어라고 보는 것이 맞다. 프로젝트 구성 뿐만아니라 AntBuilder와 그루비 스크립트를 통해 Ant의 역할, 배포 스크립트의 역할까지도 가능하다.
설치
- 설치 파일 압축을 풀고 설치 경로를
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"
- 설치 파일 압축을 풀고 설치 경로를
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 작업이 없으면 데몬이 자동 종료된다.
${HOME}/.gradle/gradle.properties
org.gradle.daemon=true
- 이제부터 Gradle이 데몬으로 떠서 실행되기 때문에 초기 로딩 시간이 줄어든다.
- 3시간동안 Gradle 작업이 없으면 데몬이 자동 종료된다.
스크립트에서 사용할 인증 정보 분리해두기
~/.gradle/gradle.properties
mavenUser=admin
mavenPassword=admin123
- 빌드 스크립트의 계정/비밀번호 필요한 부분
uploadArchives {
repositories {
mavenDeployer {
repository(url: "http://.../nexus/content/repositories/snapshots/") {
authentication(userName: mavenUser, password: mavenPassword)
}
}
}
}
~/.gradle/gradle.properties
mavenUser=admin mavenPassword=admin123
- 빌드 스크립트의 계정/비밀번호 필요한 부분
uploadArchives { repositories { mavenDeployer { repository(url: "http://.../nexus/content/repositories/snapshots/") { authentication(userName: mavenUser, password: mavenPassword) } } } }
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
- 프로젝트를 시작할 때
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
명령 실행과 옵션
- 기본적으로 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
- 기본적으로 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 태스크들
: 태스크를 실제 수행은 하지 않고, 해당 태스크와 함께 실행되는 모든 태스크 목록을 순서대로 보여준다.
projects
: 프로젝트 목록tasks
: 태스크 목록- 기본적으로 태스크 그룹에 속한 것만 보여준다.
dist { description = '태스크 설명' group = '태스크의 그룹' } // 혹은 dist.description = '태스크 설명' dist.group = '태스크의 그룹'
--all
: 태스크 그룹에 상관없이 다 보여줌
[자식프로젝트명:]dependencies
: Root 혹은 지정 프로젝트의 의존성 트리를 보여준다.–configuration runtime
: runtime 의존성만 보여준다.
[자식프로젝트명:]properties
: Root 혹은 지정 프로젝트의 속성 값들을 모두 보여준다.--profile
: 빌드 수행을 프로파일링하여 성능 정보를./build/reports/profile
디렉토리에 저장한다.-m 태스크들
: 태스크를 실제 수행은 하지 않고, 해당 태스크와 함께 실행되는 모든 태스크 목록을 순서대로 보여준다.
태스크 튜토리얼
- Gradle Task 심화
build.gradle
로 만든다.
- 의존성 :
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 Task 심화
build.gradle
로 만든다.
- 의존성 :
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 << { ... }
- 멀티 프로젝트에서 각 하위 프로젝트는 자신만의 기본 태스크를 선언할 수 있다. 기본 태스크가 없으면 부모의 기본 태스크를 수행한다.
Java 개발하기
다음 java 개발 관련 항목들을 읽어본다.
다음 java 개발 관련 항목들을 읽어본다.
DSL
- Groovy 빌드 파일은 기본적으로 Project 클래스의 인스턴스이다.
- 빌드 파일에 등장하는 변수나 메소드 중에서 미리 선언된 것이 아니라면 Project 클래스에서 해당 속성을 찾는다.
- Project 객체는
project
로도 접근 가능하다.
- 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 인스턴스
- 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}"
}
- 로컬 변수 :
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
옵션을 주면 강제 재컴파일 한다.
- 모든 빌드 스크립트는 컴파일하여
.gradle
에 저장하고 캐시된다. - 빌드 파일이 변경되면 그 때 재컴파일 한다.
--recompile-scripts
옵션을 주면 강제 재컴파일 한다.
이것 저것
디렉토리 생성
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
- 실행하면
> gradle -q -PcommandLineProjectProp=commandLineProjectPropValue -Dorg.gradle.project.systemProjectProp=systemPropertyValue printProps
commandLineProjectPropValue
gradlePropertiesValue
systemPropertyValue
envPropertyValue
systemValue
-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
- 실행하면
> gradle -q -PcommandLineProjectProp=commandLineProjectPropValue -Dorg.gradle.project.systemProjectProp=systemPropertyValue printProps commandLineProjectPropValue gradlePropertiesValue systemPropertyValue envPropertyValue systemValue
프로젝트 프라퍼티 검사
- 프로젝트 프라퍼티는 빌드 스크립트에서 프라퍼티 이름으로 바로 접근 가능하다. 하지만 프라퍼티가 존재하지 않으면 예외가 발생한다.
hasProperty('propertyName')
으로 프라퍼티의 존재 여부를 검사할 수 있다.
- 프로젝트 프라퍼티는 빌드 스크립트에서 프라퍼티 이름으로 바로 접근 가능하다. 하지만 프라퍼티가 존재하지 않으면 예외가 발생한다.
hasProperty('propertyName')
으로 프라퍼티의 존재 여부를 검사할 수 있다.
외부 빌드 스크립트로 프로젝트 구성하기
임의의 객체 구성하기
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
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
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
옵션으로 실행하면 캐시를 삭제하고 모두 다시 컴파일해서 캐시에 새로 저장한다.
Gradle은 컴파일한 스크립트를 캐싱한다. 프로젝트에서 처음으로 빌드를 실행하면 .gradle
디렉토리가 만들어지고 거기에 컴파일된 스크립트가 들어간다. 다음에 다시 빌드를 실행하면 스크립트에 변경이 없다면 컴파일 해뒀던 것을 실행한다. 그렇지 않으면 재 컴파일을 하고 캐시에 새로운 버전이 들어간다. --recompile-scripts
옵션으로 실행하면 캐시를 삭제하고 모두 다시 컴파일해서 캐시에 새로 저장한다.
자세히 살펴보기
Plugins
읽을꺼리
Gradle Task
- DSL API에서 Task types를 확장해서 사용할 수 있다.
- 빌드 파일에는 이미 수많은 gradle 관련 패키지가 기본 import 된 상태이다. 따라서 gradle 관련 클래스 사용시 import 할 필요가 없는 경우가 많다. Using Gradle without IDE support 참조.
선언
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
task 설정
- DSL API에서 Task types 생성하기
- 설정하기
// 단일 선언 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') }
- 설명(description) 추가 : 태스크에
description
프라퍼티를 설정하면gradle tasks
에서 볼 수 있게 된다.
태스크 건너 뛰기
onlyIf
// hello task에 대해 hello.onlyIf { !project.hasProperty('skipHello') } // 실행시 skipHello 프라퍼티 지정 gradle hello -PskipHello
task.enabled=true|false
이 값이 true여야만 해당 태스크가 실행된다.
이미 최신으로 갱신된 태스크 건너뛰기
- 자바 컴파일 태스크 같은 경우 이미 모든 최신 java 파일이 컴파일 돼 있다면 건너뛰는 기능이 있다. 이 같은 것을 구현하는 방법.
- 모든 태스크에는 TaskInputs
inputs
와 TaskOutputsoutputs
프라퍼티가 있다. 이 값을 설정해주면 자동으로 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
태스크의 실행 순서
- 태스크에
dependsOn [a, b]
형태로는 실행 순서를 지정할 수 없다. dependsOn 은 의존 대상을 명시할 뿐 의존 대상의 실행순서는 명시하지 않는다. - 새로운 방법(2015년 1월 현재 incubating) :
// 보통은 task1 -> task2 순서로 실행하지만 특정 상황에서는 이를 무시한다. task2.shouldRunAfter task1 // 무조건 task1 -> task2 순서를 지킨다. task2.mustRunAfter task1
tasks
- 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()
처럼 프로젝트에 상대적인 경로 우선이다.- 다양한 용법
// 일반 컬렉션 처럼 반복 가능 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" }
파일 압축
- 기본 용법
apply plugin: 'java' task zip(type: Zip) { from 'src/dist' into('libs') { // 압축 파일 안의 libs/ 디렉토리로 파일 넣음 from configurations.runtime } }
Gradle Ant 호출
- build.xml을 읽어서 태스크 생성, AntBuilder 이용, “ant clean compile”.execute() 형태의 호출 등이 가능하다.
- AntBuilder의 인스턴스
ant
를 사용하는 것이 보통이다. - AntBuilder 참조
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 태스크
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) 의존성 관리를 지원한다.
- 이행적 의존성이 아닌 일반 파일로 저장된 외부 라이브러리도 지원한다.
- 빌드 스크립트에서 직접 의존성을 지정한다.
의존성 설정 기본
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" } }
의존성 관리 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
경험상으로 볼 때 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 접근
구성하기
configurations
의 항목은 Configuration 객체이다.
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
를 이터레이션 돌면 된다. ResolvedArtifactresolvedArtifact.getModuleVersion().getId()
는 ModuleVersionIdentifier 객체이며 여기에 모듈에 관한 group, name, 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' }
의존성의 구성
- Gradle의 의존성은 여러가지 구성(configurations)을 가질 수 있다.
- 지정하지 않으면 기본 구성을 사용한다.
- Maven에는 기본 구성밖에 없다.
- Ivy에는 의존성에 여러 구성을 둘 수 있다.
dependencies { runtime group: 'org.somegroup', name: 'somedependency', version: '1.0', configuration: 'someConfiguration' }
- Gradle의 서브 프로젝트에 대한 의존성을 지정할 때는 다음과 같이한다.
dependencies { compile project(path: ':api', configuration: 'spi') }
의존성 보고서
- 명령행에서 의존성 확인하기 :
gradle -q dependencies 서브프로젝트:dependencies
- Gradle Report Plugin으로 리포트를 생성할 수도 있다.
- 의존성 확인은 API로 만들어져 있다.
모든 이행적 의존성에서 제외시키기
configurations { all*.exclude group: 'xml-apis', module: 'xmlParserAPIs' } // Equivalent to: configurations { all.collect { configuration -> configuration.exclude group: 'xml-apis', module: 'xmlParserAPIs' } }
의존성 사용하여 작업하기
- 다음과 같은 의존성 설정이 있다고 할 때
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" }
- shark-1.0 → seal-2.0, tuna-1.0
- orca-1.0 → seal-1.0
- tuna-1.0 → herring-1.0
- 각 구성의 의존성에 다음과 같이 접근할 수 있다.
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 } }
- allDependencies는 해당 구성의 부모에 있는 의존성까지 포함한 모든 의존성이다.
- dependencies는 해당 구성에만 속한(부모에 속한 것 제외) 의존성들이다.
- 구성에 속한 의존성의 모든 파일 객체는
files()
메소드로 접근할 수 있다.task allFiles << { configurations.sealife.files.each { file -> println file.name } }
Configuration.files
메소드는 해당 구성의 모든 artifact를 가져온다.
- 구성을 복사할 수 있다. 클로저로 복사될 대상을 지정할 수 있다.
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 } }
copy()
메소드는 해당 구성에 속한 의존성만 복사한다.copyRecursive()
메소드는 상속한 부모의 구성의 의존성까지 복사한다.
- 복사된 구성은 원본과 항상 같지는 않다. 복사 대상 서브셋의 의존성간에 버전 충돌이 있을 경우 최종 복사된 의존성과 원본 의존상간에 차이가 발생할 수 있다.
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
orca
는seal-1.0
에 의존하고 있는 반면,shark
는seal-2.0
에 의존하고 있는 상태.- 원본 구성은 버전 충돌이 있고, 기본 행동법칙에 따라 최신 버전인
seal-2.0
을 사용한다. 따라서 원본을 사용하는files()
메소드는orca
에 대한 이행적 의존성 판단 결과로seal-2.0
을 선택한다. - 복제된 구성에서는
orca
의 의존성만 복제했으며 따라서 버전 충돌이 없고seal-1.0
이 선택된다.
- 구성이 한번 결정되면 변경할 수 없다. 이 상태를 변경하려고 하면 예외가 발생한다.
- 변경하고자 할경우 구성 복사를 사용한다. 복사된 구성은 결정되기 전 상태이며 따라서 변경 후 결정이 가능하다.
- 더 자세히 알고자 한다면 Configuration 참조.
리포지토리(Repositories) 자세히
- Gradle의 리포지토리 관리는 Apache Ivy에 기반하고 있다.
- Gradle은 여러 종류의 리포지토리를 지원하며 각각 독립적으로 다뤄진다.
- Gradle은 특정 리포지토리에서 모듈 기술자를 만나면 동일 리포지토리에서 해당 모듈의 artifact를 다운로드한다.
- 모듈의 메타 정보와 artifact는 동일 리포지토리의 동일 위치에 있어야 하지만, 여러 URL을 가진 단일 리포지토리를 만드는 것은 가능하다.
Maven 중앙 리포지토리
- Maven 중앙(central) 리포지토리를 탐색하도록 지정돼 있다.
repositories { mavenCentral() }
로컬 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]" } }
리포지토리 다루기
- 리포지토리 구성하기
repositories { flatDir { name 'localRepository' } } repositories { localRepository { dirs 'lib' } } repositories.localRepository { dirs 'lib' }
의존성 결정은 어떻게 이뤄지는가?
Gradle은 의존성과 선언과 리포지토리 정의를 읽어서 의존성 결정 과정(dependency resolution)을 통해 모든 의존하는 파일을 다운로드한다. 그 과정은 다음과 같다.
- 지정된 의존성에 대해 Gradle은 처음에 의존성의 모듈을 결정한다. 각 저장소를 순서대로 탐색하여 처음에는 모듈 기술자 파일(pom 혹은 ivy)을 찾는다. 파일이 존재하면 모듈이 존재한다는 의미이다. 모듈 기술자 파일이 없다면 모듈의 아티팩트 파일(jar)이 존재하는지 찾을 것이다.
- 의존성이 동적 버전(
1.+
형태)으로 지정돼 있다면, Gradle이 가장 최신의 정적 버전을 리토지토리에서 찾아서 정한다. Maven에서는maven-metadata.xml
파일로, Ivy에서는 디렉토리의 파일 목록을 확인해서 결정한다. - 모듈 기술자가
pom
파일이고 부모 pom이 선언돼 있다면, Gradle은 해당 pom의 부모까지 재귀적으로 탐색하여 정하게 된다.
- 일단 모든 리포지토리에서 모듈을 다 탐색했으면 Gradle은 그 중에서 가장 사용에 적합한 것을 선택한다. 이 작업은 다음과 같은 조건에 따라 이뤄진다.
- 동적 버전에서는 낮은 버전보다는 더 높은 버전이 선택된다.
- 모듈 기술자 파일(pom/ivy)가 있는 모듈이 artifact 파일만 있는 모듈보다 우선 선택된다.
- 상단에 선언된 리포지토리에 있는 모듈이 하단에 선언된 리포지토리에 있는 것보다 우선 선택된다.
- 의존성이 정적 버전으로 선언 돼 있고 모듈 기술자 파일이 리포지토리에 존재할 경우에는 그 이후의 리포지토리로는 탐색을 하지 않는다.
- 모듈의 모든 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 방식을 추천.
- Spring Source에서 propdeps plugin을 공개한 상태라서 아래와 같은 방식은 불필요해졌다.
- Nebula Gradle extra configurations plugin도 optional, provided 지원.
컴파일시에는 클래스패스에 넣지만, 실행/배포시에는 빠져야 하는 의존성이 있을 수 있다. 예를 들면 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
- 특정 의존성에 관해 상세 정보 출력
./gradlew dependencyInsight --configuration testCompile --dependency junit
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.'
- 실행하면
> 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)라는 것으로 만들어진다. 이 기술자를 변경할 수 있다.
ProjectDescriptor를 참조한다.
초기화
Gradle이 단일 혹은 멀티 프로젝트 빌드를 실행할지 판단하는 기준이 있다. 설정 파일이 있는 디렉토리에서 멀티 프로젝트를 빌드를 실행하면 판단이 쉽다. 하지만 서브 프로젝트 아무데서나 빌드를 실행할 수도 있다. settings.gradle
이 없는 곳에서 빌드를 실행하면
- 현재 디렉토리와 동일한 계층 단계의
master
라는 디렉토리에서settings.gradle
을 찾는다. - 없으면, 부모 디렉토리에서
settings.gradle
을 찾는다. - 없으면, 단일 프로젝트로 빌드를 실행한다.
settings.gradle
가 존재하면 현재 프로젝트가 멀티 프로젝트 계층에 속하는지 판단한다. 아니라면 단일 프로젝트로 실행하고 맞다면 멀티 프로젝트로 빌드를 실행한다.
이런 식으로 작동하는 이유는 멀티 프로젝트 일 경우 모든 멀티프로젝트 빌드 구성을 생성해야하기 때문이다. -u
명령행 옵션을 주면 부모 디렉토리에서 설정파일 찾는 것을 막는다. 이 경우에는 항상 단일 프로젝트로 실행한다. settings.gradle
파일이 있는 곳에서 -u
는 아무 기능도 없다.
설정 파일 자동 탐색은 물리적으로 계층/단층 레이아웃인 멀티 프로젝트에서만 작동한다. 단층 레이아웃에서는 위에서 기술한 명명규칙을 지켜야 한다. Gradle은 멀티 프로젝트에 임의의 물리적 레이아웃을 지원하지만 이 때는 항상 설정 파일이 있는 곳에서 빌드를 실행해야 한다.
Gradle은 빌드에 참여하는 모든 프로젝트에 대해 Project 객체를 생성한다. 각 프로젝트는 기본적으로 탑레벨 디렉토리를 name
으로 갖는다. 최상위를 제외한 모든 프로젝트는 부모 프로젝트가 있고, 자식 프로젝트를 가질 수 있다.
빌드 스크립트 라이프사이클에 반응하기
라이프싸이클을 진행하는 동안 빌드 스크립트에서 알림을 받을 수 있다. 알림은 특별한 리스너 인터페이스를 구현하거나 혹은 알림이 발생했을 때 실행할 클로저를 제공해 주는 두가지 방식으로 구현한다.
프로젝트 평가
프로젝트를 평가하기 직전과 직후에 알림을 받을 수 있다. 빌드 스크립트에서 모든 정의가 적용된 이후에 추가적인 구성을 수행할 때나 로깅 혹은 프로파일링을 하고자 할 때 사용한다.
projectA.gradle
hasTests = true
- 실행하면
> gradle -q test Adding test task to project ':projectA' Running tests for project ':projectA'
Project.afterEvaluate()를 사용하여 프로젝트 평가 뒤에 실행할 클로저를 추가 하였다.
아무 프로젝트든 평가한 뒤에 알림을 받는 것도 가능하다. afterProject
는 프로젝트 평가의 성공 여부와 무관하게 호출된다.
- 실행하면
> 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
는 부모 프로젝트까지 포함한 것이다.
공통 행위 정의
- 실행하면
> 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." }
- 실행하면
> 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
tropicalFish/build.gradle
ext.arctic = false
- 실행하면
> 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의 교차 프로젝트 구성 때문에 모든 프로젝트는 어떠한 태스크를 실행할 때는 그 전에 먼저 평가 돼야 한다.
- 최상위 프로젝트에서 실행하면
> 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
- 기본 로그 레벨
- ERROR Error messages
- QUIET Important information messages
- WARNING Warning messages
- LIFECYCLE Progress information messages
- INFO Information messages
- 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
시스템 프라퍼티
- 빌드 스크립트에서
System.setProperty('키','값')
- 혹은 gradle.properties에서
systemProp.프라퍼티이름=프라퍼티값
- 현재
gradle.properties
에 지정된 시스템 프라퍼티는-D
옵션으로 덮어쓰기가 안되는 문제가 있다. [GRADLE-2122] Can props from gradle.properties be overwritten with -D parameter ? - Gradle Issues
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이 자동으로 컴파일하여 빌드 스크립트 클래스패스에 추가한다.
프라퍼티와 메소드 상속
부모 프로젝트에 정의된 모든 메소드와 프라퍼티는 서브 프로젝트에서도 접근 가능하다. 이를 통해 공통 구성을 정의하고 빌드 로직을 메소드로 만들어서 서브 프로젝트에서 사용하도록 하면 된다.
- 실행하면
> 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 Plugin, Gradle Groovy Plugin
좀 더 복잡한 것이 필요하다면 자신만의 build.gradle
을 만들면 된다. Gradle은 기본 빌드 스크립트를 1.6 버전에서는 기본 설정도 함께 넣어야 하게 바뀐 것 같다.build.gradle
이 있건 없건 무조건 적용한다. 즉, 빌드 스크립트에는 기본값 외에 다른 것만 정의하면 된다.
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-script
buildSrc/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()
를 행위를 지정한 클로저를 인자로 줘서 호출해도 된다.
- 실행하면
> gradle -q hello hello from GreetingTask
태스크에 프라퍼티를 추가하여 커스터마이징을 할 수 있게 하자. 태스크는 POGO이므로 태스크 객체에 프라퍼티를 지정하거나 메소드를 호출 할 수 있다.
- 실행하면
> 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) }
플러그인에 의존성 주입
@Inject
애노테이션을 사용한다.- MavenPlugin.java에서
@Inject
사용예를 볼 수 있다.
Gradle Initialization Scripts - 초기화 스크립트
기본 사용법
초기화 스크립트는 Gradle의 다른 스크립트와 유사하다. 하지만 빌드가 시작되기 전에 실행된다.
- 시스템 전역 설정. 예를들면 커스텀 플러그인 찾을 위치 지정.
- 현재 환경에 따른 프라퍼티 설정. 개발 장비, 지속적 통합 장비 등에 따른 프라퍼티 값 바꿔지기 같은 경우.
- 빌드에 필요한 개발자의 개인적 설정 정보.
- 장비 전용 설정. JDK 위치 등.
- 빌드 리스너(build listener)를 등록하여 Gradle 빌드 이벤트를 받는다.
- 빌드 로거를 등록한다.
buildSrc
프로젝트의 클래스에는 접근할 수 없다.
초기화 스크립트 실행
-I
혹은--init-script
뒤에 스크립트 경로를 줘서 실행. 명령행 인자를 여러번 사용하면 여러 초기화 스크립트가 등록된다.$USER_HOME/.gradle/init.gradle
$USER_HOME/.gradle/init.d/*.gradle
$GRADLE_HOME/init.d/*.gradle
여러개의 초기화 스크립트가 발견되면 그 모두를 위에 나온 순서대로 다 실행한다. 동일 디렉토리의 스크립트는 알파벳 순서로 실행된다.
초기화 스크립트 작성하기
단순 groovy 스크립트이다. 모든 초기화 스크립트는 Gradle 인스턴스를 받는다. 모든 프라퍼티 참조와 메소드 호출은 Gradle 인스턴스로 위임된다.
프로젝트 구성하기
초기화 스크립트에서 프로젝트 구성을 할 수도 있다. 이는 멀티 프로젝트 빌드에서 구성하는 것과 유사하다. 이는 프로젝트 자체가 평가되기 전에 실행된다.
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
- Gradle을 각 개발자나 CI 서버에 깔지 않고, 프로젝트에 함께 포함시켜 배포할 수 있는 방법을 제공해준다.
명령행을 통한 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에서 의존성 문자열 모두 뽑아내기
- 실행한 뒤에 맨 끝에 쉼표는 잘 조정해서 쓸 것
> 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
을 지원하고 있으며, 유사 기능을 흉내낼 수 있다.
이행적 의존성으로 인한 라이브러리 버전 변경 대비
현재 프로젝트에서 spring-data-jpa를 사용하는데, 이 모듈이 Spring Core/Beans 등에 대해 가변 버전으로 의존하고 있다. 이에 따라 현재 프로젝트의 공식 Spring 버전은 3.1인데, 이행성과 가변 버전 변경에 따라 Core/Beans 등이 3.2.0.M2로 지정되는 현상이 발생하였다.
Gradle Dependencies를 참고하여 의존성 버전 관리 전략에 failOnVersionConflict()
을 넣어 주고 항상 명시하는 것이 좋아보인다.
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의 경우에도 아무 설정도 할 필요없다. 클래스패스에만 있으면 자동으로 수행된다.
projectA의 단위테스트가 projectB의 단위테스트에 의존
projectA의 단위테스트가 projectB의 단위테스트에 의존하는 경우가 있다. 원칙적으로는 이러면 안된다. 근본적으로 projectA와 B간의 의존관계는 메인 자바 소스에 대한 것이어야지 테스트에 대한 것이면 안된다. 하지만 어쨌든 이런 상태로 컴파일과 실행이 가능하게 만들 수는 있다.
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 에 의존하도록 변경한다.
'IT_Programming > Dev Tools' 카테고리의 다른 글
Eclipse Kepler Android NDK 경로 설정하기! (0) | 2015.05.04 |
---|---|
[펌_안드로이드 스튜디오] 어플리케이션 서명(signing) 및 배포, 그리고 debug용 keystore 만들기 (0) | 2015.04.14 |
[Gradle] Gradle을 이용한 자동화 빌드 (0) | 2015.04.10 |
안드로이드 스튜디오 단축키 (0) | 2015.04.09 |
안드로이드 스튜디오에서 Git 시작하기 (0) | 2015.04.06 |