
[Spring] version catalogue 적용
Android project와 Spring Boot project 모두 Gradle 기반으로 builld 시스템을 운영하고 있다.
gradle 7.0 부터는 버전 카탈로그라는 것이 등장하여, 회사 Android project에서 build.gradle을 build.gradle.kts로 교체하고, 버전 카탈로그를 적용한 경험이 있었다.
(회사 프로젝트에 버전 카탈로그 적용시에 봤던 공식 문서 링크)
https://developer.android.com/build/migrate-to-catalogs?hl=ko
Version Catalogue를 적용하면서, libs.versions.toml에서 의존성을 모두 관리하게 되고, build.gradle.kts에서는 libs.versions.toml에서 선언한 의존성을 가져와서 build.gradle.kts를 구성할 수 있었다.
이를 통해 의존성을 통일할 수 있었고, 특히 multi module project에서 효과를 누릴 수 있었다.
Spring Boot project를 개발하면서, 의존성이 추가될 때마다 Android에 적용했던 버전 카탈로그 를 Spring Boot project에도 적용해보면 어떨까? 라는 생각이 있었고, 안드로이드와 비슷하게 적용을 진행했다.
따라서 최근 진행하고 있는 Kotlin Springboot project에서도 version catalogue를 적용해봤다.
Android 공식 문서에 나와있는 설명외에도 조금 더 정보를 확인하기 위해서, 아래 공식문서를 읽어보면서, 적용을 진행했다.
https://docs.gradle.org/current/userguide/version_catalogs.html
<version catalogue 적용 전>
일반적으로 spring initailizer를 통해 project를 구성하면, 아래와 같은 형태로 build.gradle.kts 파일이 생기는 것을 확인할 수 있다.
plugins {
val kotlinPluginVersion = "1.9.25"
kotlin("jvm") version kotlinPluginVersion
kotlin("plugin.spring") version kotlinPluginVersion
id("org.springframework.boot") version "3.4.1"
id("io.spring.dependency-management") version "1.1.7"
// ktlint
id("org.jlleitschuh.gradle.ktlint").version("12.1.0")
kotlin("plugin.allopen") version kotlinPluginVersion
kotlin("plugin.noarg") version kotlinPluginVersion
}
allOpen {
annotation("jakarta.persistence.Entity")
annotation("jakarta.persistence.MappedSuperclass")
annotation("jakarta.persistence.Embeddable")
}
noArg {
annotation("jakarta.persistence.Entity")
annotation("jakarta.persistence.Embeddable")
}
group = "com.official"
version = "0.0.1-SNAPSHOT"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
configurations {
compileOnly {
extendsFrom(configurations.annotationProcessor.get())
}
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
// webflux
implementation("org.springframework.boot:spring-boot-starter-webflux")
// coroutine
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
// test
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
// jpa
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
// swagger
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0")
}
kotlin {
compilerOptions {
freeCompilerArgs.addAll("-Xjsr305=strict")
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
<version catalogue 적용 후>
우선 libs.versions.toml을 프로젝트에 있는 gradle 폴더에 생성해준다.
libs.versions.toml에는 각 의존성에 대한 버전을 versions에 명시해줄 수 있고, libraries에는 실제 사용하는 의존성을 추가해줄 수 있다.
아래 처럼 = 좌변에는 사용할 의존성의 이름을 만들어주고, 우변에 의존성을 작성할 수 있다.
versions.ref의 경우 [versions]에 선언한 변수명을 작성해주면 된다.
jjwt-api = { module = "io.jsonwebtoken:jjwt-api", version.ref = "jjwt" }
module을 group과 name으로 분리하여, 아래처럼 작성해줄 수 있다.
실제로는 이렇게 분리해서 작성하는 것을 권장한다고 한다. (자주사용하는 의존성을 group으로 묶어서 관리할 수 있기 때문이라고 한다.)
spring-boot-starter-web = { group = "org.springframework.boot", name = "spring-boot-starter-web" }
마지막으로 [plugins]에는 build.gradle.kts에 plugin(빌드 프로세스의 동작을 확장하거나 특정 기능을 추가하는 모듈)에 해당하는 모듈들을 추가할 수 있다.
[versions]
kotlin = "1.9.25"
springBoot = "3.4.1"
springDependencyManagement = "1.1.7"
ktlint = "12.1.0"
coroutines = "1.7.3"
springdoc = "2.6.0"
jjwt = "0.11.5"
[libraries]
spring-boot-starter-web = { group = "org.springframework.boot", name = "spring-boot-starter-web" }
jackson-module-kotlin = { group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin" }
kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect" }
spring-boot-starter-webflux = { group = "org.springframework.boot", name = "spring-boot-starter-webflux" }
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }
kotlinx-coroutines-reactor = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-reactor", version.ref = "coroutines" }
spring-boot-starter-data-jpa = { group = "org.springframework.boot", name = "spring-boot-starter-data-jpa" }
springdoc-openapi-starter-webmvc-ui = { group = "org.springdoc", name = "springdoc-openapi-starter-webmvc-ui", version.ref = "springdoc" }
postgresql = { group = "org.postgresql", name = "postgresql" }
# JWT
jjwt-api = { module = "io.jsonwebtoken:jjwt-api", version.ref = "jjwt" }
jjwt-impl = { module = "io.jsonwebtoken:jjwt-impl", version.ref = "jjwt" }
jjwt-jackson = { module = "io.jsonwebtoken:jjwt-jackson", version.ref = "jjwt" }
# Test libraries
spring-boot-starter-test = { group = "org.springframework.boot", name = "spring-boot-starter-test" }
kotlin-test-junit5 = { group = "org.jetbrains.kotlin", name = "kotlin-test-junit5" }
junit-platform-launcher = { group = "org.junit.platform", name = "junit-platform-launcher" }
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" }
kotlin-allopen = { id = "org.jetbrains.kotlin.plugin.allopen", version.ref = "kotlin" }
kotlin-noarg = { id = "org.jetbrains.kotlin.plugin.noarg", version.ref = "kotlin" }
spring-boot = { id = "org.springframework.boot", version.ref = "springBoot" }
spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "springDependencyManagement" }
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }
libs.versions.toml을 작성한 뒤에 build.gradle.kts를 아래와 같이 수정했다. (allOpen, noArg dsl의 경우에는 수정할 경우에 빌드가 되지 않아 우선 수정을 진행하지 않았다.)
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.spring)
alias(libs.plugins.spring.boot)
alias(libs.plugins.spring.dependency.management)
alias(libs.plugins.ktlint)
alias(libs.plugins.kotlin.allopen)
alias(libs.plugins.kotlin.noarg)
}
allOpen {
annotation("jakarta.persistence.Entity")
annotation("jakarta.persistence.MappedSuperclass")
annotation("jakarta.persistence.Embeddable")
}
noArg {
annotation("jakarta.persistence.Entity")
annotation("jakarta.persistence.Embeddable")
}
group = "com.official"
version = "0.0.1-SNAPSHOT"
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
configurations {
compileOnly {
extendsFrom(configurations.annotationProcessor.get())
}
}
repositories {
mavenCentral()
}
dependencies {
implementation(libs.spring.boot.starter.web)
implementation(libs.jackson.module.kotlin)
implementation(libs.kotlin.reflect)
// WebFlux
implementation(libs.spring.boot.starter.webflux)
// Coroutines
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.reactor)
// JPA
implementation(libs.spring.boot.starter.data.jpa)
// Swagger
implementation(libs.springdoc.openapi.starter.webmvc.ui)
// Test
testImplementation(libs.spring.boot.starter.test)
testImplementation(libs.kotlin.test.junit5)
testRuntimeOnly(libs.junit.platform.launcher)
// postgresql
runtimeOnly(libs.postgresql)
// JWT
implementation(libs.jjwt.api)
runtimeOnly(libs.jjwt.impl)
runtimeOnly(libs.jjwt.jackson)
}
kotlin {
jvmToolchain(21)
compilerOptions {
freeCompilerArgs.add("-Xjsr305=strict")
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
버전 카탈로그를 적용하게 되면서, gradle/libs.versions.toml 에서 의존성 관리를 할 수 있게, 되었습니다.
버전 카탈로그 공식문서에는 단순히 한 파일에서 의존성 관리를 하는 것 외에도 아래와 같은 장점이 있다고 합니다. (공식문서 번역)
타입 안전 접근자(Type-Safe Accessors): Gradle은 각 카탈로그에 대해 타입 안전 접근자를 생성하여 IDE에서 자동 완성을 지원합니다.
중앙 집중화된 버전 관리(Centralized Version Management): 각 카탈로그는 빌드 내 모든 프로젝트에서 볼 수 있어 의존성 관리를 중앙에서 제어할 수 있습니다.
의존성 번들(Dependency Bundles): 카탈로그는 자주 사용하는 의존성을 그룹으로 묶어서 관리할 수 있습니다.
버전 분리(Version Separation): 카탈로그는 의존성 좌표와 버전 정보를 분리하여, 버전을 공유 선언으로 관리할 수 있도록 합니다.
충돌 해결(Conflict Resolution): 기존 의존성 표기법과 마찬가지로, 버전 카탈로그는 요청된 버전을 선언하지만, 충돌 해결 과정에서 이를 강제하지 않습니다.
그 외에 GPT 4o에서는 중복된 의존성 빌드 속도가 향상된다는 이점이 있다고 했는데, 실제
의존성 해석 과정에서의 중복 작업을 줄여 빌드 속도가 향상될 수 있다고 했다.
실제로 test를 했을 때, version catalogue를 적용하기 전에는 약 1초 363ms / 적용 후에는 약 705ms 정도로 500ms 정도 주는 것을 확인할 수 있었다.

