0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Javaの並列バッチ処理をCucumberについて考えてみた

Posted at

はじめに

業務システムでよくある「大量データのバッチ処理」。
今回は、契約番号1500万件を対象にした外部APIとの並列リクエスト処理を実装し、
その中で発生しうるAPIエラーやタイムアウトに応じた分岐処理を設計・検証していきます。

以下のような課題をどう乗り越えるかがテーマです:

  • 並列処理で順序が保証されない状況でも、仕様通りの振る舞いができるか?
  • socketTimeoutが連続で発生した場合の「途中終了」処理はどうテストする?
  • BDD(Cucumber)で仕様ベースのテストをどう設計する?

処理仕様(簡略図)

  1. インプットファイル(契約番号1500万件)

  2. 各契約番号で外部APIにリクエスト(20スレッド)

  3. レスポンス内容によって処理を分岐:

    • ✅ 200かつ内容OK → DB SELECT → UPDATE or INSERT
    • ⚠️ 400かつ「対象外」 → エラーファイルに記録
    • 🔁 500 or タイムアウト → リランファイルに記録
    • ❌ socketTimeoutが4連続発生 → 残りのデータを全件リランファイルに記録

実装のポイント(抜粋)

Sample.java
import java.util.List;
import java.util.concurrent.*;
import java.net.SocketTimeoutException;

public class BatchExecutor {

    private final ExecutorService executor;
    private final ApiClient apiClient;
    private final ResultWriter resultWriter;
    private final DbService dbService;
    private final List<ContractRecord> inputRecords;
    // timeout発生回数(スレッドセーフ)
    private final AtomicInteger socketTimeoutCount = new AtomicInteger(0); 
    // 処理中断フラグ(全スレッドで共有される)
    private volatile boolean abortRemaining = false; 


    public BatchExecutor(List<ContractRecord> inputRecords, ApiClient apiClient,
                         ResultWriter resultWriter, DbService dbService, int threadCount) {
        this.executor = Executors.newFixedThreadPool(threadCount);
        this.inputRecords = inputRecords;
        this.apiClient = apiClient;
        this.resultWriter = resultWriter;
        this.dbService = dbService;
    }

    public void execute() throws InterruptedException {
        for (ContractRecord record : inputRecords) {
            if (abortRemaining) {
            // 4連続timeout後は全件リラン出力
                resultWriter.writeRerun(record); 
                continue;
            }
            // 並列で契約番号1件ずつ処理
            executor.submit(() -> processContract(record));
        }
        executor.shutdown();
        executor.awaitTermination(30, TimeUnit.MINUTES);
    }

    private void processContract(ContractRecord record) {
        try {
            ApiResponse response = apiClient.sendRequest(record);

            if (response.status == 200 && response.isValid()) {
                dbService.saveOrUpdate(record, response);
            } else if (response.status == 400 && response.message.equals("対象外")) {
                resultWriter.writeError(record);
            } else if (response.status == 500 || response.isTimeout()) {
                resultWriter.writeRerun(record);
            }

        } catch (SocketTimeoutException e) {
            int current = socketTimeoutCount.incrementAndGet();
            resultWriter.writeRerun(record);

            if (current >= 4) {
            // 4件連続でsocketTimeoutが発生したらフラグを立てる
                abortRemaining = true;
            }
        } catch (Exception e) {
            // その他例外もリランとして記録(業務による)
            resultWriter.writeRerun(record);
        }
    }
}


Cucumberテストシナリオ例(Gherkin)

Feature: バッチ処理の並列実行とレスポンス別の分岐処理

  # ✅ 正常系
  Scenario: 200 OKでDBに登録または更新される
    Given インプットに契約番号が3件ある
    And APIはすべて200 OKを返す
    When バッチ処理を実行する
    Then データベースに3件登録されている

  # ⚠️ エラー(400)記録
  Scenario: 対象外メッセージでエラーファイルに記録
    Given 契約番号が1件あり、APIが400「対象外」を返す
    When バッチ処理を実行する
    Then エラーファイルにその契約が出力されている

  # 🔁 500系・timeout
  Scenario: 500エラーでリランファイルに記録
    Given APIが500エラーを返す
    Then リランファイルにその契約が記録されている

  Scenario: すべての契約に対してsocketTimeoutが発生した場合、リランファイルにすべて記録される
    Given インプットファイルに契約番号が10件ある
    And APIはすべてsocketTimeoutを返す
    When バッチ処理を実行する
    Then リランファイルに10件すべてが出力されている

テスト観点の整理

このような並列処理・分岐処理をテストする場合、以下の観点を意識しておくと実装やテストの品質が安定します。

観点 内容 検証方法
並列処理の順序非保証 スレッドは順不同で実行される ファイル出力の順番でなく「件数・中身の正確さ」で比較する
特定条件で処理中断 socketTimeoutが4回発生したら以降の処理を止める タイムアウト数をMockで制御し、リランファイルの件数で確認
レスポンス別の分岐 200 / 400 / 500 / timeout で処理を変える 各コードに対するAPIスタブを作成し、対応ファイル出力を確認
スレッドセーフな状態管理 カウントや中断フラグを全スレッドから安全に扱う AtomicIntegervolatile を使うことを確認する

シナリオ設計における心構え

テストは「順序通りに動くこと」ではなく、「期待した状態になること」を検証するもの。
並列処理では、レスポンス順序やログ順に頼らず、データの最終状態(件数・内容)を確認するのが原則です。

まとめ

  • 並列処理+外部API分岐という業務でありがちなシナリオを実装&検証した
  • 仕様どおりの動作を担保するためには、テスト設計時のデータ制御・状態検証の観点が重要
  • 順序に頼らず、出力件数・中断条件・エラーハンドリングの網羅性で品質を確保する

このようなパターンは金融・保険・通信などでも頻出するので、仕組みや観点を理解しておくと現場対応力が高まります

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?