Android

코인 Gradle 청소 후기

Gradle 언어를 Groovy 에서 Kotlin DSL로 변경하고, 이것저것 청소를 해봤습니다.

Gradle 언어를 Groovy 에서 Kotlin DSL로 변경하고, 이것저것 청소를 해봤습니다.

코드 먼저 확인해보려면 해당 PR을 확인해주세요!

Groovy 에서 Kotlin DSL 적용 시 고려할 점

기본적으로 Groovy 언어로 되어있는 gradle 파일을 Kotlin DSL 로 마이그레이션 할 때 주의할 점이 있습니다.

자세한 내용을 보고싶으면, 공식문서를 참고해주세요.

마이그레이션 들어가기 앞서 주의할 점은 build.gradle 파일 → build.gradle.kts 로 파일명을 변경할 때, kts 파일에서 groovy 언어를 인식하지 못하기 때문에 기존 build.gradle 파일에서 어느정도 마이그레이션을 진행한 뒤, kts 확장자 파일로 변경해주시면 스트레스를 덜 받습니다.

  • groovy 언어는 메서드 호출 시 괄호를 생략하지만, kotlin dsl 에서는 괄호가 필요합니다.
compileSdkVersion 30 // Groovy
compileSdkVersion(30) // Kotlin DSL
  • groovy 언어에서는 속성 할당 시 = 연산자가 필요없지만, kotlin dsl 는 필요하다.
sourceCompatibility JavaVersion.VERSION_17 // Groovy
sourceCompatibility = JavaVersion.VERSION_17 // Kotlin DSL
  • groovy 언어에서는 작은 따옴표를 사용하고, kotlin dsl 에서는 큰 따옴표를 사용한다.
id 'com.android.application' // Groovy
id ("com.android.application") // Kotlin DSL
  • 변수 정의 방법
def building64Bit = false // Groovy
val building64Bit = false // Kotlin DSL
  • 부울 속성의 접두사
minifyEnabled true // Groovy
isMinifyEnabled = true // Kotlin DSL
  • 리스트 및 맵 형식
jvmOptions += ["-Xms4000m", "-Xmx4000m", "-XX:+HeapDumpOnOutOfMemoryError</code>"] // Groovy
jvmOptions += listOf("-Xms4000m", "-Xmx4000m", "-XX:+HeapDumpOnOutOfMemoryError") // Kotlin DSL
def myMap = [key1: 'value1', key2: 'value2'] // Groovy
val myMap = mapOf("key1" to "value1", "key2" to "value2") // Kotlin DSL

마이그레이션하면서 진행된 코드 정리

마이그레이션을 진행하면서 흔하게 사용되는 build.gradle 은 groovy 에서 Kotlin DSL 로 어떻게 변화되었는 지 전체적인 코드를 비교하여 정리하려고 합니다. 추가적으로, 코인 서비스에서는 version catalog 가 사용되었으며, convention plugin 어떤 것을 추가했는 지 정리해보겠습니다. 코드의 순서는 Groovy, Kotlin DSL 순서입니다.

build.gradle(project)

  • Groovy
buildscript {
    repositories {
        google()
        mavenCentral()
    }

    ext {
        /* Common */
        compileSdkVersion = 34
        minSdkVersion = 21
        targetSdkVersion = 34
        versionName = "3.4.3"
        minVersionCode = 30403
    }

    dependencies {
        classpath libs.android.gradle.tool
        classpath libs.kotlin.gradle
        classpath libs.android.gradle.crashlytics
        classpath libs.firebase.appdistribution.gradle
        classpath libs.hilt.gradle.plugin
    }
}

plugins {
    alias(libs.plugins.android.application) apply false
    alias(libs.plugins.kotlin.android) apply false
    alias(libs.plugins.hilt) apply false
    alias(libs.plugins.kotlin.jvm) apply false
    alias(libs.plugins.google.service) apply false
    alias(libs.plugins.androidLibrary) apply false
}

tasks.register('clean', Delete) {
    delete rootProject.buildDir
}
  • Kotlin DSL
buildscript {
    repositories {
        google()
        mavenCentral()
    }

    extra.apply {
        set("compileSdkVersion", 34)
        set("minSdkVersion", 24)
        set("targetSdkVersion", 34)
        set("versionName", "3.4.3")
        set("versionCode", 30403)
    }

    dependencies {
        classpath(libs.android.gradle.tool)
        classpath(libs.kotlin.gradle)
        classpath(libs.android.gradle.crashlytics)
        classpath(libs.firebase.appdistribution.gradle)
        classpath(libs.hilt.gradle.plugin)
    }
}

plugins {
    alias(libs.plugins.android.application) apply false
    alias(libs.plugins.kotlin.android) apply false
    alias(libs.plugins.hilt) apply false
    alias(libs.plugins.kotlin.jvm) apply false
    alias(libs.plugins.google.service) apply false
    alias(libs.plugins.androidLibrary) apply false
}

tasks.register<Delete>("clean") {
    delete(rootProject.buildDir)
}

build.gradle 플러그인 id 추가하는 방법

// groovy
plugins {
    id 'com.android.application'
}

// Kotlin DSL
plugins {
    id ("com.android.application")
}
  • Groovy
android {
    defaultConfig {
        manifestPlaceholders = [naverMapKey: getNaverMapKey()]
    }
}

def getNaverMapKey() {
    Properties properties = new Properties()
    properties.load(project.rootProject.file('local.properties').newDataInputStream())
    return properties.getProperty('navermap_key')
}
  • Kotlin DSL
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties

android {
    defaultConfig {
        manifestPlaceholders["naverMapKey"] = getPropertyKey("navermap_key")
    }
}

fun getPropertyKey(propertyKey: String): String {
    val nullableProperty: String? =
        gradleLocalProperties(rootDir).getProperty(propertyKey)
    return nullableProperty ?: "null"
}

local.properties 에는 다음과 같이 navermap key 가 선언되어 있을겁니다.

navermap_key=******* // 쌍따음표 없이

다음 Manifest 에도 다음과 같이 메타데이터로 navermap key 를 할당할 수 있습니다.

<meta-data
  android:name="com.naver.maps.map.CLIENT_ID"
  android:value="${naverMapKey}"/>

서명 설정하는 방법

해당 관련 Kotlin DSL 코드를 자세히 보기 위해서는 문서를 참고해주세요.

  • Groovy
android {
signingConfigs {
    release {
        storeFile file("./team_kap_android.jks")
        storePassword = getPassword()
        keyAlias = "koin_release_key"
        keyPassword = getPassword()
    }
}
}

fun getPassword() {
  val properties = new Properties()
  properties.load(project.rootProject.file('local.properties').newDataInputStream())
  return properties.getProperty('koin_store_password')
}
  • Kotlin DSL
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties

android {
    signingConfigs {
        create("release") {
            storeFile = file("./team_kap_android.jks")
            storePassword = getPropertyKey("koin_store_password")
            keyAlias = getPropertyKey("koin_release_key")
            keyPassword = getPropertyKey("koin_store_password")
        }   
    }
}

fun getPropertyKey(propertyKey: String): String {
  val nullableProperty: String? =
      gradleLocalProperties(rootDir).getProperty(propertyKey)
  return nullableProperty ?: "null"
}

빌드 타입 구분, 디버그 & 릴리즈

  • Groovy
android {
    buildTypes {
        debug {
            debuggable true
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            buildConfigField "boolean", "IS_DEBUG", "true"
            firebaseCrashlytics {
                mappingFileUploadEnabled false
            }
        }
            
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            buildConfigField "boolean", "IS_DEBUG", "false"
            signingConfig = signingConfigs.release
            firebaseAppDistribution {
                artifactType = "AAB"
                releaseNotes = "${project.versionName} release"
                groups = "bcsd"
            }
        }
    }
}
  • Kotlin DSL
android {
  buildTypes {
    getByName("debug") {
      isDebuggable = true
      isMinifyEnabled = false
      proguardFiles(
          getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
      )
      buildConfigField("Boolean", "IS_DEBUG", "true")
      firebaseCrashlytics {
          mappingFileUploadEnabled = false
      }
    }

    getByName("release") {
      isMinifyEnabled = true
      proguardFiles(
          getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
      )
      buildConfigField("Boolean", "IS_DEBUG", "false")
      signingConfig = signingConfigs.getByName("release")
      firebaseAppDistribution {
          artifactType = "AAB"
          releaseNotes = "${rootProject.extra["versionName"]} release"
          groups = "bcsd"
      }
    }
  }
}

Convention Plugin, Application 과 Library Extension 구분하기

Convention Plugin extension 을 다음과 같이 진행할 때, library 모듈에서는 LibraryExtension 을 사용하고, application 모듈에서는 ApplicationExtension 을 사용하여 혼동해서 사용하지 말아야 합니다. 기존 코인 서비스에서 Hilt ConventionPlugin 을 ApplicationExtension 을 사용하여 의존성을 주입하고 있어 library 모듈인 data, core 모듈에서 사용하지 못한 경험이 있습니다.

apply("com.android.library")

extensions.configure<LibraryExtension> {
    configureAndroidLibrary(this)
}
apply("com.android.application")

extensions.configure<ApplicationExtension> {
  configureAndroidProject(extension)
} 

추가적으로, ConventionPlugin 을 진행하면서 FirebaseConventionPlugin, JavaLibraryConventionPlugin 을 추가해보았습니다.

소감

Groovy 에서 Kotlin DSL 로 마이그리이션 하면서 기존에 프로젝트를 하면서 Groovy 에서의 오류 추적이 어려운 것을 Kotlin DSL 로 한편 쉬워졌을 거라 기대합니다. 코인의 모든 모듈의 gradle 을 살피고 정리해보면서, core와 feature 단위 별 모듈화의 필요성을 느꼈습니다. 혼자 프로젝트를 하는 것이 아닌 다양한 사람들이 같이 협업하다보니 각자가 진행하는 feature 단위 모듈에서 진행하면 gradle 도 훨씬 깔끔해질 것 같습니다. core 는 단일모듈로 있는데, 해당 모듈에서도 다양한 라이브러리가 있어 analytics, notification, designsystem 등 모듈화의 필요성을 느꼈습니다. 간단히 진행될 줄 알았던 해당 작업은 저에게 모듈화의 중요성을 깨닫게 해주었습니다.

댓글

질문과 경험을 댓글로 나눠주세요. 댓글은 GitHub 계정으로 작성할 수 있습니다.