참고사항
- 이번 프로젝트에서 Quartz 를 사용하게 되어서 테스트 환경을 구축하고
테스트 코드를 작성해봤습니다. 이 글은 해당 코드에 대한 정보 공유가 목적입니다.
- 아주 간단한 테스트를 위한 것이므로 Junit 은 사용하지 않았습니다.
- 작성된 코드들은 제 github에서도 확인 가능합니다.
Maven
프로젝트 생성하고, 아래와 같이 의존성을 추가했습니다.
<dependencies>
<!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz-jobs -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>2.3.2</version>
</dependency>
<!--
quartz 의 transitive dependency 목록 중에서
slf4j-api 1.7.7 가 있는데, 더 최신 버전을 쓰고 싶어서
2 버전대로 올립니다.
-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
<!-- slf4j 의 구현체를 하나 dependecy 로 넣습니다. logback 사용했습니다. -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.8</version>
</dependency>
</dependencies>
2.3.2
로 맞췄습니다.참고 - 1. logback 설정
목차를 참고하시기 바랍니다.package coding.toast;
import coding.toast.job.SampleJob;
import org.quartz.*;
import org.quartz.core.jmx.JobDataMapSupport;
import org.quartz.impl.StdSchedulerFactory;
import java.util.Map;
import java.util.Properties;
/**
* 테스트 코드를 실행해볼 Main 클래스
*/
public class Main {
public static void main(String[] args) {
Scheduler scheduler = null;
try {
// Scheduler 의 설정값으로 사용할 Properties 를 생성합니다.
// http://www.quartz-scheduler.org/documentation/quartz-2.3.0/configuration/ 참고
// 참고: 사실 이미 디폴트 세팅값이 있어서 굳이 이렇게 세팅하지 않아도 됩니다.
// quartz-2.3.2.jar!/org/quartz/quartz.properties 경로에 기본 설정값들이 이미 있습니다.
Properties properties = new Properties();
properties.setProperty("org.quartz.scheduler.instanceName", "CodingToast_Scheduler");
properties.setProperty("org.quartz.threadPool.threadCount", "15");
properties.setProperty("org.quartz.threadPool.threadPriority", "4");
properties.setProperty("org.quartz.jobStore.class", "org.quartz.simpl.RAMJobStore");
// 스케줄러를 생성합니다. 참고로 생성했다고 스케줄러가 실제 동작 상태에 들어간 게 아닙니다.
scheduler = new StdSchedulerFactory(properties).getScheduler();
// 스케줄러가 수행해야 할 일(= JobDetail instance)에서 사용할 DataMap 을 생성합니다.
JobDataMap jobDataMap = JobDataMapSupport
.newJobDataMap(Map.of(
"param1", "value1",
"param2", "value2"
));
// JobBuilder 를 통해서 스케줄러에 줄 파리미터인 JobDetail 을 생성합니다.
JobDetail jobDetail = JobBuilder.newJob()
.ofType(SampleJob.class) // 반드시 세팅해줘야 한다!
.withIdentity("jobDetail1", "jobGroup") // 스케줄러가 JobDetail 을 다른 JobDetail 들과 구별하기 위한 일종의 아이디를 제공합니다.
.setJobData(jobDataMap) // 앞서 생성한 DataMap 을 넘겨줍니다.
.usingJobData("param3", "value3") // DataMap 에 추가적인 값들을 넣어줍니다.
.withDescription("a simple quartz job") // 이 일(JobDetail)에 대한 설명을 작성해줍니다.
.build();
// 상세하게 "어떤 것"(= JobDetail) 을 할지를 지정했습니다만,
// 정작 중요한 "언제할 지" 가 결정되지 않은 상태입니다.
// 이를 위해서 Trigger 인스턴스를 생성합니다.
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "triggerGroup")
// .usingJobData(jobDataMap) // JobDetail 과 마찬가지로 DataMap 을 사용할 수 있습니다.
// 만약에 같은 시간에 수행해야될 Trigger 가 존재한다면,
// 그때 우선순위를 어떻게 할지 지정합니다.
// .withPriority()
// cron 을 통한 주기를 줍니다.
// https://www.freeformatter.com/cron-expression-generator-quartz.html 참고
.withSchedule(CronScheduleBuilder.cronSchedule("0/2 * * * * ? *"))
// cron 이 너무 어렵다면 아래처럼 설정할 수도 있습니다.
// .withSchedule(SimpleScheduleBuilder.simpleSchedule()
// .withIntervalInSeconds(1).repeatForever())
.build(); // 빌드하여 Trigger 인스턴스 생성
// "언제(=trigger)", "무엇(=jobDetail)"을 할지를 스케줄러에게 알려줍니다.
scheduler.scheduleJob(jobDetail, trigger);
// start 를 호출해야 스케줄러가 standby mode 에 들어가서
// 주입된, 그리고 주입될 Job 들을 실행할 수 있는 상태가 됩니다.
scheduler.start();
// 10초 정도만 스케줄러를 실행시키겠습니다.
Thread.sleep(10000);
} catch (SchedulerException | InterruptedException e) {
// 테스트니까 에러는 크게 신경쓰지 않겠습니다.
e.printStackTrace();
} finally {
// scheduler 는 shutdown 해야만 프로그램이 종료됩니다.
// 참고로 scheduler 는 데몬 쓰레드가 아닌 user 쓰레드이기 때문에 shutdown 을 안 하면 프로그램이 종료되지 않습니다.
if (scheduler != null) try {
scheduler.shutdown();
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
}
참고
사실 Quartz 는 이미 Scheduler 디폴트 세팅값이 있어서 굳이 Properties 인스턴스를
생성해서 생성자에 주입할 필요는 없습니다. 해당 기본 세팅값들은
quartz-2.3.2.jar!/org/quartz/quartz.properties
경로에 있습니다.
그리고 이 기본 설정값을 사용하는 default scheduler 를 사용하고 싶다면
아래 팩토리 메소드를 호출하세요.Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
구체적으로 Quartz 에게 어떤 일을 시킬지를 결정하려면
저희가 직접 org.quartz.Job
를 구현한 클래스를 생성해야 합니다.
아래 예시는 아주 심플한 구현입니다.
package coding.toast.job;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SampleJob implements Job {
private static final Logger log
= LoggerFactory.getLogger(SampleJob.class);
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// Trigger 와 JobDetail 양쪽에서 설정한 DataMap 을 합쳐서 보여준다.
// 참고로 가져와서 setting 하는 건 안된다.
context.getMergedJobDataMap()
.forEach((key, val) -> log.info("key: {} / value: {}", key, val));
// 세팅했던 Quartz 핵심 도메인을 조회한다.
//System.out.println(context.getJobDetail());
//System.out.println(context.getTrigger());
}
}
Listener
를 사용하면 Job
, Trigger
가 수행하기 전/후에 대하여
추가적인 동작(ex: 모니터링, 로깅 등)을 지정할 수 있습니다.
등록 방법에 대해서는 코드를 통해서 확인해보시기 바랍니다.
package coding.toast;
import coding.toast.job.SampleJob;
import coding.toast.listener.MyJobListener;
import coding.toast.listener.MyTriggerListener;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import java.util.Map;
import java.util.Properties;
import static org.quartz.CronScheduleBuilder.cronSchedule;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;
import static org.quartz.core.jmx.JobDataMapSupport.newJobDataMap;
public class TestJobListenerMain {
public static void main(String[] args) {
Scheduler scheduler = null;
try {
// Scheduler 의 설정값으로 사용할 Properties 를 생성합니다.
// http://www.quartz-scheduler.org/documentation/quartz-2.3.0/configuration/ 참고
Properties properties = new Properties();
properties.setProperty("org.quartz.scheduler.instanceName", "CodingToast_Scheduler");
properties.setProperty("org.quartz.threadPool.threadCount", "15");
properties.setProperty("org.quartz.threadPool.threadPriority", "4");
properties.setProperty("org.quartz.jobStore.class", "org.quartz.simpl.RAMJobStore");
// 스케줄러를 생성합니다. 참고로 생성했다고 스케줄러가 실제 동작 상태에 들어간 게 아닙니다.
scheduler = new StdSchedulerFactory(properties).getScheduler();
// 스케줄러가 수행해야 할 일(= JobDetail instance)에서 사용할 DataMap 을 생성합니다.
JobDataMap jobDataMap = newJobDataMap(
Map.of("param1", "value1", "param2", "value2")
);
// JobBuilder 를 통해서 스케줄러에 줄 파리미터인 JobDetail 을 생성합니다.
JobDetail jobDetail = newJob()
.ofType(SampleJob.class)
.withIdentity("jobDetail-1", "jobGroup")
.setJobData(jobDataMap)
.build();
// Trigger 생성
Trigger trigger = newTrigger()
.withIdentity("trigger-1", "triggerGroup")
.withSchedule(cronSchedule("0/2 * * * * ? *"))
.build();
// 스케줄러에 특정 시점에 수행할 작업을 등록
scheduler.scheduleJob(jobDetail, trigger);
// ** 리스너 등록합니다!
ListenerManager listenerManager = scheduler.getListenerManager();
listenerManager.addJobListener(new MyJobListener());
listenerManager.addTriggerListener(new MyTriggerListener());
scheduler.start();
Thread.sleep(10000);
} catch (SchedulerException | InterruptedException e) {
e.printStackTrace();
} finally {
if (scheduler != null) try {
scheduler.shutdown();
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
}
package coding.toast.listener;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MyJobListener implements JobListener {
public static final Logger log = LoggerFactory.getLogger(MyJobListener.class);
@Override
public String getName() {
return this.getClass().getSimpleName();
}
@Override
public void jobToBeExecuted(JobExecutionContext context) {
log.info("============= [JOB Listener] - jobToBeExecuted =============");
}
@Override
public void jobExecutionVetoed(JobExecutionContext context) {
log.info("============= [JOB Listener] - jobExecutionVetoed =============");
}
@Override
public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
log.info("============= [JOB Listener] - jobWasExecuted =============");
}
}
trigger 실행을 모니터링할 Listener 를 생성합니다.
package coding.toast.listener;
import org.quartz.JobExecutionContext;
import org.quartz.Trigger;
import org.quartz.TriggerListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MyTriggerListener implements TriggerListener {
public static final Logger log = LoggerFactory.getLogger(MyTriggerListener.class);
@Override
public String getName() {
return this.getClass().getSimpleName();
}
@Override
public void triggerFired(Trigger trigger, JobExecutionContext context) {
log.info("==================== [TRIGGER Listener] - triggerFired ====================");
}
@Override
public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {
log.info("==================== [TRIGGER Listener] - vetoJobExecution ====================");
return false;
}
@Override
public void triggerMisfired(Trigger trigger) {
log.info("==================== [TRIGGER Listener] - triggerMisfired ====================");
}
@Override
public void triggerComplete(Trigger trigger, JobExecutionContext context, Trigger.CompletedExecutionInstruction triggerInstructionCode) {
log.info("==================== [TRIGGER Listener] - triggerComplete ====================");
}
}
이 메소드의 return 값을 통해서 Job 의 실제 여부를 결정합니다.
true(=Job 실행금지)
, false(=Job 실행승인)
입니다.true(=Job 실행철회)
이면 JobListener.jobExecutionVetoed
메소드만 실행false(=Job 실행승인)
이면 jobToBeExecuted
, jobWasExecuted
메소드만 실행잡 실행(JobExecution) 을 금지(veto)한다
입니다.Quartz 는 하나의 Job 에 여러 Trigger 를 등록할 수 있습니다.
하지만 반대로 하나의 Trigger 에 여러 Job 은 매칭시킬 수 없습니다 (불편🤔).
package coding.toast;
import coding.toast.job.SampleJob;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import static org.quartz.CronScheduleBuilder.cronSchedule;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;
import static org.quartz.core.jmx.JobDataMapSupport.newJobDataMap;
public class TestOneJobBindWithMultipleTriggerMain {
public static void main(String[] args) {
Scheduler scheduler = null;
try {
// http://www.quartz-scheduler.org/documentation/quartz-2.3.0/configuration/ 참고
Properties properties = new Properties();
properties.setProperty("org.quartz.scheduler.instanceName", "CodingToast_Scheduler");
properties.setProperty("org.quartz.threadPool.threadCount", "4"); // thread pool size
properties.setProperty("org.quartz.threadPool.threadPriority", "4");
properties.setProperty("org.quartz.jobStore.class", "org.quartz.simpl.RAMJobStore");
// 스케줄러를 생성합니다. 참고로 생성했다고 스케줄러가 실제 동작 상태에 들어간 게 아닙니다.
scheduler = new StdSchedulerFactory(properties).getScheduler();
// 스케줄러가 수행해야 할 일(= JobDetail instance)에서 사용할 DataMap 을 생성합니다.
JobDataMap jobDataMap = newJobDataMap(Map.of("param1", "value1"));
//하나의 Job 에 여러 Trigger 걸기
JobDetail jobDetail = newJob()
.ofType(SampleJob.class)
.withIdentity("jobDetail-1")
.setJobData(jobDataMap)
.build();
// 1 초에 한번
Trigger trigger = newTrigger()
.withIdentity("trigger-1-sec", "triggerGroup")
.withSchedule(cronSchedule("0/1 * * * * ? *"))
.build();
// 3 초에 한번
Trigger trigger2 = newTrigger()
.withIdentity("trigger-3-sec", "triggerGroup")
.withSchedule(cronSchedule("0/3 * * * * ? *"))
.build();
// 하나의 Job 에 Trigger 를 여러개 넣을 수 있는 방법들
// scheduler.scheduleJob(jobDetail, Set.of(trigger, trigger2), false); // 방법 1
scheduler.scheduleJobs(Map.of(jobDetail, Set.of(trigger, trigger2)), false); // 방법 2
// 스케줄러 시작
scheduler.start();
Thread.sleep(9000);
} catch (SchedulerException | InterruptedException e) {
e.printStackTrace();
} finally {
if (scheduler != null) try {scheduler.shutdown();} catch (SchedulerException e) {e.printStackTrace();}
}
}
}
이때는 SimpleTrigger 를 생성해서 scheduler.scheduleJob 의 파라미터로
넘겨주면 끝입니다.
package coding.toast;
import coding.toast.job.SampleJob;
import org.quartz.*;
import org.quartz.core.jmx.JobDataMapSupport;
import org.quartz.impl.StdSchedulerFactory;
import java.util.Map;
import java.util.Properties;
public class TestExecuteOnlyOnceMain {
public static void main(String[] args) {
Scheduler scheduler = null;
try {
Properties properties = new Properties();
properties.setProperty("org.quartz.scheduler.instanceName", "coding-toast");
scheduler = new StdSchedulerFactory(properties).getScheduler();
JobDetail jobDetail = JobBuilder.newJob()
.ofType(SampleJob.class)
.withIdentity("attempt-only-once", "jobGroup")
.build();
// SimpleScheduleBuilder.simpleSchedule() 를 사용하면 SimpleTrigger
// 인스턴스가 생성됩니다. SimpleTrigger 를 사용하면
// 스케줄러에서 딱 한번만 실행하고 끝낼 수 있습니다.
SimpleTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "triggerGroup")
.withSchedule(SimpleScheduleBuilder.simpleSchedule())
.build();
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
Thread.sleep(10000); // 10 초 동안에 딱 한번만 실행됩니다.
} catch (SchedulerException | InterruptedException e) {
e.printStackTrace();
} finally {
if (scheduler != null) try {
scheduler.shutdown();
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
}
위와 같이 resources 디렉토리 하단에 logback.xml
이라는 파일을 생성하고,
아래와 같이 내용을 작성해주면 됩니다.
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %magenta(%-4relative) --- [ %thread{10} ] %cyan(%logger{20}) : %msg%n
</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>