Quartz는 Job Scheduling라이브러리이다. Job은 주로 대용량의 데이터를 처리하는 배치 작업이나 프로세스를 의미하고, Scheduling은 특정한 시간이나 이벤트 발생 등의 조건을 만족할 시 Job을 자동으로 실행하는 것을 의미한다.
https://www.lgcns.com/blog/cns-tech/bigdata/13147/
단순히 몇 번 반복하는 작업이라면 for 반복문 같은 원초적인 기법으로 수행할 수도 있겠지만 작업 실패 시 재시작 처리, 애플리케이션 재시작 후에 작업 복귀, JTA 트랜잭션 및 클러스터링(Java EE 쪽 기능이라는듯) 등 좀 더 복잡한 기능이 필요해질 때 이를 단순히 구현할 수 있는 것보다 수준높은 기능을 제공한다.
https://velog.io/@park2348190/Spring-Boot-환경의-Quartz-Scheduler-활용
starter가 붙은 boot환경에서는 auto configuration이 된다. Scheduler 인터페이스, JobDetail, Calendar, Trigger 등 Quartz 스케줄러에 관련된 Bean 객체들을 자동으로 컨테이너에 등록해준다. 하지만, 우리는 Quartz의 기초를 볼 것이기 때문에 기본적인 객체를 사용해 처음부터 해보겠다.
Basic Logic
Job을 만들고, Scheduler에 Job과 Job의 실행조건인 Trigger을 달아주면, 조건에 따라서 Job이 실행된다. JobListener, TriggerListener를 달아서 Job또는 Trigger가 실행될때마다 특정 함수를 실행시킬 수도 있다.
Quartz import
quartz라이블러리를 넣어줬다.
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-quartz', version: '2.3.2.RELEASE'
실행했을 때 quartz가 뜨면 잘된 것이다.
Job
먼저 Job을 만들어보자. Job은 Job interface를 implements 받아서 만들어진다. boot에서는 QuartzJobBean 이라는 것을 imple받을 수도 있지만, 우리는 먼저 Job을 imple받아 새로운 클래스 DumbJob을 만들자.
public class DumbJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
System.out.pringln("job excute");
}
}
스케줄러에 등록하기 위해 메인클래스에 아래와 같은 코드를 추가하자. 설명은 아래에.
@SpringBootApplication
public class MainApplication{
public static void main(String[] args) throws SchedulerException {
SpringApplication.run(DjItsWsApplication.class, args);
SchedulerFactory schedFact = new org.quartz.impl.StdSchedulerFactory();
Scheduler sched = schedFact.getScheduler();
sched.start();
//job detail
JobDetail job = JobBuilder.newJob(DumbJob.class)
.withIdentity("DumbJob")
.build();
//trigger
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("DumbTrigger")
.startNow()
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(2)
.repeatForever())
.build();
sched.scheduleJob(job, trigger);
}
}
JobDetail
Job의 세부사항 (데이터, 식별자, 실행설정 등) 은 JobDetail interface를 구현해야한다. Job은 여러개의 JobDetail을 가지고 있고, JobDetail안에는 JobDataMap의 구조로 이루어진 데이터와, 여러 속성들이 들어있다.
JobDetail job = newJob(DumbJob.class)
.withIdentity("myJob1", "group1")
.usingJobData("jobSays", "Hello World!") //JobDataMap
.usingJobData("myFloatValue", 3.141f)
.build();
Trigger
trigger는 Job이 Schedule되는 메커니즘을 나타낸다. (언제 실행될지를 나타낸다.) 언제, 몇초간, 또는 몇번 반복할지 등을 결정한다.
Trigger trigger = newTrigger()
.withIdentity("myTrigger1", "group1")
.startNow()
.withSchedule(simpleSchedule()
.withIntervalInSeconds(2)
.repeatForever())
.build();
Scheduler
SchedulerFactory에서 Scheduler객체를 받아서 쓴다. scheduler객체에 job과 trigger를 달아주고 start함으로써 스케줄링을 시작한다.
SchedulerFactory schedFact = new org.quartz.impl.StdSchedulerFactory();
Scheduler sched = schedFact.getScheduler();
sched.start();
//...
sched.scheduleJob(job, trigger);
왜 SchedulerFactory에서 scheduler를 얻었는지 궁금하면 팩토리 메서드 패턴을 공부하자.
코드를 실행시켜보면 2초마다 Job class의 내용이 반복된다.
static import를 사용하면 코드를 조금 더 깔끔하게 사용할 수 있다.
import static org.quartz.JobBuilder.newJob;
import static org.quartz.SimpleScheduleBuilder.simpleSchedule;
import static org.quartz.TriggerBuilder.newTrigger;
@SpringBootApplication
public class MainApplication{
public static void main(String[] args) throws SchedulerException {
SpringApplication.run(DjItsWsApplication.class, args);
SchedulerFactory schedFact = new org.quartz.impl.StdSchedulerFactory();
Scheduler sched = schedFact.getScheduler();
sched.start();
JobDetail job = newJob(DumbJob.class)
.withIdentity("DumbJob")
.build();
Trigger trigger = newTrigger()
.withIdentity("DumbTrigger")
.startNow()
.withSchedule(simpleSchedule()
.withIntervalInSeconds(2)
.repeatForever())
.build();
sched.scheduleJob(job, trigger);
}
}
Job에 데이터 주기
job이 실행될때마다 excute를 실행하기 전, job을 새로 인스턴스화한다. excute가 끝나면 인스턴스를 버리고 garbage collection의 대상이된다. 따라서 2가지를 기억해야 한다.
- field를 유지할 수 없다.
- constructor의 argument가 없다. (default JobFactory를 사용할 경우)
그렇다면 job인스턴스의 속성(데이터)는 어떻게 줄까? 바로 JobDetail을 이용하여 줄 수 있다.
Main클래스의 JobDetail에서 데이터를 주자.
JobDetail job = newJob(DumbJob.class)
.withIdentity("myJob", "group1") //Job의 식별자인 key를 달아줌
.usingJobData("jobSays", "Hello World!") //JobDataMap형식으로 데이터를 넣음
.usingJobData("myFloatValue", 3.141f)
.build();
Job에는 식별자인 key를 지정해줄 수 있다.
.withIdentity(name, group) 인데, name은 JobDetail마다 unique해야 하며, group은 JobDetail의 group을 말한다.
DumbJob에서 데이터를 받아보자
public class DumbJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobKey key = context.getJobDetail().getKey();
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
String jobSays = dataMap.getString("jobSays");
float myFloatValue = dataMap.getFloat("myFloatValue");
System.err.println("Instance " + key + " of DumbJob says: " + jobSays + ", and val is: " + myFloatValue);
}
}
JobExecutionContext는 Job에 해당되는 JobDetail과 Trigger에 대한 정보를 얻을 수 있다. context에서 jobDetail을 정보를 얻어 찍어보면 아래처럼 2초마다 데이터가 직힌다.
이때 setter를 달아주면 명시적으로 get메소드를 불러올 필요가 없다.
Job이 인스턴스화되었을때 JobFactory 객체가 자동으로 setter를 부르기 때문이다.
- trigger ⇒ JobDetail이 로드 ⇒ 스케줄러에 설정된 Job class 를 JobFactory가 newInstance()로 인스턴스화 ⇒ setter를 call
public class DumbJob implements Job {
String jobSays;
float myFloatValue;
ArrayList state;
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobKey key = context.getJobDetail().getKey();
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
// String jobSays = dataMap.getString("jobSays");
// float myFloatValue = dataMap.getFloat("myFloatValue");
System.err.println("Instance " + key + " of DumbJob says: " + jobSays + ", and val is: " + myFloatValue);
}
//코드는 길어보이지만 IDE에 generate기능이 있으면 더 편함
public void setJobSays(String jobSays) {
this.jobSays = jobSays;
}
public void setMyFloatValue(float myFloatValue) {
this.myFloatValue = myFloatValue;
}
public void setState(ArrayList state) {
this.state = state;
}
}
Listener
Job이 실행될때, trigger가 실행될때 또는 멈출때 등 시점에따라 작업을 할 수 있다. JobListener, TriggerListener interface를 상속받아 Listener객체를 만들어서, 스케줄러에 달아준다.
MyJobListener 객체를 만들어보자
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobListener;
public class MyJobListener implements JobListener {
// JobListener의 이름
@Override
public String getName() {
return MyJobListener.class.getName();
}
/**
* Job이 수행되기 전 상태
* - TriggerListener.vetoJobExecution == false
*/
@Override
public void jobToBeExecuted(JobExecutionContext context) {
System.out.println(String.format("[%-18s][%s] 작업시작", "jobToBeExecuted", context.getJobDetail().getKey().toString()));
}
/**
* Job이 중단된 상태
* - TriggerListener.vetoJobExecution == true
*/
@Override
public void jobExecutionVetoed(JobExecutionContext context) {
System.out.println(String.format("[%-18s][%s] 작업중단", "jobExecutionVetoed", context.getJobDetail().getKey().toString()));
}
/**
* Job 수행이 완료된 상태
*/
@Override
public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
System.out.println(String.format("[%-18s][%s] 작업완료", "jobWasExecuted", context.getJobDetail().getKey().toString()));
}
}
Main클래스에서 JobDetail을 두개 만들고, DumbJob에 JobListener를 달아보자.
import static org.quartz.JobBuilder.newJob;
import static org.quartz.SimpleScheduleBuilder.simpleSchedule;
import static org.quartz.TriggerBuilder.newTrigger;
@SpringBootApplication
public class DjItsWsApplication {
public static void main(String[] args) throws SchedulerException {
SpringApplication.run(DjItsWsApplication.class, args);
SchedulerFactory schedFact = new org.quartz.impl.StdSchedulerFactory();
Scheduler sched = schedFact.getScheduler();
sched.start();
// 공식예제2
JobDetail job = newJob(DumbJob.class)
.withIdentity("myJob1") // name "myJob", group "group1"
.usingJobData("jobSays", "Hello World!")
.usingJobData("myFloatValue", 3.141f)
.build();
JobDetail job2 = newJob(DumbJob.class)
.withIdentity("myJob2") // name "myJob", group "group1"
.usingJobData("jobSays", "Hello World!2")
.usingJobData("myFloatValue", 0.0001f)
.build();
// // Trigger the job to run now, and then every 40 seconds
Trigger trigger = newTrigger()
.withIdentity("myTrigger1") //key를 안주면, 하나의 job에 여러 jobDetail의 데이터가 겹쳐서 null이 찍히기도 함
.startNow()
.withSchedule(simpleSchedule()
.withIntervalInSeconds(2)
.repeatForever())
.build();
Trigger trigger2 = newTrigger()
.withIdentity("myTrigger2")
.startNow()
.withSchedule(simpleSchedule()
.withIntervalInSeconds(2)
.repeatForever())
.build();
//리스너 달기
sched.getListenerManager().addJobListener(new MyJobListener());
sched.scheduleJob(job, trigger);
sched.scheduleJob(job2, trigger2);
}
}
JobDetail마다 하나의 스레드로 병렬실행되기 때문에 작업이 순서대로 진행되지 않고 한꺼번에 진행되는 것을 볼 수 있다. 순서대로 하고 싶다면, 어노테이션을 달아서 설정해줄 수 있다.
adsdJobListener는 인자로 JobListener와 Matcher를 받는다.
void org.quartz.ListenerManager
.addJobListener(JobListener jobListener, Matcher<JobKey> matcher)
Matcher는 interface이고 이를 구현한 KeyMatcher, GroupMatcher등이 있다.
Main클래스에 아래와 같이 Matcher를 추가해보자.
sched.getListenerManager().addJobListener(new MyJobListener(), KeyMatcher.keyEquals(job.getKey()));
sched.getListenerManager().addTriggerListener(new MyTriggerListener(), KeyMatcher.keyEquals(trigger.getKey()));
실행해보면 이전과 달리 job1에만 Listener가 달린 것을 볼 수 있다.
이전에는 spring-quartz를 이용하여 사용했지만, Spring 3.0부터는 Annotation을 더 간단한 spring scheduler사용하여 훨씬 쉽게 사용할 수 있다. 다음 포스트에서 알아보자
https://wouldyou.tistory.com/114
'java' 카테고리의 다른 글
[java] Spring JDBC에서 배치처리하기 (0) | 2023.01.17 |
---|---|
[java] FTP file upload시 한글 파일 안올라가는 문제 (0) | 2023.01.12 |
[java] POI라이브러리로 엑셀 파일에 이미지 넣기 (0) | 2022.12.20 |
[eclipse] 이클립스 lombok 적용 안될 때 (0) | 2022.12.06 |
[java] 티베로 실행 안되는 오류 해결 (0) | 2022.10.04 |