[Android] Convention Plugin

조범준·2024년 6월 24일
0

Android

목록 보기
5/5

저번 포스트에서 Version Catalog를 적용하여 멀티 모듈에서 버전 관리를 하나의 파일에서 할 수 있도록 하였습니다. 하지만 각 모듈의 build.gradle.kts에 중복되는 코드들이 존재하는 문제점을 발견했습니다. 이번에는 이런 문제를 Convention Plugin을 사용하여 해결해보려고 합니다.

Convention Plugin?

프로젝트 내의 여러 모듈의 빌드 구성 및 설정을 일괄적으로 선언 하여 모듈들에서 이를 각 모듈들에서 중복으로 사용하지 않도록 하는 plugin입니다.

Apply

먼저 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을 정의합니다.

이는 프로젝트의 상황에 맞게 정의하면 되고 저는 다음과 같이 생성했습니다.

  • AndroidApplicationConventionPlugin (application 타입의 모듈 관리)
  • AndroidDataConventionPlugin (data 모듈 관리)
  • AndroidFeatureConventionPlugin (feature 모듈 관리)
  • AndroidHiltConventionPlugin (hilt 관리)
  • AndroidLibraryConventionPlugin (android library 타입의 모듈을 관리)
  • AndroidLibraryComposeConventionPlugin (android library 타입인데 compose를 사용하는 모듈)

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의 파일 명입니다.

Use

모듈의 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 관련 기능들이기 때문에 적용하면서 많은 에러에 부딪칠 수 있습니다. 그리고 생소한 코드들이 많기 때문에 러닝커브가 높다고 느껴졌습니다. 하지만 이를 잘 적용하면 코드를 더 깔끔하게 만들어서 프로젝트의 유지보수성을 높일 수 있기 때문에 적용해보는 것이 좋다고 생각합니다.

https://github.com/BEEEAM-J/Simple-Joke

profile
https://beeamjunn.tistory.com/

0개의 댓글