- 스프링부트 기반, Quartz Schedule 구축하기
스프링부트 환경에서, Quartz(쿼츠) 스케쥴러를 연동하는 방법을 정리한다. 참고로 Pivotal 개발자 및 해외 개발자들은 스프링부트 기반을 Bootiful 이라고 부른다. 그래서 제목도 Bootiful Quartz 라고 정해봤다.
by. Eddy.Kim
Quartz는 오픈소스 스케쥴러 라이브러리이다. 기존 스프링 환경에서도 연동이 잘 되었지만, 최근 스프링 부트 2.0 에서부터는 Starter 를 제공하기 때문에, 좀 더 간결하게 스프링 + Quartz 를 연동할 수 있다. 이번 글에서는 Quartz 의 기본 개념, 스프링부트 설정, 동적 스케쥴 생성 등 관련 내용을 정리한다.
Quartz 소개 및 기본 개념
스프링 부트 2.0 연동
Scheduler
Cron Trigger
공식 홈페이지에서는 아래와 같이 소개한다.
Quartz is a richly featured, open source job scheduling library that can be integrated within virtually any Java application - from the smallest stand-alone application to the largest e-commerce system. Quartz can be used to create simple or complex schedules for executing tens, hundreds, or even tens-of-thousands of jobs; jobs whose tasks are defined as standard Java components that may execute virtually anything you may program them to do. The Quartz Scheduler includes many enterprise-class features, such as support for JTA transactions
Quartz(쿼츠) 는 Java 애플리케이션에 통합할수 있는 오픈소스 스케쥴링 라이브러리인데, 간단하거나 또는 복잡한 스케쥴을 생성하는데 사용된다.
기본적으로 사용하는 용어를 기준으로, 아래와 같이 스프링 코드로 정리하였다.
Job은 "실행 해야 할 작업"을 의미한다. Job 인터페이스는 execute 메서드를 정의하는데, execute 메서드의 파라미터인 JobExecutionContext 에는 트리거 핸들링, 스케쥴에 의한 핸들링 등을 포함하여 런타임 환경에 대한 정보를 제공한다.
@Component
public class CoffeeJob implements Job{
public void execute(JobExecutionContext context) throws JobExecutionException {
//잡 로직 수행
}
Job을 실행하기 위한 상세 정보이다. JobBuilder 에 의해서 생성된다.
JobDetail(Job을 실행하기 위한 상세정보)를 생성한다. 빌더 패턴으로 구현하였다.
public JobDetail buildJobDetail() {
JobDataMap jobDataMap = new JobDataMap(getData());
jobDataMap.put("subject", subject);
return newJob(CoffeeJob.class)
.withIdentity(getName(), getGroup())
.usingJobData(jobDataMap)
.build();
}
Job을 실행하기 위한 조건(작업 실행 주기, 횟수 등)이다. 다수의 Trigger 는 동일한 Job을 공유하여 지정할 수 있지만, 하나의 Trigger는 반드시 하나의 Job을 지정해야 한다.
TriggerBuilder 는 빌더패턴으로 트리거 객체를 생성한다.
@Bean
public Trigger trigger(JobDetail job) {
return TriggerBuilder.newTrigger().forJob(job)
.withIdentity("Qrtz_Trigger")
.withDescription("Sample trigger") .withSchedule(simpleSchedule().repeatForever().withIntervalInHours(1))
.build();
}
SchedulerFactory에 의해 생성이 되는 서비스 핵심 객체이다. JobDetail 과 Trigger를 관리한다.
dependencies {
...
compile('org.springframework.boot:spring-boot-starter-quartz')
...
}
Database 설정을 따로 안하면, 기본으로 인메모리 기반으로 동작한다. 애플리케이션이 재시작하면 휘발성으로 데이터는 사라진다. DB 에 저장하기 위해서는 Property 설정을 해야한다. 그리고, Quartz 연동 데이터베이스에 Quartz 관련 테이블을 생성하면 된다.
//propery 설정
spring.quartz.job-store-type=jdbc
필자는 만만한, Mysql + Jpa 로 구성하였다.
//build.gradle 설정
compile("mysql:mysql-connector-java:5.1.34")
compile('org.springframework.boot:spring-boot-starter-data-jpa')
Quartz 기본 테이블을 생성하는 쿼리는 아래 링크를 참고하자.
일단, 필자가 아는 범위내에서는 Quartz 버전에 따라서 테이블의 스키마가 다를 수 있다. 그러므로, 반드시 Quartz 라이브러리에 맞는 테이블 생성을 해야 한다.
자동으로 테이블 스키마가 생성되는 방법이 있을 것으로 추측(?)이 되나 방법을 아직 찾지 못하였다.
위에 설명하였지만, 스프링 부트 2.0 에서부터 Quartz 스타터를 디펜던시 추가하여 간결하게 쿼츠를 연동할 수 있다. 라이브러리를 자세히 확인하자!! org.springframework.boot.autoconfigure.quartz 패키지에서 QuartzProperties 클래스를 보면, 상단에 @ConfigurationProperties("spring.quartz")가 선언 된 것을 확인할 수 있다. Properties 설정 값은 QuartzProperties 클래스에 주입이 될 것이다. 그런데QuartzProperties 클래스에서 제공하지 않는 다른 Properties 값은 어떻게 설정할 수 있을까? 예를 들어서 쿼츠 잡의 쓰레드풀 설정을 하고 싶다면? 예전 스프링 버전에서는 아래와 같이 컨피그 설정을 구성하였다.
//예전 방법이다.
@Configuration
public class QuartzConfig {
@Value("${org.quartz.scheduler.instanceName}")
private String instanceName;
@Value("${org.quartz.scheduler.instanceId}")
private String instanceId;
@Value("${org.quartz.threadPool.threadCount}")
private String threadCount;
@Value("${job.startDelay}")
private Long startDelay;
@Value("${job.repeatInterval}")
private Long repeatInterval;
생략...
근데, 스프링 부트 2.0 에서는 이렇게 할 필요가 없다. QuartzProperties 클래스에 properties 라는 필드가 있다. 커스텀하게 작성한 application.properties 파일에 쿼츠 설정 정보를 추가하면, 해당 설정 값이 QuartzProperties 클래스에 properties 주입되고, QuartzAutoConfiguration 클래스 에서 SchedulerFactoryBean 을 생성하는 과정에서 아래와 같이 해당 속성값을 설정해 준다.
//내가 짠 소스 아님. Spring 코드이다.
@Bean
@ConditionalOnMissingBean
public SchedulerFactoryBean quartzScheduler() {
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
schedulerFactoryBean.setJobFactory(new AutowireCapableBeanJobFactory(
this.applicationContext.getAutowireCapableBeanFactory()));
if (!this.properties.getProperties().isEmpty()) {
schedulerFactoryBean
.setQuartzProperties(asProperties(this.properties.getProperties()));
}
생략..
.setQuartzProperties(asProperties(this.properties.getProperties()));
자, 그러면 가장 중요한 Properties 설정은 어떻게 하는가? prefix가 spring.quartz 이므로 아래 소스와 같이 spring.quartz 뒤에 붙여서 속성 값을 설정한다!
spring.quartz.properties.org.quartz.threadPool.threadCount=20
이렇게 추가하면 기존에 10개의 쓰레드로 동작되면 쿼츠 스케쥴러가 20개의 쓰레드에서 동작하게 된다. 해당 설정 말고 다른 설정값들도 동일하게 설정이 가능할 것이다.
(필자의견)해당 방법은 급하게 생각해낸 방법인데, 제대로 구현한 것인지 확신은 없습니다. 혹시, 전문가가 이 글을 보고 있다면 피드백 부탁드립니다...
https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-features-quartz
Quartz 라이브러리의 Scheduler 인터페이스는, SchedulerFactory에 의해 생성이 되는 서비스 핵심 객체이다. Job과 Trigger를 관리한다. 아래와 같이 주입하여 사용하면 된다.
@RequiredArgsConstructor //LomBok 사용, 생성자 주입
public class Class{
private final Scheduler scheduler;
}
아래와 같이 필드 인젝션도 가능하지만 추천하고 싶지는 않다.
@Autowired
private Scheduler scheduler;
개인적인 생각이지만 필드 인젝션 보다는, 생성자 주입이나 Setter 주입이 좋겠다는 판단이다.
인터페이스에서 제공하는 메소드는 org.quartz 라이브러리에서 직접 확인 가능하다. 자주 사용하는 메소드만 간략하게 정리하였다.
JobDetail getJobDetail
Job 을 조회한다.
List<String> getJobGroupNames()
Job 의 그룹 리스트를 조회한다.
List<? extends Trigger> getTriggersOfJob
Job에 등록된 트리거 리스트를 조회한다.
void scheduleJob
Job을 생성한다. 만약 기존에 존재한다면 덮어쓸지에 대한 값을 파라미터(replace)로 받는다. 파라미터가 true 라면, 기존 Job이 업데이트 된다.
void pauseJob
Job을 중지한다. pauseJob 메서드에서 해당 Job 에 연결되어있는 모든 트리거를 중지한다. 각각의 트리거마다 TriggerState인 트리거 상태 값이 저장되어있다. 아래는 라이브러리의 소스이다.
void resumeJob
중지 되었던, Job을 재시작한다.
Quartz(쿼츠)에서 사용하는 트리거의 종류는 아래와 같다. 필자는 Cron Trigger 에 대해서 정리한다.
Crontrigger
SimpleTrigger
Cron 표현식은 7개의 표현식으로 구성된 문자열이다. 각 단위는 공백으로 구분된다.
" 0 0 8 ? * SUN * "와 같이 표현되는데, 해당 표현식은 매주 일요일 8시를 의미한다. 요일은 SUN, MON, TUE, WED, THU, FRI, 그리고, SAT 등으로 표현 가능하지만, 숫자로도 가능하다. SUN 1 이고 SAT 이 7 이다.
" 초 분 시 일 월 요일 연도 " 의 순서로 표현된다.
항상을 표현할 때는 와일드카드(*) 로 표현한다.
" 0 0 8 ? * SUN * " 에서 5번째의 와일드카드(*) 표현은 매월을 의미한다. 제일 마지막의 와일드카드(*)는 매해 를 의미한다.
특정 숫자를 입력하면 그 숫자에 맞는 값이 설정된다.
" 0 0 8 ? * SUN * " 에서 제일 앞에 0 은 0초를 의미한다. 만약, 0초와 30초에 Cron 이 실행되도록 설정할려면, " 0,30 0 8 ? * SUN * " 이렇게 콤마(,) 를 통해서 표현할 수 있다.
값의 범위를 나타낼 때는 하이픈 (-) 으로 표현할 수 있다.
월요일 부터 수요일은 "MON-WED" 로 표현하면 된다. 8시부터 11시는 "8-11"의 형태로 표현한다.
물음표(?) 문자는 설정값 없음 을 의미한다. 일, 요일 필드에서만 허용이 된다.
" 0 0 8 ? * SUN * " 에서는 매주 일요일이라는 요일 필드 값을 설정하였다. 매주 일요일이라는 가정을 정하였기 때문에, 몇일 이라는 표현은 필요 없다. 그러므로 ? 로 표현해야 한다.
매월 1일로 설정하고 싶다면 " 0 0 8 1 * ? * " 로 표현가능하며 방금 위에서 SUN 이라고 표현되었던 필드는 물음표(?) 로 표시해야 한다.
슬래쉬(/) 문자는 값의 증가 표현을 의미한다.
분 필드에 0/5 를 사용한다면 0분 부터 시작하여 매5분마다 를 의미한다. 이것은 콤마(,)로 표현하면 0,5,10,15,20,25,30,35,40,45,50,55 와 같다.
L 문자는 일, 요일 필드에서만 허용이 된다.
일 필드에서는 매달 마지막 날을 의미하고, 요일 필드에서는 "7" 또는 "SAT" 를 의미한다. 하지만, L 이 특정 값의 뒤에 올경우에는 이달의 마지막 무슨 요일이 된다. 예를 들어서 7L 이면 이달의 마지막 토요일 을 표현한다.
http://www.quartz-scheduler.org/documentation/quartz-2.2.x/tutorials/tutorial-lesson-06.html
일반적으로 배치 잡은 코드로 이미 선언되어 구현하는 경우가 많다. 하지만, 특수한 경우에는 스케쥴 잡을 애플리케이션 배포 없이 동적으로 생성하고, 삭제하는 기능이 필요할 수도 있다. 필자가 위에 설명하였지만, scheduler 인터페이스를 사용하면 스케쥴 잡을 동적으로 생성할 수있다.
@RequiredArgsConstructor
public class CoffeeService {
private final Scheduler scheduler;
생략...
public JobDetail buildJobDetail() {
JobDataMap jobDataMap = new JobDataMap(getData());
jobDataMap.put("subject", subject);
jobDataMap.put("messageBody", messageBody);
jobDataMap.put("sample", sample);
return newJob(CoffeeJob.class)
.withIdentity(getName(), getGroup())
.usingJobData(jobDataMap)
.build();
newJob 메서드에서 빌더패턴으로 JobDetail 을 생성하여 리턴해줍니다.
public Trigger buildTrigger() {
생략...
return newTrigger()
.withIdentity(buildName(), group)
.withSchedule(cronSchedule(cron)
.withMisfireHandlingInstructionFireAndProceed()
.inTimeZone(TimeZone.getTimeZone(systemDefault())))
.usingJobData("cron", cron)
.build();
생략...
}
newTrigger 메서드에서 빌더패턴으로 Trigger을 생성하여 리턴해줍니다.
JobDetail jobDetail = testClass.buildJobDetail();
Set<Trigger> triggersForJob = testClass.buildTriggers();
try {
scheduler.scheduleJob(jobDetail, triggersForJob, false);
scheduleJob메서드를 실행하면서, JobDetail, Trigger 정보를 넘겨줍니다. 세번쨰 파라미터인 boolean 은 true 이면 기존 잡이 있으면 업데이트를 한다는 조건입니다. 즉, 신규 생성 도 가능하지만, 기존 잡 업데이트도 가능합니다.
몇 줄의 코드만으로 스프링 부트 환경에서 Quartz 스케쥴을 연동하였다. 스프링부트에서의 Starter지원으로 사용하기 간편하고, 안정적인 스케쥴링 서비스를 구축할 수 있다는 의견이다. 하지만, 개인적인 생각으로는 간단한 스케쥴은 @Schedule 어노테이션으로 구현해도 될것 같다. 굳이, 단순 스케쥴링을 위해서 Quartz를 연동할 필요는 없을 것 같다는 생각이다. 복잡한 스케쥴링 구현이 필요하다면 Quartz 를 연동하면 좋을 것이다. 그럼 이만 글을 마친다.
혹시, 부족한 이 글에 잘못된 내용이 있다면 피드백 부탁드립니다.