목차

Gradle Dependencies

의존성 소개

의존성 설정 기본

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 리포지토리를 지원한다.

의존성 관리 Best Practices

jar 이름에 버전 붙이기

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

버전 충돌

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

ResolutionStrategy

ResolutionStrategy

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

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

Java와 의존성 관리

의존성 구성(dependency configurations)

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

configuration 선언

configurations {
    compile
}

configuration 접근

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

구성하기

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

의존성 설정하기

외부 모듈 의존성

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

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
    }
}

다중 artifact 모듈에 의존한다면

해당 Artifact만 가져오기 지정

dependencies {
    runtime "org.groovy:groovy:1.8.7@jar"
    runtime group: 'org.groovy', name: 'groovy', version: '1.8.7', ext: 'jar'
}

분류자 Classifier

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 다루기

클라이언트 모듈 의존성

프로젝트 의존성

파일 의존성

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'
    }
}

선택적 속성들

의존성의 구성

의존성 보고서

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

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

의존성 사용하여 작업하기

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

리포지토리(Repositories) 자세히

Maven 중앙 리포지토리

로컬 Maven 리포지토리

Maven 사용자 정의 리포지토리

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

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

단일 디렉토리 리포지토리

Ivy 리포지토리

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

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

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

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

리포지토리 다루기

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

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

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

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

의존성 캐시

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

캐시 관련 명령행 옵션

오프라인

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

캐시 갱신

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

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

의존성 캐시 세부 설정

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

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

provided

sourceSets {
    test.compileClasspath += configurations.compileOnly
    test.runtimeClasspath += configurations.compileOnly
}

// 혹은
configurations {
    testImplementation.extendsFrom compileOnly
}

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

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

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

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

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

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

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

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

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

dependencyInsight

CompositeBuild

Source Dependency

참조할 DSL

Dependency Management Plugin

https://github.com/spring-gradle-plugins/dependency-management-plugin