問題点:モノリスが「泥の団子」に変わるとき
新しいプロジェクトを始める際、私たちは誰もが大きな期待を抱き、クリーンなアーキテクチャを目指します。おそらく、コードを service、repository、controller といった標準的なパッケージに整理することでしょう。初日は、それが構造化されているように感じられます。しかし、コードベースが5万行を超えたり、チームが5人以上に増えたりすると、それらの境界は通常崩れ始めます。
機能を迅速に提供しなければならないというプレッシャーは、しばしばアーキテクチャ上のショートカットを招きます。「Billing(請求)」を担当する開発者が、時間を節約するために「Inventory(在庫)」からリポジトリを安易にインポートしてしまうかもしれません。必然的に、アプリケーションはスパゲッティコードの網と化します。配送モジュールのロジックを1つ変更しただけで、3レイヤー離れた決済ゲートウェイが予期せず破損する、といった事態が起こり得ます。
多くのチームは、この混乱から逃れるために Microservices への移行を試みます。しかし残念ながら、コードの複雑さを解消する代わりに、ネットワーク遅延、分散トレーシング、データ整合性の問題といった運用上の悩みを抱え込むことになるケースが多いのが実情です。ここには、より優れた中間地点があります。それこそがモジュラーモノリス(Modular Monolith)であり、Spring Modulith は Java エコシステムにおいてそれを実効性のあるものにするフレームワークです。
モジュラーモノリスのコンセプト
モジュラーモノリスは、単一のデプロイユニット(1つの JAR ファイル)でありながら、内部コードが厳格に区切られている状態を指します。従来のモノリスとは逆行するように、これらのモジュールには「硬い」壁があります。モジュールは特定のパブリック API のみを公開し、内部のヘルパークラスやロジックはシステムの他の部分から隠蔽されている必要があります。
Spring プロジェクトの傘下でプロダクションレディとなった Spring Modulith は、これらの境界を検証するためのツールを提供します。Java のパッケージ構造を利用して、何が「モジュール」で何が「内部(Internal)」であるかを定義します。最も重要なのは、開発者がアーキテクチャを無視しようとした場合に、テストを失敗させてくれる点です。
Spring Modulith の主要な仕組み
- パッケージベースの境界: メインアプリケーションクラスの直下にある各サブパッケージが、個別のモジュールとして扱われます。
- 厳格なカプセル化: モジュールのトップレベルパッケージにあるコードは公開(Public)されますが、サブパッケージ(
.internalなど)にあるものは他のモジュールからは見えません。 - アーキテクチャの検証: ArchUnit ベースのテストを自動的に実行し、循環参照や不正なモジュール間アクセスを検出します。
ハンズオン:Spring Modulith の実装
Order(注文)と Inventory(在庫)という2つのモジュールを持つストアシステムを構築してみましょう。目標は、Order から公開されている InventoryService を呼び出せるようにしつつ、Inventory 内のデータベースヘルパーは厳密にプライベートに保つことです。
1. プロジェクトのセットアップ
pom.xml に Spring Modulith の BOM とコアスターターを追加します。BOM を使用することで、すべてのモジュール依存関係のバージョンが同期されます。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-bom</artifactId>
<version>1.3.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2. モジュール構造の定義
構造がアーキテクチャを決定します。ベースパッケージが com.example.store の場合、フォルダを以下のように配置します。
com.example.store.order(Order モジュール – 公開)com.example.store.inventory(Inventory モジュール – 公開)com.example.store.inventory.internal(隠蔽されたロジック)
Spring Modulith は inventory パッケージをゲートウェイとして扱います。inventory.internal 内のクラスは、order パッケージからはアクセス禁止となります。
3. Inventory モジュールの作成
まず、公開サービスを定義します。これがモジュールの「玄関」となります。
package com.example.store.inventory;
import org.springframework.stereotype.Service;
@Service
public class InventoryService {
public boolean isStockAvailable(String productId) {
return true;
}
}
次に、内部コンポーネントを作成します。このクラスは、他のチームには公開したくない生のデータベースマッピングなどを処理する可能性があります。
package com.example.store.inventory.internal;
import org.springframework.stereotype.Component;
@Component
public class InventoryDataHelper {
public void performSecretTask() {
// Orderモジュールから決して呼び出されるべきではないロジック
}
}
アーキテクチャをユニットテストにする
標準的な Java プロジェクトでは、public キーワードを使えばどこからでもアクセスできてしまいます。これがアーキテクチャが腐敗する原因です。Spring Modulith は、テストフェーズでバイトコードを分析することでこのルールを変えます。
アーキテクチャの番犬として機能するテストクラスを作成します。
package com.example.store;
import org.junit.jupiter.api.Test;
import org.springframework.modulith.core.ApplicationModules;
import org.springframework.modulith.docs.Documenter;
class ModularityTests {
ApplicationModules modules = ApplicationModules.of(StoreApplication.class);
@Test
void verifyModularity() {
// 開発者が循環参照を作成したり、外部から .internal パッケージに
// アクセスしたりした場合、ビルドを失敗させます。
modules.verify();
}
@Test
void writeDocumentation() {
// モジュールのPlantUML図を自動生成します!
new Documenter(modules).writeModulesAsPlantUml();
}
}
もし開発者が OrderService 内で InventoryDataHelper をインポートしようとすると、modules.verify() テストが失敗します。違反箇所を特定する明確なエラーメッセージが表示されます。これにより、アーキテクチャのガバナンスが PDF ドキュメントの中から CI/CD パイプラインの中へと移動します。
まとめ
モジュラーモノリスを選択することは、ほとんどのビジネスアプリケーションにとって現実的な判断です。20もの異なるマイクロサービスを管理するオーバーヘッドなしに、コードベースを整理し、テストしやすく保つことができます。Spring Modulith を使用することで、コードの「腐敗」が始まる前に食い止めることができます。
このアプローチは、両方の世界の良いとこ取りを可能にします。現在は単一のデプロイユニットというシンプルさを享受しつつ、将来的に規模の拡大が必要になった際には、いつでも Microservices へと分割できるクリーンな境界線を維持できるのです。

