저번 포스트에서 Version Catalog를 적용하여 멀티 모듈에서 버전 관리를 하나의 파일에서 할 수 있도록 하였습니다. 하지만 각 모듈의 build.gradle.kts에 중복되는 코드들이 존재하는 문제점을 발견했습니다. 이번에는 이런 문제를 Convention Plugin을 사용하여 해결해보려고 합니다.
프로젝트 내의 여러 모듈의 빌드 구성 및 설정을 일괄적으로 선언 하여 모듈들에서 이를 각 모듈들에서 중복으로 사용하지 않도록 하는 plugin입니다.
먼저 root project 단에 build-logic 모듈을 생성합니다. 그리고 build-logic 모듈 내를 비우고 settings.gradle.kts를 생성합니다. settings.gradle.kts는 다음과 같이 작성합니다.
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
rootProject.name = "build-logic"
include(":convention")
다음 build-logic 모듈 내에 convention 모듈을 생성합니다.
이와 같은 구조로 생성합니다. 그리고 root 단의 settings.gradle.kts에 자동으로 생성되는include(":build-logic")
, include(":build-logic:convention")
코드들을 제거합니다.
convention 모듈의 build.gradle.kts는 다음과 같이 작성합니다.
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
`kotlin-dsl`
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
dependencies {
compileOnly(libs.android.gradle.plugin)
compileOnly(libs.kotlin.gradle.plugin)
}
이렇게 기본 세팅을 끝내고 gradle의 공통 빌드를 선언하면 됩니다.
밑은 기존의 feature 모듈 build.gradle.kts입니다.
android 블록 내의 설정들은 다른 모듈들에서도 정의되어 있습니다. 이 중 compose 설정을 제외한 부분은 KotlinAndroid으로, Compose 설정은 AndroidCompose 파일로 관리할 것입니다.
KotlinAndroid.kt
internal fun Project.configureKotlinAndroid(
commonExtension: CommonExtension<*, *, *, *, *>,
) {
commonExtension.apply {
compileSdk = Const.compileSdk
defaultConfig {
minSdk = Const.minSdk
testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility = Const.JAVA_VERSION
targetCompatibility = Const.JAVA_VERSION
}
kotlinOptions {
jvmTarget = Const.JDK_VERSION.toString()
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
}
}
}
}
fun CommonExtension<*, *, *, *, *>.kotlinOptions(block: KotlinJvmOptions.() -> Unit) {
(this as ExtensionAware).extensions.configure("kotlinOptions", block)
}
AndroidCompose.kt
internal fun Project.configureAndroidCompose(
commonExtension: CommonExtension<*, *, *, *, *>,
) {
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
commonExtension.apply {
buildFeatures.compose = true
composeOptions {
kotlinCompilerExtensionVersion = libs.findVersion("compose.compiler").get().requiredVersion
}
}
dependencies {
"api"(platform(libs.findLibrary("compose.bom").get()))
"implementation"(libs.findBundle("compose").get())
"debugImplementation"(libs.findBundle("compose.debug").get())
}
}
다음은 Convention 모듈 내에 Convention Plugin을 정의합니다.
이는 프로젝트의 상황에 맞게 정의하면 되고 저는 다음과 같이 생성했습니다.
AndroidLibraryConventionPlugin.kt
@Suppress("UNUSED")
class AndroidLibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("com.android.library")
apply("org.jetbrains.kotlin.android")
}
extensions.configure<LibraryExtension> {
configureKotlinAndroid(this)
viewBinding.enable = true
}
dependencies {
"testImplementation"(libs.findLibrary("junit").get())
"androidTestImplementation"(libs.findLibrary("androidx.ext.junit").get())
"androidTestImplementation"(libs.findLibrary("androidx.espresso.core").get())
}
}
}
}
AndroidDataConventionPlugin.kt
internal class AndroidDataConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("simplejoke.android.library")
apply("simplejoke.android.hilt")
}
dependencies {
"implementation"(libs.findBundle("network").get())
"implementation"(libs.findLibrary("kotlinx.coroutine").get())
}
}
}
}
해당하는 모듈들에서 공통적으로 사용되는 plugin과 의존성을 개개인의 생각에 따라 작성하면 됩니다.
마지막으로 생성한 Convention Plugin들을 convention 모듈의 build.gradle.kts에 등록하면 사용할 수 있습니다.
gradlePlugin {
plugins {
register("androidApplication") {
id = "simplejoke.android.application"
implementationClass = "AndroidApplicationConventionPlugin"
}
register("androidLibrary") {
id = "simplejoke.android.library"
implementationClass = "AndroidLibraryConventionPlugin"
}
...
}
}
id
는 임의로 설정이 가능하고 implementationClass
는 위에서 작성한 Convention Plugin의 파일 명입니다.
모듈의 build.gradle.kts의 plugin에서 사용할 수 있습니다.
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
alias(libs.plugins.simplejoke.android.data)
}
android {
namespace = "com.beeeam.data"
}
dependencies {
implementation(project(":domain"))
}
data 모듈을 위한 공통 빌드는 AndroidDataConvention에서 정의하였기 때문에 이 Plugin을 선언합니다.
적용 전 | 적용 후 |
이렇게 길던 모듈의 build.gradle.kts를 Convention Plugin을 사용하면 간략하게 만들 수 있습니다.
엄청 복잡해 보이는 build.gradle.kts를 Convention Plugin을 사용하면 간단하게 만들 수 있습니다. 이를 통해서 코드의 가독성을 높일 수 있다고 생각합니다. 그리고 의존성 관리도 하나의 모듈에서 하기 때문에 프로젝트를 관리하기 더 편해질 것이라 생각합니다.
하지만 build 관련 기능들이기 때문에 적용하면서 많은 에러에 부딪칠 수 있습니다. 그리고 생소한 코드들이 많기 때문에 러닝커브가 높다고 느껴졌습니다. 하지만 이를 잘 적용하면 코드를 더 깔끔하게 만들어서 프로젝트의 유지보수성을 높일 수 있기 때문에 적용해보는 것이 좋다고 생각합니다.