概要

バッチ処理でエラーが発生したとき、どう振る舞うかは業務要件によって大きく異なります。決済データの取込なら1件でもエラーがあれば全体を止めるべきですし、大量のログ集計であればエラー行をスキップして残りを処理するほうが現実的です。この記事では、エラー時の振る舞いを「異常終了(即停止)」「継続(ログ記録して次へ)」「スキップ(N件まで許容、超過で停止)」の3つの戦略として定義し、ErrorPolicy enum と ErrorHandler クラスで切り替え可能に実装します。CSV行単位の処理を例に、レコードごとのエラーハンドリングパターンを示します。

使いどころ

決済データ取込バッチで不正レコードを検出した場合に全件ロールバックして即停止する

アクセスログ集計バッチで解析できない行をスキップし残りの集計を継続する

顧客マスタ同期バッチでエラーが100件を超えたらデータ品質に問題ありとして停止する

コード例

ErrorPolicy + ErrorHandler
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;

public class BatchErrorHandling {

    public enum ErrorPolicy { FAIL_FAST, CONTINUE, SKIP_LIMIT }

    public interface RecordProcessor { void process(String record) throws Exception; }

    public static class ErrorHandler {
        private static final Logger LOGGER = Logger.getLogger(ErrorHandler.class.getName());
        private final ErrorPolicy policy;
        private final int maxSkips;
        private int skipCount;
        private final List<String> skipped = new ArrayList<String>();

        public ErrorHandler(ErrorPolicy policy, int maxSkips) {
            this.policy = policy; this.maxSkips = maxSkips;
        }

        public void handle(int lineNum, String record, Exception e) {
            switch (policy) {
                case FAIL_FAST:
                    LOGGER.severe("行" + lineNum + "で中断: " + e.getMessage());
                    throw new RuntimeException("行" + lineNum, e);
                case CONTINUE:
                    LOGGER.warning("行" + lineNum + "スキップ: " + e.getMessage());
                    skipped.add(lineNum + ":" + record);
                    break;
                case SKIP_LIMIT:
                    skipCount++;
                    skipped.add(lineNum + ":" + record);
                    if (skipCount > maxSkips) throw new RuntimeException("スキップ上限超過", e);
                    LOGGER.warning("行" + lineNum + "スキップ(" + skipCount + "/" + maxSkips + ")");
                    break;
            }
        }
        public int getSkipCount() { return skipCount; }
        public List<String> getSkipped() { return skipped; }
    }

    public static int processRecords(List<String> records, RecordProcessor proc, ErrorHandler handler) {
        int success = 0;
        for (int i = 0; i < records.size(); i++) {
            try { proc.process(records.get(i)); success++; }
            catch (Exception e) { handler.handle(i + 1, records.get(i), e); }
        }
        return success;
    }

    public static void main(String[] args) {
        List<String> lines = Arrays.asList("1001,田中,50000", "1002,鈴木,INVALID", "1003,佐藤,30000", "1004,山田,-500", "1005,高橋,45000");
        RecordProcessor proc = new RecordProcessor() {
            @Override public void process(String record) throws Exception {
                String[] f = record.split(",");
                int amount = Integer.parseInt(f[2]);
                if (amount < 0) throw new IllegalArgumentException("金額が負数: " + amount);
                System.out.println("処理: " + f[1] + " / " + amount + "円");
            }
        };
        ErrorHandler handler = new ErrorHandler(ErrorPolicy.SKIP_LIMIT, 3);
        int ok = processRecords(lines, proc, handler);
        System.out.println("成功: " + ok + " / スキップ: " + handler.getSkipCount());
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

sealed interface でエラー結果を型安全に分類できる。switch 式で分岐を式として書ける。

Java 17
// Java 17: switch 式で分岐結果を返す
boolean shouldContinue = switch (policy) {
    case FAIL_FAST -> throw new RuntimeException("中断");
    case CONTINUE -> { yield true; }
    case SKIP_LIMIT -> { skipCount++; yield skipCount <= maxSkips; }
};

Library Comparison

Pure Java(enum + try-catch)エラー戦略が限定的で外部依存を増やしたくない場合。チャンク単位のトランザクション管理が必要になると自前コストが大きくなる。
Spring Batchskip-limit、retry-limit を宣言的に設定したい大規模バッチ。Spring 依存 + 設定の学習コスト。小規模には過剰。
jBeret(Jakarta Batch)Jakarta EE 環境でバッチ仕様に準拠したい場合。Jakarta EE コンテナが前提。スタンドアロンには追加設定が必要。

注意点

異常終了戦略でも終了前にエラー内容とどこまで処理したかをログに記録すること

スキップ上限を設定しない継続戦略はデータ全体が壊れていても走りきるリスクがある

エラーハンドリングのテストでは先頭行・末尾行・連続エラー・全行エラーを網羅する

スキップしたレコードの一覧を別ファイルに出力しておくと再処理に活用できる

FAQ

FAIL_FAST と SKIP_LIMIT はどう使い分けますか。

データの正確性が最優先なら FAIL_FAST、一部不備が許容されるなら SKIP_LIMIT です。

スキップしたレコードの再処理はどう設計しますか。

スキップしたレコードをエラーCSVに書き出し、修正後に同じバッチで再投入する構成が一般的です。

テストで最低限確認すべきケースは。

正常のみ・先頭行エラー・末尾行エラー・上限ちょうど・上限超過・全行エラーの6パターンです。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます