ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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% 안전한 코드라고는 보장할 수 없어서

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

     

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

    반응형

    댓글

Designed by Tistory.