開発記録

Spring Bootのcron定期実行|@Scheduledで期限切れデータを削除する

AppliParkでは、TODOリストと「誰がアウト?」の期限切れデータをSpring Bootの@Scheduledで定期削除しています。 AWS上でもEventBridge+Lambdaを使わなかった理由、実装方法、複数台構成での注意点を実例から紹介します。

CONTENTSこの記事の目次8項目

定期実行と聞くと、AWSのEventBridgeからLambdaを起動する構成を最初に思い浮かべていました。 しかしAppliParkでは、常時稼働しているSpring Bootバックエンドの中で、TODOリストと「誰がアウト?」の期限切れデータを定期削除しています。 この記事では、実際に使っている技術、処理の流れ、なぜこの構成を選んだのか、運用して見えてきた注意点をまとめます。

AppliParkの定期削除の流れ。常時稼働するSpring Bootバックエンド内の@ScheduledからPostgreSQLの期限切れデータを削除します。

定期削除が必要になった理由

AppliParkでは、使われなくなったデータをいつまでも保存し続けないため、機能ごとに保存期間や有効期限を決めています。 TODOリストは長期間更新されていないリストが少しずつ増えます。 「誰がアウト?」では、グループ、参加者、ラウンド、回答など、遊び終わった後は不要になる一時的なデータが作られます。 これらを手作業で確認するのではなく、期限を条件に自動で整理することにしました。

実際に使っているのはSpring Bootの@Scheduled

バックエンドの起動クラスでSpringのスケジューラーを有効にします。定期実行したいメソッドにはcron形式で時刻を指定します。 これはサーバーOSのcronへジョブを登録する仕組みではなく、起動中のSpring Bootアプリ自身が時刻を判断してメソッドを呼び出す仕組みです。

CODEBackendApplication.ktkotlin
@SpringBootApplication
@EnableScheduling
class BackendApplication
@EnableSchedulingでSpringのスケジューラーを有効にしています。

TODOリスト|毎月1日の午前1時に整理

CODETodoService.ktkotlin
@Scheduled(cron = "0 0 1 1 * *", zone = "Asia/Tokyo")
@Transactional
fun deleteExpiredLists(): Long =
    listRepository.deleteAllByUpdatedAtLessThanEqual(
        OffsetDateTime.now(clock).minusMonths(retentionMonths)
    )
毎月1日午前1時に、最終更新から保存期間を過ぎたリストを削除します。

毎月1日の午前1時になると、現在時刻から保存月数を引いて基準日時を作り、その日時以前から更新されていないリストを削除します。保存期間は環境設定から変更でき、現在の既定値は6か月です。

誰がアウト?|毎時0分に期限を確認

CODEOutGroupService.ktkotlin
@Scheduled(cron = "0 0 * * * *", zone = "Asia/Tokyo")
@Transactional
fun deleteExpiredGroups() {
    groupRepository.deleteAllByExpiresAtLessThanEqual(now())
}
毎時0分に、有効期限を過ぎたグループを削除します。

毎時0分に実行し、expires_atが現在時刻以前になったグループを削除します。 ゲームを遊んでいる最中に突然消えないよう、作成時に有効期限を持たせ、その期限を過ぎたデータだけを対象にしています。

削除処理をデータベース側でも支える

  • 削除条件に使うupdated_atとexpires_atへインデックスを設定する
  • @Transactionalを付け、削除処理を一つのトランザクションとして実行する
  • 親データを削除すると、関連するメンバーや回答も外部キーのON DELETE CASCADEで削除する
  • 期限を過ぎた行だけを条件にすることで、重複して実行されても結果が変わりにくくする

アプリ側で関連テーブルを一つずつ削除するのではなく、PostgreSQLの外部キーにON DELETE CASCADEを設定しています。 たとえば「誰がアウト?」のグループを削除すると、そのグループに属するメンバーやラウンド、その先の回答・結果も連動して削除されます。定期処理のコードを短く保ち、関連データだけが残る状態を避けやすくしています。

なぜEventBridge+Lambdaにしなかったのか

AppliParkのバックエンドはECS Fargate上で常時稼働しています。 その中で短いデータベース処理を行うため、既存のサービス、JPAのリポジトリ、設定値、データベース接続をそのまま再利用できる@Scheduledを選びました。 EventBridge、Lambda、IAMロール、VPC接続などを別途管理する必要がなく、現在の規模では構成をシンプルに保てる利点が大きいと判断しました。

EventBridge Schedulerが向いているケース

  • バックエンドが停止していても、決めた時刻に独立して起動したい
  • 失敗時の再試行やデッドレターキューをAWS側で管理したい
  • 時間のかかるバッチを通常のWeb APIと分離したい
  • 複数のサービスにまたがる定期処理を一元管理したい
  • ECSのタスク数を増やしても、起動元を一つに保ちたい

現在のAWSでは、EventBridge SchedulerからLambdaだけでなく、ECSのRunTaskを呼び出して、決まった時間だけバッチ用コンテナを起動することもできます。 将来、定期削除が重くなったり、Web APIとは独立した再試行や監視が必要になったりした場合は、この方式が移行候補になります。

現在の構成で注意していること

@Scheduledは、起動しているSpring Bootアプリごとに動きます。 AppliParkのECSサービスは通常1タスクですが、デプロイ中には新旧のタスクが一時的に同時稼働する可能性があります。 その時間と定期実行が重なると、同じ削除処理が2回始まる可能性があります。

今後改善したいこと

  1. 開始時刻、終了時刻、削除件数、失敗をログへ記録する
  2. cron式を環境設定へ移し、コード変更なしで実行時刻を調整できるようにする
  3. 「誰がアウト?」でも、期限内のデータが残ることを含めた削除テストを追加する
  4. ECSを複数台にする場合は、ShedLockやPostgreSQLのロック、EventBridge Schedulerを検討する
  5. 削除件数が増えた場合は、一括削除や分割処理で負荷を抑える

まとめ:小さく始め、必要になったら分離する

今回は、すでに動いているSpring BootバックエンドとSpring Data JPAを使い、期限切れデータを定期削除しています。share&doは毎月、「誰がアウト?」は毎時、期限を条件にPostgreSQLのデータを整理する構成です。 運用で確認したいのは、処理時間と削除件数、失敗の有無です。アプリ停止中の実行や複数台での重複、再試行が課題になった段階で、分散ロックや独立したスケジューラーを検討します。

READ MOREAppliParkのTODOリストを使ってみる複数人でも共有できるシンプルなTODOリストです。READ MORE「誰がアウト?」を遊んでみる参加者が入力した言葉をもとに、今回のOUT対象者を決める抽選アプリです。言葉の意味や気持ちの強さをAIが評価する仕組みではありません。

今回使っているcronは、サーバーOSのcronですか?

いいえ。Spring Bootアプリ内のスケジューラーが、@Scheduledに設定したcron式を読み取ってメソッドを呼び出しています。そのため、バックエンドが停止している間は実行されません。

AWSを使うならEventBridge+Lambdaに変更したほうがよいですか?

必ずしも変更する必要はありません。処理が短く、常時稼働中のバックエンド内部で完結し、重複実行にも強い処理なら@Scheduledはシンプルな選択です。アプリ停止中にも実行したい、再試行やDLQをAWS側で管理したい、複数台構成にしたい場合はEventBridge Schedulerを検討します。

出典・参考資料Task Execution and SchedulingSpring Framework公式/@Scheduledとcron式による定期実行docs.spring.io出典・参考資料Using Amazon EventBridge Scheduler to schedule Amazon ECS tasksAWS公式/EventBridge SchedulerからECSタスクを定期起動する方法docs.aws.amazon.com出典・参考資料ShedLockGitHub公式リポジトリ/複数ノードで定期処理の同時実行を防ぐための選択肢github.com