Android Gradle 플러그인 확장

Android Gradle 플러그인(AGP)은 Android 애플리케이션의 공식 빌드 시스템으로, 다양한 유형의 소스를 컴파일하고 실제 Android 기기 또는 에뮬레이터에서 실행할 수 있는 애플리케이션에 컴파일된 소스를 연결하는 지원 기능이 있습니다.

AGP에는 플러그인이 빌드 입력을 제어하고 표준 빌드 태스크와 통합할 수 있는 새 단계를 통해 기능을 확장할 수 있는 확장 지점이 있습니다. 이전 AGP 버전에는 내부 구현과 명확하게 분리된 공식 API가 없었습니다. 버전 7.0부터 AGP에는 사용할 수 있는 안정된 공식 API 세트가 있습니다.

AGP API 수명 주기

AGP는 API 상태를 지정할 때 Gradle 기능 수명 주기를 따릅니다.

  • 내부: 공용으로 사용할 수 없습니다.
  • 신규: 공용으로 사용할 수 있지만, 최종은 아닙니다. 즉, 최종 버전에서 이전 버전과 호환되지 않을 수 있습니다.
  • 공개: 공용으로 사용할 수 있으며 안정적입니다.
  • 지원 중단됨: 더 이상 지원되지 않으며 새 API로 대체되었습니다.

지원 중단 정책

AGP는 이전 API의 지원 중단과 안정된 새 API로의 교체 및 새로운 도메인별 언어(DSL)로 발전하고 있습니다. 이러한 발전은 여러 AGP 출시에도 이뤄질 예정입니다. 이에 관해서는 AGP API/DSL 이전 타임라인에서 자세히 알아볼 수 있습니다.

AGP API는 이전이나 다른 이유로 지원 중단되더라도 현재 주요 출시 버전에서 계속 사용할 수 있습니다. 하지만 경고가 생성됩니다. 지원 중단된 API는 이후 주요 출시 버전의 AGP에서 완전히 삭제됩니다. 예를 들어 AGP 7.0에서 API가 지원 중단되는 경우 그 버전에서 API를 사용할 수는 있지만 경고가 생성됩니다. 이 API는 더 이상 AGP 8.0에서 사용할 수 없습니다.

일반적인 빌드 맞춤설정에 사용되는 새로운 API의 예를 보려면 Android Gradle 플러그인 레시피를 참고하세요. 일반적인 빌드 맞춤설정의 예가 나와 있습니다. 새로운 API에 관한 자세한 내용은 참조 문서에서도 확인할 수 있습니다.

Gradle 빌드 기본사항

이 가이드에서는 전체 Gradle 빌드 시스템을 다루지는 않습니다. 그러나 Google API와 통합하는 데 도움이 되는 최소한의 필수 개념이 설명되어 있으며, 자세한 내용을 볼 수 있는 주요 Gradle 문서 링크가 나와 있습니다.

여기서는 프로젝트 구성, 빌드 파일 수정, 플러그인 적용, 태스크 실행 등 Gradle의 작동 원리에 관한 기본 지식을 갖추고 있다고 가정합니다. AGP와 관련된 Gradle의 기본사항을 알아보려면 빌드 구성을 검토하는 것이 좋습니다. Gradle 플러그인 맞춤설정을 위한 일반 프레임워크에 관해 알아보려면 맞춤 Gradle 플러그인 개발을 참고하세요.

Gradle 지연 유형 관련 용어

Gradle은 '느리게' 동작하거나 대규모 컴퓨팅 또는 Task 생성을 빌드의 이후 단계로 지연할 수 있는 여러 가지 유형을 제공합니다. 이러한 유형은 대다수 Gradle 및 AGP API의 핵심입니다. 다음 목록에는 지연 실행과 관련된 기본 Gradle 유형과 그 유형의 주요 방법이 나와 있습니다.

Provider<T>
유형 T('T'는 모든 유형을 의미함)의 값을 제공합니다. 이 값은 실행 단계 중에 get()을 사용하여 읽어오거나 map(),flatMap(), zip() 메서드를 사용하여 새로운 Provider<S>(여기서 'S'는 다른 유형을 의미함)로 변환할 수 있습니다. 구성 단계에서는 get()을 호출해서는 안 됩니다.
  • map(): 람다를 허용하고 유형 SProviderProvider<S>를 생성합니다. map()의 람다 인수는 값 T 값을 취하고 값 S를 생성합니다. 람다는 즉시 실행되지 않습니다. 그 대신 결과 Provider<S>에서 get()이 호출되는 순간까지 실행이 지연되고 그로 인해 전체 체인이 지연됩니다.
  • flatMap(): 이 역시 람다를 허용하고 Provider<S>를 생성하지만, 람다는 값 T를 취하고(값 S를 직접 생성하는 대신) Provider<S>를 생성합니다. 구성 시 S를 확인할 수 없고 Provider<S>만 얻을 수 있는 경우 flatMap()을 사용합니다. 사실상 map()을 사용해 Provider<Provider<S>> 결과 유형으로 끝난 경우 flatMap()을 대신 사용해야 했던 경우일 수 있습니다.
  • zip(): 두 개의 입력 Providers 인스턴스의 값을 결합하는 함수를 사용하여 계산된 값과 함께 두 개의 Provider 인스턴스를 결합하는 새 Provider를 생성할 수 있습니다.
Property<T>
Provider<T>를 구현하므로 유형 T의 값도 제공합니다. 읽기 전용인 Provider<T>와 달리 Property<T>의 값을 설정할 수도 있습니다. 여기에는 두 가지 방법이 있습니다.
  • 지연된 계산을 사용할 필요 없이 가능한 경우 유형 T의 값을 직접 설정합니다.
  • 또 다른 Provider<T>Property<T>의 값의 소스로 설정합니다. 이 경우 값 TProperty.get()이 호출될 때만 구체화됩니다.
TaskProvider
Provider<Task>를 구현합니다. TaskProvider를 생성하려면 tasks.create()가 아닌 tasks.register()를 사용하여 필요할 때 태스크가 천천히 인스턴스화되도록 합니다. Task가 생성되기 전에 flatMap()을 사용하여 Task의 출력에 액세스할 수 있습니다. 이는 출력을 다른 Task 인스턴스에 입력으로 사용할 때 유용할 수 있습니다.

제공자와 변환 메서드는 태스크의 입력과 출력을 천천히 설정하는 데 필수 요소입니다. 즉, 모든 태스크를 미리 만들고 값을 확인할 필요는 없습니다.

제공자도 태스크 종속 항목 정보를 갖고 있습니다. Task 출력을 변환하여 Provider를 만드는 경우 그 TaskProvider의 암시적 종속 항목이 되고 Provider의 값이 확인될 때(예: 다른 Task에서 요구하는 경우) 생성되고 실행됩니다.

다음은 GitVersionTaskManifestProducerTask의 두 태스크를 등록하면서 실제로 필요할 때까지 Task 인스턴스 생성을 지연하는 방법의 예입니다. ManifestProducerTask 입력값은 GitVersionTask의 출력에서 가져온 Provider로 설정되므로 ManifestProducerTask는 암시적으로 GitVersionTask에 종속됩니다.

// Register a task lazily to get its TaskProvider.
val gitVersionProvider: TaskProvider =
    project.tasks.register("gitVersionProvider", GitVersionTask::class.java) {
        it.gitVersionOutputFile.set(
            File(project.buildDir, "intermediates/gitVersionProvider/output")
        )
    }

...

/**
 * Register another task in the configuration block (also executed lazily,
 * only if the task is required).
 */
val manifestProducer =
    project.tasks.register(variant.name + "ManifestProducer", ManifestProducerTask::class.java) {
        /**
         * Connect this task's input (gitInfoFile) to the output of
         * gitVersionProvider.
         */
        it.gitInfoFile.set(gitVersionProvider.flatMap(GitVersionTask::gitVersionOutputFile))
    }

이 두 태스크는 명시적으로 요청되는 경우에만 실행됩니다. 예를 들어 ./gradlew debugManifestProducer를 실행하거나 ManifestProducerTask의 출력이 다른 태스크에 연결되고 관련 값이 필요한 경우 이 두 태스크가 Gradle 호출의 일부로 실행될 수 있습니다.

개발자가 입력을 사용하거나 출력을 생성하는 맞춤 태스크를 작성하는 동안에는 AGP는 자체 태스크에 대한 공개 액세스를 직접 제공하지 않습니다. 이는 버전마다 달라질 수 있는 구현 정보입니다. 대신 AGP는 Variant API를 제공하고, 태스크의 출력 또는 빌드 아티팩트(읽기 및 변환 가능)에 대한 액세스 권한을 제공합니다. 자세한 내용은 이 문서의 변형 API, 아티팩트 및 태스크를 참고하세요.

Gradle 빌드 단계

프로젝트 빌드 작업은 본질적으로 복잡하고 리소스 소모가 많은 프로세스입니다. 여기에는 태스크 구성 방지, 최신 검사, 구성 캐싱 기능과 같은 다양한 기능이 있어 재현 가능하거나 불필요한 계산의 소요 시간을 최소화하는 데 도움이 됩니다.

이러한 최적화 중 일부를 적용하기 위해 Gradle 스크립트와 플러그인은 별개의 각 Gradle 빌드 단계(초기화, 구성, 실행) 동안 엄격한 규칙을 준수해야 합니다. 이 가이드에서는 구성 단계와 실행 단계를 중점적으로 설명합니다. 모든 단계에 관한 자세한 내용은 Gradle 빌드 수명 주기 가이드를 참고하세요.

구성 단계

구성 단계 중에는 빌드에 포함된 모든 프로젝트의 빌드 스크립트가 실행되고 플러그인이 적용되며 빌드 종속 항목이 확인됩니다. DSL 객체를 사용하여 빌드를 구성하고 태스크와 입력값을 천천히 등록하고자 할 때 이 단계를 사용해야 합니다.

구성 단계는 항상 실행되므로 어떤 태스크가 실행되도록 요청되었는지와 관계없이 태스크를 가볍게 유지하고 빌드 스크립트 자체가 아닌 입력에 따라 계산을 제한하는 것이 특히 중요합니다. 즉, 외부 프로그램을 실행하거나 네트워크에서 읽어오거나 실행 단계까지 적절한 Task 인스턴스 형태로 연기될 수 있는 긴 계산을 실행해서는 안 됩니다.

실행 단계

실행 단계에서는 요청된 태스크와 종속 태스크가 실행됩니다. 구체적으로, @TaskAction으로 표시된 Task 클래스 메서드가 실행됩니다. 태스크 실행 중에 입력 (예: 파일)에서 읽어오고, Provider<T>.get()을 호출하여 지연 제공자를 확인할 수 있습니다. 이렇게 지연 제공자를 확인하면 제공자에 포함된 태스크 종속 항목 정보를 따르는 map() 또는 flatMap() 호출 시퀀스가 시작됩니다. 태스크는 필요한 값을 구체화하도록 천천히 실행됩니다.

변형 API, 아티팩트 및 태스크

변형 API는 Android Gradle 플러그인의 확장 프로그램 메커니즘으로, Android 빌드에 영향을 주는 다양한 옵션(대개 빌드 구성 파일의 DSL을 사용하여 설정됨)을 조작할 수 있습니다. 또한 변형 API는 클래스 파일, 병합된 매니페스트 또는 APK/AAB 파일 등 빌드에 의해 만들어진 중간 아티팩트와 최종 아티팩트에 대한 액세스 권한도 줍니다.

Android 빌드 흐름 및 확장 지점

AGP와 상호작용할 때는 일반적인 Gradle 수명 주기 콜백(예: afterEvaluate())을 등록하거나 명시적 Task 종속 항목을 설정하는 대신 특별히 제작된 확장 지점을 사용합니다. AGP에서 만든 태스크는 구현 세부정보로 간주되며 공개 API로 노출되지 않습니다. Task 객체의 인스턴스를 가져오거나 Task 이름을 추측하고 이러한 Task 객체에 콜백 또는 종속 항목을 직접 추가하는 것을 피해야 합니다.

AGP는 다음 단계를 완료하여 Task 인스턴스를 만들고 실행하며 결과적으로 빌드 아티팩트를 생성합니다. Variant 객체 생성과 관련된 기본 단계 다음에는 콜백이 실행됩니다. 콜백을 통해 빌드의 일부로 생성된 특정 객체를 변경할 수 있습니다. 염두에 두어야 할 점은 모든 콜백이 구성 단계(이 문서에 설명됨) 중에 발생하고, 빠르게 실행되어야 한다는 점입니다. 대신 복잡한 작업은 실행 단계에서 적절한 Task 인스턴스로 지연됩니다.

  1. DSL 파싱: 빌드 스크립트가 실행되고 android 블록에서 Android DSL 객체의 다양한 속성이 생성되고 설정될 경우입니다. 다음 섹션에 설명된 변형 API 콜백도 이 단계에서 등록됩니다.
  2. finalizeDsl(): 구성요소(변형) 생성을 위해 잠그기 전에 DSL 객체를 변경할 수 있는 콜백입니다. VariantBuilder 객체는 DSL 객체에 포함된 데이터를 기반으로 생성됩니다.

  3. DSL 잠금: 현재 DSL은 잠겨 있으며 더 이상 변경할 수 없습니다.

  4. beforeVariants(): 이 콜백은 VariantBuilder를 통해 생성되는 구성요소와 일부 속성에 영향을 줄 수 있습니다. 이 콜백을 사용하여 여전히 생성되는 아티팩트와 빌드 흐름을 수정할 수 있습니다.

  5. 변형 생성: 이제 생성될 구성요소와 아티팩트 목록이 확정되었으므로 변경할 수 없습니다.

  6. onVariants()이 콜백에서는 생성된 Variant 객체에 대한 액세스 권한을 얻을 수 있습니다. 그리고 지연 계산을 위해 객체에 포함된 값과 Property 값의 제공자를 설정할 수 있습니다.

  7. 변형 잠금: 현재 변형 객체는 잠겨 있으며 더 이상 변경할 수 없습니다.

  8. 생성된 태스크: Variant 객체 및 Property 값은 빌드를 실행하는 데 필요한 Task 인스턴스를 만드는 데 사용됩니다.

AGP에는 finalizeDsl(), beforeVariants(), onVariants()의 콜백을 등록할 수 있는 AndroidComponentsExtension이 도입되었습니다. 확장 프로그램은 androidComponents 블록을 통해 빌드 스크립트에서 사용할 수 있습니다.

// This is used only for configuring the Android build through DSL.
android { ... }

// The androidComponents block is separate from the DSL.
androidComponents {
   finalizeDsl { extension ->
      ...
   }
}

하지만 Android 블록의 DSL을 사용하는 선언적 구성에만 빌드 스크립트를 유지하고 모든 맞춤 명령 로직을 buildSrc 또는 외부 플러그인으로 옮기는 것이 좋습니다. Gradle 레시피 GitHub 저장소의 buildSrc 샘플을 확인하여 프로젝트에서 플러그인을 만드는 방법을 알아볼 수도 있습니다. 다음은 플러그인 코드에서 콜백을 등록하는 예입니다.

abstract class ExamplePlugin: Plugin<Project> {

    override fun apply(project: Project) {
        val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
        androidComponents.finalizeDsl { extension ->
            ...
        }
    }
}

사용 가능한 콜백과 각 콜백에서 플러그인이 지원할 수 있는 사용 사례 유형을 자세히 살펴보겠습니다.

finalizeDsl(callback: (DslExtensionT) -> Unit)

이 콜백에서는 빌드 파일의 android 블록에서 정보를 파싱하여 생성된 DSL 객체에 액세스하고 수정할 수 있습니다. 이러한 DSL 객체는 빌드의 이후 단계에서 변형을 초기화하고 구성하는 데 사용됩니다. 예를 들어 프로그래매틱 방식으로 새 구성을 만들거나 속성을 재정의할 수 있습니다. 하지만 모든 값은 구성 시간에 확인되어야 하므로 그러한 값이 외부 입력을 사용해서는 안 됩니다. 이 콜백 실행이 완료되면 DSL 객체는 더 이상 유용하지 않으므로 더 이상 이 객체에 관한 참조를 유지하거나 값을 수정해서는 안 됩니다.

abstract class ExamplePlugin: Plugin<Project> {

    override fun apply(project: Project) {
        val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
        androidComponents.finalizeDsl { extension ->
            extension.buildTypes.create("extra").let {
                it.isJniDebuggable = true
            }
        }
    }
}

beforeVariants()

빌드의 이 단계에서는 생성할 변형과 그 속성을 결정하는 VariantBuilder 객체에 액세스할 수 있습니다. 예를 들어 프로그래매틱 방식으로 특정 변형과 관련 테스트를 중지하거나 선택한 변형에 관한 속성값(예: minSdk)만 변경할 수 있습니다. finalizeDsl()과 마찬가지로 개발자가 제공하는 모든 값은 구성 시점에 확인되고 외부 입력에 종속되어서는 안 됩니다. beforeVariants() 콜백 실행이 한 번 완료되면 VariantBuilder 객체를 수정하면 안 됩니다.

androidComponents {
    beforeVariants { variantBuilder ->
        variantBuilder.minSdk = 23
    }
}

beforeVariants() 콜백은 선택적으로 VariantSelector를 취하며 이는 androidComponentsExtensionselector() 메서드를 통해 얻을 수 있습니다. 이 클래스를 사용하여, 이름, 빌드 유형 또는 제품 버전에 따라 콜백 호출에 참여하는 구성요소를 필터링할 수 있습니다.

androidComponents {
    beforeVariants(selector().withName("adfree")) { variantBuilder ->
        variantBuilder.minSdk = 23
    }
}

onVariants()

onVariants()가 호출되는 시점에는 AGP에서 만들 모든 아티팩트가 이미 결정되었으므로 더 이상 관련 아티팩트를 중지할 수 없습니다. 하지만 Variant 객체의 Property 속성에 값을 설정하여 태스크에 사용되는 값 중 일부를 수정할 수 있습니다. Property 값은 AGP의 태스크가 실행될 때만 해결되므로 파일 또는 네트워크 같은 외부 입력에서 읽어오는 작업 등 필요한 계산을 진행하는 자체 맞춤 태스크에서 값을 제공자로 안전하게 연결할 수 있습니다.

// onVariants also supports VariantSelectors:
onVariants(selector().withBuildType("release")) { variant ->
    // Gather the output when we are in single mode (no multi-apk).
    val mainOutput = variant.outputs.single { it.outputType == OutputType.SINGLE }

    // Create version code generating task
    val versionCodeTask = project.tasks.register("computeVersionCodeFor${variant.name}", VersionCodeTask::class.java) {
        it.outputFile.set(project.layout.buildDirectory.file("versionCode.txt"))
    }
    /**
     * Wire version code from the task output.
     * map() will create a lazy provider that:
     * 1. Runs just before the consumer(s), ensuring that the producer
     * (VersionCodeTask) has run and therefore the file is created.
     * 2. Contains task dependency information so that the consumer(s) run after
     * the producer.
     */
    mainOutput.versionCode.set(versionCodeTask.map { it.outputFile.get().asFile.readText().toInt() })
}

아티팩트 액세스 및 수정

Variant 객체의 간단한 속성을 수정할 수 있는 기능 외에도 AGP에는 빌드 중에 생성된 중간 아티팩트와 최종 아티팩트를 읽어오거나 변환할 수 있는 확장 메커니즘도 들어 있습니다. 예를 들어 맞춤 Task에 병합된 최종 AndroidManifest.xml 파일 콘텐츠를 읽어와서 분석하거나, 그 콘텐츠를 맞춤 Task에 의해 생성된 매니페스트 파일의 콘텐츠로 완전히 대체할 수 있습니다.

현재 지원되는 아티팩트 목록은 Artifact 클래스의 참조 문서에서 찾을 수 있습니다. 모든 아티팩트 유형에는 알아두면 유용한 다음과 같은 특정 속성이 있습니다.

카디널리티

Artifact의 카디널리티는 FileSystemLocation 인스턴스 수 또는 아티팩트 유형의 파일 수나 디렉터리 수를 나타냅니다. 상위 클래스를 확인하여 아티팩트의 카디널리티 정보를 가져올 수 있습니다. 단일 FileSystemLocation이 있는 아티팩트는 Artifact.Single의 서브클래스가 됩니다. 여러 FileSystemLocation 인스턴스가 있는 아티팩트는 Artifact.Multiple의 서브클래스가 됩니다.

FileSystemLocation 유형

매개변수화된 FileSystemLocation 유형(RegularFile 또는 Directory일 수 있음)을 살펴 Artifact가 파일을 나타내는지 디렉터리를 나타내는지 확인할 수 있습니다.

지원되는 작업

모든 Artifact 클래스는 다음 인터페이스를 구현하여 어떤 작업을 지원하는지 나타낼 수 있습니다.

  • Transformable: 임의 변환을 실행하고 Artifact의 새 버전을 출력하는 Task에 대한 입력으로 Artifact를 사용할 수 있습니다.
  • Appendable: Artifact.Multiple의 서브클래스인 아티팩트에만 적용됩니다. 이는 Artifact를 추가할 수 있다는 의미입니다. 즉, 맞춤 Task가 이 Artifact 유형의 새 인스턴스를 만들 수 있습니다. 새 인스턴스는 기존 목록에 추가됩니다.
  • Replaceable: Artifact.Single의 서브클래스인 아티팩트에만 적용됩니다. 대체 가능한 ArtifactTask의 출력으로 생성된 완전히 새로운 인스턴스로 바꿀 수 있습니다.

세 가지 아티팩트 수정 작업 외에도 모든 아티팩트는 get()(또는 getAll()) 작업을 지원합니다. 이 작업에서는 아티팩트에 관한 모든 작업이 완료된 후 아티팩트의 최종 버전과 함께 Provider를 반환합니다.

다양한 플러그인을 사용하여 아티팩트에 관한 작업을 원하는 만큼 onVariants() 콜백에서 파이프라인으로 추가할 수 있습니다. 모든 태스크가 올바른 시간에 실행되고 아티팩트가 정확히 생성 및 업데이트되도록 AGP가 작업이 제대로 연결되었는지 확인합니다. 즉, 작업에서 추가, 교체 또는 변환을 통해 출력을 변경할 경우 그다음 작업에는 이러한 아티팩트의 업데이트된 버전이 입력 등으로 표시됩니다.

작업 등록의 시작 지점은 Artifacts 클래스입니다. 다음 코드 스니펫은 onVariants() 콜백에서 Variant 객체의 속성에서 Artifacts의 인스턴스에 액세스하는 방법을 보여줍니다.

그런 다음 맞춤 TaskProvider를 전달하여 TaskBasedOperation 객체를 가져오고(1) 그 객체를 사용하여 wiredWith* 메서드 중 하나로 입력과 출력을 연결(2)할 수 있습니다.

카디널리티와 변환하려는 Artifact에서 구현한 FileSystemLocation 유형에 따라 선택해야 하는 정확한 메서드가 다릅니다.

마지막으로 *OperationRequest 객체에 선택한 작업을 나타내는 메서드에 Artifact 유형을 전달합니다. 그러면 그에 대한 응답으로 toAppendTo(), toTransform() 또는 toCreate()가 반환됩니다(3).

androidComponents.onVariants { variant ->
    val manifestUpdater = // Custom task that will be used for the transform.
            project.tasks.register(variant.name + "ManifestUpdater", ManifestTransformerTask::class.java) {
                it.gitInfoFile.set(gitVersionProvider.flatMap(GitVersionTask::gitVersionOutputFile))
            }
    // (1) Register the TaskProvider w.
    val variant.artifacts.use(manifestUpdater)
         // (2) Connect the input and output files.
        .wiredWithFiles(
            ManifestTransformerTask::mergedManifest,
            ManifestTransformerTask::updatedManifest)
        // (3) Indicate the artifact and operation type.
        .toTransform(SingleArtifact.MERGED_MANIFEST)
}

이 예에서 MERGED_MANIFESTSingleArtifact이며 RegularFile입니다. 따라서 입력에는 단일 RegularFileProperty 참조를, 출력에는 단일 RegularFileProperty 참조를 허용하는 wiredWithFiles 메서드를 사용해야 합니다. TaskBasedOperation 클래스에는 Artifact 카디널리티와 FileSystemLocation 유형의 다른 조합에 사용할 수 있는 다른 wiredWith* 메서드가 있습니다.

AGP 확장에 관한 자세한 내용은 Gradle 빌드 시스템 설명서의 다음 섹션을 읽어보세요.