Money Forward Developers Blog

株式会社マネーフォワード公式開発者向けブログです。技術や開発手法、イベント登壇などを発信します。サービスに関するご質問は、各サービス窓口までご連絡ください。

20230215130734

Gradle Version Catalogsでバージョン管理を社内で統一しよう

Unified Gradle Version Catalogs

こんにちは、社内でKotlin ExpertとしてKotlinバックエンドの開発支援を担当しているHirotaka Kawata(@hktechno)です。

マネーフォワードには、Kotlin/Javaのバックエンドプロジェクトのリポジトリが、現時点でおよそ80ほど存在しています(RubyとGoプロジェクトもKotlin以上に存在します)。

社内で多数のリポジトリが作られてくると問題になってくるのが、フレームワークやライブラリの種類やバージョン管理です。

この記事では、Kotlin/Javaでのバックエンド開発において、GradleのVersion Catalogsを使って社内で統一したライブラリのバージョン管理するための取り組みを紹介したいと思います。

ライブラリバージョン管理の重要性

たくさんのプロジェクトが増えてくると、それぞれのプロジェクトで多種多様なライブラリを依存関係に追加することになります。

しかし、組織が大きくなるにつれてさまざまな問題が発生してきます。

フレームワーク・ライブラリのアップデート時のコスト
常にライブラリのアップデートを、各プロジェクトで管理するのは大変で、大きなコストです。 その一方で、コストが掛かるので、ライブラリがアップデートされず放置されがちです。 例えDependabotやRenovateがあったとしても、相性問題などで、一筋縄では解決できないものが多いです。 Spring Bootなどのフレームワークのアップデート時には、(推移的なものも含めて)たくさんの依存関係も同時にアップデートされますが、それでも調査と検証はとても大変です。

セキュリティや安全上の問題
どのプロジェクトで、どんなライブラリが、どのバージョンで利用されているか(SBOM)を管理するのは大変です。 特定のライブラリに問題が起きた時、利用状況や調査方法、統一したアップグレード手段がないと、対応に時間がかかります。 そして各プロジェクトの調査では、隅々までチェックできないので安全であるとは限りません。

このような問題を解決するために、社内で統一的なライブラリのバージョン管理が重要だと考えています。

弊社の多くのKotlin/Javaバックエンドプロジェクトは、ビルドシステムにGradleを採用しています。そこで、Gradleを使ったライブラリバージョン管理方法を構築することにしました。

GradleのVersion Catalogsを利用して、共通のライブラリバージョンカタログを提供することで、以下のようなメリットを享受できます。

  • 安全で調査・動作検証がすでに行われた、社内標準のライブラリカタログが利用できる
  • カタログのバージョンひとつを更新するだけで、利用者は一気に多くのライブラリのバージョンを更新できる
  • 特定のライブラリに問題が起きた時に、影響を受けているかどうかの確認が簡単になる

Gradle Version Catalogs

突然ですが皆さんは、GradleのVersion Catalogsを使っていますか?

Gradle 7から導入された機能で、依存するライブラリやプラグインとそのバージョンを、宣言的に定義してビルドスクリプトから参照できる機能です。

以前はMaven BOMを platform() で読み込んだりSpring BootのDependency Management Pluginを使ったりと、ビルドスクリプト側で工夫することで、共通のバージョンの管理を実現していました。 しかし、現在はGradleの機能としてバージョンカタログが提供されており、Kotlin開発のベストプラクティスとして推奨されています。

基本的な使い方

バージョンカタログの定義方法や使い方はいくつかあるのですが、一番基本的な使い方は libs.versions.toml に依存ライブラリを定義して、build.gradle.kts から参照する方法です。

gradle/libs.versions.toml

[versions]
mockk = "1.14.2"

[libraries]
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }

...

build.gradle.kts

dependencies {
    // バージョンカタログを使った場合
    implementation(libs.mockk)
    // バージョンカタログを使わない場合
    implementation("io.mockk:mockk:1.14.2")
}

これが最もシンプルな形です。他にも、Gradleプラグインを同様にカタログに定義できたり、bundle と言われる依存関係のグループを定義して、その全てをビルドスクリプトから依存関係に追加できたりする便利な機能もあります。

バージョンカタログの情報は、IDEでの補完も効くので、あらかじめカタログに定義しておけば毎度ライブラリのパッケージ名とartifact名を探す必要もありません。

バージョンカタログをパブリッシュしよう

定義したバージョンカタログは、Mavenリポジトリへパブリッシュして、プロジェクトを跨いで共有できます。

Gradleで version-catalog プラグインを適用したモジュールを作ることで、そのモジュール内でパブリッシュ用のバージョンカタログを生成し、通常のライブラリと同じ手順で publish できます。

libs.versions.toml で定義されたバージョンカタログをパブリッシュするための基本的な設定は以下のような形です。

build.gradle.kts

plugins {
    `version-catalog`
    `maven-publish`
}

group = "com.moneyforward.gradle"

catalog {
    versionCatalog {
        from(files("gradle/libs.versions.toml"))
    }
}

publishing {
    publications {
        create<MavenPublication>("mavenVersionCatalog") {
            from(components["versionCatalog"])
        }
    }
    repositories {
        ...
    }
}

このパブリッシュされたカタログを他のプロジェクトから読み取るには、以下のような設定が必要です。

settings.gradle.kts

dependencyResolutionManagement {
    repositories {
        maven("https://<YOUR_MAVEN_REPO>")
    }
    versionCatalogs {
        create("mylibs") {
            from("com.moneyforward.gradle:catalog:1.0.0")
        }
    }
}

この設定により、共有されたバージョンカタログから、mylibs.mockk のような形式で依存ライブラリの情報を読み出すことができます。

それぞれのプロジェクトでは、共通のものとは別に libs.versions.toml を持つことができるので、必要であれば共通カタログに存在しない依存関係もプロジェクト毎に管理も可能です(その代わり、ライブラリの利用状況を把握しにくくなるのでメリットが薄れます)。

ここまでで、最低限の目的は達成できました。

version-catalog-generator プラグイン で BOM を展開

ただし、バージョンカタログを実際に運用すると、以下のような管理上の欠点が現れ始めます。

  • フレームワーク(Spring Boot)が推移的に依存しているライブラリは、そのフレームワークの依存バージョンをそのまま使いたいが、管理が大変(そうしないと壊れる)
  • バージョンカタログに、特定のライブラリが提供する全てのartifactを記述するのが大変
    • 例: Spring Boot Starter の一覧
    • 後から追加されるものもあるし、それを全て管理するのが難しい
    • 各ライブラリが提供するBOMと Gradle の Platform 機能を利用してバージョン依存を管理する方法もあるが、バージョンカタログのメリットが薄れてしまう

このような問題を解決するために、version-catalog-generatorというGradleプラグインを使ってMaven BOMを取り込む方法を採用することにしました。

version-catalog-generatorは、特定のBOMを指定すると、そのBOMに含まれる推移的な依存関係を全てバージョンカタログに展開してくれます。BOMに含まれるBOMなども対応してくれます。

Spring Bootであれば、org.springframework.boot:spring-boot-dependencies という Maven BOM が提供されています。

このBOMを以下のようにversion-catalog-generatorを使って展開すると、Spring Bootに含まれる全ての依存関係がバージョンカタログに含まれるようになります。とても素晴らしいプラグインです。

libs.versions.toml

[versions]
springBoot = "3.5.5"

[libraries]
springBootDependencies = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "springBoot" }

settings.gradle.kts

dependencyResolutionManagement {
    repositories {
        mavenCentral()
    }
    versionCatalogs {
        generate("mylibs") {
            fromToml("springBootDependencies")
        }
    }
}

build.gradle.kts

dependencies {
    implementation(mylibs.spring.springBootStarterWebflux)
    implementation(mylibs.h2database.h2)
}

しかし、BOMが展開されるのはあくまで version-catalog-generator プラグインを入れたプロジェクトのみであって、BOMを展開した状態でバージョンカタログをパブリッシュできません。なぜなら、先ほどのバージョンカタログをパブリッシュしたモジュールの設定では、from(files(...)) によりTOMLファイルを指定しているだけだからです。

version-catalog-generator を全てのプロジェクトに導入すれば良いのですが、煩わしさを考えると、これもあまり現実的ではありません。理想としては、BOMが展開されたバージョンカタログをパブリッシュしたいのです。

BOM が展開されたバージョンカタログをパブリッシュ

そこで、現在Gradleプロジェクトで読み込まれているバージョンカタログの内容をコピーしてパブリッシュできないかを考えてみます。

少し小細工して、以下のようなビルドスクリプトを書きました。リフレクションを使っているので、将来的な動作が保証されず、一般的にはお勧めできませんが、現在読み込まれているバージョンカタログを元にビルドスクリプト内で動的にDSLでバージョンカタログを作り上げています。

これにより、なんとかBOMが展開されたバージョンカタログをパブリッシュできるようになりました。

build.gradle.kts

catalog {
    versionCatalog {
        // Copy the version catalog from the `mylibs`.
        val config = (mylibs::class as KClass<AbstractExternalDependencyFactory>)
            .memberProperties.first { it.name == "config" }
            .let {
                it.isAccessible = true
                it.get(mylibs) as DefaultVersionCatalog
            }

        config.versionAliases.forEach {
            version(it, config.getVersion(it).version.displayName)
        }

        config.libraryAliases.forEach {
            val dependency = config.getDependencyData(it)
            library(it, dependency.group, dependency.name).also { library ->
                if (dependency.versionRef != null) {
                    library.versionRef(dependency.versionRef!!)
                } else {
                    library.version(dependency.version.displayName)
                }
            }
        }
...

以下のリポジトリで、実際にBOMが展開されたバージョンカタログをパブリッシュできるようにした例を公開しています。参考にしてみてください。

https://github.com/hktechn0/shared-gradle-version-catalog-example

Maven BOM と Gradle Version Catalogs の違い

ここまで読んで「新しいGradle Version Catalogsではなく、従来から存在するMaven BOMの方がいいんじゃないの?」と思った方も多いことでしょう。

確かに、Gradle Version CatalogsとMaven BOMは多くの類似点があります。一般的に、企業の中やライブラリ等でライブラリのバージョンセットを提供する際には、これまでMaven BOMが多く使われてきたと思います。

しかし、ビルドにGradleだけを使うのであれば、以下のような点でBOMよりもバージョンカタログのほうが優れていると思っています。

  • シンプルなTOMLで管理できる
  • 依存の指定時にIDEによる補完が効く
  • Gradleプラグインの依存も含めることができる
  • Gradle公式のバージョン管理手段で、今後の改良や機能追加にも期待ができる

バージョンカタログを使っても、Renovate による自動的なライブラリアップデートに対する追従を、カタログ内の依存関係とカタログ自体のアップデート、両方へ適用できます。その際、場合によってはプライベートMavenリポジトリに対する追加の設定が必要ですが、それほど大きな問題ではないでしょう。

まとめと現状

実際に、弊社のKotlinプロジェクトでは紹介した方法で生成した共通のバージョンカタログを社内のMavenリポジトリにパブリッシュする形で、ライブラリのバージョン管理を共通化する取り組みを進めています。

社内で標準的に使われるフレームワークやライブラリを、全て一つのバージョンカタログにまとめて提供することで、各開発チームは細かいライブラリのバージョンアップに追われる煩わしさを解消できます。

しかし、既存のすでに開発されてきたプロジェクトに全て一気に導入するのは難しいので、徐々に導入を進めているのが現状です。

これから、2025年末にかけては、Java 25とSpring Boot 4という特大アップデートが待ち構えています。このアップデートに向けて事前に多くのプロジェクトへバージョンカタログを適用できれば、多くのプロジェクトのアップデートコストを削減できるのではないかと考えています。