원문 - Gradle Kotlin DSL Primer


목차

  • 전제조건(Prerequisites)
  • IDE 지원(IDE support)
  • 코틀린 DSL 스크립트(Kotlin DSL scripts)
  • 타입 안전 모델 접근자(Type-safe model accessors)
  • 멀티 프로젝트 빌드(Multi-project builds)
  • plugins {} 블록을 사용할 수 없는 경우(When you can’t use the plugins {} block)
  • 컨테이너 객체 작업(Working with container objects)
  • 런타임 프로퍼티스 작업(Working with runtime properties)
  • 코틀린 DSL 플러그인(The Kotlin DSL Plugin)
  • 임베디드 코틀린(The embedded Kotlin)
  • 상호 호환성(Interoperability)
  • 제한사항(Limitations)

그레이들 코틀린 DSL 입문서(Gradle Kotlin DSL Primer)

그레이들의 코틀린 DSL은 우수한 콘텐츠 지원, 리팩토링, 문서화 등 지원되는 IDE의 향상된 편집 환경을 통해 기존 그루비 DSL를 대체할 문법을 제공한다. 이 장에서는 주요 코틀린 DSL 구성에 대한 세부정보와 이를 사용하여 그레이들 API와 상호작용하는 방법을 제공한다.

기존 그레이들 빌드를 코틀린 DSL로 마이그레이션하는 데 관심이 있다면 마이그레이션 장도 확인해보자.

전제조건(Prerequisites)

  • 내장된 코틀린 컴파일러는 x86-64 아키텍처의 리눅스, 맥OS, 윈도우, Cygwin, FreeBSD 및 솔라리스에서 작동한다.
  • 코틀린 문법과 기본 언어 기능에 대한 지식은 도움이 된다. 코틀린 레퍼런스 문서코틀린 Koans는 기초를 배우는 데 도움이 된다.
  • plugins {} 블록을 사용하여 그레이들 플러그인을 선언하면 편집 환경이 크게 향상되므로 적극 권장한다.

IDE 지원(IDE support)

테이블 1. IDE 지원 사항

 Build import문법 강조(Syntax highlighting) 1Semantic editor 2
IntelliJ IDEA
Android Studio
Eclipse IDE
CLion
Apache NetBeans
Visual Studio Code (LSP)
Visual Studio

1 그레이들 코틀린 DSL 스크립트에서 코틀린 문법 강조 표시 2 그레이들 코틀린 DSL 스크립트의 코드 완성, 소스 탐색, 문서, 리팩토링 등…​

제한 사항에서 언급했듯이, 인텔리J IDEA에서 코틀린 DSL 스크립트용 콘텐츠 지원 및 리팩토링 툴을 얻으려면 그레이들 모델에서 프로젝트를 가져와야 한다.

구성 시간이 느린 빌드는 IDE 응답성에 영향을 미칠 수 있으므로, 이러한 문제를 해결하려면 성능(performance) 장을 확인해보자.

빌드 자동으로 가져오기 및 스크립트 의존성 자동으로 재로드(Automatic build import vs. automatic reloading of script dependencies)

인텔리J IDEA와 인텔리J IDEA에서 파생된 안드로이드 스튜디오 모두 빌드 로직이 변경되는 시점을 감지하고 두 가지를 제공한다.

  1. 전체 빌드를 다시 가져온다.
  2. 빌드 스크립트 편집 시 스크립트 의존성 재로드

빌드 자동으로 가져오기를 비활성화(disable automatic build import)하고, 스크립트 의존성의 자동 재로드를 활성화하는 것(enable automatic reloading of script dependencies)이 좋다. 이렇게 하면 그레이들 스크립트를 편집하는 동안 초기 피드백을 받고 전체 빌드 설정이 IDE와 동기화되는 시점을 제어할 수 있다.

문제 해결(Troubleshooting)

IDE 지원은 두 가지 컴포넌트로 제공된다.

  • 인텔리J IDEA/안드로이드 스튜디오에서 사용되는 코틀린 플러그인
  • 그레이들

지원 레벨은 각 버전에 따라 다르다.

문제가 발생하면 가장 먼저 시도해야 할 일은 명령줄에서 ./gradlew tasks를 실행하여 문제가 IDE에 국한되어 있는지 확인하는 것이다.

명령줄에서 동일한 문제가 발생하면 IDE가 아니라 빌드에 문제가 있는 것이다.

위의 방법이 작동하지 않고 코틀린 DSL 스크립트 편집기에 문제가 있다고 의심되는 경우 다음을 수행할 수 있다.

  • 자세한 내용을 보기위해 ./gradle tasks를 실행해보자.
  • 다음 위치 중 하나의 로그를 확인해보자.
    • 맥OS X에서 $HOME/Library/Logs/gradle-kotlin-dsl
    • 리눅스에서 $HOME/.gradle-kotlin-dsl/log
    • 윈도우에서 $HOME/AppData/Local/gradle-kotlin-dsl/log
  • 가능한 한 많은 세부정보를 포함하여 보기 위해 그레이들 이슈 트래커에서 문제를 연다.

버전 5.1부터는 로그 디렉토리가 자동으로 정리된다. 주기적으로(최대 24시간마다) 확인하여 7일 동안 사용하지 않은 로그 파일은 삭제한다.

위의 방법으로 문제를 정확히 찾아낼 수 없다면 IDE에서 org.gradle.kotlin.dsl.logging.tapi 시스템 프로퍼티를 활성화할 수 있다. 그러면 그레이들 데몬(Daemon)이 $HOME/.gradle/daemon에 있는 로그 파일에 추가 정보를 기록한다. 인텔리J IDEA에서는 Help > Edit Custom VM Options…을 열고 -Dorg.gradle.kotlin.dsl.logging.tapi=true를 추가하여 이 작업을 수행할 수 있다.

코틀린 DSL 스크립트 편집기 외부의 IDE 문제인 경우 해당 IDE 이슈 트래커에서 문제를 확인해보자.

  • 젯브레인 IDE 이슈 트래커,
  • 구글 안드로이드 스튜디오 이슈 트래커

마지막으로, 그레이들 자체 또는 코틀린 DSL에 문제가 있는 경우 그레이들 이슈 트래커에서 문제를 확인해보자.

코틀린 DSL 스크립트(Kotlin DSL scripts)

그루비 기반과 마찬가지로 코틀린 DSL은 그레이들의 자바 API 위에서 구현된다. 코틀린 DSL 스크립트에서 읽을 수 있는 모든 것은 그레이들에서 컴파일하고 실행하는 코틀린 코드다. 빌드 스크립트에서 사용하는 객체, 기능 및 프로퍼티스 중 상당수는 그레이들 API 및 적용된 플러그인의 API에서 가져온다.

그루비 DSL 스크립트 파일은 .gradle 파일명 확장자를 사용한다.

코틀린 DSL 스크립트 파일은 .gradle.kts 파일명 확장자를 사용한다.

코틀린 DSL을 활성화하려면 .gradle 대신 빌드 스크립트에 .gradle.kts 확장자를 사용하면 된다. 이는 세팅 파일(예: settings.gradle.kts) 및 초기화 스크립트에도 적용된다.

그루비 DSL 빌드 스크립트와 코틀린 DSL 빌드 스크립트를 혼합할 수 있다. 즉, 코틀린 DSL 빌드 스크립트는 그루비 DSL 빌드 스크립트를 적용할 수 있고 멀티 프로젝트 빌드의 각 프로젝트는 둘 중 하나를 사용할 수 있다.

더 나은 IDE 지원을 얻으려면 다음 컨벤션을 적용하는 것이 좋다.

  • *.settings.gradle.kts 패턴에 따라 세팅 스크립트(또는 그레이들 Settings 객체가 지원하는 모든 스크립트)의 이름을 지정한다. 여기에는 세팅 스크립트에서 적용되는 스크립트 플러그인이 포함된다.
  • *.init.gradle.kts 또는 간단히 init.gradle.kts 패턴에 따라 초기화 스크립트명을 지정한다.

이는 IDE가 Project, Settings 또는 Gradle 등 어떤 타입의 객체가 스크립트를 “지원”하는지 알 수 있도록 하기 위한 것이다.

암시적 임포트(Implicit imports)

모든 코틀린 DSL 빌드 스크립트에는 다음 사항으로 구성된 암시적 임포트가 있다.


내부 코틀린 DSL API 사용 방지

플러그인 및 빌드 스크립트에서 내부 코틀린 DSL API를 사용하면 그레이들 또는 플러그인이 변경될 때 빌드가 중단될 가능성이 있다. 코틀린 DSL API는 하위 패키지가 아닌 org.gradle.kotlin.dsl 또는 org.gradle.kotlin.dsl.plugins.dsl 패키지에 있는 해당 API 문서에 나열된 타입그레이들 공개 API를 상속한다.


타입 안전 모델 접근자(Type-safe model accessors)

그루비 DSL을 사용하면 빌드 모델의 많은 요소가 런타임에 정의된 경우에도 이름으로 참조할 수 있다. 명명된 구성(named configurations), 명명된 소스 세트(named source sets) 등을 생각해 보자. 예를 들어, configuration.implementation을 통해 구현 구성을 얻을 수 있다.

코틀린 DSL은 이러한 동적 리솔루션(resolution)를 플러그인이 제공하는 모델 요소와 작동하는 타입 안전 모델 접근자로 대체한다.

타입 안전 모델 접근자를 사용할 수 있는 시기 이해(Understanding when type-safe model accessors are available)

코틀린 DSL은 현재 플러그인에서 제공하는 다음 항목에 대해 타입 안전 모델 접근자를 지원한다.

  • 의존성 및 아티팩트 구성(예: 자바 플러그인에서 제공하는 implementationruntimeOnly)
  • 프로젝트 익스텐션(Extensions) 및 컨벤션(예: sourceSet)
  • dependenciesrepositories 컨테이너 대한 익스텐션
  • tasksconfigurations 컨테이너의 요소
  • 프로젝트 익스텐션 컨테이너의 요소(예: sourceSets 컨테이너에 추가된 자바 플러그인에서 제공한 소스 세트)
  • 위의 각 항목에 대한 익스텐션

메인(main) 프로젝트 빌드 스크립트와 사전 컴파일된 프로젝트 스크립트 플러그인에만 타입 안전 모델 접근자가 있다. 초기화 스크립트, 세팅 스크립트, 스크립트 플러그인은 그렇지 않다. 이러한 제한사항은 향후 그레이들 릴리스에서 제거될 예정이다.

사용 가능한 타입 안전 모델 접근자 세트는 스크립트를 평가하기(evaluating) 직전, plugins {} 블록 직후에 연산된다. 해당 시점 이후의 모든 모델 요소는 타입 안전 모델 접근자와 작동하지 않는다. 예를 들어 여기에는 자체 빌드 스크립트에서 정의할 수 있는 모든 구성이 포함된다. 그러나 이 접근 방식은 상위 프로젝트에서 적용된 플러그인이 제공하는 모든 모델 요소에 대해 타입 안전 접근자를 사용할 수 있음을 의미한다.

다음 프로젝트 빌드 스크립트는 타입 안전 접근자를 사용하여 다양한 구성, 익스텐션 및 기타 요소에 접근하는 방법을 보여준다.

예제 1. 타입 안전 모델을 사용하여 접근

build.gradle.kts

plugins {
    `java-library`
}

dependencies { //.......................................... 1.
    api("junit:junit:4.13")
    implementation("junit:junit:4.13")
    testImplementation("junit:junit:4.13")
}

configurations { //........................................ 1.
    implementation {
        resolutionStrategy.failOnVersionConflict()
    }
}

sourceSets { //............................................. 2.
    main { //............................................... 3. 
        java.srcDir("src/core/java")
    }
}

java { //................................................... 4.
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
}

tasks { //.................................................. 5.
    test {                                  
        testLogging.showExceptions = true
    }
}
  1. 자바 라이브러리 플러그인에서 제공하는 api, implementationtestImplementation 의존성 구성에 대해 타입 안전 접근자를 사용한다.
  2. 접근자를 사용하여 sourceSets 프로젝트 익스텐션(extension)을 구성한다.
  3. 접근자를 사용하여 메인(main) 소스 세트(source set)를 구성한다.
  4. 접근자를 사용하여 메인(main) 소스 세트에 대한 자바 소스를 구성한다.
  5. 접근자를 사용하여 test 태스크를 구성한다.

IDE는 타입 안전 접근자를 알고 있으므로 이를 제안사항에 포함한다. 이는 빌드 스크립트의 최상위 레벨(대부분의 플러그인 익스텐션이 프로젝트(Project) 객체에 추가됨)과 익스텐션(extension)을 구성하는 블록(block) 내에서 모두 발생한다.

구성(configurations), 태스크(tasks), 소스세트(sourceSet) 등 컨테이너 요소에 대한 접근자는 그레이들의 구성 회피 API를 활용한다. 예를 들어 태스크에서는 TaskProvider<T> 타입이며 메인(main) 태스크에 대한 지연 참조(lazy reference) 및 지연 구성(lazy configuration)을 제공한다. 다음은 구성 회피(configuration avoidance)가 적용되는 상황을 보여주는 몇 가지 예제다.

build.gradle.kts

tasks.test {
    // 지연 구성
}

// 지연 참조
val testProvider: TaskProvider<Test> = tasks.test

testProvider {
    // 지연 구성
}

// 즉시 실현된 테스트 태스크, 지연 컨텍스트에서 수행된 경우 구성 회피를 무효화한다.
val test: Test = tasks.test.get()

태스크를 제외한 다른 모든 컨테이너의 경우 요소에 대한 접근자는 NamedDomainObjectProvider<T> 타입이며 동일한 동작을 제공한다.

타입 안전 모델 접근자를 사용할 수 없는 경우 수행할 작업 이해(Understanding what to do when type-safe model accessors are not available)

타입 안전 접근자의 사용을 보여주는 위의 샘플 빌드 스크립트를 생각해보자. 다음 샘플은 플러그인을 적용하기 위해 apply() 메서드를 사용한다는 점을 제외하면 완전히 동일하다. 이 경우 빌드 스크립트는 타입 안전 접근자를 사용할 수 없다. 왜냐하면 apply() 호출이 빌드 스크립트 본문에서 발생하기 때문이다. 대신 여기에 설명된 대로 다른 기법을 사용해야 한다.

예제 2. 타입 안전 접근자 없이 플러그인 구성

build.gradle.kts

apply(plugin = "java-library")

dependencies {
    "api"("junit:junit:4.13")
    "implementation"("junit:junit:4.13")
    "testImplementation"("junit:junit:4.13")
}

configurations {
    "implementation" {
        resolutionStrategy.failOnVersionConflict()
    }
}

configure<SourceSetContainer> {
    named("main") {
        java.srcDir("src/core/java")
    }
}

configure<JavaPluginConvention> {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
}

tasks {
    named<Test>("test") {
        testLogging.showExceptions = true
    }
}

다음 모델 요소에는 타입 안전 접근자를 사용할 수 없다.

  • apply(plugin = "id") 메소드를 통해 적용된 플러그인
  • 프로젝트 빌드 스크립트
  • apply(from = "script-plugin.gradle.kts")를 통한 스크립트 플러그인
  • 프로젝트 간 구성을 통해 적용되는 플러그인

또한 코틀린에 구현된 바이너리 그레이들 플러그인에서는 타입 안전 접근자를 사용할 수 없다.

타입 안전 접근자를 찾을 수 없다면, 해당 타입에 대해 일반 API를 사용하자. 그렇게 하려면 구성된 모델 요소명 및/또는 타입을 알아야 한다. 이제 위의 스크립트를 자세히 살펴보면서 이를 찾는 방법을 보여 준다.

아티팩트 구성(Artifact configurations)

다음 샘플은 타입 접근자 없이 아티팩트 구성을 참조하고 구성하는 방법을 보여준다.

예제 3. 아티팩트 구성

build.gradle.kts

apply(plugin = "java-library")

dependencies {
    "api"("junit:junit:4.13")
    "implementation"("junit:junit:4.13")
    "testImplementation"("junit:junit:4.13")
}

configurations {
    "implementation" {
        resolutionStrategy.failOnVersionConflict()
    }
}

이 경우 구성 이름이 문자열 리터럴이라는 점을 제외하면 코드는 타입 안전 접근자의 코드와 유사한다. 의존성 선언 및 configurations {} 블록 내에서 구성명에 문자열 리터럴을 사용할 수 있다.

IDE는 이 상황에서 사용 가능한 구성을 찾는 데 도움을 줄 수 없지만, 해당 플러그인의 문서에서 찾거나 gradle dependencies을 실행하여 찾을 수 있다.

프로젝트 익스텐션 및 컨벤션(Project extensions and conventions)

프로젝트 익스텐션 및 컨벤션에는 이름과 고유 타입이 모두 있지만 코틀린 DSL은 이를 구성하기 위해 타입만 알면 된다. 다음 샘플은 원본 예시 빌드 스크립트의 sourceSets {}java {} 블록에 대해 보여주듯이 해당 타입과 함께 configure<T>() 함수를 사용하여 이를 수행할 수 있다.

예제 4. 프로젝트 익스텐션 및 컨벤션

build.gradle.kts

apply(plugin = "java-library")

configure<SourceSetContainer> {
    named("main") {
        java.srcDir("src/core/java")
    }
}

configure<JavaPluginConvention> {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
}

sourceSetsSourceSetContainer 타입의 프로젝트에 대한 그레이들 익스텐션(extension)이고 javaJavaPluginExtension 타입의 프로젝트에 대한 익스텐션이다.

적용된 플러그인에 대한 문서를 보거나 적용된 모든 플러그인이 제공하는 모델 요소 접근시 필요한 코틀린 코드를 표시하는 gradle kotlinDslAccessorsReport를 실행하여, 사용 가능한 익스텐션 및 컨벤션을 확인할 수 있다. 보고서는 이름과 타입을 모두 제공한다. 최후의 수단으로 플러그인의 소스 코드를 확인할 수도 있지만 대부분의 경우 그럴 필요 없다.

익스텐션이나 컨벤션을 구성하지 않고 참조만 필요하거나 다음과 같이 한 줄 구성을 수행하려는 경우에도 <T>() 함수를 사용할 수 있다.

the<SourceSetContainer>()["main"].srcDir("src/core/java")

위의 코드 조각(snippet)은 컨테이너인 프로젝트 익스텐션의 요소를 구성하는 한 가지 방법을 보여준다.

프로젝트 익스텐션 컨테이너의 요소(Elements in project-extension containers)

SourceSetContainer와 같은 컨테이너 기반 프로젝트 익스텐션을 사용하면 해당 익스텐션이 보유한 요소를 구성할 수도 있다. 샘플 빌드 스크립트에서는 소스 세트 컨테이너 내에 main이라는 소스 세트를 구성하려고 한다. 이는 다음과 같이 접근자 대신 named() 메서드를 사용하여 수행할 수 있다.

예제 5. 컨테이너인 프로젝트 익스텐션의 요소

build.gradle.kts

apply(plugin = "java-library")

configure<SourceSetContainer> {
    named("main") {
        java.srcDir("src/core/java")
    }
}

컨테이너 기반 프로젝트 익스텐션(extension) 내의 모든 요소에는 이름이 있으므로 모든 경우에 이 기술을 사용할 수 있다.

프로젝트 익스텐션 및 컨벤션 자체의 경우 적용된 플러그인의 문서를 보거나, gradle kotlinDslAccessorsReport를 실행하여 컨테이너에 어떤 요소가 있는지 확인할 수 있다. 최후의 수단으로 플러그인의 소스 코드를 보고 기능을 확인할 수 있지만 대부분의 경우에는 그럴 필요가 없다.

태스크(Tasks)

태스크는 컨테이너 기반 프로젝트 익스텐션을 통해 관리되지 않지만 유사한 방식으로 작동하는 컨테이너의 일부다. 즉, 이 예제에서 볼 수 있듯이 소스 세트와 동일한 방식으로 태스크을 구성할 수 있다.

예제 6. 태스크

build.gradle.kts

apply(plugin = "java-library")

tasks {
    named<Test>("test") {
        testLogging.showExceptions = true
    }
}

우리는 접근자를 사용하는 대신 그레이들 API를 사용하여 이름과 타입으로 태스크를 참조하고 있다. 태스크 타입을 명시적으로 지정해야 한다. 그렇지 않으면 유추된 타입이 Test가 아닌 Task이고, testLogging 프로퍼티가 Test 태스크 타입에 특정하기 때문에 스크립트가 컴파일되지 않는다. 그러나 프로퍼티를 구성하거나 모든 태스크에 공통적인 메서드를 호출해야 하는 경우(예: Task 인터페이스에 선언된 메서드)만 필요한 경우 타입을 생략할 수 있다.

gradle tasks를 실행하면 어떤 태스크을 사용할 수 있는지 확인할 수 있다. 그런 다음 여기에 설명된 대로 gradle help --task <taskName>을 실행하여 특정 태스크의 타입을 확인할 수 있다.

❯ ./gradlew help --task test
...
Type
     Test (org.gradle.api.tasks.testing.Test)

IDE는 임포트를 지원하므로 패키지명 없이 타입의 간단한 이름만 필요하다. 이 경우 Test 태스크 타입은 그레이들 API의 일부이므로 암시적으로 가져오기 때문에 임포트(import)가 필요없다.

컨벤션(About conventions)

그레이들 핵심 플러그인 중 일부는 컨벤션(convention) 객체의 도움으로 구성한다. 이는 익스텐션(extensions) 기능과 유사한 목적으로 사용되며 현재는 익스텐션 기능으로 대체됐다. 새 플러그인을 작성할 때 컨벤션 객체를 사용하지 말자. 장기 계획은 모든 그레이들 코어 플러그인을 마이그레이션하여 익스텐션 기능을 사용하고 컨벤션 객체를 완전히 제거하는 것이다.

위에서 본 것처럼, 코틀린 DSL은 Project의 컨벤션 객체에 대해서만 접근자를 제공한다. 다른 타입의 컨벤션 객체를 사용하는 그레이들 플러그인과 상호작용해야 하는 상황이 있다. 코틀린 DSL은 이를 수행하기 위해 withConvention(T::class) {} 익스텐션 함수를 제공한다.

예제 7. 소스 세트 컨벤션 구성

build.gradle.kts

plugins {
    groovy
}

sourceSets {
    main {
        withConvention(GroovySourceSet::class) {
            groovy.srcDir("src/core/groovy")
        }
    }
}

이 기술은 자바 플러그인 이외의 언어 플러그인에 의해 추가되는 소스 세트에 필요하다. 그루비 플러그인과 스칼라 플러그인. SourceSet 레퍼런스 문서에서 어떤 플러그인이 소스 세트에 어떤 프로퍼티스를 추가하는지 확인할 수 있다.

멀티 프로젝트 빌드(Multi-project builds)

싱글 프로젝트 빌드와 마찬가지로 멀티 프로젝트 빌드에서도 plugins {} 블록을 사용하여 타입 안전 접근자를 사용할 수 있도록 해야 한다. 멀티 프로젝트 빌드에서 또 다른 고려 사항은 루트 빌드 스크립트 내에서 하위 프로젝트를 구성하거나 프로젝트 간 다른 형태의 교차 구성을 사용할 때 타입 안전 접근자를 사용할 수 없다는 것이다. 다음 장에서 두 주제에 대해 더 자세히 논의해보자.

플러그인 적용(Applying plugins)

플러그인이 적용되는 하위 프로젝트 내에서 플러그인을 선언할 수 있지만, 루트 프로젝트 빌드 스크립트 내에서도 선언하는 것이 좋다. 이를 통해 빌드 내 프로젝트 전체에서 플러그인 버전을 일관되게 유지하는 것이 더 쉬워진다. 이 접근 방식은 빌드 성능도 향상시킨다.

그레이들 플러그인 사용(Using Gradle Plugins) 장에서는 버전과 함께 루트 프로젝트 빌드 스크립트에서 플러그인을 선언한 다음 이를 적절한 하위 프로젝트의 빌드 스크립트에 적용하는 방법을 설명한다. 다음은 세 개의 하위 프로젝트와 세 개의 플러그인을 사용하는 이 접근 방식의 예제다. 자바 라이브러리 플러그인이 사용 중인 그레이들 버전에 연결되어 있으므로 루트 빌드 스크립트가 커뮤니티 플러그인만 선언하는 방법에 유의하자.

예제 8. plugins {} 블록을 사용하여 루트 빌드 스크립트에서 플러그인 의존성을 선언한다.

settings.gradle.kts

rootProject.name = "multi-project-build"
include("domain", "infra", "http")

build.gradle.kts

plugins {
    id("com.github.johnrengelman.shadow") version "4.0.1" apply false
    id("io.ratpack.ratpack-java") version "1.8.2" apply false
}

domain/build.gradle.kts

plugins {
    `java-library`
}

dependencies {
    api("javax.measure:unit-api:1.0")
    implementation("tec.units:unit-ri:1.0.3")
}

infra/build.gradle.kts

plugins {
    `java-library`
    id("com.github.johnrengelman.shadow")
}

shadow {
    applicationDistribution.from("src/dist")
}

tasks.shadowJar {
    minimize()
}

http/build.gradle.kts

plugins {
    java
    id("io.ratpack.ratpack-java")
}

dependencies {
    implementation(project(":domain"))
    implementation(project(":infra"))
    implementation(ratpack.dependency("dropwizard-metrics"))
}

application {
    mainClass.set("example.App")
}

ratpack.baseDir = file("src/ratpack/baseDir")

빌드에 그레이들 플러그인 포털 외 추가 플러그인 리포지터리가 필요한 경우 다음과 같이 settings.gradle.kts 파일의 pluginManagement {} 블록에서 이를 선언해야 한다.

예제 9. 추가 플러그인 리포지터리 선언

settings.gradle.kts

pluginManagement {
    repositories {
        mavenCentral()
        gradlePluginPortal()
    }
}

그레이들 플러그인 포털이 아닌 다른 소스에서 가져온 플러그인은 플러그인 마커 아티팩트와 함께 게시된 경우에만 plugins {} 블록에 선언할 수 있다.

이 글을 쓰는 시점에서 google() 리포지터리에 있는 최대 3.2.0까지 그레이들용 안드로이드 플러그인의 모든 버전에는 플러그인 마커 아티팩트가 부족하다.

해당 아티팩트가 누락된 경우 plugins {} 블록을 사용할 수 없다. 대신 루트 프로젝트 빌드 스크립트에서 buildscript {} 블록을 사용하여 플러그인 의존성을 선언해야 한다. 다음은 안드로이드 플러그인에 대해 이를 수행하는 예제다.

settings.gradle.kts

include("lib", "app")

build.gradle.kts

buildscript {
    repositories {
        google()
        gradlePluginPortal()
    }
    dependencies {
        classpath("com.android.tools.build:gradle:4.1.2")
    }
}

lib/build.gradle.kts

plugins {
    id("com.android.library")
}

android {
    // ...
}

app/build.gradle.kts

plugins {
    id("com.android.application")
}

android {
    // ...
}

이 기술은 안드로이드 스튜디오가 새 빌드를 생성할 때 사용하는 기술과 크게 다르지 않다. 주요 차이점은 위 샘플의 하위 프로젝트 빌드 스크립트가 plugins {} 블록을 사용하여 해당 플러그인을 선언한다는 것이다. 이는 기여하는 모델 요소에 대해 타입 안전 접근자를 사용할 수 있음을 의미한다.

해당 플러그인을 멀티 프로젝트 빌드의 루트 프로젝트 빌드 스크립트(하위 프로젝트에만 적용하는 것이 아니라) 또는 단일 프로젝트 빌드에 적용하려는 경우 이 기술을 사용할 수 없다. 그러한 경우 다른 접근 방식을 사용해야 하는데, 다른 절에서 자세히 설명한다.

교차 구성 프로젝트(Cross-configuring projects)

교차 프로젝트 구성은 다른 프로젝트의 빌드 스크립트에서 프로젝트를 구성할 수 있는 메커니즘이다. 일반적인 예제는 루트 프로젝트 빌드 스크립트에서 하위 프로젝트를 구성하는 경우다.

이 접근 방식을 취한다는 것은 플러그인이 제공하는 모델 요소에 대해 타입 안전 접근자를 사용할 수 없다는 것을 의미한다. 대신 문자열 리터럴과 표준 그레이들 API를 사용해야 합니다.

예를 들어, 루트 프로젝트 빌드 스크립트에서 하위 프로젝트를 완전히 구성하도록 Java/Ratpack 샘플 빌드를 수정해 보자.

예제 11. 교차 구성 프로젝트

settings.gradle.kts

rootProject.name = "multi-project-build"
include("domain", "infra", "http")

build.gradle.kts

import com.github.jengelman.gradle.plugins.shadow.ShadowExtension
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import ratpack.gradle.RatpackExtension

plugins {
    id("com.github.johnrengelman.shadow") version "4.0.1" apply false
    id("io.ratpack.ratpack-java") version "1.8.2" apply false
}

project(":domain") {
    apply(plugin = "java-library")
    dependencies {
        "api"("javax.measure:unit-api:1.0")
        "implementation"("tec.units:unit-ri:1.0.3")
    }
}

project(":infra") {
    apply(plugin = "java-library")
    apply(plugin = "com.github.johnrengelman.shadow")

    configure<ShadowExtension> {
        applicationDistribution.from("src/dist")
    }
    tasks.named<ShadowJar>("shadowJar") {
        minimize()
    }
}

project(":http") {
    apply(plugin = "java")
    apply(plugin = "io.ratpack.ratpack-java")

    repositories { mavenCentral() }
    val ratpack = the<RatpackExtension>()
    dependencies {
        "implementation"(project(":domain"))
        "implementation"(project(":infra"))
        "implementation"(ratpack.dependency("dropwizard-metrics"))
        "runtimeOnly"("org.slf4j:slf4j-simple:1.7.25")
    }
    configure<JavaApplication> {
        mainClass.set("example.App")
    }
    ratpack.baseDir = file("src/ratpack/baseDir")
}

plugins {} 블록이 이 컨텍스트에서는 작동하지 않으므로 apply() 메서드를 사용하여 플러그인을 적용하는 방법에 유의하자. 또한 태스크, 익스텐션 및 컨벤션을 구성하기 위해 타입 안전 접근자 대신 표준 API를 사용하고 있다. 이 접근 방식은 다른 곳에서 자세히 논의했었다.

plugins {} 블록을 사용할 수 없는 경우(When you can’t use the plugins {} block)

그레이들 플러그인 포털이 아닌 소스에서 가져온 플러그인은 plugins {} 블록과 함께 사용할 수도 있고 사용하지 못할 수도 있다. 게시 방법, 특히 필요한 플러그인 마커 아티팩트와 함께 게시되었는지 여부에 따라 다르다.

예를 들어, 그레이들용 안드로이드 플러그인은 그레이들 플러그인 포털에 게시되지 않으며(적어도 플러그인 버전 3.2.0까지는) 특정 플러그인 식별자에 대한 아티팩트를 해결하는 데 필요한 메타데이터가 구글 리포지터리에 게시되지 않았다.

빌드가 멀티 프로젝트이고 이러한 플러그인을 루트 프로젝트에 적용할 필요가 없는 경우 위에 설명된 기술을 사용하여 이 문제를 해결할 수 있다.

플러그인을 게시할 때(publishing) 그레이들에 내장된 그레이들 플러그인 개발 플러그인을 사용하자. plugins {} 블록과 함께 플러그인을 사용할 수 있도록 만드는 데 필요한 메타데이터 게시(publication)를 자동화한다.

이 장에서는 단일 프로젝트 빌드 또는 멀티 프로젝트 빌드의 루트 프로젝트에 안드로이드 플러그인을 적용하는 방법을 보여준다. 목표는 com.android.application 플러그인 식별자(identifier)를 확인 가능한 아티팩트에 매핑하는 방법에 대해 빌드를 지시하는 것이다. 이 작업은 두 단계로 수행된다.

  • 빌드 세팅 스크립트에 플러그인 리포지터리 추가
  • 플러그인 ID를 해당 아티팩트 좌표에 매핑

빌드 세팅 스크립트에서 pluginManagement {} 블록을 구성하여 두 단계를 모두 수행한다. 시연하기 위해, 다음 샘플에서는 안드로이드 플러그인이 게시된 google() 리포지터리를 검색 목록에 추가하고, ResolutionStrategy {} 블록을 사용하여 com.android.application 플러그인 ID를 com.android.tools에 매핑한다. com.android.tools.build:gradle:<version> google() 리포지터리에서 사용 가능한 아티팩트:

시연하기 위해 다음 샘플에서는 안드로이드 플러그인이 게시된 google() 리포지터리를 검색 목록에 추가하고, ResolutionStrategy {} 블록을 사용하여 com.android.application 플러그인 ID를 사용하여 com.android.tools.build:gradle:<version> 아티팩트를 google() 리포지터리에서 찾을 수 있다.

예제 12. 의존성 좌표에 플러그인 ID 매핑

settings.gradle.kts

pluginManagement {
    repositories {
        google()
        gradlePluginPortal()
    }
    resolutionStrategy {
        eachPlugin {
            if(requested.id.namespace == "com.android") {
                useModule("com.android.tools.build:gradle:${requested.version}")
            }
        }
    }
}

build.gradle.kts

plugins {
    id("com.android.application") version "4.1.2"
}

android {
    // ...
}

실제로, 위 샘플은 지정된 모듈에서 제공되는 모든 com.android.* 플러그인에 대해 작동한다. 이는 패키지 모듈에 커스텀 플러그인 작성(Developing Custom Gradle Plugins)장에 설명된 프로퍼티스 파일 메커니즘을 사용하여 어떤 플러그인 ID가 어떤 플러그인 구현 클래스에 매핑되는지에 대한 세부 정보가 포함되어 있기 때문이다.

PluginManagement {} 블록과 그 용도에 대한 자세한 내용은 그레이들 사용자 매뉴얼의 플러그인 관리 장을 참고하자.

컨테이너 객체 작업(Working with container objects)

그레이들 빌드 모델은 컨테이너 객체(또는 간단히 “컨테이너”)를 많이 사용한다. 예를 들어 configurationstasks는 모두 각각 ConfigurationTask 객체를 포함하는 컨테이너 객체다. 커뮤니티 플러그인은 안드로이드 플러그인에서 제공하는 android.buildTypes 컨테이너와 같은 컨테이너에도 제공된다.

코틀린 DSL은 빌드 작성자(build authors)가 컨테이너와 상호작용할 수 있는 여러 가지 방법을 제공한다. 다음에는 tasks 컨테이너를 예로 사용하여 이러한 각 방법을 살펴보자.

지원되는 컨테이너에서 기존 요소를 구성하는 경우 다른 절에서 설명된 타입 안전 접근자를 활용할 수 있다. 해당 절에서는 타입 안전 접근자를 지원하는 컨테이너에 대해서도 설명한다.

컨테이너 API 사용(Using the container API)

그레이들의 모든 컨테이너는 NamedDomainObjectContainer<DomainObjectType>을 구현한다. 그 중 일부는 다양한 타입의 객체를 포함하고 PolymorphicDomainObjectContainer<BaseType>을 구현할 수 있다. 컨테이너와 상호 작용하는 가장 간단한 방법은 이러한 인터페이스를 사용하는 것이다.

다음 샘플은 named() 메서드를 사용하여 기존 작업을 구성하고, register() 메서드를 사용하여 새 태스크를 생성하는 방법을 보여준다.

예제 13. 컨테이너 API 사용

build.gradle.kts

tasks.named("check") //.......................................... 1.
tasks.register("myTask1") //..................................... 2.

tasks.named<JavaCompile>("compileJava") //........................3.
tasks.register<Copy>("myCopy1") //................................4.

tasks.named("assemble") { //......................................5.
    dependsOn(":myTask1")
}
tasks.register("myTask2") { //....................................6.
    description = "Some meaningful words"
}

tasks.named<Test>("test") { //....................................7.
    testLogging.showStackTraces = true
}
tasks.register<Copy>("myCopy2") { //..............................8.
    from("source")
    into("destination")
}
  1. check라는 기존 태스크에 대한 Task 타입의 레퍼런스를 가져온다.
  2. myTask1이라는 타입이 지정되지 않은 새 태스크를 등록한다.
  3. 자바컴파일(JavaCompile) 타입의 compileJava라는 기존 태스크에 대한 레퍼런스를 가져온다.
  4. Copy 타입의 myCopy1이라는 새 태스크를 등록한다.
  5. assemble이라는 기존(타입화되지 않은) 태스크에 대한 레퍼런스를 가져오고 구성한다. 이 구문을 사용하면 Task에서 사용할 수 있는 프로퍼티스와 메서드만 구성할 수 있다.
  6. myTask2라는 타입화되지 않은 새 태스크를 등록하고 구성한다. 이 경우 Task에서 사용할 수 있는 프로퍼티스와 메서드만 구성할 수 있다.
  7. Test 유형의 test라는 기존 작업에 대한 레퍼런스를 가져오고 구성한다. 이 경우 지정된 타입의 프로퍼티스 및 메서드에 접근할 수 있다.
  8. Copy 타입의 myCopy2라는 새 태스크를 등록하고 구성한다.

위 샘플은 구성 회피(configuration avoidance) API를 사용한다. 컨테이너 요소를 즉각적(eagerly)으로 구성하거나 등록해야 하거나 원하는 경우 간단히 named()getByName()으로 바꾸고, register()create()로 바꾸면 된다.

코틀린 델리게이트 프로퍼티스 사용(Using Kotlin delegated properties)

컨테이너와 상호작용하는 또 다른 방법은 코틀린 델리게이트 프로퍼티스를 사용하는 것이다. 이는 빌드의 다른 곳에서 사용할 수 있는 컨테이너 요소에 대한 레퍼런스가 필요한 경우 특히 유용하다. 또한 코틀린 델리게이트 프로퍼티스는 IDE 리팩터링을 통해 쉽게 이름을 바꿀 수 있다.

다음 샘플은 이전 절의 샘플과 동일한 태스크를 수행하지만 델리게이트 프로퍼티스를 사용하고 문자열 리터럴 태스크 패스 대신 해당 레퍼런스를 재사용한다.

예제 14. 코틀린 델리게이트 프로퍼티스 사용

build.gradle.kts

val check by tasks.existing
val myTask1 by tasks.registering

val compileJava by tasks.existing(JavaCompile::class)
val myCopy1 by tasks.registering(Copy::class)

val assemble by tasks.existing {
    dependsOn(myTask1) //..............................1.
}
val myTask2 by tasks.registering {
    description = "Some meaningful words"
}

val test by tasks.existing(Test::class) {
    testLogging.showStackTraces = true
}
val myCopy2 by tasks.registering(Copy::class) {
    from("source")
    into("destination")
}
  1. 태스크 패스 대신 myTask1 태스크에 대한 레퍼런스를 사용한다.

위의 내용은 구성 회피 API에 의존한다. 컨테이너 요소를 적극적으로 구성하거나 등록해야 하는 경우 existing()getting()로 바꾸고 registering()creating()으로 바꾸면 된다.

여러 컨테이너 요소를 함께 구성(Configuring multiple container elements together)

컨테이너의 여러 요소를 구성할 때 각 상호 작용에서 컨테이너명이 반복되는 것을 피하기 위해 상호 작용을 블록으로 그룹화할 수 있다. 다음 예에서는 타입 안전 접근자, 컨테이너 API 및 코틀린 델리게이트 프로퍼티스의 조합을 사용한다.

예제 15. 컨테이너 스코프(scope)

build.gradle.kts

tasks {
    test {
        testLogging.showStackTraces = true
    }

    val myCheck by registering {
        doLast { /* 어떤 의미있는 것들 */ }
    }

    check {
        dependsOn(myCheck)
    }

    register("myHelp") {
        doLast { /* 도움이 될만한 것들 */ }
    }
}

런타임 프로퍼티스 작업(Working with runtime properties)

그레이들에는 런타임에 정의되는 두 가지 주요 프로퍼티스 소스인 project 프로퍼티스extra 프로퍼티스가 있다. 코틀린 DSL은 이러한 타입의 프로퍼티스를 사용하기 위한 특정 구문을 제공하며, 이에 대해서는 아래에서 살펴본다.

프로젝트 프로퍼티스(Project properties)

코틀린 DSL을 사용하면 코틀린 델리게이트 프로퍼티스를 통해 바인딩하여 프로젝트 프로퍼티스에 접근할 수 있다. 다음은 몇 가지 프로젝트 프로퍼티스에 대한 기술을 보여주는 샘플 조각이다. 적어도 하나는 정의되어야 한다.

build.gradle.kts

val myProperty: String by project //......................... .1
val myNullableProperty: String? by project //................ .2
  1. myProperty 델리게이트 프로퍼티를 통해 myProperty 프로젝트 프로퍼티를 사용할 수 있도록 한다. 이 경우 프로젝트 프로퍼티가 있어야 한다. 그렇지 않으면 빌드 스크립트가 myProperty 값을 사용하려고 할 때 빌드가 실패합니다.
  2. myNullableProperty 프로젝트 프로퍼티에도 동일한 작업을 수행하지만, null을 확인하므로 myNullableProperty 값을 사용해도 빌드가 실패하지 않는다(null 안전을 위한 표준 코틀린 규칙 적용).

각 프로젝트 대신 각 세팅스 및 그레이들 사용을 제외하고는 세팅 및 초기화 스크립트 모두에서 동일한 접근 방식이 작동한다.

엑스트라 프로퍼티스(Extra properties)

익스텐션어웨어(ExtensionAware) 인터페이스를 구현하는 모든 객체에서 엑스트라 프로퍼티스를 사용할 수 있다. 코틀린 DSL을 사용하면 다음 샘플에 설명된 by extra 양식을 사용하여 엑스트라 프로퍼티스에 접근하고 델리게이트 프로퍼티스을 통해 새 프로퍼티스를 생성할 수 있다.

build.gradle.kts

val myNewProperty by extra("initial value") //............................ .1
val myOtherNewProperty by extra { "calculated initial value" } //......... .2

val myProperty: String by extra //........................................ .3
val myNullableProperty: String? by extra //............................... .4
  1. 현재 컨텍스트(이 경우 프로젝트)에서 myNewProperty라는 새 엑스트라 프로퍼티스를 생성하고 "initial value" 값으로 초기화한다. 이 값은 프로퍼티 타입도 결정한다.
  2. 제공된 람다에 의해 초기 값이 계산되는 새로운 엑스트라 프로퍼티를 만든다.
  3. 현재 컨텍스트(이 경우 프로젝트)의 기존 엑스트라 프로퍼티스를 myProperty 레퍼런스에 바인딩한다.
  4. 이전 줄과 동일하지만 프로퍼티가 null 값을 가질 수 있도록 허용한다.

이 접근 방식은 프로젝트 빌드 스크립트, 스크립트 플러그인, 세팅 스크립트, 초기화 스크립트 등 모든 그레이들 스크립트에 적용된다.

다음 구문을 사용하여 하위 프로젝트에서 루트 프로젝트의 엑스트라 프로퍼티스에 접근할 수도 있다.

my-sub-project/build.gradle.kts

val myNewProperty: String by rootProject.extra //...... 1.
  1. 루트 프로젝트의 myNewProperty 엑스트라 프로퍼티를 동일 이름의 레퍼런스에 연결한다.

엑스트라 프로퍼티는 프로젝트에만 국한되지 않는다. 예를 들어 TaskExtensionAware를 확장하므로, 태스크에 엑스트라 프로퍼티스를 연결할 수도 있다. 다음은 test 태스크에 새로운 myNewTaskProperty를 정의한 다음 해당 프로퍼티를 사용하여 다른 태스크를 초기화하는 예제다.

build.gradle.kts

tasks {
    test {
        val reportType by extra("dev") //............................. 1.
        doLast {
            // 리포트의 사후 처리에 'suffix'를 사용하자.
        }
    }

    register<Zip>("archiveTestReports") {
        val reportType: String by test.get().extra //................. 2.
        archiveAppendix.set(reportType)
        from(test.get().reports.html.destination)
    }
}
  1. test 태스크에 대한 새로운 ReportType 엑스트라 프로퍼티를 생성한다.
  2. archiveTestReports 태스크를 구성하는 데 사용할 수 있는 test 태스크의 reportType 엑스트라 프로퍼티를 만든다.

구성 회피(configuration avoidance) API 대신 즉시 구성(eager configuration)을 사용하고 싶다면 다음과 같이 리포트 티입에 대해 단일 “global” 프로퍼티를 사용할 수 있다.

build.gradle.kts

tasks.test.doLast { ... }

val testReportType by tasks.test.get().extra("dev") //.................... 1.

tasks.create<Zip>("archiveTestReports") {
    archiveAppendix.set(testReportType) //................................ 2.
    from(test.get().reports.html.destination)
}
  1. test 태스크에 대한 엑스트라 프로퍼티를 생성 및 초기화하여 “전역(global)” 프로퍼티에 연결한다.
  2. “전역” 프로퍼티를 사용하여 archiveTestReports 태스크를 초기화한다.

우리가 다루어야 할 엑스트라 프로퍼티스에 대한 마지막 구문이 하나 있는데, 이는 엑스트라를 맵으로 처리하는 것이다. 코틀린 타입 검사의 이점을 잃게 되고 IDE가 최대한 많은 지원을 제공하지 못하게 되므로 일반적으로 이 방법을 사용하지 않는 것이 좋다. 그러나 이는 델리게이트 프로퍼티 구문보다 더 간결하며 나중에 참조하지 않고 엑스트라 프로퍼티 값을 설정해야 하는 경우에만 사용하는 것이 합리적이다.

다음은 맵 구문을 사용하여 엑스트라 프로퍼티를 설정하고 읽는 방법을 보여주는 간단한 예제다.

build.gradle.kts

extra["myNewProperty"] = "initial value" //.......................................... 1.

tasks.create("myTask") {
    doLast {
        println("Property: ${project.extra["myNewProperty"]}") //.................... 2.
    }
}
  1. myNewProperty라는 새 프로젝트 엑스트라 프로퍼티를 만들고 해당 값을 설정한다.
  2. 우리가 만든 프로젝트 엑스트라 프로퍼티에서 값을 읽는다. extra[…]에 대한 한정자(qualifier), 그레이들은 태스크에서 엑스트라 프로퍼티를 읽는다고 가정한다.

코틀린 DSL 플러그인(The Kotlin DSL Plugin)

코틀린 DSL 플러그인은 빌드 로직에 기여하는 코틀린 기반 프로젝트를 개발하는 편리한 방법을 제공한다. 여기에는 buildSrc 프로젝트, 임포트 빌드그레이들 플러그인이 있다.

플러그인은 다음을 수행하여 이를 달성한다.

  • 코틀린 소스 파일 컴파일에 대한 지원을 추가하는 코틀린 플러그인을 적용한다.
  • compileOnlytestImplementation 구성에 kotlin-stdlib-jdk8, kotlin-reflectgradleKotlinDsl() 의존성을 추가한다. 이를 통해 코틀린 코드에서 코틀린 라이브러리와 그레이들 API를 사용할 수 있다.
  • 코틀린 DSL 스크립트에 사용되는 것과 동일한 설정으로 코틀린 컴파일러를 구성하여 빌드 로직과 해당 스크립트 간의 일관성을 보장한다.
  • 미리 컴파일된 스크립트 플러그인에 대한 지원을 활성화한다

kotlin-dsl 플러그인 버전을 지정하지 말자.

각 그레이들 릴리스는 특정 버전의 kotlin-dsl 플러그인과 함께 사용하도록 되어 있으며 임의 그레이들 릴리스와 kotlin-dsl 플러그인 버전 간의 호환성은 보장되지 않는다. 빌드에서 예상치 못한 버전의 kotlin-dsl 플러그인을 사용하면 경고가 표시되고 문제를 진단하기 어려울 수 있다. ***

플러그인을 사용하기 위해 필요한 기본 구성이다.

예제 16. buildSrc 프로젝트에 코틀린 DSL 플러그인 적용

buildSrc/build.gradle.kts

plugins { 
    plugins {
        `kotlin-dsl`
    }

    repositories {
        // org.jetbrains.kotlin.jvm 플러그인에는 리포지터리가 필요하다.
        // 코틀린 컴파일러 의존성을 다운로드할 수 있는 위치다.
        mavenCentral()
    }
}

임베디드 코틀린(The embedded Kotlin)

그레이들은 코틀린 기반 스크립트를 지원하기 위해 임베디드 코틀린을 사용한다.

코틀린 버전(Kotlin versions)

그레이들은 kotlin-compiler-embeddable과 일치하는 버전의 kotlin-stdlibkotlin-reflect 라이브러리와 함께 제공한다. 자세한 내용은 그레이들 호환성 매트릭스의 코틀린 절을 참고하자. 해당 모듈의 kotlin 패키지는 그레이들 클래스패스를 통해 표시된다.

코틀린에서 제공하는 호환성 보장은 이전 버전과 이후 버전 모두에 적용된다.

하위 호환성(Backward compatibility)

우리의 접근 방식은 메이저(major) 그레이들 릴리스에서만 코틀린 업그레이드를 수행하는 것이다. 우리는 항상 출시되는 코틀린 버전을 명확하게 문서화하고 주요 릴리스 이전에 업그레이드 계획을 발표할 것이다.

이전 그레이들 버전과의 호환성을 유지하려는 플러그인 작성자는 API 사용을 이전 버전과 호환되는 하위 집합으로 제한해야 한다. 그레이들의 다른 새로운 API와 크게 다르지 않다. 예: 의존성 해결을 위한 새로운 API를 도입하고 플러그인이 해당 API를 사용하려는 경우 이전 그레이들 버전에 대한 지원을 중단하거나 최신 버전에서만 새 코드를 실행하도록 코드를 영리하게 구성해야 한다.

상위 호환성(Forward compatibility)

가장 큰 문제는 외부 kotlin-gradle-plugin 버전과 그레이들과 함께 제공되는 kotlin-stdlib 버전 간의 호환성이다. 더 일반적으로는 kotlin-stdlib에 전이적으로 의존하는 플러그인과 그레이들과 함께 제공되는 해당 버전 사이입니다. 조합이 호환되는 한 모든 것이 작동한다. 언어가 성숙해짐에 따라 이는 문제가 안될 것이다.

코틀린 컴파일러 아규먼트(Kotlin compiler arguments)

다음은 kotlin-dsl 플러그인이 적용된 프로젝트에서 코틀린 DSL 스크립트와 코틀린 소스 및 스크립트를 컴파일하는 데 사용되는 코틀린 컴파일러 아규먼트다.

-jvm-target=1.8

  • 생성된 JVM 바이트코드의 대상 버전을 1.8로 설정한다.

-Xjsr305=strict

  • null 안전성 향상을 위해 JSR-305 어노테이션을 엄격하게 따르도록 코틀린의 자바 상호 호환성을 설정한다. 자세한 내용은 코틀린 문서의 코틀린에서 자바 코드 호출을 참고하자.

상호 호환성(Interoperability)

빌드 로직에서 언어를 혼합하는 경우 언어 경계를 넘어야 할 수도 있다. 극단적인 예는 자바, 그루비, 코틀린으로 구현된 태스크와 플러그인을 사용하는 동시에 코틀린 DSL과 그루비 DSL 빌드 스크립트를 모두 사용하는 빌드다.

코틀린 레퍼런스 문서 인용.

코틀린은 자바 상호 호환성을 염두에 두고 설계했다. 기존 자바 코드를 코틀린에서 자연스럽게 호출할 수 있고, 자바에서도 코틀린 코드를 원활하게 사용할 수 있다.

코틀린에서 자바를 호출하는 것자바에서 코틀린을 호출하는 것은 모두 코틀린 레퍼런스 문서에 잘 설명되어 있다.

그루비 코드와의 상호 호환성에도 대부분 동일하게 적용된다. 또한 코틀린 DSL은 그루비 의미 체계를 선택할 수 있는 여러 가지 방법을 제공한다. 이에 대해서는 다음 내용에서 설명한다.

정적 익스텐션(Static extensions)

그루비와 코틀린 언어 모두 그루비 익스텐션 모듈코틀린 익스텐션을 통해 기존 클래스 익스텐션을 지원한다.

그루비에서 코틀린 익스텐션 함수를 호출하려면 이를 정적 함수로 호출하고 리시버(receiver)를 첫 번째 파라미터로 전달한다.

예제 17. 그루비에서 코틀린 익스텐션 호출

build.gradle

TheTargetTypeKt.kotlinExtensionFunction(receiver, "parameters", 42, aReference)

코틀린 익스텐션 함수는 패키지 레벨 함수이며 코틀린 레퍼런스 문서의 패키지 레벨 함수 장에서 특정 코틀린 익스텐션을 선언하는 타입명을 찾는 방법을 알아볼 수 있다.

코틀린에서 그루비 익스텐션 메서드를 호출하려면 동일한 접근 방식이 적용된다. 즉, 리시버(receiver)를 첫 번째 파라미터로 전달하는 정적 함수로 호출한다. 예는 다음과 같다.

예제 18. 코틀린에서 그루비 익스텐션 호출

build.gradle.kts

TheTargetTypeGroovyExtension.groovyExtensionMethod(receiver, "parameters", 42, aReference)

네임드 파라미터 및 기본 아규먼트(Named parameters and default arguments)

그루비와 코틀린 언어는 모두 네임드 함수 파라미터와 기본 아규먼트를 지원하지만 구현 방식은 매우 다르다. 코틀린은 코틀린 언어 레퍼런스의 네임드 아규먼트기본 아규먼트에 설명된 대로 두 가지 모두를 완벽하게 지원한다. 그루비는 Map<String, ?> 파라미터를 기반으로 타입이 안전하지 않은 방식으로 네임드 아규먼트를 구현한다. 즉, 기본 아규먼트와 결합할 수 없다. 즉, 특정 방법에 대해 그루비에서는 둘 중 하나만 사용할 수 있다.

그루비에서 코틀린 호출(Calling Kotlin from Groovy)

그루비에서 이름이 지정된 인수가 있는 코틀린 함수를 호출하려면 위치 파라미터가 포함된 일반 메서드 호출을 사용하면 된다. 아규먼트명으로 값을 제공할 수 있는 방법은 없다.

그루비의 기본 아규먼트가 있는 코틀린 함수를 호출하려면 항상 모든 함수 파라미터의 값을 전달해야 한다.

코틀린에서 그루비 호출(Calling Groovy from Kotlin)

코틀린에서 네임드 아규먼트를 사용하여 그부리 함수를 호출하려면 다음 예제와 같이 Map<String, ?>를 전달해야 한다.

예제 19. 코틀린의 네임드 아규먼트를 사용하여 그루비 함수 호출

build.gradle.kts

groovyNamedArgumentTakingMethod(
    mapOf(
        "parameterName" to "value",
        "other" to 42,
        "and" to aReference
    )
)

코틀린의 기본 아규먼트를 사용하여 그루비 함수를 호출하려면 항상 모든 파라미터의 값을 전달해야한다.

코틀린의 그루비 클로저(Groovy closures from Kotlin)

코틀린 코드에서 클로저 아규먼트를 가져오는 그루비 메서드를 호출해야 하는 경우도 있다. 예를 들어 그루비로 작성된 일부 서드파티 플러그인에는 클로저 아규먼트가 필요하다.

모든 언어로 작성된 그레이들 플러그인은 클로저 대신 Action<T> 타입을 선호한다. 그루비 클로저와 코틀린 람다는 해당 타입의 아규먼트에 자동으로 매핑된다.

코틀린의 강력한 타입 지정을 유지하면서 클로저를 구성하는 방법을 제공하기 위해 두 가지 헬퍼 메서드가 있다.

  • closureOf<T> {}
  • delegateClosureOf<T> {}

두 메소드 모두 다양한 상황에서 유용하며, 클로저 인스턴스를 전달하는 메소드에 따라 다르다.

일부 플러그인은 Bintray 플러그인과 같이 간단한 클로저를 기대한다.

예제 20. closureOf<T> {} 사용

build.gradle.kts

bintray {
    pkg(closureOf<PackageConfig> {
        // 패키지 구성은 여기에서
    })
}

팜을 구성할 때, Gretty 플러그인과 같은 다른 경우에는 플러그인에서 델리게이트 클로저를 예상한다.

예제 21. delegateClosureOf {} 사용

build.gradle.kts

dependencies {
    implementation("group:artifact:1.2.3") {
        artifact(delegateClosureOf<DependencyArtifact> {
            // 아티팩트 구성
            name = "artifact-name"
        })
    }
}

소스 코드를 살펴보는 것만으로는 어떤 버전을 사용할지 알 수 있는 좋은 방법이 없는 경우가 있다. 일반적으로 closureOf<T> {}와 함께 NullPointerException이 발생하는 경우 DelegateClosureOf<T> {}를 사용하면 문제가 해결된다.

이 두 유틸리티 함수는 구성 클로저에 유용하지만, 일부 플러그인에서는 다른 목적으로 그루비 클로저를 기대할 수도 있다. KotlinClosure0부터 KotlinClosure2까지의 타입을 사용하면 코틀린 기능을 그루비 클로저에 더욱 유연하게 적용할 수 있다.

예제 22. KotlinClosureX 타입 사용

build.gradle.kts

somePlugin {

    // 파라미터 없는 기능 적용
    takingParameterLessClosure(KotlinClosure0({
        "result"
    }))

    // 단항 함수 적용
    takingUnaryClosure(KotlinClosure1<String, String>({
        "result from single parameter $this"
    }))

    // 이진 함수 적용
    takingBinaryClosure(KotlinClosure2<String, String, String>({ a, b ->
        "result from parameters $a and $b"
    }))
}

코틀린 DSL 그루비 빌더(The Kotlin DSL Groovy Builder)

일부 플러그인이 그루비 메타프로그래밍을 많이 사용하는 경우 코틀린이나 자바 또는 정적으로 컴파일된 언어에서 해당 플러그인을 사용하는 것은 매우 번거로울 수 있다.

코틀린 DSL은 그루비 메타프로그래밍 의미 체계를 Any 타입의 객체에 연결하는 withGroovyBuilder {} 유틸리티 익스텐션을 제공한다. 다음 예제에서는 객체 대상에 대한 메서드의 여러 기능을 보여준다.

예제 23. withGroovyBuilder {} 사용

build.gradle.kts

target.withGroovyBuilder { //............................................... 1.

    // GroovyObject 메소드 사용 가능  //........................................ 2.
    val foo = getProperty("foo")
    setProperty("foo", "bar")
    invokeMethod("name", arrayOf("parameters", 42, aReference))

    // 코틀린 DSL 유틸리티
    "name"("parameters", 42, aReference) //................................. 3.
        "blockName" {  //................................................... 4.
            // `blockName`에 대한 동일한 Groovy Builder 의미
        }
    "another"("name" to "example", "url" to "https://example.com/") //...... 5.
}
  1. 리시버는 그루비오브젝트(GroovyObject)이며 코틀린 헬퍼를 제공한다.
  2. GroovyObject API를 사용할 수 있다.
  3. 일부 파라미터를 전달하여 methodName 메소드를 호출한다.
  4. blockName 프로퍼티를 구성하고 메소드 호출을 수행하는 클로저에 매핑된다.
  5. 네임드 아규먼트를 사용하는 다른 메서드를 호출하고 메서드 호출을 수행하는 그루비 네임드 아규먼트 Map<String, ?>에 매핑한다.

그루비 스크립트 사용(Using a Groovy script)

그루비 DSL 빌드 스크립트를 가정하는 문제가 있는 플러그인을 처리할 때 또 다른 옵션은 기본 코틀린 DSL 빌드 스크립트에서 적용되는 그루비 DSL 빌드 스크립트에서 해당 플러그인을 구성하는 것이다.

예제 24. 그루비 스크립트 사용

build.gradle.kts

plugins {
    id("dynamic-groovy-plugin") version "1.0" //..................... 1.
}
apply(from = "dynamic-groovy-plugin-configuration.gradle") //........ 2.

dynamic-groovy-plugin-configuration.gradle

native { //.......................................................... 3.
    dynamic {
        groovy as Usual
    }
}
  1. 코틀린 빌드 스크립트는 플러그인을 요청하고 적용한다.
  2. 코틀린 빌드 스크립트는 그루비 스크립트를 적용한다.
  3. 그루비 스크립트는 다이나믹 그루비를 사용하여 플러그인을 구성한다.

제한사항(Limitations)

  • 코틀린 DSL은 클린 체크아웃이나 임시 지속적 통합 에이전트 등을 사용하여 처음 사용할 때, 그루비 DSL보다 느린 것으로 알려져 있다. buildSrc 디렉토리의 내용을 변경하면 빌드 스크립트 캐싱이 무효화되므로 영향을 미친다. 그 주된 이유는 코틀린 DSL의 스크립트 컴파일 속도가 느리기 때문이다.
  • 인텔리J IDEA에서는 코틀린 DSL 빌드 스크립트에 대한 콘텐츠 지원 및 리팩토링 지원을 받으려면 그레이들 모델에서 프로젝트를 임포트해야 한다.
  • 코틀린 DSL은 중단된 그레이들 소프트웨어 모델의 일부인 model {} 블록을 지원하지 않는다.
  • 진단하기 매우 어려운 이슈가 발생할 수 있으므로 인큐베이팅 구성 디멘드 기능을 활성화하지 않는 것이 좋다.

이슈가 발생하거나 의심되는 버그를 발견한 경우 그레이들 이슈 트래커에 문제를 보고해보자.