본문 바로가기

Android/Gradle

[Android] toml 파일을 사용하여 Gradle을 개선해보자.

728x90

회사 동료와 이야기를 하던 도중, python의 pyproject.toml이라는 것을 알게 되었고 확인해 보니 프로젝트의 빌드 시스템을 정의하는 파일이었다.

그리고 toml에 대해서 확인해보니 android 환경의 gradle에서도 적용이 가능하였고, 알고 보니 많은 프로젝트들에서 toml 파일을 사용하여 gradle의 의존성을 관리하고 있다는 것을 알게 되었다.

 

사용할 수 있다는 것을 알게되었으니, 당연히 한번 적용해봐야 하지 않겠는가?

라는 생각을 가지고, 샘플 프로젝트의 gradle에 사용되는 의존성을 toml 파일을 사용하여 관리할 수 있도록 수정해 보았고, 그 과정에 대해 간단하게 글을 작성해 보도록 하겠다.


우선,

가장 기본적인 toml이란 무엇인가 부터 알아보자.

toml이란,

Tom's Obvious, Minimal Language. 의 축약어로 설정 파일 형식 중 하나이다.
주로 구성 파일이나 설정 파일에서 사용되며, 간결하고 직관적인 문법을 제공하여 가독성이 뛰어나다.

 

라고 한다.

toml 공식 사이트에서도 정확히 어떤 동작을 하는지에 대해서는 명시되어있지 않은데,

TOML의 목표에 대해서는 다음과 같이 나와있다.

TOML은 명확한 의미를 가져 읽기 쉬운 최소한의 구성 파일 포맷을 목표로 합니다.
TOML은 해시 테이블에 분명하게 대응되도록 설계되었습니다.

 

결국,

key-value 쌍을 사용하여 구조화된 데이터를 기반으로, 직관적인 문법으로 가독성을 높여 구성 파일을 작성하는데 도움을 주는 파일 포맷이라고 생각하면 될 것 같다.

 

공식 사이트에 들어가보면 toml 파일에 대한 문법이 잘 나와있는데,

사실 기본적인 gradle 파일을 설정할 때는 크게 많은 것을 알지 못해도 상관없는 것으로 보인다.

 

그중에서, 사용되는 몇 가지만 간단하게 짚어보고 넘어가도록 하자.

  1. 주석
  2. key-value
  3. 테이블
  4. 배열

이 4가지 문법만 알 수 있다면 기본적인 gradle 파일의 의존성을 toml으로 관리할 수 있게 된다.

 

주석

# 이 줄은 전부 주석입니다
key = "value"  # 이건 줄 끝에 붙은 주석입니다
another = "# 이건 주석이 아닙니다"

 

간단하게 우리가 코드에서 사용하던 // , /* */ , /** */ 대신에 #을 사용하면 된다.

물론 주석은 사용하지 않아도 크게 문제는 없지만, 혼자 작업하는 경우가 아니라면 어떤 의존성의 모음인지, 한글로 직관적으로 알 수 있도록 추가해 주는 것도 좋은 방법이기 때문에 알고 있으면 도움이 될 것이다.

 

key-value

key = "value"

# value를 누락시킨 key 값은 유효하지 않음
key = 

# 개행되지 않고 연속적으로 작성한 key-value는 유효하지 않음
first = "Tom" last = "Preston-Werner"

 

이 부분도 가장 기본적인 것이고, 언급하지 않아도 당연히 알 수 있을 것이다.

key-value 형태이기 때문에 반드시 key와 value는 쌍으로 존재해야하며, 개행하지 않고 하나의 라인에 여러 가지 key-value를 선언할 수는 없다.

 

이때, value 형태로 들어갈 수 있는 타입은 문자열, 정수, 불리언, 배열, 시간 등 많은 타입이 존재하는데,

의존성 관리를 위해 사용한다면 문자열과 불리언 타입 등 실제 gradle에 선언하는 타입만 사용하게 될 것이다.

 

테이블

갑자기 무슨 테이블이냐? 싶겠지만, 간단하게 생각하면 하나의 블럭을 나누는 기준이 테이블이라고 생각하면 된다.

[table-1]
key1 = "some string"
key2 = 123

[table-2]
key1 = "another string"
key2 = 456

 

대괄호로 둘러쌓인 값이 테이블의 이름이며, 다음 테이블이 나오기 전까지 선언되는 모든 key-value 값의 쌍은 헤더에 선언된 테이블 안에 종속한다고 생각할 수 있다.

 

즉, table-1.key1 , table2-key2 이렇게 볼 수 있다는 것이다.

 

테이블 또한 사용하지 않아도 문제는 없겠지만, gradle에서는 버전과 plugins, library 등 여러가지 데이터를 관리해야 하는데, 이런 테이블로 명시하지 않으면 관리하는데 불편함이 반드시 존재할 것이다.

이것에 대해서는 이후에 나오는 실제 적용된 예시를 보면 이해할 수 있을 것이다.

 

마지막으로,

인라인 테이블

이다.

name = { first = "Tom", last = "Preston-Werner" }
point = { x = 1, y = 2 }
animal = { type.name = "pug" }

 

 

key의 value로 들어가는 데이터 타입인데, 왜 이 녀석만 따로 설명을 하는가 싶겠지만,

여기서 사용한 인라인 테이블을 사용하여 library의 의존성을 관리할 수 있는데,

 

 

실제 적용 예시를 본다면 동일한 형태로 라이브러리 의존성을 추가할 수 있다는 것을 알 수 있다.

 

인라인 테이블은 중괄호 사이에 정의되며, 한 줄에 표현하는 key-value 쌍의 모음이라고 볼 수 있다.

각 key-value는 쉼표로 구분이 되며, 마지막 key-value 뒤에는 쉼표가 와서는 안된다는 문법을 가지고 있다.

 

자, 이렇게 어떻게 사용하는지 알아봤으므로 실제 gradle 파일을 정리해 보도록 하자.

app module 단위의 gradle 파일을 확인해 보면 크게 apply, android, dependencies. 3가지 블럭으로 나뉘어 있다.

apply {
    plugin('com.android.application')
    plugin('org.jetbrains.kotlin.android')
    plugin('kotlin-parcelize')
    plugin('com.google.devtools.ksp')

    from('../version.gradle')
    from('../config.gradle')
    from('../core-dependencies.gradle')
}
android {
    defaultConfig {
        applicationId "com.example.composesample"
        versionCode build_version_code
        versionName build_version_name
    }

    buildFeatures {
        compose true
    }

    composeOptions {
        kotlinCompilerExtensionVersion compose_version
    }

    namespace 'com.example.composesample'
}
dependencies {
    // Modules
    implementation(project(':coordinator'))
    implementation(project(':domain'))
    implementation(project(':data'))
    implementation(project(':core'))

    // AndroidX Core
    implementation('androidx.lifecycle:lifecycle-runtime-ktx:2.8.6')
    implementation('androidx.activity:activity-compose:1.5.1')

    // Compose
    implementation("androidx.compose.ui:ui:$compose_version")
    implementation("androidx.compose.ui:ui-tooling-preview:$compose_version")
    implementation("androidx.compose.runtime:runtime-livedata:$compose_version")
    implementation("androidx.compose.material:material:$material_version")
    implementation("androidx.compose.material3:material3:$material3_version")
    ...
}

 

여기서 따로 빼내어 관리할 수 있을 것 같은 부분은 간단하게 생각해도 버전, 라이브러리 2가지가 존재할 것이다.

toml 파일은 key-value로 되어있기 때문에 versionName = "version"과 같은 형태로 선언할 수 있을 것이고,

라이브러리는 위에 언급한 것처럼 인라인 테이블을 사용하여 관리가 가능할 것이다.

 

우선 toml 파일을 만들어보도록 하자.

버전 관리를 위한 toml파일이기 대문에 libs.versions.toml이라는 파일명과 확장자를 갖도록 파일을 생성하는데,

view 타입이 android 인 경우 gradle Scripts 아래

 

view 타입이 project 인 경우 gradle package 아래에 해당 파일을 생성하면 된다.

 

파일을 만들었으면, 위의 문법에 따라서 버전을 명시해 주도록 하자.

[versions]
# Android
compileSdk = "34"
minSdk = "24"
targetSdk = "34"
versionCode = "1"
versionName = "1.0"

# Java
javaVersion = "17"

# Kotlin & KSP
kotlinVersion = "1.9.0"
kotlinCompilerExtension = "1.5.2"
ksp = "1.9.0-1.0.13"
applicationVersion = "8.5.2"
libraryVersion = "8.5.2"

 

주석과 테이블을 통해 보다 직관적이고 관리하기 쉽도록 버전을 작성해 주었다.

 

이렇게 선언해 준 후, gradle에 해당 버전을 가져다 써보도록 하자.

toml파일에서 gradle에 버전을 가져올 때는

파일이름. 테이블명. 변수명

을 통해 사용하면 된다.

defaultConfig {
    applicationId "com.example.composesample"
    versionCode libs.versions.versionCode.get().toInteger()
    versionName libs.versions.versionName.get()
}

 

versionCode는 정수형이기 때문에 get 이후 toInteger를 사용하여 정수형으로 컨버팅 해주었고,

versionName은 문자열이기 때문에 get으로 가져오기만 하였다.

 

이것으로 아주 간단하게 version을 적용하고 사용해 보았으니,

다음은 dependencies에 위치한 라이브러리들의 의존성을 옮기도록 하자.

[libraries]
# Module
# Compose
composeBom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
composeUi = { module = "androidx.compose.ui:ui", version.ref = "composeVersion" }
composeAnimation = { module = "androidx.compose.animation:animation", version.ref = "composeAnimation" }
runtime = { module = "androidx.compose.runtime:runtime", version.ref = "composeVersion" }
runtimeLiveData = { module = "androidx.compose.runtime:runtime-livedata", version.ref = "composeVersion" }
uiToolingPreview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "composeVersion" }
activityCompose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }

 

작성한 toml파일 중 일부만 가져왔다.

라이브러리라는 테이블을 생성하고, 그 아래 인라인 테이블을 값으로 갖는 key 값들을 선언하였다.

여기서 module 부분은 우리가 많이 봐왔던 gradle의 디펜던시와 동일하게 생긴 것을 알 수 있을 것이다.

module:version과 같은 형태로 선언되어 있는 것을 module, version으로 나누어두었다.라고 생각하면 될 것이다.

 

version.ref에 선언된 "composeVersion"은 예상할 수 있겠지만 상단의 version 테이블에 선언해 둔 값을 그대로 사용한 것이다. 

[versions]

...

# Compose
activityCompose = "1.5.1"
composeVersion = "1.5.2"
composeBom = "2024.02.00"
composeAnimation = "1.7.5"

 

이렇게 말이다.

 

여기서 versions에 선언하지 않고 직접 입력하고 싶다면, 

coreKtx = { module = "androidx.core:core-ktx", version = "1.13.1" }

 

이와 같이 .ref를 제거하고 직접 버전을 string 형태로 넣어주면 된다.

 

이렇게 라이브러리까지 선언해 주었으면, gradle에서 해당 파일을 참조하도록 수정하면 된다.

버전을 가져왔을 때와 비슷하게 호출하면 되는데, 

파일이름.라이브러리이름

으로 호출하면 된다.

dependencies {
    ...
    // AndroidX Core
    implementation(libs.lifecycleRuntimeKtx)
    implementation(libs.activityCompose)
    ...
}

 

이렇게 말이다.

 

이렇게 사용을 하고 보니, 왜 버전을 가져올 때와 라이브러리를 가져올 때 방식이 다른가? 할 수 있을 것이다.

필자도 궁금하여 찾아보니, gradle에서 toml파일을 사용할 때는 추가적으로 적용되는 문법과 규칙들이 있는 것으로 확인되었다.

 

즉,

그냥 그렇게 사용하도록 문법이 정해져 있다.

그러므로 그냥 이렇게 사용하면 된다고 한다.

 

versions과 libraries는 내부적으로 무언가 다르게 동작하고, 그것에 따라 다른 문법을 통해 사용할 수 있도록 되어있기 때문에 그대로 사용하면 될 것 같다.

더 깊게 찾아보면 어떻게 다른지 알 수 있겠지만, 아직 그렇게까지 깊게 찾아볼 단계는 아닌 것 같다.

 

이것을 확인해 보다가 다른 문법의 차이를 발견한 것이 있는데,

그것은 바로 테이블의 선언하는 방식이다.

 

공식 홈페이지를 보고 확인해 보면, 사용자가 편한 대로 테이블을 생성하고 사용할 수 있도록 되어있는데

gradle에서는 그렇지 않다.

 

임의로 테이블을 만들면 이와 같은 오류가 발생한다.

 

이것도 위와 마찬가지로, gradle에서 사용할 때는 toml 파일에서 특정 테이블 이름만 혀용이 되고, 그것이 위에 사진에 나온 4가지만 가능하게 된다.

 

그렇다면 plugins와 bundles에 대해서도 한번 작성해 보고 적용해 보도록 하자.

 

plugins는 우선 gradle의 최상단에 위치한 녀석을 따로 빼서 관리할 수 있는 부분인 것으로 보인다.

이 부분을 선언할 때는 라이브러리와 동일하게 사용하고, gradle에서 사용할 때는 alias 키워드를 통해 호출하면 된다.

[plugins]
android-application = { id = "com.android.application", version.ref = "applicationVersion" }
android-library = { id = "com.android.library", version.ref = "libraryVersion" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlinVersion" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }

 

이와 같이 테이블을 선언하고, 사용하는 플러그인들을 작성해 준 후,

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)

    id 'kotlin-parcelize'
}

 

이와 같이 간단하게 호출하여 사용하였다.

 

여기서, kotlin-parcelize 부분은 어떤 방식으로 plugins에 넣어도 정상적으로 동작을 하지 않아서 우선 빼두었다.

다른 플러그인과 같은 경우 버전이 어딘가에는 명시가 되어있기 때문에 해당 버전을 가져와서 사용해 주었는데, parcelize는 버전이 명시되어있지 않아 위와 같은 문법을 통해 적용하지 못하였다.

 

마지막으로, bundles를 적용해 보자.

bundles는 라이브러리에 선언된 여러 개의 라이브러리를 한 번에 묶어서 사용할 수 있도록 도와주는 정말 "번들"의 역할을 한다.

우리가 보통 라이브러리를 추가할 때 여러 개의 라이브러리를 한번에 추가하여 사용하거나, A라는 라이브러리를 사용할 때는 반드시 B라는 라이브러리도 사용한다. 라는 형태로 여러개의 라이브러리를 엮어서 사용할 때가 있다.

 

그럴 때, 간단하게 bundles로 묶어서 한번에 여러개의 라이브러리의 의존성을 추가하는 방식이다.

[libraries]
...
# Image Loader
glideCompose = { module = "com.github.bumptech.glide:compose", version.ref = "glideCompose" }
coilCompose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" }
coilGif = { module = "io.coil-kt:coil-gif", version.ref = "coilGif" }
[bundles]
image = [
    "glideCompose",
    "coilCompose",
    "coilGif",
]

 

지금은 간단하게 이미지 로더 라이브러리를 모아보았다.

libraries에 선언한 key값을 배열 안에 value로 넣어주면 된다.

 

그리고 gradle에서는 일반 라이브러리와 동일하게 호출해 주면 된다.

// Image Loading
implementation(libs.glideCompose)
implementation(libs.coilCompose)
implementation(libs.coilGif)
->
implementation(libs.bundles.image)

 

위에 3개를 추가해 주었었다면, bundles를 사용하면 아래의 1개의 의존성 추가로 3개의 라이브러리의 의존성을 추가해 줄 수 있는 것이다.


이것으로 toml 파일을 사용하여 gradle의 버전 및 의존성 관리를 보다 편리하게, 하나의 파일에서 할 수 있도록 구조를 변경해 보았다.

 

글에 작성된 내용은 일부분이고, 실제로 해당 프로젝트의 모든 라이브러리를 toml파일에 모아서 작성한 후 gradle을 변경하였는데,

module을 나누면서 core-dependencies.gradle 파일을 만들어서 관리할 때 느꼈던 것과 마찬가지로 보다 편리하게 gradle을 관리할 수 있게 구조가 변경됐다는 것을 체감할 수 있었다.

 

각 gradle 파일에서는 필요한 의존성을 가져다 쓰고, 필요하면 bundle로 묶어서 한 번에 모든 라이브러리를 추가할 수도 있어서 보일러플레이트 코드가 확실히 줄어든 것으로 느껴지고,

toml파일 하나로 해당 프로젝트의 모든 라이브러리 및 버전을 관리할 수 있기 때문에,

버전을 확인하려고 gradle파일을 이것저것 확인한다던지, 버전을 올리기 위해 여기저기 퍼진 라이브러리의 버전을 바꾼다던지 등의 작업을 할 필요 없이 하나의 파일로 모든 의존성이 관리된다는 것이 정말 큰 메리트라고 생각되었다.

 

글에 작성했다시피, 아직 적용하지 못한 친구들이 있는데 저런 부분을 감안하더라도 충분히 toml파일을 도입하여 gradle을 관리할 수 있도록 하는 것이 유지보수 측면에서 좋을 것 같다는 생각을 하였다.

 

우연히, 동료와의 대화에서 알게 된 파일 타입이지만,

정말 좋은 기회가 되어 프로젝트를 보다 편하게 관리할 수 있게 도움을 받았다고 생각한다.

 

해당 게시글에 사용한 예제는 Github에 올려두었다.

https://github.com/HeeGyeong/ComposeSample

 

GitHub - HeeGyeong/ComposeSample: This project provides various examples needed to actually use Jetpack Compose.

This project provides various examples needed to actually use Jetpack Compose. - HeeGyeong/ComposeSample

github.com

728x90