Mavenのバージョン競合はなぜ起きるのか

Mavenの「バージョン競合」は、依存関係を自動解決してくれる仕組みそのものが原因で起きます。Mavenは便利さと引き換えに、「どのバージョンを採用するか」を機械的なルールで決めています。そのルールを知らないまま使うと、ビルドは通るのに実行時にエラーが出たり、環境によって挙動が変わったりします。

つまり、バージョン競合は「設定ミス」ではなく、「仕組みを知らないまま正しく動いてしまう」ことが最大の落とし穴です。この記事では、なぜMavenでバージョン競合が起きるのかを、実際の依存関係の例とともに丁寧に整理します。

Mavenのバージョン競合とは何か

Mavenのバージョン競合とは、同じライブラリに対して異なるバージョンが同時に要求される状態を指します。直接依存していなくても、依存しているライブラリのさらに依存先、いわゆる「推移的依存関係」によって発生します。

たとえば、アプリケーションがAとBという2つのライブラリに依存しているとします。

  • ライブラリAは commons-logging 1.1 を使っている
  • ライブラリBは commons-logging 1.2 を使っている

このとき、Mavenは「どちらか一方」しかクラスパスに配置しません。両方は共存できないためです。その結果、意図しないバージョンが選ばれることで問題が起きます。

Mavenはどうやってバージョンを決めているのか

Mavenには明確なルールがあります。それが「近いもの勝ち(Nearest Definition)」です。

依存関係ツリーの中で、

  • プロジェクトからの距離が近い依存関係
  • 同じ距離なら、先に宣言されたもの

が優先されます。このルール自体はシンプルですが、実際のプロジェクトでは依存関係が何十、何百と連なり、結果が非常に分かりにくくなります。

具体例:nearestが招く意外な結果

以下のような依存関係を考えてみます。

<dependencies>
  <dependency>
    <groupId>com.example</groupId>
    <artifactId>lib-a</artifactId>
    <version>1.0.0</version>
  </dependency>
  <dependency>
    <groupId>com.example</groupId>
    <artifactId>lib-b</artifactId>
    <version>1.0.0</version>
  </dependency>
</dependencies>
  • lib-a は jackson-databind 2.9.0 に依存
  • lib-b は jackson-databind 2.13.0 に依存

この場合、依存関係ツリー上でより「近い」方が選ばれます。lib-aとlib-bが同じ距離なら、宣言順で決まります。多くの人が「新しいバージョンが選ばれるだろう」と思いがちですが、Mavenはそんな判断をしません。

なぜMavenは自動で解決してくれないのか

「衝突しているならエラーにしてほしい」と思う人も多いですが、Mavenはビルドツールであって依存関係の正解を知っているわけではないという前提で設計されています。

どのバージョンが正しいかは、プロジェクトの文脈によって変わります。

  • 古いAPIに依存したコードがあるかもしれない
  • 新しいバージョンでは挙動が変わるかもしれない
  • 実行環境の制約があるかもしれない

そのため、Mavenは「ルールに従って一つを選ぶ」だけに留まっています。

実際に起きがちなトラブル

バージョン競合が原因で起きるトラブルは、ビルドエラーよりも実行時エラーが多いです。

  • NoSuchMethodError が突然出る
  • ClassNotFoundException が特定環境だけで起きる
  • テストでは通るのに本番で落ちる

これらは「コンパイル時には存在したクラスやメソッドが、実行時には違うバージョンに置き換わっている」ことが原因である場合が少なくありません。

dependency:treeを見ずに進む危険性

Mavenには依存関係を可視化する公式手段があります。

mvn dependency:tree

この結果を見ずに開発を進めると、「何が入っているのか分からないブラックボックス」状態になります。特に、Spring Bootのように多くの依存を抱えるフレームワークでは、明示的に指定していないライブラリが大量に含まれます。

dependencyManagementが重要な理由

バージョン競合への基本的な対策がdependencyManagementです。これは「使うバージョンを一元管理する宣言」です。

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.13.0</version>
    </dependency>
  </dependencies>
</dependencyManagement>

これにより、どこから依存されても同じバージョンが使われます。直接dependencyに書かなくても、バージョンはここで固定されます。

exclusionsは最後の手段

exclusionsを使えば、特定の推移的依存関係を除外できます。

<exclusions>
  <exclusion>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
  </exclusion>
</exclusions>

ただし、これは依存関係の構造を壊す行為でもあります。安易に使うと、別の場所でクラスが見つからなくなることもあります。

Mavenのバージョン競合に向いていない考え方

「Mavenが何とかしてくれるだろう」という期待は、残念ながら現実的ではありません。Mavenはあくまでルールベースで動きます。

また、「とりあえず動いたからOK」という判断も危険です。将来のアップデートや環境差分で問題が表面化する可能性があります。

注意すべきリスク

バージョン競合を放置すると、次のようなリスクがあります。

  • セキュリティパッチが当たっていない古いライブラリが使われ続ける
  • フレームワークのアップデート時に一気に破綻する
  • 問題調査に時間がかかり、原因特定が難しくなる

これらはすぐに致命的にならなくても、プロジェクトの寿命を縮めます。

結局どうすればいいのか

Mavenのバージョン競合を完全に避けることは難しいですが、コントロールすることはできます。

  • dependency:tree を定期的に確認する
  • dependencyManagement でバージョンを明示的に固定する
  • exclusionsは理由を明確にしたうえで最小限に使う
  • フレームワーク任せにせず、中身を理解する

Mavenは強力ですが、魔法ではありません。仕組みを理解したうえで使えば、バージョン競合は「不可解な事故」ではなく「管理できる問題」になります。