Spring Modulithでモジュラモノリスライクな依存関係のコントロールを試してみた

# Spring Modulithとは
> Spring Modulith は、Spring Boot を使用してドメイン駆動型のモジュール型アプリケーションを構築するための独自のツールキットです。Spring Boot がアプリケーションの技術的な配置について独自の見解を持っているのと同じように、Spring Modulith は、アプリを関数に構造化する方法について独自の見解を実装し、個々の論理部分が相互にやり取りできるようにします。その結果、Spring Modulith を使用すると、開発者は更新が容易なアプリケーションを構築して、時間の経過とともに変化するビジネス要件に対応できるようになります。

https://spring.pleiades.io/spring-modulith/reference/

# 本記事の内容
Spring Modulith の依存関係をコントロールする機能を試してみた記録です。
先日参加したJJUG CCC 2024 Fallで紹介されていて興味が湧き試してみました◎

https://www.docswell.com/s/Simplex/ZJ4XQE-simplex_sugawara01#p1

他にもアプリケーションモジュールのドキュメント化やテスト実行に関する機能もありますが、今回は依存関係のコントロールに絞った内容となります。

# コントロールできる依存関係
- 公開パッケージ以外の参照の禁止
- 循環参照の禁止

デフォルトの制御としては上記が行われ、必要に応じてカスタマイズすることができます。
デフォルトの制限を守ることでモジュラモノリスライクな依存関係のコントロールが実現できます。

# 試してみる
## テスト用アプリケーションの構成
一つのアプリケーションの中に `inventory:在庫` と `order:注文` という2つのモジュールが存在し、それぞれのモジュールが疎結合なモジュラモノリスアプリケーションを作成したい場合を考えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1126770/60c1ff73-bca6-7aff-fab3-63c897395818.png)

`com.example.demo.DemoApplication.java` がSpringBootのApplicationクラスです。
Applicationクラスを配置したパッケージのサブパッケージである `inventory` や `order` が各モジュールのAPIパッケージとして認識されます。

Spring ModulithではこのAPIパッケージ以下が一つのモジュールとして扱われ、直下のクラスのみが他のモジュールから参照可能になります。
そのため `inventory` や `order` パッケージの直下には他のモジュールから参照されることを想定したインターフェースやオブジェクトクラスを配置します。

それぞれのモジュールには `entity` `repository` `service` のようなサブパッケージがありますが、これらのパッケージ以下のクラスは他のモジュールから参照することができません。この制御によってモジュール間の依存が整理されたアプリケーションを構築することができます。

## 検査の実行
以下のようなテストを書いて実行します。
依存関係の違反があった場合にはこのテストがfailします。
テストを実行する必要があるため、実際にプロダクト開発に検査の仕組みを組み込むためにはCIでテストを実行する仕組みが必要になります。

```Java
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 を直接参照するという不届きな実装をしてみます。

```Java
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します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1126770/dd6a7a98-f957-4a1a-50cc-030dc088de4b.png)

```
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 の公開されていない型にアクセスしている、というエラーが表示されます。


次に公開されているパッケージへの参照を実装してみます。

```Java
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します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1126770/58b02bff-d00a-c925-27ff-c355f63c82ec.png)

このように公開されているパッケージ以外へのアクセスを禁止することができます。

## 循環参照の禁止

上記の `OrderService` が `IInventoryModule` を参照している状態で `InventoryService` が `IOrderModule` を参照するような実装をしてみます。

```Java
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します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1126770/d035b58e-f593-678f-9670-4df74e0f2272.png)

```
org.springframework.modulith.core.Violations: - Cycle detected: Slice inventory
```

循環参照しているというエラーが表示されます。

## カスタマイズ

### 特定のパッケージ以下を他モジュールから参照可能にする
例えばどのモジュールでも共通で利用するユーティリティーなどを以下のように配置したい場合があると思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1126770/f6d7fce1-77fd-71dd-112e-ae609361dd6c.png)

こういった時は `common` パッケージの `package-info.java` を以下のように記述すると、common以下のパッケージが参照可能になります。

```Java
@org.springframework.modulith.ApplicationModule(
  type = Type.OPEN
)
package com.example.demo.common;

import org.springframework.modulith.ApplicationModule.Type;
```

### 特定のパッケージだけを他モジュールから参照可能にする

特定のパッケージ以下を全て、ではなく特定のパッケージ直下だけを参照可能にしたい場合があると思います。
例えば以下の `SpiInterface` は他モジュールから参照可能にしたいが、 `spi.repository` パッケージ以下のクラスは参照可能にしたくない場合。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1126770/43e64d51-4e8f-8ba7-4c5d-367671292621.png)

こういった場合は `spi` パッケージの `package-info.java` を以下のように記述すると、spiパッケージ直下のクラスのみが参照可能になります。

```Java
@org.springframework.modulith.NamedInterface("spi")
package com.example.demo.inventory.spi;
```

### 依存できるモジュールを指定して制限する

以下のように

![参照の図.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1126770/441625ab-def4-fd49-d27a-eea5b1a6f609.png)

`order` は `inventory` のみを参照できる。
`inventory` は `purchase:仕入れ` のみを参照できる。
という制限を行いたい場合は各々の `package-info.java` を以下のようにすると実現できます。


`order` の  `package-info.java` 
```Java
@org.springframework.modulith.ApplicationModule(allowedDependencies = "inventory")
package com.example.demo.order;
```

`inventory` の `package-info.java` 
```Java
@ApplicationModule(allowedDependencies = "purchase")
package com.example.demo.inventory;
```

`purchase` の `package-info.java` 
```Java
@org.springframework.modulith.ApplicationModule(allowedDependencies = "purchase")
package com.example.demo.purchase;
```
purchaseは自分自身のみを `allowedDependencies` に指定することで他のモジュールを参照できなくしています。
このように参照を許可するモジュールを指定することで、許可されていないモジュールには公開されているパッケージも参照できないように制御することができます。

また `order` は `inventory` と `purchase` の両方を参照できるようにしたい、というような場合は複数指定することで制御できます。

```Java
@org.springframework.modulith.ApplicationModule(allowedDependencies = {"inventory","purchase"})
package com.example.demo.order;
```

おわり。