이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- cron 표현식은 무엇을 보장하고 무엇을 보장하지 않는가?
- scheduler 설계에서 timezone, missed run, overlapping run을 어떻게 다뤄야 하는가?
- Java/Spring
@Scheduled작업에 어떤 운영 메타데이터를 붙여야 하는가?- 실행 시각과 처리 대상 범위를 분리하지 않으면 어떤 문제가 생기는가?
개요
cron은 실행 시각을 표현하는 방법이다.
cron은 작업 성공, 중복 실행 방지, 장기 실행 제어, 재시작, timezone 문제를 해결하지 않는다.
따라서 scheduler 설계는 cron 표현식보다 실행 모델을 먼저 정해야 한다.
백엔드 서비스에서 @Scheduled를 붙였다고 batch 운영이 끝나는 것은 아니다.
언제 실행할지, 무엇을 처리할지, 놓친 실행을 어떻게 복구할지, 겹친 실행을 어떻게 막을지를 함께 정해야 한다.
원리
scheduler는 trigger다.
batch는 처리 모델이다.
trigger는 “지금 작업을 시작해도 된다”는 신호일 뿐이다.
작업 대상 범위는 trigger 시각에서 바로 추론하면 위험하다.
예를 들어 00:05에 실행되는 job이 전날 데이터를 처리한다면, 처리 대상은 yesterday이지 실행 시각 자체가 아니다.
Cron이 보장하지 않는 것
- 이전 실행이 끝났는지 모른다.
- 실행 중 프로세스가 죽었는지 모른다.
- missed run을 자동으로 보정하지 않는다.
- timezone과 daylight saving 정책을 대신 결정하지 않는다.
- job parameter와 target range를 기록하지 않는다.
- 실패 후 어디서 재시작할지 모른다.
이 한계를 문서화하지 않으면 운영자는 cron 표현식만 보고 시스템이 안전하다고 착각한다.
Spring Scheduler 예시
@Scheduled(cron = "0 5 0 * * *", zone = "Asia/Seoul")
public void requestDailySettlement() {
LocalDate targetDate = LocalDate.now(ZoneId.of("Asia/Seoul")).minusDays(1);
settlementJobRequestService.createIfAbsent("daily-settlement", targetDate);
}이 코드는 실제 정산을 수행하지 않는다.
대상 날짜의 job request를 만들 뿐이다.
실제 worker는 job table에서 pending job을 가져와 lock과 chunk 처리로 실행한다.
실행 시각과 대상 범위 분리
trigger_at: 2026-07-01 00:05 KST
job_name: daily-settlement
target_date: 2026-06-30
range: 2026-06-30 00:00:00 <= paid_at < 2026-07-01 00:00:00실행 시각과 처리 범위를 분리하면 missed run을 복구하기 쉽다.
프로세스가 00:05에 죽어도 target_date=2026-06-30 job이 pending 상태로 남아 있으면 나중에 실행할 수 있다.
Job Request Table
CREATE TABLE scheduled_job_request (
job_name varchar(100) NOT NULL,
target_date date NOT NULL,
status varchar(20) NOT NULL,
requested_at timestamp NOT NULL,
started_at timestamp NULL,
finished_at timestamp NULL,
error_message text NULL,
PRIMARY KEY (job_name, target_date)
);PRIMARY KEY (job_name, target_date)는 같은 날짜 job이 중복 생성되는 것을 막는다.
cron trigger가 두 번 실행되어도 request 생성은 idempotent하게 만들 수 있다.
Missed Run
서버가 내려가 있으면 cron은 실행되지 않을 수 있다.
중요한 batch라면 복구 루틴이 필요하다.
public void recoverMissingRequests(LocalDate from, LocalDate to) {
for (LocalDate date = from; !date.isAfter(to); date = date.plusDays(1)) {
settlementJobRequestService.createIfAbsent("daily-settlement", date);
}
}복구 루틴은 cron보다 중요할 때가 많다.
실행 시각이 아니라 처리 대상 날짜를 기준으로 복구하기 때문이다.
Overlapping Run
이전 실행이 끝나기 전에 다음 trigger가 올 수 있다.
이 경우 선택지는 세 가지다.
- skip: 이미 실행 중이면 새 실행을 건너뛴다.
- queue: 새 target range를 pending으로 쌓는다.
- parallelize: target range가 독립적일 때만 병렬 실행한다.
정산처럼 같은 target table을 쓰는 작업은 보통 skip 또는 queue가 안전하다.
판단 축
- cron 표현식과 job parameter를 분리한다.
- timezone을 명시하고 서버 기본 timezone에 의존하지 않는다.
- missed run 복구 방법을 둔다.
- overlapping run 정책을 skip, queue, parallelize 중 하나로 정한다.
- trigger 성공이 job 성공을 의미하지 않게 기록을 분리한다.
실패 모델
@Scheduledmethod 안에서 바로 대량 처리를 수행한다.- 서버가 내려간 시간대의 job이 조용히 누락된다.
- 이전 실행이 길어져 다음 실행과 겹친다.
- target date를
now()로 계산해 재실행 때 다른 범위를 처리한다. - timezone 설정이 없어 배포 환경마다 실행 날짜가 달라진다.
운영 지표
scheduled_request_created_count: 생성된 job request 수다.missed_request_count: 기대했지만 생성되지 않은 target range 수다.overlap_skip_count: 겹침 때문에 건너뛴 실행 수다.job_request_pending_age: pending job이 얼마나 오래 쌓였는지다.trigger_to_start_lag: trigger 이후 실제 시작까지의 지연이다.
개인 프로젝트 기준
개인 프로젝트에서는 @Scheduled와 job request table 하나면 충분하다.
다만 cron method가 실제 batch 로직 전체를 수행하지 않게 한다.
target date와 status를 남기면 재실행과 누락 복구가 가능해진다.
기업 운영 기준
기업 운영에서는 scheduler 자체도 관측 대상이다.
job이 실패하지 않았더라도 request가 생성되지 않으면 장애다.
timezone, holiday calendar, maintenance window, dependency window를 문서에 남긴다.
중요 job은 missed run 감지와 자동 request 생성이 필요하다.
실전 팁
- cron 표현식 옆에 사람이 읽는 실행 정책을 주석이나 문서로 남긴다.
now()로 target range를 암묵 계산하지 말고 job parameter로 저장한다.createIfAbsent패턴으로 request 생성을 idempotent하게 만든다.- scheduler log에는 trigger time, job name, target range를 함께 남긴다.
- batch가 길어질 가능성이 있으면 overlapping policy를 먼저 정한다.
위험 신호!
- cron 표현식만 있고 target range 기록이 없다.
- missed run을 운영자가 기억으로 찾아야 한다.
@Scheduledmethod가 수천 건을 직접 처리한다.- 서버 timezone 변경이 batch 날짜를 바꿀 수 있다.
- 같은 job이 동시에 떠도 결과가 어떻게 되는지 설명하지 못한다.
확인 질문
확인 질문
- cron이 보장하지 않는 것은 무엇인가?
- 중복 실행 방지, 실패 재시작, missed run 복구, target range 기록, timezone 정책을 보장하지 않는다.
- 실행 시각과 처리 대상 범위를 분리해야 하는 이유는 무엇인가?
- 재실행과 missed run 복구 때 같은 target range를 안정적으로 다시 처리하기 위해서다.
- scheduler 설계에서 overlapping run은 어떻게 처리해야 하는가?
- skip, queue, parallelize 중 정책을 정하고 lock 또는 job request 상태로 구현해야 한다.
참고 문서
- Spring Framework Reference, Task Execution and Scheduling
- Spring Batch Reference
- Quartz Scheduler Documentation
- Martin Kleppmann, Designing Data-Intensive Applications