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

Spring Modulith - イベント発行

Spring Modulithにおけるイベント発行とは

Spring Modulithにおけるイベント発行は、モジュール間の疎結合な通信を実現するための重要な機能です。イベント駆動アーキテクチャを採用することで、モジュール間の直接的な依存関係を減らし、モジュールの独立性を高めることができます。Spring Modulithは、Spring Frameworkのアプリケーションイベント機能を拡張し、モジュラーアプリケーションに適したイベント処理機能を提供します。

イベント発行を使用すると、あるモジュールで発生した重要な出来事(ドメインイベント)を他のモジュールに通知することができます。例えば、「注文」モジュールで注文が作成されたとき、「在庫」モジュールや「配送」モジュールにその事実を通知することができます。これにより、各モジュールは他のモジュールの内部実装に依存することなく、自律的に動作することができます。

+------------------------------------------+
|           Spring Boot Application        |
|                                          |
|  +-------------+                         |
|  |             |                         |
|  |  Module A   |                         |
|  |             |                         |
|  +------+------+                         |
|         |                                |
|         | イベント発行                    |
|         v                                |
|  +------+------+                         |
|  |             |                         |
|  | Event Bus   |                         |
|  |             |                         |
|  +------+------+                         |
|         |                                |
|         | イベント購読                    |
|         |                                |
|  +------v------+    +-------------+     |
|  |             |    |             |     |
|  |  Module B   |    |  Module C   |     |
|  |             |    |             |     |
|  +-------------+    +-------------+     |
|                                         |
|        Spring Modulith Framework        |
+------------------------------------------+
        

図1: Spring Modulithにおけるイベント発行と購読の流れ

Spring Modulithのイベント発行機能

Spring Modulithは、以下のイベント発行機能を提供しています:

1. 同期イベント処理

同期イベント処理は、イベントが発行されたときに、すべてのイベントリスナーが同じトランザクション内で処理を完了するまで、イベント発行元の処理が待機する方式です。これにより、イベントの処理が確実に完了することが保証されますが、処理時間が長くなる可能性があります。

@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;
    }
}

2. 非同期イベント処理

非同期イベント処理は、イベントが発行されたときに、イベントリスナーの処理を別のスレッドで実行する方式です。これにより、イベント発行元の処理がイベントリスナーの処理を待つことなく、すぐに次の処理に進むことができます。非同期イベント処理を使用するには、@Asyncアノテーションをイベントリスナーメソッドに付加します。

@Service
class InventoryService {

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

3. トランザクショナルイベント処理

トランザクショナルイベント処理は、トランザクションの特定のフェーズでイベントを発行する方式です。Spring Modulithは、@TransactionalEventListenerアノテーションを使用して、トランザクションの開始前、コミット前、コミット後、ロールバック後などの特定のフェーズでイベントを処理することができます。

@Service
class InventoryService {

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    void on(OrderCreatedEvent event) {
        // トランザクションがコミットされた後に在庫を更新
        // ...
    }
}

4. イベントの永続化

Spring Modulithは、イベントを永続化する機能を提供しています。イベントを永続化することで、システムの障害が発生した場合でも、イベントが失われることなく、システムの回復後に処理を再開することができます。イベントの永続化を使用するには、spring-modulith-events-jpaまたはspring-modulith-events-mongodbなどの依存関係を追加し、設定を行います。

<dependency>
    <groupId>org.springframework.modulith</groupId>
    <artifactId>spring-modulith-events-jpa</artifactId>
    <version>1.1.0</version>
</dependency>
# application.properties
spring.modulith.events.persistence.enabled=true

イベントの設計と実装

Spring Modulithでイベントを効果的に活用するためには、適切なイベントの設計と実装が重要です。以下に、イベントの設計と実装のポイントを説明します。

1. イベントクラスの設計

イベントクラスは、発生した出来事に関する情報を含むPOJO(Plain Old Java Object)として設計します。イベントクラスは、不変(イミュータブル)であることが望ましく、必要な情報のみを含めるべきです。

public class OrderCreatedEvent {

    private final String orderId;
    private final String customerId;
    private final List<OrderItem> items;
    private final LocalDateTime createdAt;
    
    public OrderCreatedEvent(Order order) {
        this.orderId = order.getId();
        this.customerId = order.getCustomerId();
        this.items = new ArrayList<>(order.getItems());
        this.createdAt = LocalDateTime.now();
    }
    
    // getterメソッド
    public String getOrderId() {
        return orderId;
    }
    
    public String getCustomerId() {
        return customerId;
    }
    
    public List<OrderItem> getItems() {
        return Collections.unmodifiableList(items);
    }
    
    public LocalDateTime getCreatedAt() {
        return createdAt;
    }
}

2. イベント発行の実装

イベントの発行は、ApplicationEventPublisherを使用して行います。ApplicationEventPublisherは、Spring Frameworkが提供するインターフェースで、Spring Modulithはこのインターフェースを拡張して、モジュラーアプリケーションに適したイベント発行機能を提供しています。

@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;
    }
}

3. イベントリスナーの実装

イベントリスナーは、@EventListenerアノテーションを使用して実装します。イベントリスナーは、特定のイベントタイプを購読し、そのイベントが発行されたときに処理を実行します。

@Service
class InventoryService {

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

4. 条件付きイベントリスナー

条件付きイベントリスナーは、特定の条件を満たす場合にのみイベントを処理するリスナーです。@EventListenerアノテーションのcondition属性を使用して、SpEL(Spring Expression Language)式で条件を指定することができます。

@Service
class InventoryService {

    @EventListener(condition = "#event.items.size() > 0")
    @Transactional
    void on(OrderCreatedEvent event) {
        // 注文アイテムが存在する場合のみ在庫を更新
        // ...
    }
}

イベント発行のユースケース

Spring Modulithのイベント発行機能は、様々なユースケースで活用することができます。以下に、代表的なユースケースを紹介します。

1. ドメインイベントの通知

ドメインイベントは、ドメインモデル内で発生した重要な出来事を表すイベントです。例えば、注文の作成、支払いの完了、商品の出荷などがドメインイベントに該当します。ドメインイベントを発行することで、他のモジュールにその出来事を通知し、適切な処理を行うことができます。

@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;
    }
}

2. 副作用の処理

副作用は、主要な処理の結果として発生する追加の処理です。例えば、注文が作成されたときに、在庫の更新、顧客への通知メールの送信、ポイントの付与などが副作用に該当します。イベント発行を使用することで、主要な処理と副作用を分離し、モジュールの責務を明確にすることができます。

@Service
class NotificationService {

    @EventListener
    void on(OrderCreatedEvent event) {
        // 顧客への通知メールの送信
        // ...
    }
}

@Service
class PointService {

    @EventListener
    @Transactional
    void on(OrderCreatedEvent event) {
        // ポイントの付与
        // ...
    }
}

3. 非同期処理

非同期処理は、主要な処理の完了を待たずに、別のスレッドで実行される処理です。例えば、注文が作成されたときに、時間のかかる在庫の更新や配送の手配などを非同期で行うことができます。非同期イベント処理を使用することで、ユーザーの応答時間を短縮し、システムの応答性を向上させることができます。

@Service
class ShippingService {

    @Async
    @EventListener
    void on(OrderCreatedEvent event) {
        // 配送の手配(時間のかかる処理)
        // ...
    }
}

4. イベントソーシング

イベントソーシングは、システムの状態変更をイベントとして記録し、それらのイベントを再生することでシステムの状態を再構築する手法です。Spring Modulithのイベント永続化機能を使用することで、イベントソーシングを実装することができます。

@Configuration
@EnableJpaRepositories
class EventSourcingConfiguration {

    @Bean
    ApplicationListener<OrderCreatedEvent> orderEventStore(JdbcTemplate jdbc) {
        return event -> {
            // イベントをデータベースに保存
            jdbc.update(
                "INSERT INTO order_events (order_id, event_type, event_data, created_at) VALUES (?, ?, ?, ?)",
                event.getOrderId(),
                "OrderCreated",
                objectMapper.writeValueAsString(event),
                event.getCreatedAt()
            );
        };
    }
}

イベント発行のベストプラクティス

Spring Modulithでイベント発行を効果的に活用するためのベストプラクティスを紹介します。

1. 適切なイベント粒度の選択

イベントの粒度は、システムの要件や設計方針に応じて適切に選択する必要があります。粒度が細かすぎると、イベントの数が増えてシステムの複雑性が増し、粒度が粗すぎると、イベントの汎用性が低下します。一般的には、ドメインモデルの変更を表す意味のある単位でイベントを設計することが望ましいです。

2. イベントの命名規則の統一

イベントの命名規則を統一することで、イベントの意味や目的を明確にし、開発者間のコミュニケーションを円滑にすることができます。一般的には、「〜Created」「〜Updated」「〜Deleted」などの過去分詞形を使用して、発生した出来事を表現することが多いです。

3. イベントの不変性の確保

イベントは、発生した出来事を表すものであり、その内容が変更されるべきではありません。イベントクラスは不変(イミュータブル)に設計し、すべてのフィールドをfinalにして、変更できないようにすることが望ましいです。

4. イベントの永続化の検討

重要なイベントは、システムの障害が発生した場合でも失われないように、永続化することを検討すべきです。Spring Modulithは、JPA、MongoDB、Redisなど、様々なデータストアを使用したイベントの永続化をサポートしています。

5. イベントリスナーの例外処理

イベントリスナーで例外が発生した場合の動作を明確にし、適切に処理する必要があります。同期イベント処理の場合、リスナーの例外はイベント発行元に伝播しますが、非同期イベント処理の場合は、例外を適切にキャッチして処理する必要があります。

@Service
class InventoryService {

    @Async
    @EventListener
    void on(OrderCreatedEvent event) {
        try {
            // 在庫の更新
            // ...
        } catch (Exception e) {
            // 例外の処理
            log.error("在庫の更新に失敗しました", e);
            // 必要に応じて、エラーイベントを発行するなどの処理を行う
            events.publishEvent(new InventoryUpdateFailedEvent(event.getOrderId(), e));
        }
    }
}

6. イベントの順序の考慮

イベントの処理順序が重要な場合は、@Orderアノテーションを使用して、リスナーの実行順序を指定することができます。ただし、非同期イベント処理の場合は、実行順序が保証されないことに注意が必要です。

@Service
class InventoryService {

    @Order(1)
    @EventListener
    @Transactional
    void on(OrderCreatedEvent event) {
        // 在庫の更新(最初に実行)
        // ...
    }
}

@Service
class NotificationService {

    @Order(2)
    @EventListener
    void on(OrderCreatedEvent event) {
        // 顧客への通知(在庫の更新の後に実行)
        // ...
    }
}

7. イベントの冪等性の確保

イベントが重複して処理される可能性がある場合は、イベントリスナーの処理を冪等(何度実行しても同じ結果になる)にすることが重要です。これにより、システムの障害からの回復時などに、同じイベントが複数回処理されても問題が発生しないようにすることができます。

@Service
class InventoryService {

    @EventListener
    @Transactional
    void on(OrderCreatedEvent event) {
        // イベントが既に処理されているかチェック
        if (isEventProcessed(event.getOrderId())) {
            return;
        }
        
        // 在庫の更新
        // ...
        
        // イベントを処理済みとしてマーク
        markEventAsProcessed(event.getOrderId());
    }
}

まとめ

Spring Modulithのイベント発行機能は、モジュール間の疎結合な通信を実現するための強力なツールです。イベント駆動アーキテクチャを採用することで、モジュールの独立性を高め、システムの柔軟性と拡張性を向上させることができます。

Spring Modulithは、同期イベント処理、非同期イベント処理、トランザクショナルイベント処理、イベントの永続化など、様々なイベント処理機能を提供しています。これらの機能を適切に活用することで、モジュラーアプリケーションの品質と保守性を向上させることができます。

イベント発行を効果的に活用するためには、適切なイベントの設計と実装、ベストプラクティスの適用が重要です。イベントの粒度、命名規則、不変性、永続化、例外処理、順序、冪等性などを考慮して、イベント駆動アーキテクチャを設計・実装することで、モジュラーアプリケーションの成功に貢献することができます。