안드로이드 - build-logic 모듈 gradle plugin 구축

이우건·2024년 7월 3일
0

안드로이드

목록 보기
17/20

이 글은 드로이드나이츠 githubGradle kotlin 컨벤션 플러그인으로 효율적으로 모듈 관리하기를 참고하여 작성하였습니다.

개요

안드로이드 프로젝트를 멀티 모듈로 설계할 경우 다음과 같은 이점들이 있다.

빠른 빌드 시간
관심사 분리
테스트 용이

하지만 프로젝트가 커질수록 많은 모듈들이 생겨나고 이에 따른 build.gradle.kts 파일이 모듈마다 중복으로 생성되게 된다.

build.gradle.kts(:app)

plugins {
    alias(libs.plugins.androidApplication)
    alias(libs.plugins.jetbrainsKotlinAndroid)
}

android {
    namespace = "com.hmm.alarm_project"
    compileSdk = 34

    defaultConfig {
        applicationId = "com.hmm.alarm_project"
        minSdk = 26
        targetSdk = 34
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables {
            useSupportLibrary = true
        }
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = "1.8"
    }
    buildFeatures {
        compose = true
    }
    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.1"
    }
    packaging {
        resources {
            excludes += "/META-INF/{AL2.0,LGPL2.1}"
        }
    }
}

dependencies {
	...
}

위와 같은 build.gradle 스크립트가 모듈마다 생성될 것이고 만약 Hilt 같은 라이브러리를 추가할 경우 hilt가 필요한 모듈마다 Hilt 관련 의존성을 추가해줘야 할 것이다.

이를 하나의 build-logic 모듈의 gradle plugin을 통해 중복되는 코드를 하나로 모으고 새로운 모듈을 생성할 때 plugin에 한 줄만 추가해주면 되는 커스텀 gradle plugin을 만들어보고자 한다.

Gradle version Catalog

최근 버전의 안드로이드 스튜디오에서 프로젝트를 생성하면 루트 프로젝트의 gradle 폴더에 다음과 같은 파일이 생성된다. libs.versions.toml

[versions]
...
coreKtx = "1.13.1"
...

[libraries]
...
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
...

[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

Gradle 버전 카탈로그를 사용하면 확장 가능한 방식으로 종속 항목 및 플러그인을 추가하고 유지할 수 있다. Gradle 버전 카탈로그를 사용하면 여러 모듈이 있을 때 종속 항목과 플러그인을 더 쉽게 관리할 수 있다.

위와 같은 카탈로그 파일은 build.gradle.kts에서 다음과 같이 대응되어 사용이 가능하다.

build.gradle.kts(:module)

// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
    alias(libs.plugins.androidApplication) apply false
    alias(libs.plugins.jetbrainsKotlinAndroid) apply false
}

build.gradle.kts(:app)

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

위의 Gradle 버전 카탈로그 파일을 이용해 build-logic 모듈을 만들고 Gradle plugin을 만들 예정이다.

Gradle 버전 카탈로그의 자세한 설명은 공식문서에서 확인 할 수 있습니다.
안드로이드 공식문서 버전 카탈로그로 빌드 이전

모듈 생성

프로젝트를 다음과 같은 모듈 구성으로 구성했다고 하자.

모듈을 생성하는 방법은 다음과 같다.

1. 루트 프로젝트에서 우클릭 -> New -> Module

여기서 보통 우리가 사용하는 모듈은 다음과 같다.

phone & Tablet : 안드로이드 프로젝트를 만들 때 기본으로 생성되는 app 모듈이며 빌드 결과로 APK 파일이 생성됩니다. 하나의 프로젝트에 여러 개의 app 모듈이 들어갈 수 있으며 각각 빌드 가능합니다. (앱이 처음 시작하는 부분)

Android Library : 안드로이드 프로젝트에서 지원되는 모든 파일 형식을 포함할 수 있습니다. 다른 Application 모듈의 종속 항목으로 추가할 수 있습니다. 빌드 결과로 AAR 파일이 생성됩니다. (AndroidManifest 생성)

Java or Kotlin Library : 순수한 Java or Kotlin 코드로 이루어진 모듈입니다. 안드로이드 프레임워크로 부터 독립적인 기능을 구현할 때 사용합니다. 빌드 결과로 JAR 파일이 생성됩니다. (domain 모듈, build-logic 모듈)

일반적으로 phone & Tablet으로 모듈은 초록색 원 (app 모듈) 아이콘이 생기고
Java & kotlin 모듈은 파란색 사각형, Android Library 모듈은 막대 그래프 같은 아이콘이 생긴다. 이 아이콘이 절대적인 것이 아니며 내부 구성요소에 따라 변경 될 수 있다.

이제 build-logic 모듈을 생성해보겠다.

build-logic

Settings.gradle.kts

build-logic에는 AndroidManifest 파일이 필요없으므로 Java or Kotlin Library로 모듈로 생성했다.

build-logic 모듈을 만들고 필요없는 파일, 클래스들을 삭제하면 다음과 같은 상태일 것이다.

여기서 루트 프로젝트의 libs.versions.toml에 접근하기 위해 settings.gradle.kts을 만들어줘야한다.

build-logic 우클릭 -> New -> kotlin class/file -> kotlin script로 setting.gradle.kts를 만들어주자.

settings.gradle.kts(build-logic)

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

처음에 settings.gradle.kts를 만들고 위의 코드를 넣었다면 script를 인식할 수 없을 것이다. 이를 해결하기 위해 루트 프로젝트의 settings.gradle.kts의 수정이 필요하다.

settings.gradle.kts(root project)

pluginManagement {
    includeBuild("build-logic") // 이 코드를 추가!
    repositories {
        google {
            content {
                includeGroupByRegex("com\\.android.*")
                includeGroupByRegex("com\\.google.*")
                includeGroupByRegex("androidx.*")
            }
        }
        mavenCentral()
        gradlePluginPortal()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}

rootProject.name = "side-project"
include(":app")
include(":core")
include(":core:data")
include(":core:database")
include(":core:designsystem")
include(":core:domain")
include(":core:model")
include(":feature:main")
include(":core:commons")
include(":feature:home")
include(":feature:moviedetail")
include(":feature:mymovie")
include(":feature:component")
// include(":build-logic") // 중복 부분 제거

위 처럼 root 프로젝트의 settings.gradle.kts를 수정하고 sync now를 한다면 build-logic의 settings.gradle.kts가 정상적으로 동작 할 것이다.
이로써 build-logic 모듈에서 libs.versions.toml 파일에 접근 할 수 있을 것이다.
(settings.gradle.kts 추가)

build.gradle.kts

build.gradle 파일에서는 사용자가 만든 커스텀 플러그인을 등록하고 다른 모듈에서 커스텀 플러그인을 id로 접근 할 수 있게 한다.

build.gradle.kts(build-logic)

plugins {
    `kotlin-dsl`
    `kotlin-dsl-precompiled-script-plugins`
}

dependencies {
    implementation(libs.android.gradlePlugin)
    implementation(libs.kotlin.gradlePlugin)
    compileOnly(libs.compose.compiler.gradle.plugin) // kotlin 2.0.0 이상을 사용하는 경우 
                                                     // compose compiler gradle plugin을 주입해야합니다.
}

gradlePlugin {
    plugins {
        register("androidHilt") {
            id = "hmm.android.hilt" // 이 id가 플러그인의 id가 됩니다.
            implementationClass = "com.hmm.sideproject.HiltAndroidPlugin"
        }
        register("kotlinHilt") {
            id = "hmm.kotlin.hilt"
            implementationClass = "com.hmm.sideproject.HiltKotlinPlugin"
        }
        register("androidRoom") {
            id = "hmm.android.room"
            implementationClass = "com.hmm.sideproject.AndroidRoomPlugin"
        }

        register("androidRetrofit") {
            id = "hmm.android.retrofit"
            implementationClass = "com.hmm.sideproject.AndroidRetrofitPlugin"
        }
    }
}

위의 dependencies에서 gradlePlugin은 libs.versions.toml에 추가를 해줘야 한다.

libs.versions.toml

[versions]
androidGradlePlugin = "8.3.2"
Kotlin = “2.0.0”

[libraries]
android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" }

kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
compose-compiler-gradle-plugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" }

kotlin 버전 2.0.0 부터는 compose compiler gradle plugin을 추가해줘야한다.

커스텀 플러그인 설정

필자는 처음에 안드로이드 스튜디오로 프로젝트를 만들 때 기본적으로 만들어지는 build.gradle의 내용은 신경을 쓰지 않고 개발을 해왔었다. 하지만 커스텀 플러그인을 만들기 위해서는 개발자가 직접 Android Library, kotlin, compose 설정들을 직접 플러그인을 만들어 추가를 해줘야한다.

필수 설정 plugin을 만들고 전체적으로 플러그인과 script를 완성하면 다음과 같은 build-logic 모듈의 구조를 나타낸다.

위 plugin, script 코드는 드로이드나이츠 github 에서 자세하게 확인 할 수 있습니다.

Hilt, Room, Retrofit은 필자가 따로 만들어서 build.gradle.kts(build-logic)에 등록을 해주어 사용 할 수 있다.

여기서 script의 파일 이름은 OOO.gradle.kts의 형태로 만들어줘야한다. kotlin-dsl로 gradle script를 만들고 있기 때문이다.

간단하게 몇가지 파일만 짚어보고 넘어가보도록 하자.

KotlinAndroid.kt

package com.hmm.sideproject

import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.provideDelegate
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

/**
 * https://github.com/android/nowinandroid/blob/main/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt
 */
internal fun Project.configureKotlinAndroid() {
    // Plugins
    pluginManager.apply("org.jetbrains.kotlin.android")

    // Android settings
    androidExtension.apply {
        compileSdk = 34

        defaultConfig {
            minSdk = 26
        }

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

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

    configureKotlin()

    val libs = extensions.libs

    dependencies {
        add("coreLibraryDesugaring", libs.findLibrary("android.desugarJdkLibs").get())
    }
}

internal fun Project.configureKotlin() {
    tasks.withType<KotlinCompile>().configureEach {
        kotlinOptions {
            jvmTarget = JavaVersion.VERSION_17.toString()
            // Treat all Kotlin warnings as errors (disabled by default)
            // Override by setting warningsAsErrors=true in your ~/.gradle/gradle.properties
            val warningsAsErrors: String? by project
            allWarningsAsErrors = warningsAsErrors.toBoolean()
            freeCompilerArgs = freeCompilerArgs + listOf(
                "-opt-in=kotlin.RequiresOptIn",
            )
        }
    }
}

KotlinAndroid.kt 파일은 build.gradle.kts(:app)의 설정들이 포함되어 있는 것을 볼 수 있다.
Project 인터페이스를 확장하여 구현하였으며, script 환경이 아니기 때문에 android 블록은 androidExtension으로 설정을 하고 plugin 블록은 pluginManager으로 설정한다.
extension 관련 변수를 사용하기 위해서는 Extension.kt 파일의 내용을 구현해야한다.

ComposeAndroid.kt

package com.hmm.sideproject

import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.dependencies
import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension

internal fun Project.configureComposeAndroid() {
    with(plugins) {
        apply("org.jetbrains.kotlin.plugin.compose")
    }

    val libs = extensions.libs
    androidExtension.apply {
        dependencies {
            val bom = libs.findLibrary("androidx-compose-bom").get()
            add("implementation", platform(bom))
            add("androidTestImplementation", platform(bom))

            add("implementation", libs.findBundle("androidx").get())

            add("androidTestImplementation", libs.findLibrary("androidx.test.ext").get())
            add("androidTestImplementation", libs.findLibrary("androidx.test.espresso.core").get())
            add("androidTestImplementation", libs.findLibrary("androidx.compose.ui.test").get())
            add("debugImplementation", libs.findLibrary("androidx.compose.ui.tooling").get())
            add("debugImplementation", libs.findLibrary("androidx.compose.ui.testManifest").get())
        }
    }

    extensions.getByType<ComposeCompilerGradlePluginExtension>().apply {
        enableStrongSkippingMode.set(true)
        includeSourceInformation.set(true)
    }
}

ComposeAndroid.kt 파일은 Compose 관련 dependencies 설정들이 포함되어 있는 것을 알 수 있다.

libs 변수를 통해 libs.versions.toml 파일의 내용에 접근 할 수 있으며 findLibrary 함수로 libs.versions.toml [libraries]의 라이브러리들을 찾아올 수 있다. findBundle 같은 경우는 libs.versions.toml 에서 [bundles]로 라이브러리를 묶어놓은 것을 찾아오는 함수이다.

OOO.android.feature.gradle.kts

import com.hmm.sideproject.configureHiltAndroid
import com.hmm.sideproject.libs

plugins {
    id("hmm.android.library")
    id("hmm.android.compose")
}

android {
    defaultConfig {
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }
}

configureHiltAndroid()

dependencies {
    implementation(project(":core:model"))
    implementation(project(":core:designsystem"))
    implementation(project(":core:domain"))
    implementation(project(":core:data"))

    val libs = project.extensions.libs
    implementation(libs.findLibrary("hilt.navigation.compose").get())
    implementation(libs.findLibrary("androidx.compose.navigation").get())
    implementation(libs.findLibrary("material").get())
    androidTestImplementation(libs.findLibrary("androidx.compose.navigation.test").get())

    implementation(libs.findLibrary("androidx.lifecycle.viewModelCompose").get())
    implementation(libs.findLibrary("androidx.lifecycle.runtimeCompose").get())
}

우리가 가장 많이 사용하게 될 feature 커스텀 플러그인이다. 개발자는 feature 관련 모듈을 추가할 때마다 플러그인에 id(OOO.android.feature) 한 줄로 feature 개발에 필요한 설정들을 한 번에 가져올 수 있을 것 이다.

build.gradle.kts(:feature:home)

plugins {
    id("hmm.android.feature")
    alias(libs.plugins.kotlin.android)
}

android {
    namespace = "com.hmm.home"
}

dependencies {
    implementation(projects.core.data)
    implementation(projects.core.domain)
    implementation(projects.core.designsystem)
    implementation(projects.core.commons)

    // image load
    implementation(libs.landscapist.bom)
    implementation(libs.landscapist.coil)
}

feature/home 모듈의 build.gradle 파일이다. 기존 중복되는 android 관련 설정은 지우고 namespace만 설정하면 된다. plugin의 OOO.android.feature 플러그인을 추가하면 다른 kotlin, compose 관련 설정들을 사용 할 수 있다.

참조

드로이드나이츠 github
Gradle kotlin 컨벤션 플러그인으로 효율적으로 모듈 관리하기
[DroidKnights 2023] 윤영직 - Gradle Kotlin 컨벤션 플러그인으로 효율적으로 멀티 모듈 관리하기
https://everyday-develop-myself.tistory.com/309#article-5-1--%EB%AA%A8%EB%93%88%EC%9D%98-%ED%98%95%ED%83%9C

profile
머리가 나쁘면 기록이라도 잘하자

0개의 댓글