포스트

Spring Quartz

JobDetail·Trigger·Scheduler를 Cron으로 묶는 기본 사용법과, Job 정지 vs Trigger 정지의 차이, JDBCJobStore와 RAMJobStore 선택 기준.

Spring Quartz

Quartz Scheduler는 유연하고 확장 가능한 Job 스케줄링을 위한 라이브러리로, Java 기반의 오픈소스 라이브러리이다. Job과 Trigger를 등록하고 실행하는 기능을 제공하며, Job이 실행되는 시점을 Cron 표현식을 통해 정의할 수 있다.

구성요소

  • Scheduler: Job과 Trigger를 등록하고 작업을 실행하는 주체
  • JobDetail: Job을 실행하기 위한 정보를 가지고 있는 객체
  • Trigger: Job 실행 조건 및 시점을 정의하는 객체
  • Job: 작업의 실제 실행 로직. Job 인터페이스를 구현하여 작성
  • JobDataMap: JobDetail과 Trigger에 데이터를 전달하기 위한 객체

사용 방법

  1. Job 정의: Job 인터페이스를 구현하여 Job을 정의
  2. JobDetail 생성: Job을 실행하기 위한 정보를 가지고 있는 JobDetail 객체 생성
  3. Trigger 생성: Job을 실행할 시점을 정의하는 Trigger 객체 생성
  4. Scheduler에 등록: JobDetail과 Trigger를 Scheduler에 등록
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 // 1. Job 정의
public class QueryJob implements Job {
  @Override
  public void execute(JobExecutionContext context) throws JobExecutionException {
    String query = context.getMergedJobDataMap().getString("query");
    System.out.println("Executing query: " + query);
    // 여기에 쿼리 실행 로직 추가
  }
}

// 2. JobDetail 생성
JobDetail jobDetail = JobBuilder.newJob(QueryJob.class)
  .withIdentity("myJob", "group1")
  .usingJobData("query", "SELECT * FROM users")
  .build();

// 3. Trigger 생성
Trigger trigger = TriggerBuilder.newTrigger()
  .forJob(jobDetail)
  .withIdentity("myTrigger", "group1")
  .withSchedule(CronScheduleBuilder.cronSchedule("0 0/5 * * * ?"))
  .build();

 // 4. Scheduler 등록
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
scheduler.start();
scheduler.scheduleJob(jobDetail, trigger);

Job과 Trigger 관계

  • Job과 Trigger는 1:1 또는 1:N 관계로 연결 가능 (하나의 Job에 여러 Trigger를 연결할 수 있으나, 하나의 Trigger는 하나의 Job에만 연결 가능)
  • Job이 삭제 될 때 Trigger도 함께 삭제됨

Job 정지 vs Trigger 정지

구분Job 정지Trigger 정지
대상Job 자체특정 Trigger
영향 범위Job과 연결된 모든 Trigger가 멈춤선택한 Trigger만 멈춤
Job 상태Job은 여전히 등록되어 있지만 실행되지 않음Job은 활성 상태로 유지되며 다른 Trigger에 의해 실행될 수 있음
제어 수준Job 단위의 전역적인 제어 가능Trigger 단위로 세밀한 제어 가능
사용 사례Job 전체를 멈추거나 모든 트리거를 한꺼번에 멈추고 싶을 때특정 Trigger만 멈추고 나머지는 동작하도록 유지하고 싶을 때

JDBCJobStore vs RAMJobStore

  • JDBCJobStore: Job, Trigger, JobDataMap 등의 정보를 DB에 저장
  • RAMJobStore: Job, Trigger, JobDataMap 등의 정보를 메모리에 저장
  • JDBCJobStore는 Quartz Scheduler가 종료되어도 정보가 유지되지만, RAMJobStore는 종료 시 정보가 사라짐
  • JDBCJobStore는 Quartz Scheduler를 여러 대의 서버에서 공유할 때 사용

클러스터링 — 스케줄러를 여러 대에 띄울 때

인스턴스를 2대 이상 띄우면 같은 Job이 양쪽에서 동시에 실행되는 문제가 생긴다. 클러스터링은 이걸 막고, 한 노드가 죽어도 다른 노드가 이어받게(fail-over) 한다. 전제는 JDBCJobStore(공유 DB) — RAMJobStore로는 불가능하다.

1
2
3
org.quartz.jobStore.isClustered=true
org.quartz.scheduler.instanceId=AUTO          # 노드마다 고유 ID 자동 부여
org.quartz.jobStore.clusterCheckinInterval=15000
  • 조율은 애플리케이션이 아니라 DB의 qrtz_locks 행 락으로 이뤄진다. 트리거를 획득할 때 노드들이 이 락을 놓고 경쟁해, 한 번의 실행은 한 노드만 가져간다.
  • instanceId노드마다 반드시 달라야 한다. AUTO가 호스트명+타임스탬프로 생성해주니 직접 지정하지 않는다. 같은 ID를 두 노드에 주면 서로를 죽은 노드로 오인한다.
  • 모든 노드의 시계(NTP)를 맞춰야 한다. clock skew가 크면 checkin·트리거 시점 판정이 어긋난다.
  • Job이 한 노드에서만 돌면 충분할 때(중복 실행 절대 금지)는 @DisallowConcurrentExecution을 함께 건다.

Misfire — 놓친 실행 처리

스케줄된 시각에 트리거를 못 쏜 경우(=misfire)를 어떻게 만회할지의 정책이다. misfire는 생각보다 자주 난다 — 서버 다운타임, 스레드 풀 고갈, DB 지연, @DisallowConcurrentExecution으로 이전 실행이 안 끝난 경우 등. org.quartz.jobStore.misfireThreshold(기본 60초)를 넘겨 지각하면 misfire로 판정된다.

정책을 지정하지 않으면 기본값은 smart policy인데, 트리거 종류에 따라 동작이 갈려 헷갈린다. Cron 트리거의 대표 선택지:

정책동작
withMisfireHandlingInstructionFireAndProceed놓친 것 중 1회만 즉시 실행하고 이후 정상 스케줄 복귀 (기본, 대개 이걸 원함)
withMisfireHandlingInstructionDoNothing놓친 건 버리고 다음 정상 시각을 기다림
withMisfireHandlingInstructionIgnoreMisfires놓친 모든 실행을 몰아서 실행(catch-up)
1
2
CronScheduleBuilder.cronSchedule("0 0/5 * * * ?")
    .withMisfireHandlingInstructionFireAndProceed();

흔한 오해: “5분마다”인데 서버가 1시간 죽었다 살아나면, 기본 정책은 밀린 12번을 다 돌리지 않는다. 밀린 실행을 반드시 채워야 하는 정산·집계라면 Ignore를, 중복이 해로우면 DoNothing을 명시적으로 골라야 한다.

Quartz Database Schema

Quartz 2.x의 표준 스키마는 11개 테이블이다(접두어 QRTZ_org.quartz.jobStore.tablePrefix로 변경 가능). 배포판 docs/dbTablestables_*.sql에 DB별 DDL이 있다. (Quartz 1.x의 리스너 테이블·qrtz_job_trigger_rel 등은 2.x에 존재하지 않는다.)

Table NameDescription
qrtz_job_detailsJobDetail 정보를 저장
qrtz_triggersTrigger 정보(Job 연결·상태 포함)를 저장
qrtz_cron_triggersCron Trigger의 표현식을 저장
qrtz_simple_triggersSimple Trigger(반복 횟수·간격)를 저장
qrtz_simprop_triggersCalendarInterval 등 프로퍼티 기반 Trigger를 저장
qrtz_blob_triggers커스텀 Trigger를 직렬화(Blob)해 저장
qrtz_calendars실행 제외일 등 Calendar 객체를 직렬화해 저장
qrtz_paused_trigger_grpsPause된 Trigger 그룹을 저장
qrtz_fired_triggers현재 실행(fire) 중인 Trigger의 상태를 저장
qrtz_scheduler_state클러스터 노드별 인스턴스 상태(heartbeat)를 저장
qrtz_locks클러스터 동시성 제어용 비관적 락 행

Quartz Trigger State

Trigger StateDescription
NormalTrigger가 실행될 준비가 되어 있으며, 스케줄에 따라 실행됨
PausedTrigger가 일시 중지되어 있으며, 실행되지 않음
CompleteTrigger가 더 이상 실행되지 않으며, 실행할 “fire times”가 없음
ErrorTrigger가 오류가 발생하여 더 이상 실행되지 않음
BlockedTrigger가 @DisallowConcurrentExecution이 설정된 Job과 연결되어 있어 대기 중인 상태
NoneTrigger가 존재하지 않음
WaitingTrigger가 대기 중인 상태로, Job이 실행될 준비가 되어 있음
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.