09
01

์ƒˆ๋กœ ์‹œ์ž‘ํ•œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋ฉ€ํ‹ฐ๋ชจ๋“ˆ์˜ ์žฅ์ ์„ ๋” ๊ทน๋Œ€ํ™” ํ•˜๊ธฐ ์œ„ํ•ด์„œ feature ๋ณ„๋กœ ๋ชจ๋“ˆ์„ ๋ชจ๋‘ ๋‚˜๋ˆ„๊ณ  ์žˆ๋‹ค. core๋„ ํ•ฉ์ณ์„œ ์“ฐ๋˜๊ฑธ data, network, ui์˜ ํ˜•ํƒœ๋กœ ๋‹ค ๋ถ„๋ฆฌํ•ด์„œ ์‚ฌ์šฉ ์ค‘์ด๋‹ค. ์ด๋ ‡๋‹ค๋ณด๋‹ˆ ๋งค๋ฒˆ build.gradle.kts ํŒŒ์ผ์— ๋˜‘๊ฐ™์€ ์ฝ”๋“œ๋“ค์„ ๋„ฃ์–ด์ค˜์•ผ ํ–ˆ๋Š”๋ฐ, ์ด ๋ถ€๋ถ„์ด ๋„ˆ๋ฌด ๋ถˆํŽธํ•ด์„œ ๋ฐฉ๋ฒ•์„ ์ฐพ์•„๋ณด๋˜ ์ค‘ build-logic์ด๋ผ๋Š” ๊ฒƒ์„ ๋ฐœ๊ฒฌํ–ˆ๋‹ค.

version-catalog์™€ ๊ฐ™์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋„๊ตฌ๋กœ ๋นŒ๋“œ์— ํ•„์š”ํ•œ ๊ณตํ†ต ๋กœ์ง๋“ค์„ ๋”ฐ๋กœ ๋นผ๋‚ด์–ด ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๋„๊ตฌ๋กœ, ์ง€๊ธˆ์€ ํ•ด๋ณด์ง€ ์•Š์•˜์ง€๋งŒ debug, release ๋ชจ๋“œ๋ฅผ ๋‚˜๋ˆ ์„œ ๋นŒ๋“œ ๋‚ด์šฉ์„ ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ๋„ ๊ฐ–๊ณ  ์žˆ๋‹ค๊ณ  ํ•œ๋‹ค.

 

๋‚˜๋Š” Now in Android ๋ฅผ ๊ต๋ณด์žฌ๋กœ build-logic์„ ๊ตฌํ˜„ํ–ˆ๋‹ค.

ํด๋” ๊ตฌ์กฐ๋‹ค. build-logic ์ž์ฒด๋Š” ๋”ฐ๋กœ build.gradle.kts๋ฅผ ๊ฐ–์ง€ ์•Š๊ณ  convention ๋ชจ๋“ˆ์—์„œ ํ•ด๋‹น ํŒŒ์ผ์„ ๊ด€๋ฆฌํ•œ๋‹ค. build-logic์—๋Š” `settings.gradle.kts`๋ฅผ ์ƒ์„ฑํ•ด์„œ ์ด๊ฑธ buildํ• ๋•Œ ๊ฐ€์ ธ๊ฐ€๋„๋ก ์„ธํŒ…ํ•œ๋‹ค.

 

build-logic์„ ๋งŒ๋“ค๊ณ , `settings.gradle.kts` ๋ฅผ ์•„๋ž˜์ฒ˜๋Ÿผ ์ž‘์„ฑํ•˜์ž.

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
    versionCatalogs {
        create("libs") {
            from(files("../gradle/libs.versions.toml"))
        }
    }
}

rootProject.name = "build-logic"
include(":convention")

build-logic ์ž์ฒด๋Š” ๊ทธ๋ƒฅ directory๋กœ ๋งŒ๋“ค๊ณ , convention ๋ชจ๋“ˆ๋งŒ ๋”ฐ๋กœ ๋งŒ๋“ค์–ด์ฃผ๋ฉด ๋œ๋‹ค. ํ”„๋กœ์ ํŠธ์— ์žˆ๋Š” ๋ฒ„์ „ ์นดํƒˆ๋กœ๊ทธ ํŒŒ์ผ์„ ๊ฐ€์ ธ์™€์„œ libs๋กœ ์ด๋ฆ„ ๋ถ™์ด๋Š” ์ฝ”๋“œ๊ฐ€ ๋“ค์–ด์žˆ๋‹ค. ๋ชจ๋“ˆ์„ ๋งŒ๋“œ๋Š” ๋„๊ตฌ๋กœ ๋งŒ๋“ค๋ฉด, Project ์ˆ˜์ค€์˜ `settings.gradle.kts` ํŒŒ์ผ์— include ๋˜์–ด์žˆ์„ํ…๋ฐ, ์ด๊ฑธ ์‚ญ์ œํ•ด์ค˜์•ผํ•œ๋‹ค. build-logic๋ชจ๋“ˆ์€ ํ”„๋กœ์ ํŠธ๊ฐ€ ๋นŒ๋“œํ•  ํ•„์š”์—†๋Š” ๋ชจ๋“ˆ์ด๊ณ , ๋นŒ๋“œ์— ์‚ฌ์šฉ๋  ๋ชจ๋“ˆ์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

๊ทธ๋ž˜์„œ project ์ˆ˜์ค€ gradle์„ ๋ณด๋ฉด ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

pluginManagement {
    includeBuild("build-logic")
    ... ์ƒ๋žต
}

rootProject.name = "DRTAA"

include(":app")
// ...

build-logic์„ build๋•Œ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด `includeBuild`๋กœ ๊ฐ์‹ธ์ค˜์•ผํ•œ๋‹ค.

๋ฉ€ํ‹ฐ๋ชจ๋“ˆ์˜ ํšจ์œจ์„ ์˜ฌ๋ ค์ฃผ๊ธฐ ์œ„ํ•ด `gradle.properties`๋„ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.

org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configureondemand=true

์ˆœ์„œ๋Œ€๋กœ ๋ณด์ž.

๋ณ‘๋ ฌ build๋ฅผ ํ™œ์„ฑํ™” ํ–ˆ๊ณ , ๋นŒ๋“œ ์บ์‹œ๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ ๋งŒ๋“ค์–ด์„œ ์ด์ „ ๋นŒ๋“œ ๊ฒฐ๊ณผ๋ฅผ ์žฌ์‚ฌ์šฉํ•ด ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์—†๋‹ค๋ฉด ๋”์šฑ ๋น ๋ฅธ ๋นŒ๋“œ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค. configureondemand๋ฅผ true๋กœ ํ•˜๋ฉด ์ „์ฒด gradle ๋นŒ๋“œ๊ฐ€ ์•„๋‹ˆ๋ผ ํ˜„์žฌ ํƒœ์Šคํฌ์— ํ•„์š”ํ•œ ๊ฒƒ๋งŒ ๋นŒ๋“œํ•œ๋‹ค๊ณ  ํ•œ๋‹ค.

 

์ด์ œ ๋‚ด๊ฐ€ ์‚ฝ์งˆ์„ ๊ฐ€์žฅ ๋งŽ์ดํ•œ convention ๋ชจ๋“ˆ์„ ๊ตฌํ˜„ํ•ด๋ณด์ž.

import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
    `kotlin-dsl`
}

group = "com.drtaa.convention"

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

kotlin {
    compilerOptions {
        jvmTarget = JvmTarget.JVM_17
    }
}

dependencies {
    compileOnly(libs.android.gradlePlugin)
    compileOnly(libs.kotlin.gradlePlugin)
}

tasks {
    validatePlugins {
        enableStricterValidation = true
        failOnWarning = true
    }
}

gradlePlugin {
    plugins {
        register("AndroidHiltPlugin") {
            id = "drtaa.plugin.hilt"
            implementationClass = "AndroidHiltConventionPlugin"
        }
        register("AndroidCorePlugin") {
            id = "drtaa.plugin.core"
            implementationClass = "AndroidCoreConventionPlugin"
        }
        register("AndroidCommonPlugin") {
            id = "drtaa.plugin.common"
            implementationClass = "AndroidCommonConventionPlugin"
        }
        register("AndroidCoreNetworkPlugin") {
            id = "drtaa.plugin.network"
            implementationClass = "AndroidCoreNetworkConventionPlugin"
        }
        register("AndroidFeaturePlugin") {
            id = "drtaa.plugin.feature"
            implementationClass = "AndroidFeatureConventionPlugin"
        }
    }
}

gradlePlugin ์Šค์ฝ”ํ”„์•ˆ์— ์žˆ๋Š” ๊ฒŒ ๋‚ด๊ฐ€ ์ปค์Šคํ…€์œผ๋กœ ๋งŒ๋“ค ํ”Œ๋Ÿฌ๊ทธ์ธ๋“ค์ด๋‹ค. implementationClass๋Š” ์‹ค์ œ๋กœ ๋‚ด๊ฐ€ ๋งŒ๋“  class ํŒŒ์ผ์ด๊ณ , id๋กœ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

๋‚˜๋Š” AGP 8.5.0์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์–ด์„œ jvm ํƒ€๊ฒŸ์„ 17 ์ด์ƒ์œผ๋กœ ํ•ด์•ผ๋œ๋‹ค. ์ด์ œ compileOnly์—์„œ ์ฒซ๋ฒˆ์งธ ์‚ฝ์งˆ์ด ๋ฐœ์ƒํ•œ๋‹ค. ๋‚ด ๋ฒ„์ „ ์นดํƒˆ๋กœ๊ทธ์—๋Š” ์ €๊ฒŒ ์—†๋Š”๋ฐ, ์–ด๋А ์˜ˆ์‹œ๋ฅผ ๋ณด๋”๋ผ๋„ ์ €๊ฑธ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค.

 

์นจ์ฐฉํ•˜๊ฒŒ ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ toml์— ์ถ”๊ฐ€ํ•ด์ฃผ์ž. gradle ๋„๊ตฌ์— ์‚ฌ์šฉ๋˜๋Š” ํ”Œ๋Ÿฌ๊ทธ์ธ ๊ฐ™์€๋ฐ ๊ธฐ๋ณธ toml ๊ตฌ์„ฑ์—๋Š” ๋น ์ ธ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ถ”๊ฐ€ํ•ด์ค˜์•ผํ•œ๋‹ค,

[libraries]
android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "agp" }
kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }

์—ฌ๊ธฐ๊นŒ์ง€ ๋งˆ์ณค์œผ๋ฉด, ์ด์ œ ์ž‘์—…ํ•  ๊ณต๊ฐ„์€ `build-logic/convention/src/main/kotlin` ์ด๋‹ค.

๋‹ค๋ฅธ ์˜ˆ์ œ๋ฅผ ๋ณด๋ฉด Application(gradle ํŒŒ์ผ์˜ android ์Šค์ฝ”ํ”„)๊นŒ์ง€ build-logic์— ๋„ฃ๊ณ  ์žˆ์—ˆ๋Š”๋ฐ ์ผ๋‹จ ๋‚˜๋Š” dependency ๋ฐ˜๋ณต์„ ๋ง‰๋Š”๊ฒŒ ๋ชฉ์ ์ด๋ผ์„œ ํ•ด๋‹น ๋ถ€๋ถ„์€ ๋„˜๊ธฐ๊ฒ ๋‹ค.

 

๊ธฐ๋ณธ dependency๋งŒ ์ผ๋‹จ ๋ชจ์•„๋‘” ํŒŒ์ผ ๋จผ์ € ๋ณด์ž.

internal class AndroidCommonConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            dependencies {
                add("implementation", libs.findLibrary("androidx.core.ktx").get())
                add("implementation", libs.findLibrary("androidx.appcompat").get())
                add("implementation", libs.findLibrary("material").get())
                add("implementation", libs.findLibrary("junit").get())
                add("androidTestImplementation", libs.findLibrary("androidx.junit").get())
                add("androidTestImplementation", libs.findLibrary("androidx.espresso.core").get())
                add("implementation", libs.findLibrary("timber").get())
            }
        }
    }
}

์ด ๋ชจ๋“ˆ ๋ฐ–์—์„œ ์‚ฌ์šฉํ•˜์ง€ ์•Š์„ ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— internal๋กœ ์„ ์–ธํ•˜์—ฌ ์™ธ๋ถ€๋กœ ๋ถ€ํ„ฐ ๊ณต๊ฐœ๋ฅผ ๋ง‰๋Š”๋‹ค. ๊ทธ๋ฆฌ๊ณ  dependency ์Šค์ฝ”ํ”„์— ๋“ค์–ด๊ฐˆ ๊ฐ implementation ๊ตฌ๋ฌธ์„ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค. ์—ฌ๊ธฐ์„œ ์•„์ฃผ ์ค‘์š”ํ•œ ์ ์ด ์žˆ๋‹ค. ๋ฒ„์ „ ์นดํƒˆ๋กœ๊ทธ์—์„œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ •๋ณด๋ฅผ ๊ธ์–ด์˜ฌ ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ์ € `libs`๊ฐ€ build-logic์˜ settings.gradle.kts์— ์žˆ๋Š” `create("libs")`์ผ๊นŒ? ์•„๋‹ˆ๋‹ค.

 

create("libs")๋Š” libs๋ผ๋Š” ์ด๋ฆ„์„ ๋ถ™์—ฌ์„œ ์›๋ž˜ ๋ฃจํŠธ ์ˆ˜์ค€ ๋ฒ„์ „ ์นดํƒˆ๋กœ๊ทธ ๋‚ด์šฉ์„ ์‚ฌ์šฉํ•ด ์ƒˆ ๋ฒ„์ „ ์นดํƒˆ๋กœ๊ทธ๋ฅผ ์ƒ์„ฑํ•˜๋ฉฐ settings.gradle.kts ํŒŒ์ผ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ด๊ณ  `val Project.libs get()` ๋Š” ์œ„์—์„œ ์ •์˜ํ•œ libs ๋ฒ„์ „ ์นดํƒˆ๋กœ๊ทธ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•œ ์ฝ”๋“œ๋‹ค. ๋นŒ๋“œ ์Šคํฌ๋ฆฝํŠธ๋‚˜ ํ”Œ๋Ÿฌ๊ทธ์ธ ์ฝ”๋“œ ๋‚ด์—์„œ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ์ด ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•ด์•ผ๋œ๋‹ค.

 

์ด๊ฑฐ ๋•Œ๋ฌธ์— 2์‹œ๊ฐ„์„ ๋‚ ๋ ธ๋‹ค.

 

์ € libs๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด Project์˜ ํ™•์žฅ ํ”„๋กœํผํ‹ฐ๋ฅผ ๋งŒ๋“ค์–ด ์ฃผ๊ฒ ๋‹ค.

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

์ด๊ฑธ ์‚ฌ์šฉํ•˜๋ฉด libs์— ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋‹ค. ์ด์ œ hilt๋„ ํ•œ๋ฒˆ ์Šคํฌ๋ฆฝํŠธ ํ™” ํ•ด๋ณด์ž.

import extenstion.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies

internal class AndroidHiltConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            with(pluginManager){
                apply("com.google.dagger.hilt.android")
                apply("org.jetbrains.kotlin.kapt")
            }

            dependencies {
                add("kapt", libs.findLibrary("hilt.compiler").get())
                add("kapt", libs.findLibrary("androidx.hilt.compiler").get())
                add("implementation", libs.findLibrary("hilt.android").get())
            }
        }
    }
}

pluginManager๋กœ ์ง€์ •ํ•ด์ค˜์•ผ build.gradle.kts์˜ plugin ์Šค์ฝ”ํ”„์— ๋“ค์–ด๊ฐ„๋‹ค. ์ฃผ์˜ํ•  ์ ์€ libs.findPlugin("").get()์œผ๋กœ ์ ‘๊ทผํ•˜๋ฉด ๋นŒ๋“œ๊ฐ€ ์•ˆ๋œ๋‹ค๋Š” ์ ์ด๋‹ค. apply๋Š” ํ”Œ๋Ÿฌ๊ทธ์ธ id๋ฅผ ๋ฐ›์•„์•ผ๋˜๋Š”๋ฐ get์œผ๋กœ ์ ‘๊ทผํ•˜๋ฉด `Provider<PluginDependency>`๋ฅผ ์ œ๊ณตํ•ด์„œ ๋นŒ๋“œ์— ์‹คํŒจํ•œ๋‹ค.

 

libs๋ฅผ ํ™œ์šฉํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์•„๋ž˜์ฒ˜๋Ÿผ ๋ฐ”๊ฟ”์•ผํ•œ๋‹ค. ์ฒซ๋ฒˆ์งธ get์œผ๋กœ Provider๋ฅผ ๋จผ์ € ๊ฐ€์ ธ์˜ค๊ณ , ๋‘๋ฒˆ์งธ get์œผ๋กœ ์‹ค์ œ ํ”Œ๋Ÿฌ๊ทธ์ธ ๊ฐ์ฒด๋ฅผ ๊ฐ€์ ธ์˜จ ๋’ค, ๊ทธ๊ฒƒ์— ๋Œ€ํ•œ id๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐฉ์‹์ด๋‹ค.

import extenstion.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies

internal class AndroidHiltConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            with(pluginManager){
                apply(libs.findPlugin("hilt.gradle.plugin").get().get().pluginId)
                apply(libs.findPlugin("kotlin.kapt").get().get().pluginId)
            }

            dependencies {
                add("kapt", libs.findLibrary("hilt.compiler").get())
                add("kapt", libs.findLibrary("androidx.hilt.compiler").get())
                add("implementation", libs.findLibrary("hilt.android").get())
            }
        }
    }
}

dependency๋Š” implement๋‚˜ kapt ๊ฐ™์€ configure method๋กœ ๊ฐ์ฒด ๋Œ์–ด๋‹ค ์“ฐ๋Š” ๋ฐ˜๋ฉด plugin์€ ๋ฐ”๋กœ ์‚ฌ์šฉํ•˜๋Š” ์  ๋•Œ๋ฌธ์— ์ด๋Ÿฐ ์ฐจ์ด๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

 

์ด๋ ‡๊ฒŒ ๋งŒ๋“  ํ”Œ๋Ÿฌ๊ทธ์ธ๋“ค์€ ๋‹ค๋ฅธ ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์ •์˜ํ•  ๋•Œ๋„ ์žฌ์‚ฌ์šฉ ํ•  ์ˆ˜ ์žˆ๋‹ค.

internal class AndroidFeatureConventionPlugin : Plugin<Project> {

    override fun apply(target: Project) {
        with(target) {
            with(pluginManager) {
                apply("drtaa.plugin.common")
                apply("drtaa.plugin.hilt")
                apply(libs.findPlugin("navigation.safe.args").get().get().pluginId)
            }

            dependencies {
                add("implementation", project(":core-data"))
                add("implementation", project(":core-ui"))
                add("implementation", project(":core-model"))
                add("implementation", libs.findLibrary("navigation.ui.ktx").get())
                add("implementation", libs.findLibrary("navigation.fragment.ktx").get())
                add("implementation", libs.findLibrary("androidx.hilt.navigation.fragment").get())
                add("implementation", libs.findLibrary("datastore.preferences").get())
                add("implementation", libs.findLibrary("glide").get())
                add("implementation", libs.findLibrary("lifecycle.runtime.ktx").get())
                add("implementation", libs.findLibrary("lifecycle.extensions").get())
                add("implementation", libs.findLibrary("coroutines.android").get())
                add("implementation", libs.findLibrary("coroutines.core").get())
            }
        }
    }
}

 

๊ทธ๋Ÿผ ์–ผ๋งˆ๋‚˜ ๋ณด์ผ๋Ÿฌ ํ”Œ๋ ˆ์ดํŠธ๊ฐ€ ์ค„์—ˆ์„๊นŒ?

plugins {
    alias(libs.plugins.android.library)
    alias(libs.plugins.jetbrains.kotlin.android)
    alias(libs.plugins.hilt.gradle.plugin)
    alias(libs.plugins.kotlin.kapt)
    alias(libs.plugins.navigation.safe.args)
}

...

dependencies {
// modules
    implementation(project(":core-data"))
    implementation(project(":core-ui"))
    implementation(project(":core-model"))

    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.appcompat)
    implementation(libs.material)
    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)

    implementation(libs.bundles.feature.default)

    kapt(libs.hilt.compiler)
    kapt(libs.androidx.hilt.compiler)
}

์ด๋žฌ๋˜ ์ฝ”๋“œ๊ฐ€ ์ด๋ ‡๊ฒŒ ๋ฐ”๋€Œ์—ˆ๋‹ค.

plugins {
    alias(libs.plugins.android.library)
    alias(libs.plugins.jetbrains.kotlin.android)
    id("drtaa.plugin.feature")
}

...

dependencies {

}

 

 

๋„์›€์ด ๋๋‹ค๋ฉด ๋Œ“๊ธ€์ด๋‚˜ ๊ณต๊ฐ ๋ฒ„ํŠผ ํ•œ ๋ฒˆ์”ฉ ๋ˆ„๋ฅด๊ณ  ๊ฐ€์ฃผ์„ธ์š”!

 

๋ฐ˜์‘ํ˜•
COMMENT