안드로이드

Kotlin-DSL을 활용하여 Dependency 관리하기 - (2)

익세망 2021. 5. 17. 02:19
 

Kotlin-DSL을 활용하여 Dependency 관리하기 - (1)

Groovy를 활용해서 Dependency를 관리하는 방법은 흔히 알려져있고 자료도 많이 존재한다. Kotlin-DSL-Gradle을 설정하는 방법을 작성하고 이를 활용해서 Dependency를 관리하는 방법을 소개하려고 한다. 일

developer-munny.tistory.com

이전 포스팅에서 Kotlin-DSL Gradle 설정하는 법 까지 진행 했다.

이를 활용해서 Dependency를 관리 하는법을 알아보자

 

Kotlin-DSL을 활용하여 Dependency 관리하기 - (1)


 

build.gradle.kts (:buildSrc)


gradle 파일 외부에서 kotlin 언어의 기능을 사용하기 위해서는 먼저 buildSrc라는 디렉토리를 만든다.

project 단에서 buildSrc라는 폴더를 만든 후 하위 디렉토리에 build.gradle.kts 파일을 만든 후 'sync now'를 누른다.

// project/buildSrc/build.gradle.kts

plugins {
    `kotlin-dsl`
}

repositories {
    jcenter()
}

plugins을 작성하지 않으면 다른 gradle에서 해당 디렉토리 하위에 있는 코드를 인식할 수 없다.

 

이후 buildSrc/src/main/java 디렉토리를 만들어주고 Version 과 Libs 파일을 만든다.

제대로 진행하고 있다면 위 이미지와 같은 구성이 된다.

 

 

Dependency를 Kotlin 코드로 관리 하기


internal object Versions {
    const val KOTLIN_STDLIB = "1.3.72"

    const val ANDROID_CORE_KTX = "1.3.2"
    const val ANDROID_APPCOMPAT = "1.2.0"
    const val ANDROID_MATERIAL = "1.3.0"
    const val ANDROID_CONSTRAINT = "2.0.4"

    const val TEST_JUNIT = "4.+"

    const val ANDROID_TEST_JUNIT = "1.1.2"
    const val ANDROID_TEST_ESPRESSO = "3.3.0"
}

object Libs {
    const val KOTLIN_STDLIB = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.KOTLIN_STDLIB}"

    const val ANDROID_CORE_KTX = "androidx.core:core-ktx:${Versions.ANDROID_CORE_KTX}"
    const val ANDROID_APPCOMPAT = "androidx.appcompat:appcompat:${Versions.ANDROID_APPCOMPAT}"
    const val ANDROID_MATERIAL = "com.google.android.material:material:${Versions.ANDROID_MATERIAL}"
    const val ANDROID_CONSTRAINT = "androidx.constraintlayout:constraintlayout:${Versions.ANDROID_CONSTRAINT}"

    const val TEST_JUNIT = "junit:junit:${Versions.TEST_JUNIT}"

    const val ANDROID_TEST_JUNIT = "androidx.test.ext:junit:${Versions.ANDROID_TEST_JUNIT}"
    const val ANDROID_TEST_ESPRESSO = "androidx.test.espresso:espresso-core:${Versions.ANDROID_TEST_ESPRESSO}"
}
plugins {
    id("com.android.application")
    id("kotlin-android")
}

android {
    compileSdkVersion(29)
    buildToolsVersion("29.0.3")

    defaultConfig {
        applicationId = "com.munny.kotlindslsample"
        minSdkVersion(23)
        targetSdkVersion(29)
        versionCode(1)
        versionName("1.0")

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        getByName("release") {
            minifyEnabled(false)
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_1_8.toString()
    }
}

dependencies {

    implementation(Libs.KOTLIN_STDLIB)
    
    implementation(Libs.ANDROID_CORE_KTX)
    implementation(Libs.ANDROID_APPCOMPAT)
    implementation(Libs.ANDROID_MATERIAL)
    implementation(Libs.ANDROID_CONSTRAINT)
    
    testImplementation(Libs.TEST_JUNIT)
    
    androidTestImplementation(Libs.ANDROID_TEST_JUNIT)
    androidTestImplementation(Libs.ANDROID_TEST_ESPRESSO)
}

 

상수로 작성된 dependency들을 kotlin 코드로 옮겼다.

이렇게 작성된 코드를 보고 이런 생각을 가졌다.

똑같은 implementation이 반복되고 있으니 반복문으로 처리할 수 있지 않을까?

 

그래서 비슷한 성격을 가진 Dependency끼리 묶어주기로 했다.

 

 

라이브러리 단위로 클래스 묶기


interface Libs {
    fun getDependencies(): List<String>

    object Kotlin : Libs {
        private const val STDLIB = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.KOTLIN_STDLIB}"

        override fun getDependencies() = listOf(
            STDLIB
        )
    }

    object Android : Libs {
        private const val CORE_KTX = "androidx.core:core-ktx:${Versions.ANDROID_CORE_KTX}"
        private const val APPCOMPAT = "androidx.appcompat:appcompat:${Versions.ANDROID_APPCOMPAT}"
        private const val MATERIAL = "com.google.android.material:material:${Versions.ANDROID_MATERIAL}"
        private const val CONSTRAINT = "androidx.constraintlayout:constraintlayout:${Versions.ANDROID_CONSTRAINT}"

        override fun getDependencies() = listOf(
            CORE_KTX,
            APPCOMPAT,
            MATERIAL,
            CONSTRAINT
        )
    }

    object Test : Libs {
        private const val JUNIT = "junit:junit:${Versions.TEST_JUNIT}"

        override fun getDependencies() = listOf(
            JUNIT
        )
    }


    object AndroidTest : Libs {
        private const val JUNIT = "androidx.test.ext:junit:${Versions.ANDROID_TEST_JUNIT}"
        private const val ESPRESSO = "androidx.test.espresso:espresso-core:${Versions.ANDROID_TEST_ESPRESSO}"

        override fun getDependencies() = listOf(
            JUNIT,
            ESPRESSO
        )
    }
}
// build.gradle.kts (:app)

enum class ImplementationType {
    IMPLEMENTATION, TEST_IMPLEMENTATION, ANDROID_TEST_IMPLEMENTATION
}

dependencies {

    implementationDependencies(ImplementationType.IMPLEMENTATION, Libs.Kotlin)
    implementationDependencies(ImplementationType.IMPLEMENTATION, Libs.Android)
    implementationDependencies(ImplementationType.TEST_IMPLEMENTATION, Libs.Test)
    implementationDependencies(ImplementationType.ANDROID_TEST_IMPLEMENTATION, Libs.AndroidTest)
}

fun DependencyHandler.implementationDependencies(type: ImplementationType, libs: Libs) {
    libs.getDependencies().forEach {
        when (type) {
            ImplementationType.IMPLEMENTATION -> implementation(it)
            ImplementationType.TEST_IMPLEMENTATION -> testImplementation(it)
            ImplementationType.ANDROID_TEST_IMPLEMENTATION -> androidTestImplementation(it)
        }
    }
}

비슷한 유형에 속하는 라이브러리들을 클래스 단위로 묶고 getDependencies() 함수를 가진

Libs 인터페이스를 상속받는 구조로 바꿨다.

 

이런식으로 관리하면 코드가 간결해지지만 아직 의문이 남아있다.

"androidx.room:room-runtime:${Versions.ROOM}"
"androidx.room:room-ktx:${Versions.ROOM}"
"androidx.room:room-compiler:${Versions.ROOM}"

예시로 Room을 사용 할 때의 포함하는 Dependency를 가져왔다.

위 2개의 Dependency는 implementation으로 포함시켜야 하는데 아래의 compiler는 kapt로 포함 시켜야한다.

 

이 때 Room 단위로 클래스를 묶게되면

object Room : Libs {
    private const val CORE = "androidx.room:room-runtime:${Versions.ROOM}"
    private const val KTX = "androidx.room:room-ktx:${Versions.ROOM}"
    private const val COMPILER = "androidx.room:room-compiler:${Versions.ROOM}"

    override fun getDependencies() = listOf(
        CORE,
        KTX,
        COMPILER
    )
}

이렇게 되는데 아까 만든 함수는 여러 종류의 Dependency를 포함할 수 없으므로

코드를 추가로 작성하기로 했다.

 

 

sealed class를 활용해서 Dependency와 종류를 묶어내기


sealed class DependencyType(val value: Any) {
    data class Implementation(private val notation: Any) : DependencyType(notation)
    data class TestImplementation(private val notation: Any) : DependencyType(notation)
    data class AndroidTestImplementation(private val notation: Any) : DependencyType(notation)
    data class Kapt(private val notation: Any) : DependencyType(notation)
}

interface Libs {
    fun getDependencies(): List<DependencyType>

    object Kotlin : Libs {
        private const val STDLIB = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.KOTLIN_STDLIB}"

        override fun getDependencies() = listOf(
            DependencyType.Implementation(STDLIB)
        )
    }

    object Android : Libs {
        private const val CORE_KTX = "androidx.core:core-ktx:${Versions.ANDROID_CORE_KTX}"
        private const val APPCOMPAT = "androidx.appcompat:appcompat:${Versions.ANDROID_APPCOMPAT}"
        private const val MATERIAL = "com.google.android.material:material:${Versions.ANDROID_MATERIAL}"
        private const val CONSTRAINT = "androidx.constraintlayout:constraintlayout:${Versions.ANDROID_CONSTRAINT}"

        override fun getDependencies() = listOf(
            DependencyType.Implementation(CORE_KTX),
            DependencyType.Implementation(APPCOMPAT),
            DependencyType.Implementation(MATERIAL),
            DependencyType.Implementation(CONSTRAINT)
        )
    }

    object Room : Libs {
        private const val CORE = "androidx.room:room-runtime:${Versions.ROOM}"
        private const val KTX = "androidx.room:room-ktx:${Versions.ROOM}"
        private const val COMPILER = "androidx.room:room-compiler:${Versions.ROOM}"

        override fun getDependencies() = listOf(
            DependencyType.Implementation(CORE),
            DependencyType.Implementation(KTX),
            DependencyType.Kapt(COMPILER)
        )
    }

    object JUnit : Libs {
        private const val JUNIT = "junit:junit:${Versions.TEST_JUNIT}"
        private const val ANDROID_JUNIT = "androidx.test.ext:junit:${Versions.ANDROID_TEST_JUNIT}"

        override fun getDependencies() = listOf(
            DependencyType.TestImplementation(JUNIT),
            DependencyType.AndroidTestImplementation(ANDROID_JUNIT)
        )
    }


    object AndroidTest : Libs {
        private const val ESPRESSO = "androidx.test.espresso:espresso-core:${Versions.ANDROID_TEST_ESPRESSO}"

        override fun getDependencies() = listOf(
            DependencyType.AndroidTestImplementation(ESPRESSO)
        )
    }
}
// build.gradle.kts (:app)


dependencies {

    implementationDependencies(Libs.Kotlin)
    implementationDependencies(Libs.Android)
    implementationDependencies(Libs.Room)
    implementationDependencies(Libs.JUnit)
    implementationDependencies(Libs.AndroidTest)
}

fun DependencyHandler.implementationDependencies(libs: Libs) {
    libs.getDependencies().forEach {
        val notation = it.value

        when (it) {
            is DependencyType.Implementation -> implementation(notation)
            is DependencyType.TestImplementation -> testImplementation(notation)
            is DependencyType.AndroidTestImplementation -> androidTestImplementation(notation)
            is DependencyType.Kapt -> kapt(notation)
        }
    }
}

이렇게 작성된 코드에서는 app모듈의 gradle에서는 해당 라이브러리가 어떠한 Dependency인지 종류인지

알 필요 없이 간단하게 포함할 수 있게 됐다.

 

 

implementationDependencies 함수를 전역적으로 사용하기


이렇게 작성하게 되면 어떤 이점이 있는가 의문이 들 수 있는데

라이브러리 단위로 Dependency를 관리하면 멀티 모듈 환경에서 손쉽게 동일한

라이브러리를 추가할 수 있는 장점이 있다.

 

그런데 현재 implementationDependencies 함수는 app 모듈에 종속되어 있으므로 buildSrc로 옮겼다.

 

하지만 간단하게 옮겨질거란 예상과는 달리 함수들을 찾을 수 없다고 나온다.

아마도 내부 구현이 전부 extension 형태로 되어있어서 buildSrc에서 접근 할 수 없는 것 같다.

그런데 자세히 보면 공통적으로 DependencyHandler.add(함수명, Dependency) 형태를 호출하게

되어있다.

 

그래서 같은 형태로 호출하게 작성했다. 

// buildSrc/src/main/java/DependencyHandlerExtension.kt


fun DependencyHandler.implementationDependencies(libs: Libs) {
    libs.getDependencies().forEach { dependencyType ->
        val typeName = dependencyType.javaClass.simpleName.run {
            replace(first(), first().toLowerCase())
        }

        add(typeName, dependencyType.value)
    }
}
// build.gradle.kts (:app)


dependencies {

    implementationDependencies(Libs.Kotlin)
    implementationDependencies(Libs.Android)
    implementationDependencies(Libs.Room)
    implementationDependencies(Libs.JUnit)
    implementationDependencies(Libs.AndroidTest)
}

아까 만든 sealed class의 하위목록의 첫글자를 소문자로 변환해서 add의 인자에 넣었다.

이제 새로 만드는 모든 module에서 implementationDependencies 함수를 이용해서 쉽게 의존성을 추가할 수 있게 됐다.

 


코드가 많아서 포스팅이 생각보다 길어졌지만 과정의 코드를

기록해두고 싶은 마음에 결과물에 비해 길게 써져버렸다.

 

마지막 문단의 코드는 아무래도 상수가 들어가는 코드이기 때문에 100% 안전한 코드라고는 보장할 수 없어서

안전하게 작성하려면 이전 문단의 함수를 모듈마다 작성하면 될것같다.

 

예시로 작성한 코드는 제 깃허브 링크에서 확인 할 수 있습니다.

반응형