👻

架空のプロジェクトとしてマラソン42kmチャレンジアプリの「バッチ処理設計・実装・テスト計画」をChatGPTに考えてもらった。

に公開


色々と書いたのですが、Mapifyで要約したものを一番下に貼り付けました。

はじめに

バッチ設計ことを考える機会があったため、勉強のためにも、ChatGPTにシナリオや要件定義などを考えてもらい、架空のプロジェクト「マラソン42キロ達成を目指すアプリのリリース」を使って学習してみようと思います。

以下はChatGPTのDeepResearchに出してもらった結果ですが、検証はしてないため、あくまで参考としてください。元々インフラエンジニアなので、アプリのことは「なにそれ?美味しいの?」ぐらいです。
ただ個人的に知らない言葉がたくさん出てくるので以外に面白いと感じています。Grokでもだしてもらったのですが、いろいろと間違いがひどかったです。 間違いをそのまま正解のようにこのように記事にするのは間違いを量産しているなと思い、これまでの投稿を反省しました。なのであくまで参考として。。
(色々と付け足したらめちゃくちゃ長くなりました。)

なので、本来は実装してやったものは、少し時間はかかりますが、分けて書きたいと思います。

ChatGPTのDeepResearchの出力結果

マラソン42キロ達成を目指すアプリのリリースに関連するプロジェクトで、要件定義が済んだ状態からのバッチ設計・実装・テストまでを一連の業務シナリオとしてまとめます。

主に対象となるのは以下の条件:

  • バッチ処理の内容:ユーザーのトレーニングログ(距離・時間・心拍など)の集計処理
  • 実行タイミング:日次実行
  • 技術スタック:Java + Spring Batch
  • 重視するテスト観点:再実行性、エラー耐性

この条件に基づき、設計思想、構成、処理の流れ、設計ドキュメントの構成、実装方針、テスト観点などを含むフルシナリオを用意します。少しお時間ください、まとまり次第お届けします。

マラソン42kmチャレンジアプリのバッチ処理設計・実装・テスト計画

業務シナリオ(ビジネス目的と機能概要)

本プロジェクトは、ユーザーがフルマラソン相当の42km走破を目指すトレーニング支援アプリの開発です。ユーザーは日々のランニング記録(走行距離、時間、平均心拍数など)をアプリにログとして蓄積し、自身の進捗状況や分析結果を閲覧できます。このアプリの目的は、日々のトレーニングログをもとに42kmの目標達成率や週間の走行傾向を算出し、ユーザーのモチベーション維持と計画的なトレーニング支援を行うことです。

アプリの主な機能概要は以下の通りです:

  • トレーニングログ登録機能: ユーザーが毎日のランニング結果を入力(距離、所要時間、平均心拍数など)。これらのデータはサーバ上のデータベースに記録されます。
  • 進捗ダッシュボード: ユーザーごとの目標達成率(42kmに対する累積走行距離の割合)週間走行距離の推移グラフなどを表示し、トレーニングの傾向を可視化します。
  • 通知・フィードバック: 一定の距離達成時や走行ペースの変化に応じて、ユーザーにフィードバック通知を送る(例:「累積距離が21kmを突破しました!目標の50%達成です」など)。

上記の進捗ダッシュボード等の実現のために、本アプリでは日次バッチ処理を導入しています。バッチ処理は毎日夜間に実行され、当日の全ユーザーデータを集計して進捗指標を計算します。このバッチ処理により、オンライン処理(ユーザーがアプリを開いてデータを見る際)では最新の集計結果を即座に提供でき、リアルタイム計算の負荷を避けられます。またバッチは大量データの処理に適しており、全ユーザーのログを一括して分析する業務ロジックを安全かつ効率的に実行できます (〖Spring Batch〗Jobについて #Java - Qiita)。

このように、バッチ処理のビジネス上の役割は、日々蓄積されるランニングログをまとめて分析し、「42km達成」というゴールに対する各ユーザーの現在地を定量的に示すことです。具体的には、各ユーザーについて次の情報を算出します:

  • 目標達成率: 累計走行距離 ÷ 42km × 100%(例:累計21kmなら50%達成)。
  • 週間走行距離推移: 直近7日間の合計走行距離と、その前の7日間との比較(増減傾向)。
  • その他の指標: 直近日毎の走行距離・時間の記録や平均心拍数など(必要に応じて)。

バッチ処理によって生成されたこれらの進捗データはデータベースに保存され、アプリのダッシュボード画面やメール通知機能からユーザーに提供されます。例えば毎朝、前日までの集計結果に基づいて「累積距離○○km(達成率△△%)、先週比+○km」といった情報が閲覧できるようになります。こうした夜間バッチによるデータ集計は、オンライン処理と相互補完的に働き、アプリのユーザー体験向上に貢献します (定期実行のBatchを作成する際の観点・注意点などについて)。

バッチ処理設計

バッチ処理概要とビジネスロジック

本アプリで設計するバッチ処理は「日次進捗集計バッチジョブ」です。毎日決まった時間(例えば深夜)に起動し、その日までに記録された全ユーザーのランニングログを対象に集計処理を行います。ビジネスロジックの概要は次の通りです:

  • 入力データ: ユーザーのトレーニングログ(例:training_logテーブル)から、対象日(通常は直近の1日、必要に応じて直近7日間など)のデータを取得します。各ログにはユーザーID、日付、距離、時間、平均心拍数等が含まれます。
  • 処理内容: ユーザー単位にログを集約し、日毎および累積の走行距離時間を計算します。さらに目標42kmに対する達成率を算出し、直近週の合計距離前週との差分など週単位の推移も計算します。たとえば、あるユーザーが当日5km走った場合、当日までの累計が30kmから35kmに増加し達成率は83.3%となり、直近7日間の合計が例えば20km(前週比+5km)といった具合です。
  • 出力データ: 計算結果はユーザー別の進捗サマリとしてデータベースに保存します(例:user_progressテーブル)。各ユーザーの記録として、最新累計距離や達成率、週計などを保持します。このテーブルはアプリの参照用データソースとなります。
  • 通知・派生処理: 必要に応じて、目標達成や異常値検出時にメール送信や別システムへの連携を行うことも検討されますが、ここでは主に集計と保存にフォーカスします。

技術スタックはJava + Spring Batchです。Spring Batchフレームワークを用いることで、標準化された方法でデータの読み取り(Reader)-加工(Processor)-書き込み(Writer)の処理フローを実装できます。加えて、トランザクション管理やチェックポイントによるリスタート(再開)制御、例外時のリトライ・スキップ制御などが備わっており、今回重視する再実行性(冪等性)エラー耐性の確保に役立ちます (〖Spring Batch〗Jobについて #Java - Qiita) (Spring Batchのアーキテクチャ)。業務ロジック自体は上記のようにシンプルな集計ですが、Spring Batchにより大量データ処理時でも効率と信頼性を両立できる設計とします。

バッチ処理構成図と全体アーキテクチャ

(Spring Batchのアーキテクチャ) 図: Spring Batchの基本構成(Job/Step単位の実行モデル)。ジョブ(Job)は1つ以上のステップ(Step)から構成され、各Step内部でItemReader・ItemProcessor・ItemWriterの順にデータ読み出し~加工~書き込み処理を行う。StepやJobの実行状況はJobRepository(メタデータ管理用DB)に保存され、ジョブの異常終了時にはこの情報を元に途中から再開(リスタート)が可能となる (Spring Batchのアーキテクチャ)。ジョブ起動はJobLauncherによってトリガーされる。

上図のように、Spring Batchではジョブ(Job)がバッチ処理全体の単位となり、その中に1つ以上のステップ(Step)を持ちます。それぞれのStepが実際の処理シーケンス(読み取り・処理・書き込み)を担当します。本プロジェクトでは 「日次進捗集計ジョブ」 というジョブを定義し、その中に単一のステップとして「進捗集計ステップ」を持たせる構成とします。1つのステップで一連の集計処理が完結するため、ジョブはこのステップを実行して終了します(将来的に処理を分割する場合は複数Step化も可能です)。

各Step内ではSpring Batchのチャンクモデルを用いて処理を行います (Spring Batchのアーキテクチャ) (Spring Batchのアーキテクチャ)。チャンクモデルでは、ItemReaderでアイテム(データ)を一件ずつ読み込み、ItemProcessorで必要な変換・集計処理を施し、一定数のアイテムをまとめてからItemWriterでバルク書き込みする流れを取ります (Spring Batchのアーキテクチャ)。本ステップでは「ユーザーの日次集計データ」を1アイテムと見なし、一定数(例えば100件)のユーザーデータを一括書き込みするような実装とします(このチャンクサイズはチューニング可能ですが、ここでは仮に100とします)。チャンク単位でトランザクション管理され、100件ずつコミットすることで性能と一貫性のバランスを取ります。

Spring BatchのJobRepositoryにはジョブ/ステップ実行のメタデータ(実行開始・終了時刻、処理件数、異常発生時のチェックポイント情報等)が記録されます (Spring Batchのアーキテクチャ)。この仕組みにより、ジョブが異常中断した場合でも前回の実行状態を参照して途中から処理を再開(リスタート)できます (Spring Batchのアーキテクチャ)。再開時には既に処理済みのデータはスキップされ、中断時点から続きが処理されます。このようなステートフルな再実行をデフォルトでサポートすることが、Spring Batchを採用する大きなメリットです(自前で実装すると難易度が高いため、フレームワークに任せます (バッチ処理について考える #読み物 - Qiita))。

ジョブ設計 (Job設計)

本バッチのジョブ設計について、主要な事項を以下にまとめます。

  • ジョブ名とスケジュール: ジョブ名は例えばDailyProgressJobとします。ジョブは1日1回、毎日深夜(例:午前2時)に定時実行されます。実行トリガーはサーバ内のスケジューラ(CRON)や外部ジョブスケジューラツールから発行され、Spring BatchのJobLauncher経由でジョブを開始します。運用都合により休日はスキップするなどのスケジュール調整も可能ですが、本アプリでは基本的に年中無休で日次実行されます (定期実行のBatchを作成する際の観点・注意点などについて)。
  • ジョブパラメータ: 冪等性確保や再実行管理のために、ジョブ起動時にパラメータとして「対象日付」(例:executionDate=2025-04-12)を渡します。これにより、どの日のログを集計するジョブ実行かを一意に識別できます。Spring Batchではジョブパラメータが同一で正常終了済みのジョブを再度実行しようとすると、JobInstanceAlreadyCompleteException により二重実行を防止します (Spring Batchのアーキテクチャ)。そのため、通常運用では同じ日付でジョブを二度起動することはありません(万一再実行が必要な場合は、失敗したジョブのリスタートか、あるいはパラメータを変えて別インスタンスとして起動します)。
  • ステップ構成: このジョブには1つのステップ(進捗集計ステップ)があります。複数のステップに分割しない理由は、処理内容がシンプルで1フェーズで完結するためです。もし処理前後に別のETL作業や他システム連携が必要であれば、前段/後段にステップを追加することになります。今回は単一ステップで、内部でReader→Processor→Writerのチャンク処理を実装します。
  • トランザクションとコミット: Spring Batchではステップ内の処理がトランザクション制御されます。本ジョブでは、データ読み書きにデータベースを使用しているため各チャンクごとにトランザクションを区切ります(デフォルトでItemReader~Processor~Writerを含めたチャンク単位が1トランザクションです)。commit-interval(コミット間隔、すなわちチャンクサイズ)はシステム負荷とデータ量に応じて設定します。例えば100件単位でコミットすることで、トランザクションが長くなりすぎず適度にスループットを確保できます。
  • エラーハンドリング戦略: ジョブ全体に対する基本戦略として、失敗時はジョブを停止し、後述のリカバリ手順で再実行します。ただし、個々のレコード処理で発生しうる一時的な問題(一時的なDB接続エラーや整合性制約違反など)は、Spring Batchの仕組みでリトライスキップを設定し、ジョブを即失敗させずに可能な限り完遂させます (定期実行のBatchを作成する際の観点・注意点などについて)。たとえば「一部ユーザーのデータに想定外の値があり計算できない」場合はそのレコードをスキップしログに記録、また「書き込み時の一時的なデッドロック」が起きた場合は数回までリトライを試みる、といったポリシーを取ります。こうしたエラー耐性設定については後述の「テスト設計」で詳述します。

ステップ設計 (Step設計:リーダー/プロセッサー/ライター)

進捗集計ステップはチャンクモデルによる以下の処理で構成されます。

  • ItemReader(データ読み取り): ユーザーのトレーニングログをデータベースから読み出します。具体的には当日分のログを対象とするSQLクエリを発行し、結果セットを1件ずつJavaオブジェクト(例えばTrainingLogエンティティ)にマッピングします。ここではSpring Batchが提供するJDBCベースのJdbcCursorItemReaderを使用し、例えば次のようなクエリを実行します。

    SELECT user_id, SUM(distance) AS sum_distance, SUM(time) AS sum_time, AVG(heart_rate) AS avg_hr
    FROM training_log
    WHERE log_date = :executionDate
    GROUP BY user_id
    

    (各ユーザーごとの当日分集計をDB側で計算して取得する例です。)

    上記のようにSQLでグルーピング集計することで、1ユーザーにつき1件のレコードをリーダーから取得できます。これによりProcessor側での集約負荷を軽減しています(データ量次第では、あえて生ログをそのまま読み込んでJava側で集約する手もありますが、今回はDB集計のほうが効率的と判断します)。取得した各レコードにはユーザーIDと当日の総距離・総時間・平均心拍数が含まれます。また、必要に応じて過去の累積値も併せて取得します。例えば累積距離を計算するため、user_progressテーブル(ユーザーの現在までの累積データを保持)から当日以前の累計距離を参照し、JOIN句でまとめて読み込む設計も考えられます。

  • ItemProcessor(データ処理・変換): Readerから受け取った各ユーザーの当日集計データに対し、ビジネスロジックを適用します。具体的には:

    • 累積距離・時間の更新: そのユーザーのこれまでの累計走行距離・時間に当日分を加算し、新たな累計値を算出します(初回実行時など過去データが無い場合は当日分がそのまま累計になります)。
    • 目標達成率の計算: 累計走行距離 ÷ 42km * 100 を計算し、達成率(%)を算出します。達成率は100%を上限とし、超過した場合も100%(目標達成)とみなします。
    • 週間走行距離・トレンドの算出: 直近7日間の合計距離を集計します。今回のバッチは日次で走行距離を集計しているため、user_progressテーブル等に直近数日分の履歴を保持していればそれを元に計算できます。一つの方法は、training_logテーブルから「実行日を含む過去7日間」のデータを集計するクエリを発行し直近週の距離を得ることです。同様に「実行日を含む過去14日間」のデータから前週分を差し引くことで前週の合計距離を算出し、両者を比較して増減を記録します(例:「今週20km、先週15km:+5km」)。この計算もProcessor内で実施します。
    • オブジェクト組み立て: 上記の計算結果を格納したユーザー進捗オブジェクト(例:UserProgressクラス)を生成します。このオブジェクトにはユーザーID、累計距離・時間、達成率、今週距離、先週距離差分、更新日などがプロパティとして含まれます。Processorの戻り値としてこのUserProgressオブジェクトを返し、後続のWriterに渡します。

    Processorでは主にビジネス計算ロジックを担います。なお、設計上冪等性を保つことが重要です。例えば、同じ入力データに対してProcessor処理を複数回実行しても結果が重複更新されないようにします。今回のケースでは、累積距離の計算で「過去の累積 + 当日距離」を行っていますが、再実行時には過去の累積値も再取得するため、最終算出される累積距離は常に正しい値に上書きされる形になります(過去の結果にさらに加算して二重計上しないよう注意)。必要であれば一時的な計算結果をExecutionContextに保持し、二重実行時にはそれを利用するなどの工夫も考えられますが、基本はバッチ再実行時にはProcessorも再度計算を行い、最終値を正とする方針です。

  • ItemWriter(データ書き込み): Processorで作成されたUserProgressオブジェクトをデータベースに書き込みます。具体的にはuser_progressテーブルに対してINSERTまたはUPDATEを行います。すでに該当ユーザーの進捗レコードが存在する場合はUPDATEで値を更新し、初めてのユーザーならINSERTで新規作成します(Primary KeyはユーザーIDや日付などの複合キーを想定)。Spring BatchのJdbcBatchItemWriterを使用し、バルク更新SQLを発行する形で効率的に書き込みます。擬似コード例を示すと、以下のような動作になります。

    String sql = "MERGE INTO user_progress as up " +
                 "USING (VALUES (?, ?, ?, ?, ?, ?)) AS vals(user_id, total_dist, total_time, achievement_rate, week_dist, week_diff) " +
                 "ON up.user_id = vals.user_id " +
                 "WHEN MATCHED THEN UPDATE SET total_dist=..., achievement_rate=..., ... " +
                 "WHEN NOT MATCHED THEN INSERT (...columns...) VALUES (...vals...);";
    writer.setSql(sql);
    // パラメータバインドはSpringが各UserProgressから抽出
    

    (実際のSQL構文はデータベース製品に依存します。MERGE文やUPSERT相当の機能を用いる想定です。)

    Writerにおいても冪等性の考慮が必要です。最も簡潔なのは**「その日の集計結果でテーブルの内容を上書きする」**ことです。つまり、同じ日付のバッチ処理を再実行してもuser_progressテーブルの各ユーザー行は同じ値で更新される(結果的に何度実行しても同じ状態になる)ようにします。上記のようなMerge/Upsertによる更新は、その性質上同じデータで繰り返し実行しても内容は変わらず、二重反映を防ぐことができます。

    なお、書き込み処理ではトランザクション管理のもと複数行を一括コミットしますが、一部の行でエラーが発生した場合(例えば特定ユーザーのデータが長すぎてDB制約違反となった等)は、該当チャンク全体がロールバックされます。その後の対応として、エラーが致命的ならジョブを失敗させ、そうでなければ当該ユーザーをスキップする、といった戦略を取ります。今回はスキップ可能なエラーとして想定するのは「個別ユーザーデータの不備」に限り、DB障害など全体に影響するエラーは即ジョブ失敗とします。

以上がステップ内の処理フローです。まとめると、Readerで当日ログを読み込み(ユーザーごと集計)、Processorで累積計算と進捗指標の生成、Writerで進捗テーブル更新という一連の流れになります。各コンポーネントの役割を以下の表に整理します。

コンポーネント 実装クラス (候補) 説明 (役割)
Job(ジョブ) DailyProgressJob 日次のユーザー進捗集計バッチ。1つのStepから構成される。
Step(ステップ) progressAggregationStep チャンクモデルを用いたステップ。Reader/Processor/Writerで構成。
ItemReader JdbcCursorItemReader<TrainingLog> トレーニングログテーブルから当日分データを取得。SQLでユーザー単位に集計。
ItemProcessor CustomItemProcessor<TrainingLogSummary, UserProgress> 当日ログ集計データを受け取り、累積計算・達成率や週計算を実施。UserProgressオブジェクトを生成。
ItemWriter JdbcBatchItemWriter<UserProgress> user_progressテーブルに対し集計結果を書き込み(各ユーザーの進捗レコードをUPSERT)。
JobRepository (Spring Batch組み込み) ジョブ/ステップ実行メタ情報の格納先。再実行時のチェックポイント等に利用 (Spring Batchのアーキテクチャ)。

※上記のCustomItemProcessor部分では、場合によってはCompositeProcessor等を使い、まず過去の累積情報を読み込むProcessorと、次に計算を行うProcessorを組み合わせる実装も考えられます。また、Spring BatchのStepExecutionContextに前日までの累計データをロードしておきProcessor内で参照する、といった高度な実装も可能ですが、ここでは設計を分かりやすくするためリーダーで可能な限り集約取得する方針としています。

バッチ定義コード例(Java + Spring Batch)

以上の設計に基づき、Spring Batchでのジョブ定義コードの例を示します。Spring BootでのJava Config形式で記述しており、JobBuilderFactoryStepBuilderFactoryを用いてジョブとステップを生成しています(実際のコードではこれらは@Autowiredされます)。ポイントとして、リトライ/スキップの設定も組み込んでいます。

@Configuration
@EnableBatchProcessing
public class BatchJobConfig {

    @Bean
    public Job dailyProgressJob(JobBuilderFactory jobBuilders, Step progressStep) {
        return jobBuilders.get("DailyProgressJob")
                .start(progressStep)
                .build();
    }

    @Bean
    public Step progressStep(StepBuilderFactory stepBuilders, 
                             DataSource dataSource, PlatformTransactionManager transactionManager) {
        return stepBuilders.get("progressAggregationStep")
                .<TrainingLogSummary, UserProgress>chunk(100, transactionManager)
                .reader(trainingLogReader(dataSource))
                .processor(progressProcessor())
                .writer(progressWriter())
                // フォールトトレラント設定:一時エラー時にリトライ、データ不備はスキップ
                .faultTolerant()
                .skipLimit(10)                          // 最大10件までスキップ許容
                .skip(DataFormatException.class)        // データ形式不正など業務起因エラーはスキップ
                .retryLimit(3)                          // リトライは最大3回
                .retry(DeadlockLoserDataAccessException.class)  // 一時的なDBデッドロックはリトライ
                .build();
    }

    @Bean
    @StepScope
    public JdbcCursorItemReader<TrainingLogSummary> trainingLogReader(DataSource dataSource) {
        JdbcCursorItemReader<TrainingLogSummary> reader = new JdbcCursorItemReader<>();
        reader.setDataSource(dataSource);
        reader.setSql("SELECT user_id, SUM(distance) as total_distance, SUM(time) as total_time, AVG(heart_rate) as avg_hr" +
                      " FROM training_log WHERE log_date = :executionDate GROUP BY user_id");
        reader.setRowMapper(new TrainingLogSummaryRowMapper());
        return reader;
    }

    @Bean
    @StepScope
    public ItemProcessor<TrainingLogSummary, UserProgress> progressProcessor() {
        return new ItemProcessor<TrainingLogSummary, UserProgress>() {
            @Override
            public UserProgress process(TrainingLogSummary summary) {
                // 過去の累積距離・時間を取得(例:別テーブルやキャッシュから)
                double prevTotalDist = fetchPreviousTotalDistance(summary.getUserId());
                double prevTotalTime = fetchPreviousTotalTime(summary.getUserId());
                // 当日分を加算して累計算出
                double newTotalDist = prevTotalDist + summary.getTotalDistance();
                double newTotalTime = prevTotalTime + summary.getTotalTime();
                // 目標達成率計算
                int achievementRate = (int)Math.min(100, (newTotalDist / 42.195) * 100);
                // 直近7日間の合計距離算出(必要であれば別途取得)
                double weekDist = fetch7DayDistance(summary.getUserId(), executionDate);
                double lastWeekDist = fetch8to14DayDistance(summary.getUserId(), executionDate);
                double weekDiff = weekDist - lastWeekDist;
                // 結果オブジェクト構築
                UserProgress progress = new UserProgress(summary.getUserId(), newTotalDist, newTotalTime,
                                                        achievementRate, weekDist, weekDiff);
                return progress;
            }
        };
    }

    @Bean
    @StepScope
    public JdbcBatchItemWriter<UserProgress> progressWriter() {
        JdbcBatchItemWriter<UserProgress> writer = new JdbcBatchItemWriter<>();
        writer.setDataSource(dataSource);
        writer.setSql("INSERT INTO user_progress(user_id, total_distance, total_time, achievement_rate, week_distance, week_diff) " +
                      "VALUES(?, ?, ?, ?, ?, ?) " +
                      "ON DUPLICATE KEY UPDATE total_distance = VALUES(total_distance), " +
                      "total_time = VALUES(total_time), achievement_rate = VALUES(achievement_rate), " +
                      "week_distance = VALUES(week_distance), week_diff = VALUES(week_diff)");
        writer.setItemPreparedStatementSetter(new UserProgressPreparedStatementSetter());
        return writer;
    }
}

上記コードでは、progressStep内で.faultTolerant()を指定し、skipLimitretryLimitを設定している点に注目してください。これにより、最大10件までのスキップ可能なエラー最大3回までのリトライが適用されます。例えばProcessorでデータ形式不正(DataFormatException)が発生した場合はそのレコードをスキップし、Writerで一時的なデッドロック(DeadlockLoserDataAccessExceptionなどSpringによるラップ例外)が発生した場合は最大3回再試行します。Spring Batchではこうした設定によりエラー耐性を高めることができ、ジョブ全体の堅牢性に寄与します (定期実行のBatchを作成する際の観点・注意点などについて)。

※コード中のfetchPreviousTotalDistance等は累積情報を取得するダミー関数として示しています。また、SQL文はMySQL系のON DUPLICATE KEY UPDATE構文を例示していますが、実際の環境に合わせて適切なUPSERT手段(MERGE文やINSERT ... ON CONFLICTなど)を使用します。

テスト設計

バッチ処理について、要件に重視されている再実行性(冪等性)エラー耐性を中心に、様々なユースケースを想定したテスト設計を行います。テストはユニットテスト(個々のコンポーネント)、結合テスト(バッチ全体の動作検証)、および運用シナリオテストの観点で準備します。ここでは主に結合テストレベルでのシナリオを列挙します。

正常系ユースケースのテスト

  • 通常日の集計テスト: 典型的な入力データを用意し、バッチを実行して期待通りの集計結果が得られるか検証します。例えば、あるユーザーが当日5km走り累計が35kmになったケースで、user_progressテーブルの該当ユーザー行が距離35km・達成率約83%に更新されていることを確認します。他のユーザーについても集計ロジック通りの結果となっているか、複数ケースを含めチェックします。
  • データ無し日のテスト: 当日ログが全く無い場合(ユーザーが誰もトレーニングしなかった日)の挙動を確認します。期待結果は、バッチがエラー無く完了し、user_progressテーブルも更新無し(もしくは各ユーザーの値が前日から変化しない)状態であることです。特定ユーザー単位で当日ログが無い場合も同様に、そのユーザーの進捗は前日比で変化ゼロとなることを確認します。
  • 多数データのパフォーマンステスト: ユーザー数やログ件数が大規模な場合でもバッチが所要時間内に完了するか検証します。例えば1万ユーザー×1日1件ログ程度のデータセットで実行し、性能測定と結果の一貫性をチェックします(これは要件というより性能試験ですが、正常系の一環として実施)。
  • 集計精度テスト(端数処理等): 距離や時間など小数を扱う項目の丸め処理や百分率計算の誤差が問題ないか確認します。達成率は整数%表示のため、小数点以下の扱い(四捨五入等)が仕様通りか検証します。

エラー・異常系パターンのテスト

  • 単一レコード処理エラーのスキップテスト: 特定ユーザーのデータに不備があるケースをシミュレートします。例えば「心拍数が数値でなく文字列になっている」などProcessorでNumberFormatExceptionが発生するようなログを混入させます。この場合、そのレコードがskip対象として正しくスキップされ、他の正常なデータは問題なく処理されることを確認します。ジョブ終了時にexit statusCOMPLETED(スキップあり)となり、StepExecutionのスキップ件数カウンタが1増えていることなどをアサートします。スキップされたデータについてはエラーログに記録されていることも確認します。
  • 一時的障害のリトライテスト: Writerで使用するデータベースに対し、一時的に障害が発生するケースをテストします。具体的には、モックまたはテストダブルを使用して、初回の書き込みで例外(例えばDeadlockLoserDataAccessException)を投げ、再試行では正常に書き込めるような挙動を再現します (定期実行のBatchを作成する際の観点・注意点などについて)。バッチ実行時、該当チャンクで1度目の書き込みが失敗しても自動的にリトライし、2度目で成功してジョブ全体が完了することを確認します。StepExecutionのリトライ回数メトリクスやログメッセージから、設定通り最大3回までリトライが行われたかを検証します。
  • 非一時的エラーの異常終了テスト: 致命的なエラーが発生した場合にジョブが適切に失敗終了することを確認します。例えば、ReaderのSQLで存在しないテーブルを参照してしまうケースや、Writerで主キー重複の致命的エラーが発生するケースなどを想定します。テストでは意図的にReaderのSQLを誤らせるか、異常データを投入して整合性制約違反を起こします。その結果、ジョブがFAILEDステータスで終了し、エラーが抑止されず上位にスローされたこと、そしてトランザクションがロールバックされ不完全な更新が残っていないことを確認します。エラー通知(アラート)が発報される仕組みもここで後述の運用設計どおり動作するか合わせて検証します。

再実行およびリカバリ動作のテスト

  • ジョブ中断・再開テスト: バッチ実行中に異常終了し、再度リスタートするシナリオをテストします。例えばWriter書き込みの途中でプロセスを強制終了させ、ジョブを異常中断させます。その後、Spring Batchのジョブ再開機能を使ってジョブをリスタートします (Spring Batchのアーキテクチャ)。期待結果は、途中まで処理済みだったレコード以降から処理が再開し、最終的に全データが正しく処理されることです。具体的には、中断前にコミット済みのチャンクについては再実行時にスキップされ、未処理だった部分だけが処理されるはずです (Spring Batchのアーキテクチャ)。テストでは、再開後に重複更新や抜け漏れがないこと(冪等性が保たれていること)を検証します。例えば累積距離が二重に加算されていないか、最終結果が中断なしに1回で実行した場合と一致するかを比較します。
  • ジョブ再実行の冪等性テスト: 正常終了したジョブを改めて再実行した場合の挙動を確認します。通常、本番運用では同一パラメータでの二重実行は行いませんが、テストのため一度成功した日付に対しもう一度ジョブを起動してみます。この際、Spring Batchは既に完了したJobInstanceとして実行を拒否するか(デフォルト動作 (Spring Batchのアーキテクチャ))、あるいは強制的に新インスタンスとして実行した場合でも**結果が変わらない(冪等)**ことを確認します。設定にもよりますが、今回の設計では同一日付で再度実行する必要が出た場合、user_progressテーブルへの書き込みは同じ値で上書きするだけなので副作用はありません。テストでは、再実行前後でデータベースの内容に差異が無いこと(件数・値のすべて)を検証します。加えて、再実行時に新規のJobInstanceを発行するにはパラメータを変える必要があるため、例えばexecutionDate=2025-04-12-rerunのように別IDを付与して実行し、その場合も処理結果が冪等であることを確認します。
  • 長期間運用後の再実行テスト: これは少し異色ですが、例えば1ヶ月分のバッチ結果が蓄積した状態で、過去日のジョブを再実行したらどうなるかをテストします。user_progressが最新状態を持っている中で、仮に10日前の集計をもう一度やり直した場合、その日以降の累積計算に矛盾が生じうるため、本来は避けるべき操作です。しかしシナリオとして、過去日のログ誤り修正→その日だけ再集計、といった要求があり得るため、その際に問題が起きないか検証します。今回の設計では累積値は毎日上書き更新しているため、過去日を再実行しても当日以降のデータとの不整合が起こる可能性があります。このテストから、必要に応じて過去日の再実行は禁止または全期間再集計など運用ルールを検討する材料とします(設計上の前提を確認するテスト)。

以上のように、多角的なテストケースを準備します。特に再実行性については「どこまで処理済みか把握し、途中から再開できること」が重要であり (バッチ処理について考える #読み物 - Qiita)、エラー耐性については「一時エラーで止まらずリカバリする」「データ不備は他のデータに波及させない」ことを重視して検証します。テストはSpring Batchのテスト用ユーティリティ(JobLauncherTestUtils等)を活用し、自動化された形で行います。また、結合テストでは実データベースを使ったインテグレーションテストを行い、本番同等の挙動を確認します。

運用設計(スケジューラ連携・監視と運用管理)

最後に、バッチ処理の運用フェーズにおける設計ポイントです。日次バッチを安定稼働させ、問題発生時に速やかに検知・対応するための仕組みを整えます。

ジョブスケジューラとの連携

バッチジョブは自動実行が必要なため、適切なスケジューラと連携させます。Spring Bootで実装している場合、サーバ内で@Scheduledによるスケジューリングも可能ですが、ここでは独立したジョブスケジューラを使用することを想定します。例えばUnix系OSのcron、あるいは企業内で用いられているJP1やAutosys等の業務スケジューラツール、クラウド環境ならAWS EventBridgeやAzure Logic Apps等が考えられます。毎日深夜2時に起動コマンドを発行し、Spring Batchアプリケーションのエントリポイント(java -jar app.jar executionDate=...等)を実行します (Spring Batch Architecture)。ジョブパラメータの当日付付与もスケジューラ側で行います(実行日付を自動計算して渡す)。

スケジューラ連携において考慮すべき点は以下です:

  • 再実行トリガー: 万一ジョブが失敗した場合、自動で再実行するか、人手で再実行するかの方針を決めます。critical度が高いバッチであれば一定回数まで自動リトライする設定をスケジューラ側で組んだり、リカバリ手順書に従い手動再実行とすることもあります。本ジョブはユーザー向けサービスに直結するため、**即時に再実行(ないし担当者に通知して手動リスタート)**できる体制を敷きます。
  • 並行実行の防止: 前日分のジョブが長引いているのに次のジョブが起動しないように、同時起動を抑制します。具体的には、スケジューラ側で前回実行が完了していなければ新規起動しない設定にしたり、Spring Batch側でJobRepositoryを用いた重複検知に任せることもできます(ジョブインスタンスが存在する場合起動しない)。
  • カレンダー考慮: 原則毎日実行ですが、サーバメンテナンス日など特定日に実行を止める必要があればスケジューラのカレンダー機能で制御します (定期実行のBatchを作成する際の観点・注意点などについて)。今回のケースでは不要かもしれませんが、将来的に大晦日だけ実行しない等の要件が出た際に対応可能です。

ログ出力・監視設計

ログ出力はバッチ運用において欠かせません。設計段階でどのような情報をログに残すかを定義しておきます (定期実行のBatchを作成する際の観点・注意点などについて)。本ジョブでは以下のログ出力を行います:

  • ジョブ開始・終了ログ: ジョブ開始時にジョブ名とパラメータ(対象日付)をINFOレベルで記録し、終了時には成功・失敗ステータスと処理件数などのサマリ情報をINFOレベルで記録します。
  • 進捗ログ: Stepごとの読込件数・処理件数・書込件数を取得し、終了後に「Read ###, Write ###, Skip ###」のように出力します(Spring BatchのStepExecutionから取得可能)。
  • デバッグログ: 必要に応じて、特定ユーザーの計算結果など詳細をDEBUGレベルで記録します。ただしログ出力量が膨大にならないよう注意し、本番運用時はDEBUGログは抑制します。
  • エラーログ: スキップしたデータやリトライ発生などはWARNINGレベルで詳細情報(エラー内容、該当ユーザーID等)を記録します。致命的エラーでジョブがFAILEDとなる場合はスタックトレースを含めてERRORレベルで記録します。

ログはファイル出力とし、日次でローテーションします。例えばログファイル名に日付を含め(DailyProgressJob_20250412.logなど)、毎日新しいログファイルを生成するか、あるいはロガーの設定で1日ごとにローテーションするようにします (定期実行のBatchを作成する際の観点・注意点などについて)。過去ログの保存期間は30日程度とし、それより古いものは自動的に削除またはアーカイブします(この期間は運用ポリシー次第ですが、障害解析用途に十分な長さとします)。

また、監視として以下を導入します:

  • 外部監視ツール連携: サーバにZabbixやPrometheusなどのエージェントを導入し、バッチプロセスの終了ステータスや所要時間を監視します (定期実行のBatchを作成する際の観点・注意点などについて)。例えば終了ステータスが失敗だった場合や、通常5分で終わる処理が30分経っても終わらない場合に異常と検知し、アラートを発報するように設定します。
  • メトリクス収集: Spring Batchのメトリクス(何件処理したか、スキップ何件か等)をPrometheusなどで数値収集し、Grafana等で可視化することも検討できます。これにより日々の処理件数推移やスキップ発生傾向を分析できます。

アラート設計(異常検知と通知)

バッチ処理でエラーが発生した場合、即座に運用担当者へ通知される仕組みが必要です (定期実行のBatchを作成する際の観点・注意点などについて)。本システムでは以下のアラート設計を行います:

  • メール通知: ジョブ失敗時に、予め設定した管理者メールアドレスへ自動メールを送信します。メールにはジョブ名、失敗した日時、エラー内容のサマリ(ログへのパスやエラーメッセージ)を記載します。これにより夜間バッチ失敗時も担当者が朝一で気付けます。
  • SNS連携通知: 迅速な対応のため、Slack等のチャットツールやPagerDutyのようなオンコール通知システムとも連携します (定期実行のBatchを作成する際の観点・注意点などについて)。例えばSlackの運用チャネルに「DailyProgressJobが失敗しました:JobInstance=2025-04-12、エラー=NullPointerException...」というメッセージを自動投稿します。重大度に応じて担当者のスマホにプッシュ通知が飛ぶように設定することも検討します。
  • リトライ通知: 自動リトライで対処可能な一時エラーが発生した場合も、一度でも起きたら運用上は注意を払うべきなので、Slack通知等では「※一時エラーX件発生(自動リカバリ済)」といった情報も残すようにします。これによりシステムの不安定兆候を早期に察知できます。
  • 定期レポート: これはリアルタイム通知ではありませんが、バッチの実行結果(成功/失敗、処理件数、所要時間)を日次または週次でレポートし、関係者と共有します。これにより、継続的な運用モニタリングと改善につなげます。

異常検知については、ジョブのReturn Codeやログメッセージを監視する仕組みを取ります。具体的には、バッチプロセスの終了コードが0以外だった場合に監視ツールが異常とみなして通知を飛ばす、といった設定を行います。また、ログ中に「ERROR」レベルの行が出現した場合にも拾って通知するようなログ監視も有効です。

運用上のその他考慮事項

  • 冗長化とリカバリ: バッチ実行サーバ自体に障害が起きた場合に備え、予備系サーバでもジョブを実行できるようにしておきます。ただし同一ジョブを二重起動しない仕組み(スケジューラ設定やDBのロック機構)も必要です。
  • データバックアップ: 万一誤った集計が行われた場合に巻き戻せるよう、user_progressテーブルの更新前後状態をバックアップしておくことも検討します。シンプルには日次バッチ前に該当テーブルをエクスポートする運用や、過去ログからリカバリできる体制を用意します。
  • 変更管理: バッチロジックに変更があった際、過去データに遡及して再集計が必要になる場合があります。その際の手順(全ユーザーの累計を再計算するスクリプトの用意など)もドキュメント化しておきます。

以上が、本プロジェクトのバッチ処理に関する設計・実装・テスト・運用計画です。日次バッチはアプリの重要コンポーネントであり、ビジネス価値を継続的に提供するための安定稼働が求められます。適切な設計と検証、そして運用監視により、ユーザーに常に正確な42kmチャレンジの進捗を届けられるよう努めます。各種考慮事項を盛り込んだ今回のシナリオが、実際の開発・運用において有益な指針となることを期待します。

参考文献・資料:


✅ 必要なスキルセット一覧(分類別)

🔧 1. バッチ処理設計スキル

スキル 説明 リンク
バッチ業務の基本 バッチ処理の目的、利点、設計観点(定期性、冪等性、再実行性、ログ出力)などの理解 Qiita: バッチ処理について考える
データ設計 バッチ入力・出力用のテーブル設計(主キー、インデックス、履歴管理) Zenn: バッチ設計の考慮点
ジョブ・ステップ分割 Spring BatchにおけるJob/Step/Chunk/Taskletの使い分け 公式: Spring Batchアーキテクチャ

☕ 2. Java + Spring Batch 実装スキル

スキル 説明 リンク
Javaの基本構文 クラス、例外処理、コレクション、ラムダなど Java入門(公式)
Spring Boot 基礎 @Configuration、DI、Bean定義、プロファイル切替など Spring Boot公式ガイド
Spring Batch 実装 JobBuilderFactory, StepBuilderFactory, Reader/Processor/Writerの定義方法 Spring Batch公式ドキュメント
DBアクセス(JDBC) JdbcTemplate / JdbcCursorItemReader / JdbcBatchItemWriterの使い方 Baeldung: Spring JDBC
SQL 集計・JOIN・ウィンドウ関数など SQLBolt(日本語対応)

💥 3. エラー対策・リトライ制御スキル

スキル 説明 リンク
faultTolerant設定 retry, skip, noRollbackExceptionClasses の使い方 Baeldung: Fault Tolerant Spring Batch
例外設計 リカバリ可能例外と致命的例外の設計方針 Spring公式: Retry処理
トランザクション制御 チャンクサイズとコミットの関係 Spring公式: チャンクベース処理

🔁 4. テスト・再実行性検証スキル

スキル 説明 リンク
Job再実行設計 JobParameterによる一意性管理、再開時の動作確認 Terasoluna: JobRepositoryと再実行
JUnit + SpringBatchテスト JobLauncherTestUtilsの使い方、テストデータ準備 公式: Spring Batch テスト

📊 5. 運用・監視スキル

スキル 説明 リンク
ログ出力設計 Logback/Log4j2設定、ローテーション、出力レベルの使い分け Baeldung: Spring Boot Logging
スケジューラ連携 cronやAWS EventBridgeからのバッチ起動 Spring公式: Scheduling Tasks
メトリクス/監視ツール Prometheus/Grafana、Slack通知など Datadog × Spring Batch 連携例(外部)

📚 6. おすすめ書籍・教材

書籍名 説明 出版社 / リンク
『Spring徹底入門』 Spring Framework全体(Boot/Bacth含む)を網羅 技術評論社/https://amzn.asia/d/0J8Vi7x
『悲惨なミスをなくすSpringBatch入門書』 バッチ専用、アーキテクチャから実装・テスト・運用まで網羅(※日本語少ないので英語書籍 or Qiita・Zennも併用) (https://amzn.asia/d/5meknXE)

🎯 学習・実践の流れ(おすすめステップ)

  1. Java + SQL + Spring Boot の基本復習
    • 『Effective Java』やJavaチュートリアル、SQLBoltなどで基礎固め
  2. Spring Batchのアーキテクチャ理解
    • Terasolunaの図解資料 or Spring公式を読み込み
  3. 小さなバッチから構築してみる
    • 実際に ItemReader → ItemProcessor → ItemWriter を試して動かす
  4. リトライ・スキップ・再実行シナリオの実装
    • faultTolerant構成を導入、Jobパラメータと再実行設計に慣れる
  5. テスト&監視設定を導入
    • JobLauncherTestUtilsを使った結合テスト、Slack通知やログローテートまで構築

素晴らしい視点です。
レビュー指摘されがちなポイントに事前対策し、限られた時間内で最大の効果を出すには、やみくもにやるのではなく、**「重大インパクト × 発生頻度」**の高いポイントに絞って対応すべきです。


✅ 優先度マトリクス:バッチ設計のレビュー指摘 × 対策 × 優先順位

項目 指摘されやすさ 影響度 対策案 優先度
① 冪等性の担保 ★★★★☆ ★★★★★ UPDATE系の処理はWHERE条件・一意制約を入れる。事前にリトライ時を想定 ★★★★★
② リトライ・スキップの方針不明 ★★★★☆ ★★★★★ 例外ハンドリングをskip, retry, no-rollbackで明記 ★★★★★
③ chunk-size設計・OOM ★★★☆☆ ★★★★☆ 処理件数を見積もり、100~500件で検証実施 ★★★★☆
④ 排他制御の漏れ(多重起動) ★★★★☆ ★★★★☆ 起動時にロックテーブル or DynamoDB lockを導入 ★★★★☆
⑤ JobParameter再実行不可 ★★★★☆ ★★★☆☆ ジョブ起動パラメータに日付・キーなどを明記 ★★★★☆
⑥ ログが読めない/エラーが追えない ★★★★☆ ★★★★☆ ログはINFO, ERROR, DEBUGに分離。原因・パラメータ出力 ★★★☆☆
⑦ Job構成が不明確(責務分離) ★★★☆☆ ★★★☆☆ Step単位で責務を明示、Job構成図を別資料にまとめる ★★★☆☆
⑧ テスト不十分 ★★☆☆☆ ★★★☆☆ JobLauncherTestUtilsとモックを使ったテストを書く ★★★☆☆

🎯 時間がないときに重点的にやるべき3つ

1. 冪等性と例外処理方針の明確化(最優先)

  • リトライ・スキップ・停止の挙動は仕様に残し、コードにも反映。
  • 特に外部連携やDB更新の箇所は**複数回呼ばれても安全か?**を検証。

2. 再実行を考慮した設計(JobParameterと排他)

  • 再実行できない設計=運用事故に直結
  • JobParameters に一意キー(日付, ID, ファイル名など)を渡す。
  • 起動制御は、DBのロックテーブルDynamoDBの排他制御 でガード。

3. ログ・監視設計

  • CloudWatch LogsやDatadogなどに出すログ粒度を明確に。
  • ERRORレベルログには処理対象ID・原因・例外スタックまで必須。

✍️ 対策に使えるテンプレ/コード例

  • @StepScopeでのパラメータ受取
  • @EnableBatchProcessingとJobBuilderFactoryの使い方
  • RetryPolicy, SkipPolicyの実装例
  • DynamoDBロックの処理サンプル
  • JobLauncherTestUtilsのテストテンプレ

✅ ① @StepScopeでのパラメータ受取

@Bean
@StepScope
public FlatFileItemReader<MyDto> reader(@Value("#{jobParameters['inputFile']}") String inputFile) {
    FlatFileItemReader<MyDto> reader = new FlatFileItemReader<>();
    reader.setResource(new FileSystemResource(inputFile));
    // 他設定…
    return reader;
}
  • jobParameters['inputFile'] で動的パラメータを受け取れる
  • テストや再実行時に柔軟性が出る

✅ ② @EnableBatchProcessingJobBuilderFactoryの基本構成

@Configuration
@EnableBatchProcessing
public class BatchConfig {

    @Autowired
    private JobBuilderFactory jobBuilderFactory;

    @Autowired
    private StepBuilderFactory stepBuilderFactory;

    @Bean
    public Job myJob(Step step1) {
        return jobBuilderFactory.get("myJob")
                .start(step1)
                .build();
    }

    @Bean
    public Step step1(ItemReader<MyDto> reader, ItemProcessor<MyDto, MyDto> processor,
                      ItemWriter<MyDto> writer) {
        return stepBuilderFactory.get("step1")
                .<MyDto, MyDto>chunk(100)
                .reader(reader)
                .processor(processor)
                .writer(writer)
                .build();
    }
}

✅ ③ RetryPolicy, SkipPolicyの実装例

@Bean
public Step stepWithRetry(StepBuilderFactory stepBuilderFactory) {
    return stepBuilderFactory.get("stepWithRetry")
        .<Input, Output>chunk(100)
        .reader(reader())
        .processor(processor())
        .writer(writer())
        .faultTolerant()
        .retry(MyTemporaryException.class)
        .retryLimit(3)
        .skip(MyValidationException.class)
        .skipLimit(10)
        .build();
}
  • 一時障害にはretry、業務エラーはskip、それぞれ設計に合わせて明示
  • 例外クラスはアプリケーション内で定義しておくこと

✅ ④ DynamoDBによる排他制御(簡易版)

public boolean acquireLock(String jobName) {
    String key = "lock_" + jobName;
    try {
        dynamoDbClient.putItem(PutItemRequest.builder()
            .tableName("BatchLockTable")
            .item(Map.of(
                "LockKey", AttributeValue.fromS(key),
                "CreatedAt", AttributeValue.fromS(Instant.now().toString())
            ))
            .conditionExpression("attribute_not_exists(LockKey)")
            .build());
        return true;
    } catch (ConditionalCheckFailedException e) {
        return false;
    }
}
  • 起動前にこの関数を呼び、falseなら即停止
  • TTL(Time to Live)も設定しておくとベター

✅ ⑤ JobLauncherTestUtilsによるテストテンプレ

@RunWith(SpringRunner.class)
@SpringBatchTest
@ContextConfiguration(classes = { MyJobConfig.class })
public class MyJobTest {

    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;

    @Test
    public void testJob() throws Exception {
        JobParameters jobParameters = new JobParametersBuilder()
                .addString("inputFile", "test-data/input.csv")
                .toJobParameters();

        JobExecution jobExecution = jobLauncherTestUtils.launchJob(jobParameters);

        assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus());
    }
}
  • @SpringBatchTestで専用のテスト機能が有効に
  • launchJob()でまるごと起動テストが可能

🔚 総括

このテンプレを活用すれば、以下の観点が 事前にレビュー対策された状態になります:

  • パラメータでの再実行制御
  • リトライ・スキップ明確化
  • 排他制御
  • ロジック単位の責務分離
  • 自動テストによる品質担保

💡補足:レビューではここを突っ込まれがち

  • 「このジョブ、失敗したら誰が何を見て対応するの?
  • 「再実行したら二重登録しない保証は?
  • 「ログでエラー原因は追えますか?
  • データ件数が10倍になっても落ちませんか?

🔥 超重要レビュー観点4点について、実装対策+説明の切り返し例をセットでまとめました。
**「これ答えられるだけで格が違う」**レベルです👇


①「このジョブ、失敗したら誰が何を見て対応するの?」

✅ 対策

  • CloudWatch Logs / Datadog でアラート通知
  • JobExecutionListener or StepExecutionListener開始・終了・異常ログ出力
  • 失敗時出力例:ジョブ名、JobInstance ID、例外内容、再実行方法

💬 説明の切り返し例

「バッチがFAILEDになった場合、CloudWatch Logs に例外スタックを記録しており、DatadogのモニターでSlack通知も飛びます。対応者はログのJobInstance IDを元にエラー詳細を即確認できます。」


②「再実行したら二重登録しない保証は?」

✅ 対策

  • JobParametersのユニーク性で多重実行防止
  • DynamoDB or RDBでのロック制御
  • 処理単位で冪等性の担保(たとえば「UPSERT」or「処理済みフラグ」確認)

💬 説明の切り返し例

「JobParameterにバッチ日付などを含め、既実行JobInstanceと重複しないようにしてます。またDynamoDBロックを併用して多重起動を防ぎます。個別データ処理もUPSERTベースで二重処理を防いでいます。」


③「ログでエラー原因は追えますか?」

✅ 対策

  • @Slf4jstep開始/終了/エラー発生点をINFO・ERRORで出力
  • 例外ハンドリングで原因例外(ex.getCause())までログ出力
  • JobInstance IDとパラメータを全ログに埋め込む

💬 説明の切り返し例

「全ログにJobInstance IDとパラメータを付与しており、リトライ・スキップの有無、例外発生箇所もすべて追えるように設計しています。CloudWatch Logs Insightで絞り込みも可能です。」


④「データ件数が10倍になっても落ちませんか?」

✅ 対策

  • チャンクサイズ調整スレッド数の検討
  • ItemReaderでストリーム型読み込み
  • 大量データはStep分割 or パーティショニング

💬 説明の切り返し例

「ItemReaderはカーソルベースでメモリ消費を抑えています。将来的に件数が増えた場合に備え、チャンクサイズと同時にスレッド分割・Step分割によるスケーラビリティも見込んでいます。」


✨補足:レビューで光る一言

  • 「障害時のトラブルシュートを前提にログとパラメータ設計してます」
  • 「壊れないより、“壊れても直しやすい”が基本思想です」
  • 「あえてRetry/Skipを最小構成にとどめて、後で拡張しやすくしています」

🚀 成功のための戦略

  • 仕様整理+処理フローを図にする(レビュー前に説明できるように)
  • 「1回失敗しても壊れない」を最小限で実装(全部やろうとしない)
  • ログ・例外・再実行ができる状態にさえしておけば、後で拡張しやすい

🚀 成功のための戦略(詳細付き)

項目 内容 補足
① 仕様整理+処理フローを図に 開発着手前に、紙でもMiroでもいいから 処理の全体像を図にして 口頭で説明できるように → 上流設計レビューでの理解共有、認識齟齬防止に絶大な効果
② 「1回失敗しても壊れない」をミニマム実装 完璧を狙わず、まずは Retry+ログ出力+再実行可能 にしておく @StepScopeJobParameterで再実行性確保、DynamoDBロックで多重起動防止も◎
③ ログ+例外+再実行の担保 どこで・何が・どうなったかが分かるログ、必要最小限の例外ハンドリングを入れておく → 統合後でも運用しやすく、バグ切り分けも早くなる

🛠 重点実装ポイント(時間がないときはここだけ)

優先度 実装内容 理由
✅ 最優先 @StepScope + JobParameter 再実行設計に必要、レビュー指摘回避しやすい
✅ 最優先 ログ出力(INFO/WARN/ERROR) 処理追跡・障害分析の基本、レビューで絶対見られる
🔄 あとでOK Retry/Skip実装 後追いでよい、まずはリトライ方針とログ出力だけでも良い
💡 任意 DynamoDBによる排他制御 チームの要件次第、もし多重起動NGなら優先度UP

💡 Tips:処理フロー図はこう描くと伝わる

[ファイル取り込み開始]
      ↓
[ファイル存在チェック]
      ↓
[ItemReader] --(chunk)--> [ItemProcessor] --> [ItemWriter]
      ↓
[正常終了 or 例外ハンドリング]
  • 特に「どのタイミングで何をログ出すか」「失敗時はどう分岐するか」を明確にするとレビュー通過しやすい。

以下に「構成図」「リトライの設計パターン」「リファレンス一覧」を実務レビュー突破レベルでまとめました👇


📊 構成図(Spring Batch × DynamoDBロック × 再実行設計)

                       +--------------------+
                       |    Scheduler       |
                       | (e.g. EventBridge) |
                       +--------------------+
                                |
                                v
                      +---------------------+
                      |   Spring Boot App   |
                      |     (Batch)         |
                      +---------------------+
                                |
        +-----------------------+----------------------+
        |                      |                      |
        v                      v                      v
+---------------+     +----------------+      +----------------+
| JobParameter  | --> | JobRepository  | <--> | DynamoDB Lock  |
| 受け取りと再実行 |     | 状態管理           |      | 排他制御           |
+---------------+     +----------------+      +----------------+
                                |
                                v
                       +------------------+
                       | Job + Steps      |
                       | @StepScope等で設計 |
                       +------------------+
                                |
                     +----------+----------+
                     |                     |
                     v                     v
              ItemReader           Retry + SkipPolicy
            + Logging + Exception Handling

🔁 リトライ設計パターン

パターン 概要 実装例 使いどころ
RetryPolicy 例外発生時にリトライ回数/間隔を制御 SimpleRetryPolicy, RetryTemplate DB接続やAPI一時障害
SkipPolicy 特定の例外をスキップし次に進む AlwaysSkipItemSkipPolicyなど データ不整合が許容されるバッチ
Retry + Skip併用 Retry後もダメならスキップして続行 FaultTolerantStepBuilder 可用性優先時の定番
BackOffPolicy リトライ間隔を制御(指数バックオフなど) ExponentialBackOffPolicy API等にレート制限がある場合
DynamoDBロック 排他制御で多重起動を防ぐ 手動実装 or spring-batch-integration 再実行・並列実行対策

📚 リファレンス一覧(リンク付き)

カテゴリ リンク 内容
Spring Batch公式 Spring Batch Docs フル機能解説
Retry機構 Spring Retry Docs @RetryableRetryTemplateの使い方
DynamoDBロック実装例 AWS SDK V2 DynamoDB Java example JavaでのDynamoDB操作
JobParameter受け取り Baeldung - Job Parameters @StepScopeで動的受け取り
JobLauncherTestUtilsでのテスト Test with Spring Batch バッチの単体・結合テスト

✅ あと一歩を支えるテンプレ

  • RetryTemplateのテンプレ
  • @StepScope@Value("#{jobParameters['param']}")
  • JobLauncherTestUtilsのJUnitベースのコード
  • DynamoDBのロック処理のシンプル実装(putItem with ConditionExpression)

以下に、Spring Batchでのジョブ設計やレビュー用に使える**説明資料テンプレート(構成と内容)**を提示します。説明は 5〜10分でレビュー通過を狙える構成にしています👇


✅ レビュー説明資料テンプレート(構成案)

1. 概要(1枚)

項目 内容
ジョブ名 UserImportJob(例)
処理目的 ユーザー情報CSVをDBに登録
想定実行タイミング 毎日 3:00 AM
処理件数 最大5万件(想定)
開発状況 実装完了(要レビュー)

2. 処理フロー図(1枚)

  • 処理全体を簡潔に図解(例)
Start
  ↓
Read CSV → Validate → Insert to DB
  ↓
通知 or エラー出力

※Lucidchart / draw.io / Mermaid などでOK。
チャンク処理、ステップ分割があるなら明示。


3. 処理詳細(Stepごとに簡単に)

Step名 内容 備考
readCsvStep S3からCSV取得 → バリデーション バリデーションエラーはskip
writeDbStep DBにInsert(UPSERT) 主キー重複は上書き

4. 再実行・多重起動対策(1枚)

  • JobParameterに日付を含めてユニークに
  • JobLauncherTestUtilsで再実行テスト済
  • DynamoDBロック導入(併用)

5. ログ・監視設計(1枚)

対象 内容
CloudWatch Logs step単位でINFO, ERROR出力
通知 DatadogでSlack通知(失敗時)
ログ出力例 JobInstanceId=xxx Step=yyy Error=NullPointerException

6. リトライ・スキップ設計(1枚)

対象 対応内容
リトライ対象 DB接続エラー (SQLException)
スキップ対象 バリデーション (BindException)
RetryPolicy 3回まで、1秒間隔

7. 拡張性・今後の課題(1枚)

  • チャンクサイズ調整でスケーラブルに対応可能
  • 今後はPartition Step導入で並列処理対応も検討
  • 処理対象が10倍になってもメモリ消費を抑制するストリーム設計済

💡補足Tips(冒頭で言えると強い)

  • 「障害発生時のログ設計と、再実行を考慮した構成です」
  • 「今回は最小限で作って、今後の機能追加も見越した設計にしてます」
  • 「レビューでよく聞かれる観点(ログ・リトライ・再実行)を先に説明します」

なぜか、空白が、、w



Discussion