현대 웹 애플리케이션에서 배치 작업, 정기적인 데이터 처리, 알림 발송과 같은 스케줄링 작업은 필수적입니다. 이러한 요구사항을 효과적으로 해결할 수 있는 Java 생태계의 표준 스케줄링 라이브러리인 Quartz의 핵심 개념부터 실제 구현까지 체계적으로 알아보겠습니다.
Quartz는 Java 애플리케이션에서 작업 스케줄링을 위한 강력한 오픈소스 라이브러리입니다. 단순한 시간 기반 작업부터 복잡한 엔터프라이즈급 스케줄링 요구사항까지 모든 것을 처리할 수 있으며, Spring Framework와의 뛰어난 통합성으로 많은 개발자들이 선택하는 솔루션입니다.
Quartz의 아키텍처를 이해하기 위해서는 다음 핵심 클래스와 인터페이스들을 반드시 알아야 합니다.
public interface Job {
void execute(JobExecutionContext context) throws JobExecutionException;
}
Job은 실제로 실행될 작업의 로직을 담고 있는 인터페이스입니다. 모든 작업은 이 인터페이스를 구현해야 합니다.
@Component
public class EmailNotificationJob implements Job {
@Autowired
private EmailService emailService;
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// 작업 실행 로직
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
String recipient = dataMap.getString("recipient");
String message = dataMap.getString("message");
try {
emailService.sendEmail(recipient, message);
log.info("이메일 전송 완료: {}", recipient);
} catch (Exception e) {
throw new JobExecutionException("이메일 전송 실패", e);
}
}
}
public interface JobDetail extends Serializable, Cloneable {
JobKey getKey();
Class<? extends Job> getJobClass();
JobDataMap getJobDataMap();
boolean isDurable();
// ... 기타 메서드들
}
JobDetail은 Job 클래스의 인스턴스와 실행에 필요한 추가 정보들을 포함하는 클래스입니다. Job의 "설명서" 역할을 한다고 생각하면 됩니다.
// JobDetail 생성 예시
JobDetail jobDetail = JobBuilder.newJob(EmailNotificationJob.class)
.withIdentity("emailJob", "notification") // job 이름과 그룹
.withDescription("일일 이메일 알림 작업")
.usingJobData("recipient", "user@example.com") // 작업에 전달할 데이터
.usingJobData("template", "daily-report")
.storeDurably(true) // 트리거가 없어도 저장
.build();
JobDetail의 주요 속성:
Trigger는 언제, 얼마나 자주 Job을 실행할지를 정의하는 클래스입니다.
public abstract class Trigger implements Serializable, Cloneable {
public abstract Date getNextFireTime();
public abstract Date getPreviousFireTime();
public abstract boolean mayFireAgain();
// ... 기타 메서드들
}
지정된 시간 간격으로 Job을 실행하기 위한 Trigger입니다.
// 매 30초마다 10번 실행
Trigger simpleTrigger = TriggerBuilder.newTrigger()
.withIdentity("simpleTrigger", "group1")
.startNow()
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(30)
.withRepeatCount(9)) // 총 10번 실행 (0~9)
.build();
// 5분 후 시작해서 무한 반복
Trigger delayedTrigger = TriggerBuilder.newTrigger()
.withIdentity("delayedTrigger", "group1")
.startAt(new Date(System.currentTimeMillis() + 300000)) // 5분 후
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInMinutes(10)
.repeatForever())
.build();
Unix cron과 유사한 표현식으로 복잡한 스케줄링을 정의할 수 있습니다.
// 매일 오전 9시에 실행
Trigger cronTrigger = TriggerBuilder.newTrigger()
.withIdentity("dailyReportTrigger", "reports")
.withSchedule(CronScheduleBuilder.cronSchedule("0 0 9 * * ?"))
.build();
// 평일 오전 9시부터 오후 6시까지 30분마다 실행
Trigger businessHoursTrigger = TriggerBuilder.newTrigger()
.withIdentity("businessHoursTrigger", "monitoring")
.withSchedule(CronScheduleBuilder.cronSchedule("0 0/30 9-18 ? * MON-FRI"))
.build();
// 매월 마지막 날 오후 11시 30분에 실행
Trigger monthEndTrigger = TriggerBuilder.newTrigger()
.withIdentity("monthEndTrigger", "reports")
.withSchedule(CronScheduleBuilder.cronSchedule("0 30 23 L * ?"))
.build();
Cron 표현식 구조:
초(0-59) 분(0-59) 시(0-23) 일(1-31) 월(1-12) 요일(1-7) [연도]
특수 문자:
* : 모든 값
? : 특정 값 없음 (일/요일에서만 사용)
- : 범위 (예: 1-5)
, : 여러 값 (예: 1,3,5)
/ : 증분 (예: 0/15 = 0,15,30,45)
L : 마지막 (월의 마지막 날, 주의 마지막 요일)
W : 평일 (가장 가까운 평일)
# : N번째 요일 (예: 2#3 = 3번째 화요일)
Scheduler는 Job과 Trigger를 관리하고 실제 작업 실행을 담당하는 핵심 인터페이스입니다.
public interface Scheduler {
void start() throws SchedulerException;
void standby() throws SchedulerException;
void shutdown(boolean waitForJobsToComplete) throws SchedulerException;
Date scheduleJob(JobDetail jobDetail, Trigger trigger) throws SchedulerException;
boolean deleteJob(JobKey jobKey) throws SchedulerException;
void triggerJob(JobKey jobKey) throws SchedulerException;
List<JobExecutionContext> getCurrentlyExecutingJobs() throws SchedulerException;
// ... 기타 메서드들
}
@Service
public class SchedulerService {
@Autowired
private Scheduler scheduler;
public void scheduleEmailJob() throws SchedulerException {
// JobDetail 생성
JobDetail job = JobBuilder.newJob(EmailNotificationJob.class)
.withIdentity("dailyEmail", "notifications")
.build();
// Trigger 생성
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("dailyEmailTrigger", "notifications")
.withSchedule(CronScheduleBuilder.cronSchedule("0 0 9 * * ?"))
.build();
// 스케줄러에 등록
scheduler.scheduleJob(job, trigger);
log.info("일일 이메일 작업이 스케줄링되었습니다.");
}
public void pauseJob(String jobName, String groupName) throws SchedulerException {
JobKey jobKey = JobKey.jobKey(jobName, groupName);
scheduler.pauseJob(jobKey);
}
public void resumeJob(String jobName, String groupName) throws SchedulerException {
JobKey jobKey = JobKey.jobKey(jobName, groupName);
scheduler.resumeJob(jobKey);
}
}
SchedulerFactory는 Scheduler 인스턴스를 생성하고 구성하기 위한 인터페이스입니다.
public interface SchedulerFactory {
Scheduler getScheduler() throws SchedulerException;
Scheduler getScheduler(String schedName) throws SchedulerException;
Collection<Scheduler> getAllSchedulers() throws SchedulerException;
}
가장 일반적으로 사용되는 구현체는 StdSchedulerFactory입니다:
// 기본 설정으로 Scheduler 생성
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
// 사용자 정의 설정으로 Scheduler 생성
Properties props = new Properties();
props.put("org.quartz.scheduler.instanceName", "MyScheduler");
props.put("org.quartz.threadPool.threadCount", "10");
props.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX");
SchedulerFactory customFactory = new StdSchedulerFactory(props);
Scheduler customScheduler = customFactory.getScheduler();

Scheduler는 다음과 같은 생명주기 상태를 가집니다:
isShutdown() = false, isStarted() = false, isInStandbyMode() = truestart() 호출로 실행 상태로 전환 가능standby() 호출로 일시 중단 가능@Component
public class SchedulerLifecycleManager {
@Autowired
private Scheduler scheduler;
@PostConstruct
public void initializeScheduler() throws SchedulerException {
if (!scheduler.isStarted()) {
scheduler.start();
log.info("Scheduler 시작됨: {}", scheduler.getSchedulerName());
}
}
@PreDestroy
public void shutdownScheduler() throws SchedulerException {
if (scheduler.isStarted() && !scheduler.isShutdown()) {
scheduler.shutdown(true); // 실행 중인 작업 완료 후 종료
log.info("Scheduler 종료됨: {}", scheduler.getSchedulerName());
}
}
public void pauseScheduler() throws SchedulerException {
if (scheduler.isStarted() && !scheduler.isInStandbyMode()) {
scheduler.standby();
log.info("Scheduler 일시 중단됨");
}
}
public void resumeScheduler() throws SchedulerException {
if (scheduler.isInStandbyMode()) {
scheduler.start();
log.info("Scheduler 재시작됨");
}
}
}
Spring Boot에서는 @EnableScheduling과 함께 Quartz를 쉽게 통합할 수 있습니다:
@Configuration
@EnableScheduling
public class QuartzConfig {
@Bean
public SchedulerFactoryBean schedulerFactoryBean() {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
// 애플리케이션 컨텍스트 설정 (Spring Bean 주입 가능)
factory.setApplicationContextSchedulerContextKey("applicationContext");
// 시작 지연 설정
factory.setStartupDelay(10);
// 종료 시 실행 중인 작업 완료 대기
factory.setWaitForJobsToCompleteOnShutdown(true);
// 사용자 정의 속성
Properties quartzProperties = new Properties();
quartzProperties.put("org.quartz.scheduler.instanceName", "MyAppScheduler");
quartzProperties.put("org.quartz.scheduler.instanceId", "AUTO");
quartzProperties.put("org.quartz.threadPool.threadCount", "10");
quartzProperties.put("org.quartz.threadPool.threadPriority", "5");
quartzProperties.put("org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread", "true");
factory.setQuartzProperties(quartzProperties);
return factory;
}
@Bean
public JobDetail emailJobDetail() {
return JobBuilder.newJob(EmailNotificationJob.class)
.withIdentity("emailJob", "notification")
.withDescription("일일 이메일 알림")
.storeDurably(true)
.build();
}
@Bean
public Trigger emailJobTrigger() {
return TriggerBuilder.newTrigger()
.forJob(emailJobDetail())
.withIdentity("emailTrigger", "notification")
.withDescription("매일 오전 9시 실행")
.withSchedule(CronScheduleBuilder.cronSchedule("0 0 9 * * ?"))
.build();
}
}
@Component
@Slf4j
public class DatabaseBackupJob implements Job {
@Autowired
private BackupService backupService;
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
String backupType = dataMap.getString("backupType");
String targetPath = dataMap.getString("targetPath");
try {
log.info("데이터베이스 백업 시작 - Type: {}, Path: {}", backupType, targetPath);
BackupResult result = backupService.performBackup(backupType, targetPath);
if (result.isSuccess()) {
log.info("데이터베이스 백업 완료 - File: {}, Size: {} MB",
result.getFilePath(), result.getFileSizeMB());
} else {
throw new JobExecutionException("백업 실패: " + result.getErrorMessage());
}
} catch (Exception e) {
log.error("데이터베이스 백업 중 오류 발생", e);
// 재시도 설정
JobExecutionException jee = new JobExecutionException("백업 실패", e);
jee.setRefireImmediately(false); // 즉시 재시도 안함
throw jee;
}
}
}
@Configuration
public class BackupSchedulerConfig {
@Bean
public JobDetail backupJobDetail() {
return JobBuilder.newJob(DatabaseBackupJob.class)
.withIdentity("databaseBackup", "maintenance")
.withDescription("일일 데이터베이스 백업")
.usingJobData("backupType", "full")
.usingJobData("targetPath", "/backup/database")
.storeDurably(true)
.requestRecovery(true) // 시스템 재시작 시 복구
.build();
}
@Bean
public Trigger backupJobTrigger() {
return TriggerBuilder.newTrigger()
.forJob(backupJobDetail())
.withIdentity("backupTrigger", "maintenance")
.withDescription("매일 새벽 2시 백업 실행")
.withSchedule(CronScheduleBuilder.cronSchedule("0 0 2 * * ?")
.withMisfireHandlingInstructionFireAndProceed()) // 누락 시 즉시 실행
.build();
}
}
다음 편에서는 실제 운영 환경에서 Scheduler를 관리하는 시스템 구현, 모니터링, 에러 처리 등 더 깊이 있는 내용을 다뤄보겠습니다.