2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ZOZOAdvent Calendar 2024

Day 22

Spring Bootにおける非同期処理: ApplicationEventPublisherとSpockのPollingConditionsによるテスト

Last updated at Posted at 2024-12-21

はじめに

この記事では、Spring Bootを用いたREST APIの非同期処理の実装と、テストフレームワークSpockを使用した非同期処理のテスト方法について解説します。

特に、ApplicationEventPublisherを利用したイベント駆動型の非同期処理の実装と、非同期処理の結果を検証するためのPollingConditionsを使ったテスト手法に焦点を当てます。

非同期処理は、APIがクライアントに素早くレスポンスを返しつつ、バックグラウンドで時間のかかる処理を実行する場合に非常に有用です。

具体的な実装例を通じて、非同期処理の流れ、コードの構造、そしてテストの詳細を紹介します。

処理の流れ

実装サンプルとして、以下の処理を想定します。

  1. クライアントがREST APIを呼び出し、注文データを送信
  2. APIは即座に202 Acceptedでレスポンス
  3. 非同期イベントが発行され、バックグラウンドで注文データをデータベースに保存
  4. 注文完了メールの送信など、後続の処理を実行

実装環境

  • Java 21
  • Spring Boot 3.4
  • Spock(テスト用)

エンティティの定義

@Entity
@Table(name = "orders")
public record OrderEntity(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id,
    
    @Column(name = "order_number", nullable = false)
    String orderNumber,
    
    @Column(name = "created_at", nullable = false)
    LocalDateTime createdAt
) {}

注文情報を保存するJPAエンティティで、注文番号と作成日時を持ちます。

リポジトリの実装

public interface OrderRepository extends JpaRepository<OrderEntity, Long> {
    Optional<OrderEntity> findByOrderNumber(String orderNumber);
}

Spring Data JPAを使用して注文情報を管理するリポジトリインターフェースです。

イベントの定義

public record OrderCreatedEvent(
    String orderNumber,
    LocalDateTime occurredAt
) {
    public OrderCreatedEvent(String orderNumber) {
        this(orderNumber, LocalDateTime.now());
    }
}

注文作成時に発行される非同期イベントで、シンプルで不変なイベントを表現しています。このサンプルではJava 14以降で導入されたrecordを使用していますが、従来のPOJO(Plain Old Java Object)を使用することも可能です。

Spring Framework 4.2より前のバージョンではApplicationEventの継承が必須でしたが、以降のバージョンでは継承が任意となりました。

コントローラーの実装

このコントローラーは、注文リクエストを受け付け、非同期イベントを発行する役割を担っています。特に重要なのは、ApplicationEventPublisherを利用してイベントを発行する点です。このアプローチにより、システムはリクエストを受け取った後、即座にレスポンスを返すことが可能になります。

ApplicationEventPublisherの役割

ApplicationEventPublisherは、Spring Frameworkにおけるイベント駆動型プログラミングの中心的なコンポーネントです。コントローラー内でこのインターフェースを使用することで、以下のような利点があります:

  • 非同期処理の実現: イベントが発行されると、その処理はバックグラウンドで行われるため、APIはクライアントに対して迅速にレスポンスを返すことができます。
  • 疎結合な設計: イベント駆動型アーキテクチャでは、コントローラーとイベントリスナーが疎結合になります。
  • 柔軟な処理フロー: イベントを通じて異なる処理を連携させることができ、例えば注文データの保存後にメール送信などの後続処理を簡単に追加できます。

コード例

以下は、ApplicationEventPublisherを使用してイベントを発行するコントローラーの実装例です。

@RestController
@RequestMapping("/api/orders")
public class OrderController {
    private final ApplicationEventPublisher publisher;

    public OrderController(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    @PostMapping
    @ResponseStatus(HttpStatus.ACCEPTED)
    public void createOrder(@RequestBody CreateOrderRequest request) {
        var orderNumber = generateOrderNumber();
        // イベントを発行
        publisher.publishEvent(new OrderCreatedEvent(orderNumber));
    }

    private String generateOrderNumber() {
        return "ORD-" + UUID.randomUUID().toString().substring(0, 8);
    }
}

この実装では、クライアントから注文データを受け取った際に、新しい注文番号を生成し、その情報を持つOrderCreatedEventを発行しています。このイベントは非同期で処理されるため、APIはすぐに202 Acceptedのレスポンスを返します。

イベントリスナーの実装

この章では、コントローラーから発行されたイベントを受信し、処理を行うイベントリスナーの実装について説明します。@EventListenerアノテーションを使用することで、特定の型のイベントを受信することができます。このアプローチにより、コントローラーで発行されたイベントが適切に処理されることが保証されます。

@EventListenerとイベントの対応

@EventListenerは、Spring Frameworkにおけるイベントリスニングのためのアノテーションです。このアノテーションを付与したメソッドは、指定された型のイベントが発行された際に呼び出されます。重要な点は、発行されたイベントオブジェクトの型が、リスナーで指定された型と一致する必要があるということです。これにより、特定のイベントに対して正確な処理を行うことができます。例えば、以下のようにOrderCreatedEvent型のイベントを受信するリスナーを実装します。

@Component
public class OrderEventListener {
    private final OrderRepository orderRepository;

    public OrderEventListener(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Async
    @EventListener
    @Transactional
    public void handleOrderCreatedEvent(OrderCreatedEvent event) {
        var order = new OrderEntity(
            null,
            event.orderNumber(),
            event.occurredAt()
        );
        orderRepository.save(order);
    }
}

この例では、OrderCreatedEventが発行されると、handleOrderCreatedEventメソッドが呼び出されます。イベントオブジェクトが一致することで、このメソッドは正しく機能します。

非同期処理の実現

さらに、このリスナーには@Asyncアノテーションが付与されています。これにより、イベント処理は非同期的に実行され、メインスレッドとは別のスレッドで処理されます。非同期処理を有効にすることで、APIから即座にレスポンスを返した後も、バックグラウンドで時間のかかる処理を実行できるため、システム全体のパフォーマンスが向上します。もし@Asyncアノテーションを付与しなかった場合、このメソッドは同期的に実行され、APIからのレスポンス待ちとなってしまいます。

エントリポイントの実装

重要なポイントは、@EnableAsyncアノテーションを付与することで、@Asyncアノテーションが機能するようになる点です。@EnableAsyncは、アプリケーション全体で非同期処理を有効化するために必要な設定です。このアノテーションは、@Configurationクラスに記載しても問題ありません。

非同期処理の有効化

以下のコードは、エントリポイントの実装例です。

@SpringBootApplication
@EnableAsync
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(2);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("AsyncTask-");
        executor.initialize();
        return executor;
    }
}

このコードでは、@SpringBootApplication@EnableAsyncを組み合わせて使用しています。これにより、アプリケーション内で非同期処理が可能になります。

### ExecutorのBean登録

非同期処理を行うためのExecutorのBean登録は必須ではありません。上記の例のように細かくチューニングを行うことができる一方で、省略した場合でもSpringが提供するデフォルトのExecutorによって非同期実行は行われます。デフォルト設定でも基本的な非同期処理は可能ですが、特定の要件やパフォーマンス向上を目指す場合には、自分自身でExecutorを設定することが推奨されます。

テストの実装

非同期処理のテストにおいて重要なのは、APIが即座にレスポンスを返す一方で、バックグラウンドで行われる処理が正しく実行されることを確認することです。このため、テストでは遅延非同期処理の結果を検証する必要があります。ここでは、SpockフレームワークとGroovy SQLを使用して、非同期処理の完了を確認する方法としてPollingConditionsを利用します。

テストの流れ

以下に示すテストは、注文APIを呼び出した際に、データベースに注文情報が正しく保存されることを確認するものです。具体的な流れは以下の通りです。

  1. 準備(Given): テストリクエストとしてサンプルの注文データを用意し、PollingConditionsを設定します。これにより、指定した条件が満たされるまで待機することができます。
  2. API呼び出し(When): MockMvcを使用して注文APIを呼び出します。この際、APIは即座に202 Acceptedのステータスコードでレスポンスを返します。
  3. 結果検証(Then): PollingConditionsを使用して、データベースに注文が保存されるまで待機します。条件が満たされた場合には、最新の注文情報が正しく保存されているかどうかを確認します。

実装例

以下は具体的なテストコードです。

@SpringBootTest
@AutoConfigureMockMvc
class OrderControllerTest extends Specification {
    
    @Autowired
    MockMvc mockMvc
    
    @Autowired
    DataSource dataSource
    
    def "注文が非同期で保存されること"() {
        given:
        def request = [orderData: "sample order"]
        def conditions = new PollingConditions(timeout: 5, initialDelay: 0.1, factor: 1.25)
        def sql = new Sql(dataSource)
        
        when: "注文APIを呼び出し"
        mockMvc.perform(post("/api/orders")
                .contentType("application/json")
                .content(toJson(request)))
                .andExpect(status().isAccepted())
        
        then: "データベースに注文が保存されるまで待機"
        conditions.eventually {
            def result = sql.firstRow("""
                SELECT * FROM orders 
                ORDER BY created_at DESC 
                LIMIT 1
            """)
            assert result.order_number.startsWith("ORD-")
        }
        
        cleanup:
        sql.close()
    }
}

PollingConditionsの役割

PollingConditionsは、非同期処理の結果を待機するために非常に便利なツールです。このクラスは指定した条件が満たされるまで繰り返し評価し、タイムアウトや初期遅延などの設定も可能です。これにより、非同期処理が完了するまでテストがブロックされることなく進行し、効率的な検証が可能となります。

注意点

  • 非同期処理の特性: 非同期処理では、APIから即座にレスポンスが返ってくるため、その後の処理が完了するタイミングは不確定です。そのため、テストでは結果が得られるまで待機する必要があります。
  • タイムアウト設定: PollingConditionsのタイムアウト設定は重要です。適切な時間を設定することで、テストが無限ループに陥ることを防ぎます。

このようにして、APIから即座にレスポンスを受け取った後でも、バックグラウンドで行われる非同期処理の結果を検証することができます。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?