안드로이드

[안드로이드] 컴포즈 build-logic, convention 멀티모듈 빌드 로직 관리하기

이손안나 2025. 6. 18. 11:20

1. Version Catalog

- 버전 카탈로그는 의존성 버전과 라이브러리를 중앙에서 관리할 수 있게 해주는 기능

root project 의 gradle 폴더 -> libs.versions.toml 파일에 정의

 

  • versions : 라이브러리 버전
  • libraries : 라이브러리 의존성
  • bundles : 라이브러리를 묶어서 한 번에 선언
  • plugins: 어떤 플러그인을 사용하는지
[versions]
kotlin = "2.0.0"
hilt = "2.51"

[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "kotlin" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }

 

사용하는 방법

dependencies {
    implementation(libs.androidx.core.ktx)
    implementation(libs.hilt.android)
}

 

2. Convention Module 

- 모듈 간 중복되는 gradle 설정을 하나의 공통 규칙으로 추출한 것 . 규칙을 정의한 다음 해당 컨벤션을 필요한 모듈에 적용함으로써 보다 간편하게 빌드 관리를 수행할 수 있음.

 

3. Build-logic

- Build-logic 모듈은 공통 빌드 스크립트를 포함해 빌드 구성 자체도 모듈화. 

 

 

생성 방법

먼저 build-logic 모듈을 생성하고 (Android Library로 생성) , 안에 convention 모듈을 생성한다. (java or kotlin library로 생성)

 

 

2. build-logic 모듈의 settings.kts 수정

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
    versionCatalogs{
        create("libs"){
            // 작성한 버전 카탈로그 toml 파일을 가져와준다
            from(files("../gradle/libs.versions.toml"))
        }
    }
}
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
rootProject.name = "build-logic"
include(":convention")

 

3. root 수준의 settings.gradle.kts 에서 includeBuild("build-logic") 추가

이후 include(":build-logic"), include(":build-logic:convention") 는 제거

pluginManagement {
    includeBuild("build-logic")
    repositories {
        google {
            content {
                includeGroupByRegex("com\\.android.*")
                includeGroupByRegex("com\\.google.*")
                includeGroupByRegex("androidx.*")
            }
        }
        mavenCentral()
        gradlePluginPortal()
    }
}

 

4. custom gradle plugin 구현

gradle plugin을 커스텀화하면 의존성을 grouping하여 필요한 의존성을 한번에 설정 가능하다

 

내가 작성한 플러그인

AndroidCompose.kt , KotlinAndroid.kt , ProjectExtensions.kt 파일은 보통 컨벤션 plugin 내부에서 중복설정을 피하고 코드 재사용하기 위해 따로 정의해두는 유틸 함수 파일이다.

// 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("androidx.compose.bom").get()))
        "implementation"(libs.findBundle("compose").get())
        "debugImplementation"(libs.findBundle("compose.debug").get())
    }
}

 

//KotlinAndroid.kt

internal fun Project.configureKotlinAndroid(
    commonExtension: CommonExtension<*,*,*,*,*,*>
) {
    commonExtension.apply {
        compileSdk = 34

        defaultConfig {
            minSdk = 26

            testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
            vectorDrawables.useSupportLibrary = true
        }

        compileOptions {
            sourceCompatibility = JavaVersion.VERSION_17
            targetCompatibility = JavaVersion.VERSION_17
        }

        kotlinOptions {
            jvmTarget = JavaVersion.VERSION_17.toString()
        }
    }
}

fun CommonExtension<*,*,*,*,*,*>.kotlinOptions(block:KotlinJvmOptions.()->Unit) {
    (this as ExtensionAware).extensions.configure("kotlinOptions",block)
}

 

//ProjectExtensions.kt

val Project.libs: VersionCatalog
    get() = extensions.getByType<VersionCatalogsExtension>().named("libs")

 

커스텀 플러그인 구현

//AndroidApplicationComposeConventionPlugin

class AndroidApplicationComposeConventionPlugin:Plugin<Project> {

    override fun apply(target: Project) {
        with(target) {
            pluginManager.apply("com.android.application")
            val extension = extensions.getByType<BaseAppModuleExtension>()
            configureAndroidCompose(extension)
        }
    }
}

 

//AndroidApplicationConventionPlugin

internal class AndroidApplicationConventionPlugin : Plugin<Project> {

    override fun apply(target: Project) {
        with(target) {
            with(pluginManager) {
                apply("com.android.application")
                apply("org.jetbrains.kotlin.android")
            }

            extensions.configure<ApplicationExtension> {
                configureKotlinAndroid(this)
            }
        }
    }
}

 

//AndroidHiltConventionPlugin

class AndroidHiltConventionPlugin:Plugin<Project> {

    override fun apply(target: Project) {
        with(target) {
            with(pluginManager) {
                apply("com.google.devtools.ksp")
                apply("dagger.hilt.android.plugin")
            }

            val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")

            dependencies {
                "implementation"(libs.findLibrary("hilt.android").get())
                "ksp"(libs.findLibrary("hilt.compiler").get())
            }
        }
    }
}

 

//AndroidLibraryComposeConventionPlugin

class AndroidLibraryComposeConventionPlugin:Plugin<Project> {

    override fun apply(target: Project) {

        with(target) {
            pluginManager.apply("com.android.library")
            val extension = extensions.getByType<LibraryExtension>()
            configureAndroidCompose(extension)
        }
    }
}

 

//AndroidLibraryConventionPlugin

class AndroidLibraryConventionPlugin:Plugin<Project> {

    override fun apply(target: Project) {
        with(target) {
            with(pluginManager) {
                apply("com.android.library")
                apply("org.jetbrains.kotlin.android")
                apply("kotlin-parcelize")
            }

            extensions.configure<LibraryExtension> {
                configureKotlinAndroid(this)

                defaultConfig.targetSdk = 35

                defaultConfig {
                    testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
                    vectorDrawables.useSupportLibrary = true
                }

                viewBinding.enable = true

                buildTypes {
                    getByName("release") {
                        isMinifyEnabled = true
                        proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
                    }
                }
            }

            val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
            dependencies{
                "implementation"(libs.findLibrary("junit").get())
            }

        }
    }
}

 

//FeatureConventionPlugin

class FeatureConventionPlugin: Plugin<Project> {

    override fun apply(target: Project) {
        with(target) {
            with(pluginManager) {
                apply("todolist.plugin.android.library")
                apply("todolist.plugin.hilt")
            }

            val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
            dependencies {


                "implementation"(libs.findLibrary("androidx.appcompat").get())
                "implementation"(libs.findLibrary("androidx.core.ktx").get())
                "implementation"(libs.findLibrary("androidx.hilt.navigation.compose").get())

                "implementation"(libs.findLibrary("androidx.lifecycle.runtime.ktx").get())
                "implementation"(libs.findLibrary("androidx.lifecycle.viewmodel.ktx").get())

                "implementation"(libs.findLibrary("androidx.lifecycle.viewmodel.compose").get())
                "implementation"(libs.findLibrary("androidx.lifecycle.runtime.compose").get())

                "implementation"(libs.findLibrary("coroutines.android").get())
                "implementation"(libs.findLibrary("coroutines.core").get())

            }
        }
    }
}

 

//JavaLibraryConventionPlugin

class JavaLibraryConventionPlugin: Plugin<Project> {

    override fun apply(target: Project) {
        with(target) {
            with(pluginManager) {
                apply("java-library")
                apply("org.jetbrains.kotlin.jvm")
            }

            extensions.configure<JavaPluginExtension>
            {
                sourceCompatibility = JavaVersion.VERSION_17
                targetCompatibility = JavaVersion.VERSION_17
            }

            extensions.configure<KotlinProjectExtension>
            {
                jvmToolchain(17)
            }
        }
    }
}

 

나는 이렇게 총 7개의 커스텀 플러그인을 작성했다.

그 다음 최종적으로 플러그인을 등록해준다.

 

5. build.gradle 에 플러그인 등록 

id : 내가 외부에서 사용 할 이름 정의

implementationClass: 작성한 플러그인 클래스명

//build.gradle.kts(:build-logic:convention)

plugins {
    `kotlin-dsl`
}

group = "com.example.buildlogic"
java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}
dependencies {
    compileOnly(libs.kotlin.gradle.plugin)
    compileOnly(libs.android.gradle.plugin)
    compileOnly(libs.compose.compiler.extension)
    compileOnly(libs.ksp.gradlePlugin)
}
gradlePlugin {
    plugins {
        register("AndroidApplicationPlugin") {
            id = "todolist.plugin.application"
            implementationClass = "AndroidApplicationConventionPlugin"
        }
        register("AndroidApplicationComposePlugin") {
            id = "todolist.plugin.application.compose"
            implementationClass = "AndroidApplicationComposeConventionPlugin"
        }
        register("JavaLibraryPlugin") {
            id = "todolist.plugin.java.library"
            implementationClass = "JavaLibraryConventionPlugin"
        }
        register("AndroidHiltPlugin") {
            id = "todolist.plugin.hilt"
            implementationClass = "AndroidHiltConventionPlugin"
        }
        register("AndroidLibraryPlugin") {
            id = "todolist.plugin.android.library"
            implementationClass = "AndroidLibraryConventionPlugin"
        }
        register("AndroidLibraryComposePlugin") {
            id = "todolist.plugin.android.library.compose"
            implementationClass = "AndroidLibraryComposeConventionPlugin"
        }
        register("FeaturePlugin") {
            id = "todolist.plugin.feature"
            implementationClass = "FeatureConventionPlugin"
        }
    }
}

 

이제 사용을 해보자!

현재 나의 프로젝트 모듈 구조는 아래와 같이 구성되어 있다.

멀티 모듈

예를 들어 core 모듈의 build.gradle.kts 파일 변경 코드이다

 

plugins {
    id("todolist.plugin.android.library")
    id("todolist.plugin.hilt")
}

android {
    namespace = "com.example.core"
}

 

이렇게 간단하게 작성해주면 끝!! 정말 쉽다. 그 많던 코드가 많이 간소화 된걸 볼 수 있다.