Gradle Plugin - Android 생산성 향상을 위한 Log 탐색 플러그인 구현법 (2)
해당 포스트는 이전 포스트와 연결되므로 이전 포스트를 먼저 보고 오시기를 추천드립니다.
구현
이전 포스트에서 커스텀 Gradle Task
를 구현하는 방법과 그에 대한 사전지식을 다루었습니다.
지금까지에 과정을 돌아보면 구현된 Task를 바탕으로 이제 프로젝트 내에서 Log를 호출한 구문의 위치를 빠르게 탐색할 수 있게 되었습니다.
하지만 이렇게 구현된 Task는 다른 프로젝트에서 재사용하는 방법이 존재하지 않으며, 무엇보다 Task를 등록하고 환경을 구성하는 데에 있어 어려움이 존재합니다.
이러한 문제를 해결하기 위해 Gradle에서는 Plugin을 제공합니다. Plugin을 통해 Task를 등록할 환경을 만들 수 있으며, 무엇보다 빌드로직을 재사용할 수 있도록 지원해 줍니다.
아래에서부터는 구현된 Task에 대하여 Plugin 생성과 적용하는 과정으로 내용을 이어 나가겠습니다.
Plugin 생성
Plugin은 간단한 방법으로 생성할 수 있습니다. org.gradle.api
에 존재하는 Plugin
을 상속하는 서브 클래스를 생성하기만 하면 됩니다. Plugin의 이름은 LogPlugin
으로 지정하였습니다.
class LogPlugin : Plugin<Project> {
...
}
Plugin의 인터페이스 명세를 살펴보면 제네릭 타입을 명시해 줘야 한다는 것을 알 수 있습니다. 해당 타입은 일반적으로 이전 포스트에서 언급한 Project
로 지정합니다.
지금까지의 과정을 살펴보면 Plugin을 정의함과 동시에 Project를 타입으로 지정하였으며, 이를 통해 Plugin에서는 Project의 기반을 바탕으로 확장할 수 있게 되었습니다.
앞선 포스트에서 Project는 내부에서 Task, Plugin, Dependency, Extension등을 정의하고 제어할 수 있다고 언급하였는데, 이는 즉 Plugin에서 Task, Plugin, Dependency, Extension을 적용할 수 있게 되었다는 의미와 같습니다. Plugin은 Project를 확장하여 구성할 수 있기 때문입니다.
이제 Plugin 내에서는 Task, Dependecy, Extension뿐만 아니라 다른 Plugin을 추가할 수도 있게 되었고, 더 나아가 해당 Plugin을 다른 Plugin 이나 Project에 주입을 할 수 있게 되었습니다. (이러한 재사용성을 이용해 체계적인 빌드 시스템을 구성해 나갈 수 있다는 것은 Gradle의 큰 장점 중 하나입니다.)
Plugin 내 Project 구조를 적용하기 위한 방법은 Plugin 인터페이스의 유일한 메서드인 void apply(T target)
의 target 파라미터를 바탕으로 기능을 구현해 나가는 것입니다.
class LogPlugin : Plugin<Project> {
override fun apply(target: Project) {
...
}
}
Task 연동
Project를 파라미터로 제공하는 apply 메서드 블럭 내에서 이제 Task를 등록할 수 있게 되었습니다. Task를 등록하기 위해서는 Project의 TaskContainer
가 제공하는 register()
메서드를 이용해야 합니다. TaskContainer
는 Task 인스턴스를 관리하는 인터페이스이며 getTasks()를 통해 Project에서 관리되는 TaskContainer를 호출할 수 있습니다.
Task의 이름은 로그를 탐색하는 역할을 가졌기 때문에, checkLog
로 지정하였습니다.
target.tasks.register("checkLog", CheckLogTask::class.java) {
...
}
이전 포스트에서 Task를 구성하는 과정에서 Input과 Output에 대한 정의를 하였지만, 실제로 해당 Input과 Output이 어떻게 주입되는지에 대한 방식은 나타내지 않았습니다. Gradle에서는 Task의 유연한 구성을 위해 Input과 Output을 외부에서 제어할 수 있도록 구성하였으며 TaskContainer의 register
메서드에는 해당 Task에 대해 추가적인 빌딩을 할 수 있도록 Action
이라는 함수형 인터페이스를 제공하고 있습니다.
그렇다면 이제 Input과 Output은 정확히 어떻게 정의해야 할지에 대한 의문이 생길 것입니다. 앞서 우리는 Input과 Output에 대한 추상 프로퍼티를 정의해 두었기 때문에 해당 프로퍼티의 타입이 제공하는 메서드를 바탕으로 구현을 할 수 있을 것입니다.
Input의 타겟은 소스 디렉터리에 속해있는 .java와 .kt 파일입니다. 더 정확하게 표현하자면, Plugin을 적용한 소스 디렉터리의 .java와 .kt가 될 것입니다. Task는 Task가 포함된 Project를 호출할 수 있도록 기능을 제공하며, Project는 자신이 속해 있는 프로젝트 내 소스 디렉터리의 특정 경로 및 파일에 접근할 수 있습니다.
특정 경로 및 파일은 Input의 타입인 ConfigurableFileCollection
의 from()
메서드를 통해 전달받아 Input의 위치를 설정하였습니다.
sourceFiles.from(project.fileTree("src/main") {
include("**/*.kt", "**/*.java")
})
Output의 타입인 RegularFileProperty
는 set()
메서드를 통해 쓰기 파일을 지정할 수 있습니다. 파일의 위치는 빌드 시 생성되는 빌드 디렉터리 특정 경로에 설정되도록 하였습니다.
Project 인터페이스에는 디렉터리를 엑세스할 수 있도록 제공해 주는 ProjectLayout
과 그 내부에는 빌드 디렉터리에 엑세스할 수 있는 getBuildDirectory()
메서드를 제공합니다.
reportFile.set(project.layout.buildDirectory.file("reports/log-check/report.txt"))
Task를 등록하고 Input과 Output을 정의한 최종 Plugin의 구조는 아래와 같습니다
import io.goodmidnight.buildsystem.task.CheckLogTask
import org.gradle.api.Plugin
import org.gradle.api.Project
class LogPlugin : Plugin<Project> {
override fun apply(target: Project) {
target.tasks.register("checkLog", CheckLogTask::class.java) {
group = GROUP_NAME
description = "Detects all Log calls in source files and generates a report."
// Collects source files from specified source sets
sourceFiles.from(
project.fileTree("src/main") {
include("**/*.kt", "**/*.java")
})
reportFile.set(project.layout.buildDirectory.file("reports/log-check/report.txt"))
}
}
companion object {
private const val GROUP_NAME = "Logging"
}
}
Plugin 등록
Plugin을 최종적으로 사용하기 위해서는 Gradle 시스템에 Plugin을 등록하는 과정이 필요합니다. Plugin을 등록하는 방법은 앞서 배경지식의 Plugin 파트를 설명하면서 잠깐 언급한 적이 존재합니다. 동일한 접근 방식을 이용하여 LogPlugin을 등록하겠습니다.
gradlePlugin {
plugins {
register("LogPlugin") {
id = "io.goodmidnight.buildsystem.log"
implementationClass = "io.goodmidnight.buildsystem.plugin.LogPlugin"
}
}
}
위 코드에서 Plugin의 id를 io.goodmidnight.buildsystem.log
로 명시하였는데, 이는 Android 개발자들이 흔히 사용하는 org.jetbrains.kotlin.android
나 com.android.application
처럼 플러그인을 호출할 때에 대한 아이디를 의미합니다. 해당 아이디를 plugins 블럭에 작성하여 플러그인을 사용할 수 있습니다.
이제 특정 모듈을 생성하고 해당 플러그인 등록을 시도해 보겠습니다.
plugins {
id("io.goodmidnight.buildsystem.log")
...
}
...
플러그인을 블럭에 작성하였으니, 이제 Plugin 내 등록된 checkLog Task를 사용할 수 있게 되었습니다. ./gradlew [모듈이름]:checkLog
명령어를 호출하여 Task를 실행합니다.
Task가 수행이 완료되었다면 작업대한 리포트가 build/reports/log-check/report.txt에 저장된 것을 확인하실 수 있습니다. 해당 경로는 앞서 Plugin을 만드는 과정에서 지정된 경로입니다.
예시를 위해, 프로젝트 내 여러 곳에 Log 구문을 넣어두었는데, 아래와 같이 Task가 프로젝트 파일들을 탐색하여 호출된 부분의 위치를 보고해 준 것을 확인할 수 있습니다.
1편에 이어 Gradle 내 Log 구문을 탐색해 주는 Task와 그것을 재사용할 수 있도록 도와주는 Plugin 등록 방법에 대한 주제에 대해 다루어보았습니다.
해당 포스트에서는 Plugin을 등록하는 방법에 대해서만 다루었지만, 실제로는 Plugin을 배포하거나 복합 빌드(Composite Build)
를 통해 프로젝트 내에 Plugin을 사용할 수 있도록 하는 환경을 구성하는 작업을 하여 더욱 모듈화된 빌드 시스템을 만들어야만 합니다. 이후 Gradle 관련 포스트에서는 이 중 하나의 방식인 Coposite Build를 활용한 Plugin 등록 방법에 대해서 자세하게 다루어 보도록 하겠습니다.
해당 시리즈에 구현된 내용에 대한 자세한 소스는 [해당 주소] 에서 확인하실 수 있으니, 아이디어를 얻고자 하시는 분은 참고하시길 바랍니다.