사용자 도구

사이트 도구


gradle:multiproject

문서의 이전 판입니다!


Gradle Multi Project

멀티 프로젝트의 기본

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

서브 프로젝트간 의존성

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

교차 프로젝트 구성 Cross Project Configuration

공통 행위 정의하기

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

서브 프로젝트 구성

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

공통 행위 정의

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

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

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

프로젝트 필터링

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

이름으로 필터링

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

프라퍼티로 필터링하기

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

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

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

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

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

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

프로젝트와 태스크의 경로

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

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

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

실행 의존성

의존성과 실행 순서

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

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

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

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

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

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

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

실전 예제

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

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

프로젝트 lib 의존성

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

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

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

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

분리된(decoupled) 프로젝트

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

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

  • Java 플러그인의 build 태스크를 사용하여 컴파일, 테스트, 코드 스타일 검사(CodeQuality 플러그인 사용시)등을 할 수 있다.
  • 다중 프로젝트에서 여러 범위의 프로젝트에 대해 빌드를 할 경우가 있는데 이 때 buildNeededbuildDependents 태스크를 사용한다.
  • “프로젝트 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/multiproject.1373337677.txt.gz · 마지막으로 수정됨: 2013/07/09 11:41 저자 kwon37xi