メインページに戻る | Spring Framework & Spring Bootに戻る | Spring Modulithに戻る

Spring Modulith - モジュール間の依存関係

モジュール間の依存関係とは

Spring Modulithにおけるモジュール間の依存関係は、アプリケーションのモジュール構造を明確に定義し、アーキテクチャの整合性を確保するための重要な概念です。モジュール間の依存関係を適切に管理することで、モジュールの独立性を高め、変更の影響範囲を限定し、アプリケーションの保守性と拡張性を向上させることができます。

Spring Modulithは、モジュール間の依存関係を制御するための強力な機能を提供します。デフォルトでは、モジュールは他のモジュールの内部実装に依存することはできず、明示的に公開されたAPIを通じてのみ依存関係を持つことができます。これにより、モジュールの内部実装の詳細が他のモジュールに漏れることを防ぎ、モジュールの独立性を確保します。

+------------------------------------------+
|           Spring Boot Application        |
|                                          |
|  +-------------+       +-------------+   |
|  |             |       |             |   |
|  |  Module A   |------>|  Module B   |   |
|  |             |       |   (API)     |   |
|  +-------------+       +-------------+   |
|                                          |
|  +-------------+       +-------------+   |
|  |             |       |             |   |
|  |  Module C   |------>|  Module D   |   |
|  |             |       |   (API)     |   |
|  +-------------+       +-------------+   |
|         |                                |
|         |         +-------------+       |
|         +-------->|             |       |
|                   |  Module E   |       |
|                   |   (API)     |       |
|                   +-------------+       |
|                                         |
|        Spring Modulith Framework        |
+------------------------------------------+
        

図1: Spring Modulithにおけるモジュール間の依存関係 - モジュールは公開APIを通じてのみ依存関係を持つ

モジュール間の依存関係の管理方法

Spring Modulithでは、モジュール間の依存関係を管理するために、以下の機能を提供しています:

1. 公開APIの定義

モジュールの公開APIは、他のモジュールが依存することができるインターフェースやクラスを定義します。Spring Modulithでは、@org.springframework.modulith.ApplicationModule.APIアノテーションを使用して、公開APIを明示的に定義することができます。

package com.example.application.orders.api;

import org.springframework.modulith.ApplicationModule;

@ApplicationModule.API
public interface OrderService {
    // 公開APIメソッド
    Order createOrder(OrderRequest request);
    Order getOrder(String orderId);
    List<Order> getOrdersByCustomer(String customerId);
}

このアノテーションを付けたクラスやインターフェースは、他のモジュールからアクセス可能な公開APIとして扱われます。公開APIは、モジュールの境界を越えて他のモジュールに提供される機能を明確に定義し、モジュールの内部実装の詳細を隠蔽します。

2. 名前付きインターフェースの定義

より細かい粒度でモジュールの公開APIを定義するために、@org.springframework.modulith.NamedInterfaceアノテーションを使用することができます。このアノテーションを使用すると、特定のパッケージを名前付きインターフェースとして定義し、他のモジュールがそのインターフェースに依存することを許可できます。

@org.springframework.modulith.NamedInterface("api")
package com.example.application.orders.api;

import org.springframework.modulith.NamedInterface;

この例では、com.example.application.orders.apiパッケージを「api」という名前のインターフェースとして定義しています。他のモジュールは、このインターフェースに依存することができます。

3. 依存関係の制約の定義

モジュール間の依存関係に制約を設けるために、@org.springframework.modulith.ApplicationModuleアノテーションのallowedDependencies属性を使用することができます。この属性を使用すると、モジュールが依存することができる他のモジュールを明示的に指定できます。

@org.springframework.modulith.ApplicationModule(
    allowedDependencies = { "inventory", "customers" }
)
package com.example.application.orders;

import org.springframework.modulith.ApplicationModule;

この例では、「orders」モジュールが「inventory」モジュールと「customers」モジュールにのみ依存することを許可しています。他のモジュールへの依存は禁止されます。

4. 依存関係の検証

Spring Modulithは、モジュール間の依存関係を検証するための機能を提供します。Modulithクラスのverify()メソッドを使用すると、アプリケーションのモジュール構造を検証し、不正な依存関係がないかをチェックできます。

@Test
void verifyModuleDependencies() {
    
    // モジュール間の依存関係の検証
    new Modulith(Application.class)
        .verify()
        .assertThatDependenciesAreValid();
}

このテストは、アプリケーションのモジュール構造を検証し、不正な依存関係がある場合にテストが失敗します。これにより、モジュール間の依存関係の整合性を継続的に確保することができます。

モジュール間の依存関係のパターン

Spring Modulithでは、モジュール間の依存関係を管理するために、以下のパターンを活用することができます:

1. 直接依存パターン

直接依存パターンでは、モジュールAがモジュールBの公開APIに直接依存します。このパターンは、モジュール間の関係が明確で、直接的な依存関係が必要な場合に適しています。

// モジュールA内のコード
@Service
class OrderService {

    private final InventoryService inventoryService;
    
    OrderService(InventoryService inventoryService) {
        this.inventoryService = inventoryService;
    }
    
    @Transactional
    public Order createOrder(Order order) {
        // 在庫の確認
        boolean inStock = inventoryService.checkStock(order.getItems());
        
        if (!inStock) {
            throw new OutOfStockException();
        }
        
        // 注文の処理
        // ...
        
        return order;
    }
}

この例では、「orders」モジュール内のOrderServiceが「inventory」モジュールのInventoryServiceに直接依存しています。

2. イベント駆動パターン

イベント駆動パターンでは、モジュール間の直接的な依存関係を避け、イベントを通じて通信します。このパターンは、モジュール間の疎結合性を高め、モジュールの独立性を確保するために適しています。

// モジュールA内のコード
@Service
class OrderService {

    private final ApplicationEventPublisher events;
    
    OrderService(ApplicationEventPublisher events) {
        this.events = events;
    }
    
    @Transactional
    public Order createOrder(Order order) {
        // 注文の処理
        // ...
        
        // イベントの発行
        events.publishEvent(new OrderCreatedEvent(order));
        
        return order;
    }
}

// モジュールB内のコード
@Service
class InventoryService {

    @EventListener
    @Transactional
    void on(OrderCreatedEvent event) {
        // 在庫の更新
        // ...
    }
}

この例では、「orders」モジュール内のOrderServiceOrderCreatedEventを発行し、「inventory」モジュール内のInventoryServiceがそのイベントを購読しています。これにより、「orders」モジュールは「inventory」モジュールに直接依存することなく、通信することができます。

3. 共有カーネルパターン

共有カーネルパターンでは、複数のモジュールが共有するドメインモデルやユーティリティを定義します。このパターンは、モジュール間で共通の概念や機能を共有する必要がある場合に適しています。

// 共有カーネルモジュール内のコード
package com.example.application.shared;

public class Money {
    private final BigDecimal amount;
    private final Currency currency;
    
    // コンストラクタ、メソッドなど
}

// モジュールA内のコード
@Service
class OrderService {

    @Transactional
    public Order createOrder(Order order) {
        // Money クラスの使用
        Money totalPrice = calculateTotalPrice(order.getItems());
        order.setTotalPrice(totalPrice);
        
        // 注文の処理
        // ...
        
        return order;
    }
}

// モジュールB内のコード
@Service
class PaymentService {

    @Transactional
    public Payment processPayment(Order order) {
        // Money クラスの使用
        Money amount = order.getTotalPrice();
        
        // 支払いの処理
        // ...
        
        return payment;
    }
}

この例では、「shared」モジュール内のMoneyクラスが「orders」モジュールと「payments」モジュールで共有されています。

モジュール間の依存関係の可視化

Spring Modulithは、モジュール間の依存関係を可視化するための機能を提供します。ModulithクラスのtoModuleModel()メソッドを使用すると、アプリケーションのモジュール構造をモデル化し、そのモデルを使用して依存関係を可視化できます。

@Test
void createModulithDiagram() {
    
    // モジュール構造のモデル化
    ModuleModel model = new Modulith(Application.class).toModuleModel();
    
    // PlantUML図の生成
    new PlantUmlModuleWriter().write(model, Paths.get("modulith-diagram.puml"));
    
    // AsciiDoc図の生成
    new AsciiDocModuleWriter().write(model, Paths.get("modulith-diagram.adoc"));
}

このコードは、アプリケーションのモジュール構造をPlantUML図とAsciiDoc図として生成します。これらの図を使用して、モジュール間の依存関係を視覚的に理解することができます。

モジュール間の依存関係のテスト

Spring Modulithは、モジュール間の依存関係をテストするための機能を提供します。@ModuleTestアノテーションを使用すると、特定のモジュールとその依存モジュールのみをロードしてテストを実行できます。

@SpringBootTest
@ModuleTest(value = "orders")
class OrderModuleTests {

    @Autowired OrderService orders;
    
    @Test
    void testCreateOrder() {
        // テストコード
        // ...
    }
}

このテストは、「orders」モジュールとその依存モジュールのみをロードしてテストを実行します。これにより、モジュール間の依存関係が適切に定義されていることを確認できます。

モジュール間の依存関係のベストプラクティス

  1. 明確な公開APIの定義: モジュールの公開APIを明確に定義し、内部実装の詳細を隠蔽します。公開APIは、モジュールが提供する機能を明確に表現し、他のモジュールが依存するためのインターフェースを提供します。
  2. 最小限の公開API: モジュールの公開APIは最小限に保ち、必要な機能のみを公開します。不必要な機能を公開すると、モジュール間の結合度が高まり、変更の影響範囲が広がります。
  3. 依存関係の方向性の管理: モジュール間の依存関係の方向性を管理し、循環依存を避けます。依存関係は一方向であるべきで、上位レベルのモジュールが下位レベルのモジュールに依存するようにします。
  4. イベント駆動通信の活用: モジュール間の直接的な依存関係を減らすために、イベント駆動通信を活用します。イベントを通じて通信することで、モジュール間の疎結合性を高めることができます。
  5. 共有カーネルの適切な使用: 共有カーネルは、複数のモジュールで共有される概念や機能を定義するために使用します。共有カーネルは最小限に保ち、モジュール間の結合度を低く保ちます。
  6. 依存関係の検証の自動化: モジュール間の依存関係の検証を自動化し、継続的インテグレーションパイプラインに組み込みます。これにより、不正な依存関係が導入されることを防ぎます。
  7. 依存関係の可視化: モジュール間の依存関係を定期的に可視化し、アーキテクチャの整合性を確認します。依存関係の可視化により、アーキテクチャの理解と保守が容易になります。

アンチパターン

まとめ

Spring Modulithにおけるモジュール間の依存関係の管理は、モジュラーアプリケーションの成功に不可欠な要素です。適切な依存関係の管理により、モジュールの独立性を高め、変更の影響範囲を限定し、アプリケーションの保守性と拡張性を向上させることができます。

Spring Modulithは、モジュール間の依存関係を管理するための強力な機能を提供します。公開APIの定義、名前付きインターフェースの定義、依存関係の制約の定義、依存関係の検証など、様々な機能を活用することで、アーキテクチャの整合性を確保しながら、モジュール間の適切な依存関係を構築することができます。

モジュール間の依存関係を適切に管理するためには、明確な公開APIの定義、最小限の公開API、依存関係の方向性の管理、イベント駆動通信の活用、共有カーネルの適切な使用、依存関係の検証の自動化、依存関係の可視化などのベストプラクティスを実践することが重要です。これらのプラクティスを実践することで、モジュラーアプリケーションの品質と持続可能性を向上させることができます。