Spring Modulithとは
Spring Modulith は、Spring Boot を使用してドメイン駆動型のモジュール型アプリケーションを構築するための独自のツールキットです。Spring Boot がアプリケーションの技術的な配置について独自の見解を持っているのと同じように、Spring Modulith は、アプリを関数に構造化する方法について独自の見解を実装し、個々の論理部分が相互にやり取りできるようにします。その結果、Spring Modulith を使用すると、開発者は更新が容易なアプリケーションを構築して、時間の経過とともに変化するビジネス要件に対応できるようになります。
本記事の内容
Spring Modulith の依存関係をコントロールする機能を試してみた記録です。
先日参加したJJUG CCC 2024 Fallで紹介されていて興味が湧き試してみました◎
他にもアプリケーションモジュールのドキュメント化やテスト実行に関する機能もありますが、今回は依存関係のコントロールに絞った内容となります。
コントロールできる依存関係
- 公開パッケージ以外の参照の禁止
- 循環参照の禁止
デフォルトの制御としては上記が行われ、必要に応じてカスタマイズすることができます。
デフォルトの制限を守ることでモジュラモノリスライクな依存関係のコントロールが実現できます。
試してみる
テスト用アプリケーションの構成
一つのアプリケーションの中に inventory:在庫
と order:注文
という2つのモジュールが存在し、それぞれのモジュールが疎結合なモジュラモノリスアプリケーションを作成したい場合を考えます。
com.example.demo.DemoApplication.java
がSpringBootのApplicationクラスです。
Applicationクラスを配置したパッケージのサブパッケージである inventory
や order
が各モジュールのAPIパッケージとして認識されます。
Spring ModulithではこのAPIパッケージ以下が一つのモジュールとして扱われ、直下のクラスのみが他のモジュールから参照可能になります。
そのため inventory
や order
パッケージの直下には他のモジュールから参照されることを想定したインターフェースやオブジェクトクラスを配置します。
それぞれのモジュールには entity
repository
service
のようなサブパッケージがありますが、これらのパッケージ以下のクラスは他のモジュールから参照することができません。この制御によってモジュール間の依存が整理されたアプリケーションを構築することができます。
検査の実行
以下のようなテストを書いて実行します。
依存関係の違反があった場合にはこのテストがfailします。
テストを実行する必要があるため、実際にプロダクト開発に検査の仕組みを組み込むためにはCIでテストを実行する仕組みが必要になります。
package com.example.demo;
import org.junit.jupiter.api.Test;
import org.springframework.modulith.core.ApplicationModules;
public class DemoApplicationDependenciesTest {
@Test
public void test() {
ApplicationModules.of(DemoApplication.class).verify();
}
}
公開パッケージ以外の参照の禁止
公開パッケージ以外への参照として、OrderService から InventoryRepository を直接参照するという不届きな実装をしてみます。
package com.example.demo.order.service;
import com.example.demo.inventory.Inventory;
import com.example.demo.inventory.repository.InventoryRepository;
public class OrderService {
private InventoryRepository inventoryRepository;
public OrderService(InventoryRepository inventoryRepository) {
this.inventoryRepository = inventoryRepository;
}
public void buy(String productId, int quantity) {
List<Inventory> allInventory = inventoryRepository.loadAllInventory();
}
}
この実装で DemoApplicationDependenciesTest
はFailします。
org.springframework.modulith.core.Violations: - Module 'order' depends on non-exposed type
com.example.demo.inventory.repository.InventoryRepository within module 'inventory'!
order module が inventory module の公開されていない型にアクセスしている、というエラーが表示されます。
次に公開されているパッケージへの参照を実装してみます。
package com.example.demo.order.service;
import com.example.demo.inventory.IInventoryModule;
import com.example.demo.inventory.Inventory;
public class OrderService {
private IInventoryModule inventoryModule;
public OrderService(IInventoryModule inventoryModule) {
this.inventoryModule = inventoryModule;
}
public void buy(String productId, int quantity) {
Inventory orderProductInventory = inventoryModule.loadInventory(productId);
}
}
この実装で DemoApplicationDependenciesTest
はPassします。
このように公開されているパッケージ以外へのアクセスを禁止することができます。
循環参照の禁止
上記の OrderService
が IInventoryModule
を参照している状態で InventoryService
が IOrderModule
を参照するような実装をしてみます。
package com.example.demo.inventory.service;
import org.springframework.stereotype.Service;
import com.example.demo.order.IOrderModule;
@Service
public class InventoryService {
private IOrderModule orderModule;
public InventoryService(IOrderModule orderModule) {
this.orderModule = orderModule;
}
}
この実装はお互いの公開されているパッケージのクラスにのみアクセスしていますが、モジュールが相互依存しているため DemoApplicationDependenciesTest
はFailします。
org.springframework.modulith.core.Violations: - Cycle detected: Slice inventory
循環参照しているというエラーが表示されます。
カスタマイズ
特定のパッケージ以下を他モジュールから参照可能にする
例えばどのモジュールでも共通で利用するユーティリティーなどを以下のように配置したい場合があると思います。
こういった時は common
パッケージの package-info.java
を以下のように記述すると、common以下のパッケージが参照可能になります。
@org.springframework.modulith.ApplicationModule(
type = Type.OPEN
)
package com.example.demo.common;
import org.springframework.modulith.ApplicationModule.Type;
特定のパッケージだけを他モジュールから参照可能にする
特定のパッケージ以下を全て、ではなく特定のパッケージ直下だけを参照可能にしたい場合があると思います。
例えば以下の SpiInterface
は他モジュールから参照可能にしたいが、 spi.repository
パッケージ以下のクラスは参照可能にしたくない場合。
こういった場合は spi
パッケージの package-info.java
を以下のように記述すると、spiパッケージ直下のクラスのみが参照可能になります。
@org.springframework.modulith.NamedInterface("spi")
package com.example.demo.inventory.spi;
依存できるモジュールを指定して制限する
以下のように
order
は inventory
のみを参照できる。
inventory
は purchase:仕入れ
のみを参照できる。
という制限を行いたい場合は各々の package-info.java
を以下のようにすると実現できます。
order
の package-info.java
@org.springframework.modulith.ApplicationModule(allowedDependencies = "inventory")
package com.example.demo.order;
inventory
の package-info.java
@ApplicationModule(allowedDependencies = "purchase")
package com.example.demo.inventory;
purchase
の package-info.java
@org.springframework.modulith.ApplicationModule(allowedDependencies = "purchase")
package com.example.demo.purchase;
purchaseは自分自身のみを allowedDependencies
に指定することで他のモジュールを参照できなくしています。
このように参照を許可するモジュールを指定することで、許可されていないモジュールには公開されているパッケージも参照できないように制御することができます。
また order
は inventory
と purchase
の両方を参照できるようにしたい、というような場合は複数指定することで制御できます。
@org.springframework.modulith.ApplicationModule(allowedDependencies = {"inventory","purchase"})
package com.example.demo.order;
おわり。