모듈이 늘어나면서 build.gradle.kts마다 흩어진 버전 문자열이 어긋나기 시작했습니다. 한 모듈은 kotlinx-serialization-json:1.7.3을, 다른 모듈은 1.7.1을 물고 있었는데, 각 파일을 열어서 눈으로 비교하기 전까지는 잘 드러나지 않았습니다. Gradle이 공식으로 지원하는 Version Catalog(libs.versions.toml)로 버전 선언을 한 곳에 모은 과정을 정리합니다.
왜 필요한가
버전 문자열을 각 모듈의 build.gradle.kts에 직접 적어두면 두 가지 문제가 생깁니다.
첫째, 같은 라이브러리를 여러 모듈에서 쓸 때 버전이 어긋나기 쉽습니다. 특히 KMP 프로젝트는 commonMain, androidMain, iosMain에 걸쳐 관련 아티팩트(예: Ktor의 core/okhttp/darwin 엔진)를 나눠 선언하는데, 이 중 하나만 업데이트를 놓치면 런타임에야 문제가 드러납니다.
둘째, 라이브러리를 업그레이드할 때 grep으로 버전 문자열을 찾아 하나씩 바꿔야 합니다. IDE의 "Find in files"로 되긴 하지만, 같은 숫자(1.7.3 같은)가 다른 라이브러리 버전과 겹치면 잘못된 곳을 바꿀 위험도 있습니다.
핵심 개념: libs.versions.toml
Version Catalog는 gradle/libs.versions.toml 파일 하나에 버전, 라이브러리, 번들, 플러그인을 선언해두고, 각 모듈에서는 타입 세이프한 접근자(libs.kotlinx.serialization.json 같은)로 참조하는 방식입니다. 파일은 네 개 섹션으로 나뉩니다.
[versions]: 버전 문자열에 이름을 붙여둠[libraries]:group:artifact좌표를 versions 섹션의 이름과 연결[bundles]: 자주 같이 쓰는 라이브러리 묶음에 별칭을 붙임[plugins]: Gradle 플러그인 좌표와 버전
새 Kotlin Multiplatform 프로젝트를 만들면 이 파일이 이미 생성되어 있는 경우가 많은데, 기존 프로젝트에 나중에 붙일 때는 직접 작성해야 합니다.
실전 예시
gradle/libs.versions.toml에 버전과 라이브러리를 선언합니다.
[versions]
kotlin = "2.1.0"
ktor = "3.0.3"
kotlinx-serialization = "1.7.3"
koin = "4.0.0"
[libraries]
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
ktor-serialization-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
[bundles]
ktor-client = ["ktor-client-core", "ktor-client-content-negotiation", "ktor-serialization-json"]
[plugins]
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
각 모듈의 build.gradle.kts에서는 문자열 좌표 대신 libs 접근자를 씁니다.
// composeApp/build.gradle.kts
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.bundles.ktor.client)
implementation(libs.kotlinx.serialization.json)
implementation(libs.koin.core)
}
androidMain.dependencies {
implementation(libs.ktor.client.okhttp)
}
iosMain.dependencies {
implementation(libs.ktor.client.darwin)
}
}
}
토글에 오타가 있으면 IDE가 빨간 줄로 바로 표시하고, ./gradlew build 시점에도 컴파일 에러로 걸립니다.
Unresolved reference: kotlinxSerializationJson
toml의 키 이름(kotlinx-serialization-json)과 Kotlin 코드에서 참조하는 이름(libs.kotlinx.serialization.json)은 하이픈이 카멜케이스로 자동 변환되는 규칙을 따르는데, 이 변환 규칙을 착각하면 나는 에러입니다. [libraries] 섹션의 키를 그대로 하이픈 구분자 기준으로 캐멀케이스화한 게 접근자 이름이라고 생각하면 됩니다.
버전을 올릴 때는 [versions] 섹션의 값 하나만 바꾸면 됩니다.
[versions]
ktor = "3.1.0" # 3.0.3에서 업그레이드
이 변경 하나로 ktor-client-core, ktor-client-okhttp, ktor-client-darwin을 포함한 모든 Ktor 아티팩트가 같이 올라갑니다.
장단점 정리
장점
- 버전이 한 파일에 모여 있어서 업그레이드할 때 바꿔야 할 곳을 찾아다닐 필요가 없음
- 같은 라이브러리 계열(Ktor 엔진들처럼)을 버전 하나로 묶어서 버전 불일치를 원천적으로 막을 수 있음
- 접근자가 타입 세이프해서 오타를 컴파일 시점에 잡아줌
단점
[libraries]섹션의 키 이름과 Kotlin 접근자 이름 변환 규칙(하이픈 → 카멜케이스)이 처음에는 헷갈림- 모듈이 하나뿐인 작은 프로젝트에서는 문자열 좌표를 직접 쓰는 것과 체감 차이가 크지 않음
- 카탈로그 파일 자체가 커지면 버전과 라이브러리, 번들 사이 참조 관계를 파악하기 위해 스크롤을 많이 해야 함
모듈이 두세 개를 넘어가거나, 같은 라이브러리 계열을 여러 소스셋에 나눠 선언하는 시점부터는 Version Catalog로 옮기는 편이 버전 불일치를 찾아다니는 시간을 줄여줍니다.