【Maven 大規模開発編】Mavenプロファイルとマルチモジュール:複雑なプロジェクトをスマートに管理する

なぜあなたのプロジェクトは「複雑」になったのか?

現代のJava開発、特に大規模なエンタープライズシステムやマイクロサービス、モノレポにおいては、ビルド管理の複雑性が増しています。
Mavenは長らくその中心的な役割を担ってきましたが、Gradleのような新しいビルドツールも台頭し、その特性に応じて使い分けられています。

プロジェクトが成長するにつれ、単一の巨大なコードベースは様々な問題を引き起こします。

「ちょっとした修正なのに、毎回プロジェクト全体のビルドに10分もかかる…」「あのモジュールの共通クラスを、こっちのAPIでも使いたいのに…」

このような「複雑さの沼」から抜け出すための強力な武器が、Mavenの「マルチモジュール」と「プロファイル」です。

この記事では、大規模プロジェクトを複数のモジュールに分割し、環境ごとの設定をプロファイルでスマートに切り替える方法を、具体的なシナリオとコード例で徹底的に解説します。

ビルドの悩みを過去のものにし、統制の取れた効率的な開発を実現しましょう。


目次


対象読者

  • Mavenのマルチモジュールやプロファイルについて学びたい開発者
  • 大規模なJavaプロジェクトにおけるビルド管理に課題を感じているエンジニア
  • マイクロサービス、モノレポ、CI/CD環境でのMaven活用に興味があるITアーキテクト、DevOpsエンジニア
  • ビルド時設定と実行時設定の分離、およびモダンな設定管理戦略に関心がある方

1. なぜマルチモジュール化するのか? 具体的なメリット

プロジェクトを複数のモジュールに分割する主なメリットは以下の3つです。

  1. 関心の分離とコードの再利用性向上:
    例えば、DBアクセス層をcommon-dbモジュールとして切り出せば、user-apiモジュールとproduct-apiモジュールの両方から再利用できます。責務が明確になり、コードの見通しが格段に良くなります。
  2. ビルド時間の短縮:
    user-apiに少し変更を加えただけなのに、無関係なproduct-apiまで含めてプロジェクト全体を再ビルドするのは時間の無駄です。マルチモジュールなら、変更があったモジュールだけを対象にビルドを実行できます。
  3. チーム開発の効率化:
    各チームが担当するモジュールに集中できるため、並行開発がスムーズに進みます。

1.1. 現代のアーキテクチャにおけるマルチモジュール:マイクロサービスとモノレポ

マルチモジュール化は、特に現代の複雑なシステム設計、すなわちマイクロサービスアーキテクチャモノレポ(Monorepository)戦略において、特に有効な手段となります。

  • マイクロサービスアーキテクチャ:
    各マイクロサービスを独立したMavenモジュールとして管理することで、サービス間の疎結合が促進されます。
    例えば、user-serviceモジュール、product-serviceモジュールといった形で、ビジネスドメインに応じた独立したサービスを定義できます。
    これにより、各サービスは独立して開発、テスト、デプロイが可能となり、アジリティが向上します。
  • モノレポ戦略:
    モノレポとは、複数の独立したプロジェクトやライブラリを単一のGitリポジトリで管理する戦略です。
    Mavenマルチモジュールは、このモノレポ内で各プロジェクトや共有ライブラリを論理的に分離し、一貫したビルド・依存関係管理を実現するための強力な手段となります。

1.1.1. 共有ライブラリの重要性

マイクロサービスやモノレポ環境では、複数のモジュールやサービス間で共通のコード(ドメインモデル、ユーティリティ、APIクライアント、共通の認証ロジックなど)が必要となるケースが頻繁に発生します。

これらを独立したMavenモジュール(例: shared-kernel, common-utils, api-contract)として切り出すことで、コードの重複を避け、再利用性を高め、一貫性を保ちながらメンテナンスコストを削減できます。


2. マルチモジュールプロジェクトの構造:親子の役割分担

マルチモジュールプロジェクトは、プロジェクト全体のルールを定める「親POM」と、個別の機能を持つ「子モジュール」で構成されます。

my-app/
├── pom.xml            # 親POM (ルールブック)
├── user-api/
   └── pom.xml        # 子モジュール (ユーザーAPI)
└── product-api/
    └── pom.xml        # 子モジュール (製品API)

2.1. 親POM:プロジェクトの「ルールブック」

親POMは、プロジェクト全体のルールブックの役割を果たします。

  • <packaging>pom</packaging>: 「これはJARやWARのような成果物を作らない、モジュールを束ねるための特別なPOMです」という宣言です。
  • <modules>: ビルド対象となる子モジュールをリストアップします。
  • <dependencyManagement>: プロジェクト全体で利用可能な依存関係のバージョンカタログを定義します。
  • <pluginManagement>: プロジェクト全体で利用可能なプラグインのバージョンカタログを定義します。
<!-- 親POM: my-app/pom.xml -->
<project>
  <groupId>com.example</groupId>
  <artifactId>my-app-parent</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <packaging>pom</packaging> <!-- 重要! -->

  <modules>
    <module>user-api</module>
    <module>product-api</module>
  </modules>

  <properties>
    <spring-boot.version>4.0.1</spring-boot.version>
  </properties>

  <!-- ↓ 依存関係のバージョンカタログを定義 ↓ -->
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>${spring-boot.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
</project>

2.2. 子モジュール:親のルールに従い、自身の責務を果たす

子モジュールは、親POMを継承し、必要な依存関係を宣言します。

<!-- 子モジュール: user-api/pom.xml -->
<project>
  <parent>
    <groupId>com.example</groupId>
    <artifactId>my-app-parent</artifactId>
    <version>1.0.0-SNAPSHOT</version>
  </parent>
  <artifactId>user-api</artifactId>

  <dependencies>
    <!-- 親のカタログにあるライブラリを使うと宣言 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
      <!-- バージョンは親のカタログから自動で適用される -->
    </dependency>
  </dependencies>
</project>

2.3. 最も重要な概念:dependencyManagement vs <dependencies>

初心者が最も混乱するポイントです。以下の図で関係性を理解しましょう。

親の <dependencyManagement> は、「このプロジェクトでは spring-web は v4.0.1 を使う」というルール(カタログ)を定義する子モジュールが <dependencies> で「spring-web を使います」と宣言して、カタログに記載されたバージョンのライブラリが実際にプロジェクトに追加される
  • 親の <dependencyManagement> は、あくまで「このプロジェクトでは spring-webv4.0.1 を使う」というルール(カタログ)を定義するだけです。これだけでは依存関係は追加されません。
  • 子モジュールが <dependencies> で「spring-web を使います」と利用を宣言して初めて、カタログに記載されたバージョンのライブラリが実際にプロジェクトに追加されるのです。

この仕組みにより、バージョン管理を親POMに集約し、プロジェクト全体の整合性を保ちます。


2.3.1. BOM (Bill Of Materials) の活用:依存関係管理の強力な味方

dependencyManagementをさらに強力にするのが、BOM(Bill Of Materials) です。

BOMは、特定のフレームワークやライブラリ群において、互換性のある依存関係のバージョンセットを定義した特別なpom.xmlファイルです。

例えば、Spring Bootプロジェクトでは、親POMで spring-boot-dependencies というBOMをインポートするのが一般的です。

<!-- 親POM: my-app/pom.xml の <dependencyManagement> 内 -->
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-dependencies</artifactId>
      <version>${spring-boot.version}</version>
      <type>pom</type>
      <scope>import</scope> <!-- ここが重要! -->
    </dependency>
  </dependencies>
</dependencyManagement>

この設定により、spring-boot-dependencies が推奨するSpring関連ライブラリのバージョンが、親POMのバージョンカタログに自動的に追加されます。

子モジュールでは、バージョンを記述することなく、spring-boot-starter-web などの依存関係を利用できるようになり、バージョン管理の手間を大幅に削減し、Spring Bootエコシステム全体での整合性を簡単に維持できます。


3. マルチモジュールビルドを加速するコマンド

親ディレクトリで ./mvnw clean install を実行すると全モジュールがビルドされますが、これでは時間がかかります。

以下のコマンドで、対象を指定して効率的にビルドしましょう。

  • 特定のモジュールだけをビルド:

-pl (--projects) で対象モジュールを指定します。

# user-apiモジュールだけをビルド
./mvnw -pl user-api clean install
  • 特定のモジュールと、それが依存するモジュールもビルド:

-am (--also-make) オプションを追加します。これが最も実用的です。

# user-api と、user-apiが依存するcommon-dbもビルド
./mvnw -pl user-api -am clean install

3.1. ビルドパフォーマンス最適化のモダンアプローチ

大規模プロジェクトやモノレポにおいて、ビルド時間の短縮は開発効率に直結する重要な課題です。Mavenは以下の機能でビルドパフォーマンスを最適化できます。


3.1.1. 並列ビルド (-T / --threads)

マルチコアCPUを最大限に活用し、複数のモジュールを並行してビルドします。MavenのReactorはモジュール間の依存関係を考慮し、正しい順序でビルドを実行します。

# CPUコア数に応じて並列ビルド(例:CPUコア数と同じスレッド数)
./mvnw clean install -T 1C

# CPUコア数の1.5倍のスレッド数で並列ビルド
./mvnw clean install -T 1.5C

3.1.2. インクリメンタルビルド

変更があったモジュールとその依存関係のみを再ビルドすることで、ビルド時間を大幅に短縮します。

  • Maven Build Cache:
    • 外部ツールやプラグイン(例: Maven Build Cache Extension)を利用することで、ビルド成果物をキャッシュし、変更がない場合に再利用します。
  • maven-compiler-plugin の設定:
    • インクリメンタルコンパイルを有効にすることで、ソースコードの変更差分のみをコンパイルします。

3.1.3. Maven Daemon

Mavenコマンドを実行するたびにJVMを起動するオーバーヘッドを削減するため、バックグラウンドでMavenプロセスを常駐させるデーモンプロセスです。

Gradle Daemonと同様のコンセプトで、特に小規模なビルドや頻繁なビルド実行において起動時間を大幅に短縮します。


4. プロファイルで環境ごとのビルドを管理する

「開発環境ではH2 DB、本番環境ではPostgreSQLを使いたい」といった要求に応えるのがプロファイルです。

プロファイルは、環境や目的に応じてビルドの振る舞いをカスタマイズするための強力な機能です。


4.1. プロファイルのアクティベーション方法とベストプラクティス

プロファイルはさまざまな方法で有効化できますが、ビルドの再現性と管理の容易性を考慮し、以下のベストプラクティスを強く推奨します。


4.1.1. コマンドラインからの明示的な有効化 (-P フラグ) – 最も推奨

ビルドの再現性を担保するため、プロファイルはコマンドラインから -P フラグで明示的に有効化する方法を強く推奨します。

これにより、どのプロファイルが適用されているかが明確になり、誰が実行しても同じビルド結果が得られます。

# 開発プロファイルでビルド
./mvnw package -Pdev

# 本番プロファイルでビルド
./mvnw package -Pprod

# 複数のプロファイルを同時に有効化
./mvnw package -Pdev,integration-test

# 特定のプロファイルを無効化
./mvnw verify -P !itTest

4.1.2. システムプロパティ/環境変数による有効化

CI/CDパイプラインなどで、実行環境に応じて自動的にプロファイルを切り替えたい場合に有用です。pom.xml 内の <activation> セクションで設定します。

<profiles>
    <profile>
        <id>ci-build</id>
        <activation>
            <property>
                <name>env.CI</name> <!-- 環境変数CIが設定されていれば有効 -->
                <value>true</value>
            </property>
        </activation>
        <!-- CI環境用の設定 -->
    </profile>
    <profile>
        <id>macOS-dev</id>
        <activation>
            <os>
                <family>mac</family>
            </os>
        </activation>
        <!-- macOS開発環境用の設定 -->
    </profile>
</profiles>

上記の場合、CI/CD環境で CI=true という環境変数が設定されていれば ci-build プロファイルが、macOSでビルドすれば macOS-dev プロファイルが有効になります。


4.1.3. settings.xml による有効化 (非推奨)

個人の settings.xml ファイルでプロファイルを有効化することも可能ですが、これはあくまで個人環境でのみ利用を留めるべきです。

チーム開発やCI/CD環境では、開発者間で設定が異なることによる再現性の問題が発生するため、避けるべきです。


4.1.4. <activeByDefault>true</activeByDefault> (原則非推奨)

pom.xml 内で <activeByDefault>true</activeByDefault> を設定すると、他のプロファイルが明示的に有効化されていない場合にそのプロファイルが自動的に有効になります。

しかし、他のプロファイルが一つでも有効化されると、activeByDefault のプロファイルは自動的に無効になるため、予期せぬビルド結果を招きやすく、混乱の原因となります。

特定のプロファイルを常に有効にしたい場合は、コマンドラインで明示的に指定するか、適切なシステムプロパティ/環境変数によるアクティベーションを検討してください。


4.2. プロファイルの応用ユースケース

プロファイルは、環境ごとの設定切り替えだけでなく、ビルドの目的や条件に応じた柔軟なカスタマイズにも利用できます。


4.2.1. テストの種類に応じたビルド:

単体テスト(デフォルト)とは別に、統合テスト (integration-test) やパフォーマンス測定用テスト (perf-test) を実行するプロファイルを定義できます。

# 統合テストプロファイルを有効にしてビルド
./mvnw clean install -Pintegration-test

4.2.2. 異なる成果物の生成:

同じソースコードから、Webアプリケーション用のWARファイル、スタンドアロンの実行可能JAR、ライブラリJARなど、異なるパッケージングタイプの成果物を生成する際にプロファイルを活用できます。


4.2.3. プラットフォーム/JDK固有のビルド:

特定のOS (Windows, Linux) やJDKバージョン (JDK 8, JDK 11) でのみ必要となる依存関係やプラグイン設定をプロファイルで切り替えることができます。


4.3. プロファイルの乱用を避ける

プロファイルは強力な機能ですが、乱用すると pom.xml が複雑化し、可読性やメンテナンス性が著しく低下します。以下の点を意識しましょう。

  • シンプルさを保つ:
    • プロファイルはビルドの「振る舞い」を制御するために使い、アプリケーションの「実行時設定」とは明確に分離すべきです。
  • 必要な時にだけ使う:
    • プロファイルの数や複雑さを最小限に抑え、本当に必要不可欠な場合にのみ使用を検討してください。
  • ビルド時設定と実行時設定の分離を徹底する:
    • これについては次の「ITアーキテクトの視点」で詳しく解説します。

4.4. ITアーキテクトの視点:ビルド時設定と実行時設定の完全分離とモダンな管理戦略

プロファイルによるリソースフィルタリングは、ビルド時に設定を成果物(JAR/WAR)に焼き付ける方法です。

これはシンプルですが、「一度ビルドした成果物を、設定だけ変えて複数環境にデプロイする」 という現代的なデプロイ戦略(Immutable Infrastructure)とは相性が良くありません。

よりモダンなアプローチは、アプリケーションの挙動に関わる設定を「実行時に注入」します。

この「ビルド時設定」と「実行時設定」の完全な分離は、クラウドネイティブアプリケーション開発における必須プラクティスとなっています。


4.4.1. Spring Bootにおける外部設定の優先順位

Spring Bootは、多様なソースから外部設定プロパティを読み込み、以下の優先順位で適用します(優先度が高い順)。

この順序の理解は、予期せぬ設定の上書きや、効果的な設定管理を行う上で重要です。

  1. コマンドライン引数: java -jar app.jar --property="value"
  2. SPRING_APPLICATION_JSON: 環境変数またはシステムプロパティとして指定されたインラインJSON。
  3. Javaシステムプロパティ: -Dproperty="value"
  4. OS環境変数: PROPERTY_NAME="value" (Spring BootのRelaxed Bindingにより MY_APP_PROPERTYmy.app.property にマッピング可能)
  5. ランダム値 (random.*)
  6. プロファイル固有のアプリケーションプロパティ(JAR外): application-{profile}.properties / application-{profile}.yml (例: /config ディレクトリ)
  7. アプリケーションプロパティ(JAR外): application.properties / application.yml (例: /config ディレクトリ)
  8. プロファイル固有のアプリケーションプロパティ(JAR内): application-{profile}.properties / application-{profile}.yml (クラスパス内)
  9. アプリケーションプロパティ(JAR内): application.properties / application.yml (クラスパス内)
  10. @PropertySource アノテーション
  11. デフォルトプロパティ (SpringApplication.setDefaultProperties)

4.4.2. クラウドネイティブ環境における設定管理戦略

マイクロサービスやクラウドネイティブな環境では、上記Spring Bootの機能に加え、さらに高度な設定管理ツールが利用されます。

  • 環境変数 (Environment Variables):
    コンテナベースのデプロイ(Docker, Kubernetes)では、最も一般的かつシンプルな設定注入方法です。機密情報でない設定や、環境ごとに必ず変更される設定に適しています。Spring BootのRelaxed Bindingにより、シェル変数命名規則 (_ や大文字) でもJavaプロパティ (. やキャメルケース) に自然にマッピングされます。
  • Kubernetes ConfigMap と Secret:
    Kubernetes環境での設定管理の標準的な方法です。
    • ConfigMap: 機密情報ではない設定(例: ログレベル、APIエンドポイントURL)をキーバリュー形式で管理します。アプリケーションはファイルとしてマウントするか、環境変数として利用できます。
    • Secret: データベース認証情報、APIキーなどの機密情報を安全に管理します。Base64でエンコードされますが、暗号化は別途考慮が必要です。アプリケーションはConfigMapと同様にファイルマウントや環境変数として利用できます。
  • Spring Cloud Config:
    マイクロサービスアーキテクチャにおいて、設定を一元的に管理するための専用サーバーです。Gitリポジトリをバックエンドとして利用し、各サービスはConfig Serverから設定を取得します。動的な設定変更(Hot Reload)にも対応しており、多数のサービスの設定を効率的に管理できます。
  • HashiCorp Vault:
    データベース認証情報、APIキー、シークレットトークンなどの機密情報を安全に保存、管理、監査するためのツールです。アプリケーションはVaultから一時的なクレデンシャルを取得したり、暗号化されたデータを復号したりできます。セキュリティ要件の高いシステムで利用されます。

4.4.3. 機密情報の安全な管理の重要性

パスワード、APIキー、トークンなどの機密情報は、決してソースコードや公開リポジトリ、ビルド成果物の中に含めてはなりません。これらは実行時に安全な方法でアプリケーションに注入されるべきです。

上記で挙げた環境変数、Kubernetes Secret、HashiCorp Vaultのような専用のシークレット管理ツールを積極的に利用し、セキュリティリスクを最小限に抑えることがITアーキテクトの重要な責務です。

Mavenプロファイルは「ビルドそのものの振る舞い(例: ビルド対象モジュールの変更)」を制御するために使い、アプリケーションの挙動に関わる設定は「実行時」に注入する方法を検討しましょう。


5. CI/CDパイプラインとMavenマルチモジュールプロジェクト

Mavenマルチモジュールプロジェクトは、継続的インテグレーション/継続的デリバリー(CI/CD)パイプラインとの相性が非常に良く、大規模開発の自動化に不可欠です。

効果的なCI/CDパイプラインを構築するためのベストプラクティスを紹介します。


5.1. 単一のCI設定ファイルとルートレベルからの実行

通常、リポジトリのルートディレクトリに単一のCI設定ファイル(例: Jenkinsfile, .gitlab-ci.yml, .github/workflows/*.yml)を配置し、そこからMavenのビルドコマンドを実行します。これにより、パイプラインの管理が簡素化され、一貫性が保たれます。

# ルートプロジェクトでclean installを実行
./mvnw clean install

このコマンドは、親POMの定義に基づき、すべてのサブモジュールを適切な順序でビルド、テスト、インストールします。


5.2. 選択的モジュールビルドによるCI時間短縮

大規模なモノレポでは、わずかなコード変更でも全体のビルドが走り、CIの実行時間が長くなりがちです。
これを解決するために、「変更があったモジュールのみをビルド・テストする」戦略が有効です。

  • Gitの変更検知: CIツール(例: GitLab CI, GitHub Actions)の機能を利用して、前回のコミットから変更があったファイルやモジュールを特定します。
  • Mavenの -pl, -am, -amd オプションの活用: 変更があったモジュールだけを対象にビルドを実行したり、そのモジュールが依存している他のモジュールもビルドに含めたりすることで、CIパイプラインの実行時間を大幅に短縮できます。
    • -pl: 特定のプロジェクト(モジュール)のみをビルド
    • -am (--also-make): 指定したプロジェクトが依存するプロジェクトもビルド
    • -amd (--also-make-dependents): 指定したプロジェクトに依存するプロジェクトもビルド

5.3. コード品質ツールの統合

CI/CDパイプラインにコード品質ツールを組み込むことで、開発プロセス全体で品質を維持・向上させることができます。

  • 静的コード分析:
    • SonarQube, Checkstyle, PMDなどをMavenプラグインとしてビルドに組み込み、コードスタイルや潜在的なバグを自動でチェックします。
  • コードカバレッジ:
    • JaCoCoなどのツールを導入し、テストのカバレッジを測定・レポートします。これにより、テストが十分に行き届いているかを可視化できます。

5.4. 成果物のデプロイとバージョン管理

ビルドされた成果物(JAR, WARなど)は、NexusやArtifactoryのようなMavenリポジトリマネージャにデプロイし、バージョン管理を行います。

CIパイプラインの最終ステップで、テストが成功した成果物を自動的にデプロイすることで、リリースプロセスを効率化できます。


まとめ:現代のプロジェクトにおけるビルドを統制し、複雑さに打ち勝つ

Mavenのプロファイルとマルチモジュールは、大規模かつ複雑なJavaプロジェクトにおいて、ビルドプロセスを統制し、効率的な開発を実現するための非常に強力な武器です。

現代のソフトウェア開発、特にマイクロサービスアーキテクチャやモノレポ戦略においては、ビルドツールの賢明な選択と、その機能を最大限に活用することが不可欠です。

  • マルチモジュール:
    • 「関心の分離」「コードの再利用」「ビルド時間の短縮」といった従来のメリットに加え、マイクロサービスやモノレポにおける論理的な分割と管理の基盤となります。
    • 親子POMの関係、そしてBOM (Bill Of Materials) を含めたdependencyManagementの役割を正しく理解することが鍵です。
    • ビルドパフォーマンス最適化の現代的アプローチも積極的に取り入れましょう。
  • 高度なビルドコマンド:
    • -pl-am-amd といったオプションを使いこなし、日々の開発やCI/CDパイプラインでのビルド作業を効率化しましょう。
  • プロファイル:
    • -Pフラグによる明示的な有効化を徹底し、再現性の高いビルドを実現します。
    • 環境ごとのビルド要件、テストの種類に応じたカスタマイズなど、プロファイルの応用ユースケースを理解しつつも、乱用は避け、シンプルさを保つことが重要です。
  • ビルド時設定と実行時設定の完全分離:
    • ITアーキテクトの視点として強調したように、アプリケーションの挙動に関わる設定は、Spring Bootの外部設定機能やクラウドネイティブな設定管理ツール(ConfigMap, Secret, Spring Cloud Config, Vaultなど)を利用して、実行時に注入するモダンなアプローチを実践しましょう。
    • 機密情報の安全な管理も常に意識してください。
  • CI/CDパイプラインとの統合:
    • MavenマルチモジュールプロジェクトをCI/CDパイプラインに組み込み、選択的モジュールビルドやコード品質ツールの自動化を通じて、開発からデプロイまでのプロセス全体を最適化しましょう。

これらのテクニックと現代的なプラクティスを駆使することで、あなたのプロジェクトは「複雑さの沼」から解放され、より堅牢でスケーラブルなシステム開発を効率的に推進できるようになるでしょう。


免責事項

本記事は、Mavenのマルチモジュールおよびプロファイルに関する一般的な情報提供を目的としています。
記載されている情報は、その正確性、完全性、最新性を保証するものではありません。
特定のプロジェクトや環境に適用する際には、必ずご自身の責任において検証および判断を行ってください。
本記事の内容に起因するいかなる損害に対しても、著者は一切の責任を負いません。


SNSでもご購読できます。

コメントを残す

*