1. Quartz API
- Job and JobDetail : 이 API는 execute라는 하나의 메서드만 가진 Job 인터페이스를 제공한다. 실제 비즈니스 로직을 담아 이 인터페이스를 구현해야 한다. Trigger가 작동하면 스케줄러는 이 execute 메서드를 불러와 이를 jobExecutionContext에 전달한다.
- JobExcecutionContext는 런타임 환경에서의 job 인스턴스를 제공한다. 또한 스케줄러 핸들링, 트리거 핸들링 그리고 Job의 정보가 담겨있는 JobDetail 객체를 포함한다.
@Component
public class SampleJob implements Job {
@Autowired
private SampleJobService jobService;
public void execute(JobExecutionContext context) throws JobExecutionException {
jobService.executeSampleJob();
}
}
- 쿼츠는 job 클래스의 인스턴스를 저장하지 않는다. 대신 Job의 인스턴스를 JobDetail 클래스를 이용해 정의할 수 있다. Job의 클래스는 JobDetail에 의해서만 제공되어야 한다. 그렇게 함으로써 쿼츠가 어떤 job 타입(Job을 구현한 각각의 Task)이 실행되는 지 알 수 있도록 한다.
- Quartz JobBuilder : 빌더 스타일로 JobDetail 엔티티들을 구성할 수 있다.
@Bean
public JobDetail jobDetail() {
return JobBuilder.newJob().ofType(SampleJob.class)
.storeDurably()
.withIdentity("Qrtz_Job_Detail")
.withDescription("Invoke Sample Job service...")
.build();
}
- Spring JobDetailFactoryBean : Bean 스타일로 jobDetail 인스턴스 구성 가능하다. 기본적으로 Spring Bean 이름으로 Job 이름을 쓴다.
- 모든 Job의 실행들은 JobDetail 인스턴스를 새로 생성한다. JobDetail은 job의 프로퍼티들을 운반한다. 실행이 종료되면 참조들은 drop 된다.
- Trigger : job을 스케줄링하는 메커니즘. job의 실행을 "fires"한다. Job이 task개념이고, Trigger는 스케줄링 메커니즘이다. 트리거 또한 타입을 필요로 한다. 요구사항에 따라 개발자가 선택할 수 있다. 예를 들어 Quartz의 TriggerBuilder를 사용할 수도 있고 Spring의 SimpleTriggerFactoryBean을 사용하여 구성할 수도 있다.(인메모리나 DB를 통해서도 가능하다.)
@Bean
public Trigger trigger(JobDetail job) {
return TriggerBuilder.newTrigger().forJob(job)
.withIdentity("Qrtz_Trigger")
.withDescription("Sample trigger")
.withSchedule(simpleSchedule().repeatForever().withIntervalInHours(1))
.build();
}
@Bean
public SimpleTriggerFactoryBean trigger(JobDetail job) {
SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean();
trigger.setJobDetail(job);
trigger.setRepeatInterval(3600000);
trigger.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY);
return trigger;
}
- In-Memory Jobstore 방식 : RAMJobStore라는 것을 사용할 수 있다. 셧다운되면 당연히 정보가 다 날라가므로 JDBCJobStore를 사용한다.
org.quartz.jobStore.class=org.quartz.simpl.RAMJobStore // 해당 Class
spring.quartz.job-store-type=memory // 프로퍼티 설정
- JDBC JobStore : JobStoreTX, JobStoreCMT 두 가지가 있다. JobStoreTX는 자기 자신의 트랜잭션을 시작하고 관리하는 반면 JobStoreCMT는 어플리케이션의 트랜잭션을 필요로 한다.
- JDBCJobStore의 타입을 정하고, data source와 database driver class를 정해줘야 한다.
- 드라이버 클래스의 경우 StdJDBCDelegate가 대부분의 DB를 커버할 수 있다.
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate org.quartz.jobStore.dataSource=quartzDataSource
spring.quartz.job-store-type=jdbc
-> 스프링 프로퍼티 설정
- Scheduler
- 스케줄러 인터페이스는 job scheduler와 인터페이스하는 메인 API이다.
- Quartz StdSchedulerFactory
@Bean
public Scheduler scheduler(Trigger trigger, JobDetail job, SchedulerFactoryBean factory)
throws SchedulerException {
Scheduler scheduler = factory.getScheduler();
scheduler.scheduleJob(job, trigger);
scheduler.start();
return scheduler;
}
-> ScehdulerFactoryBean의 getScheduler를 통해 스케줄러 인스턴스를 가져올 수 있다. 스케줄러를 인스턴스화한다는 것은 JobStore와 ThreadPool을 초기화하고 이들을 핸들링하는 API를 리턴한다는 것이다.
- Spring SchedulerFactoryBean : 스프링이 제공하는 기능으로 bean 스타일로 스케줄러를 구성할 수 있다. 어플리케이션 컨텍스트 내에서의 라이프 사이클을 관리하고 의존성 주입을 위해 스케줄러를 빈으로서 노출시킨다.
결론
- 미리 정한 스케줄에 대해서 혹은 사용자가 등록한 스케줄에 대해서 DB에서 가져오든 In-Memory 방식이든 자유롭게 스케줄을 등록할 수 있음
- 스케줄을 실행시키는 Trigger, Trigger가 실행시키는 Job이 핵심요소이며 스프링의 Bean 스타일로 정의할 수 있고 마음대로 구성할 수 있다. 결국 Scheduler -> Trigger , Job -> Trigger 가 Job 실행(execute 메서드).
2. 구현
@Component
public class BatchConfig implements ApplicationContextAware { // ApplicationContextAware인터페이스의 구현 메서드는 setApplicationContext 하나. ApplicationContext를 받아오는 역할만을 수행하는 것
private boolean init= false;
private String serverName;
// ApplicationContext에서 서버 이름을 가져올 경우 사용. 이를 통해 개발, 품질, 운영 등을 구분해 배치 실행 여부를 판단할 수도 있음.
@Autowired
private BatchController controller;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this. serverName = applicationContext.getEnvironment().getProperty("SERVER_NAME");
}
@PostConstruct
public void initialize() {
if(serverName.equalsIgnoreCase("DEV"))
{
init = false;
return;
}
controller.init();
controller.makeJob();
controller.run();
init = true;
}
// 스케줄러를 서버가 돌아가는 상태에서 시작/중지/재시작 할 수 있도록 하는 코드
public static void action(String command) {
if (command == null) {
return;
}
synchronized (BatchConfig.class) { // BatchConfig의 Bean객체를 다른 쓰레드가 점유하지 못하게 함
switch (command) {
case "START":
if (!init) {
controller.init();
controller.makeJob();
controller.run();
init = true;
}
break;
case "STOP":
controller.stop();
init = false;
break;
case "RESTART":
if (init) {
controller.stop();
controller.init();
controller.makeJob();
controller.run();
init = true;
}
break;
default:
// Handle unsupported command
break;
}
}
}
}
@Component
public class BatchController {
JobDataMap dsJob; // 등록할 job list
Scheduler scheduler = null;
boolean running = false;
public void init()
{
try
{
/*
DB에서 스케줄 정보 가져오는 소스코드
1. 위에서 본 것처럼 JobStore를 활용해도 되고, 프로퍼티 파일을 사용해도 되고 DB에 직접 다녀와도 됨
*/
dsJob = service.getJobDataMap();
}
catch(Exception e)
{
e.printStackTrace();
}
}
// job을 생성
public void makeJob()
{
SchedulerFactory sf = new StdSchedulerFactory(); // 스케줄러 생성을 위한 StdSchedulerFactory 객체 생성
try {
scheduler = sf.getScheduler(); // 스케줄러 생성
} catch (SchedulerException e) {
e.printStackTrace();
}
for(int index = 0 ; index < dsJob.getCount() ; index++) // DB에서 가져온 데이터의 개수 만큼 Trigger, Job 생성
{
JobDetail jobDetail = JobBuilder.newJob(BatchJOB.class).withIdentity("job"+index, "group"+index).build();
// 크론표현식을 이용한 트리거, SCHEDULE은 크론식이 담겨있음
CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger"+index, "group"+index).withSchedule(CronScheduleBuilder.cronSchedule(dsJob.get(index, "SCHEDULE"))).build();
// Job을 구성하기 위해 JobDetail에 프로퍼티 추가
jobDetail.getJobDataMap().put("DOMAIN", domain);
jobDetail.getJobDataMap().put("ID", dsJob.getString(index, "ID"));
jobDetail.getJobDataMap().put("PARAM", dsJob.getString(index, "PARAM"));
try {
Date scheduledDate = scheduler.scheduleJob(jobDetail, trigger); // 스케줄러에 trigger, job 추가, 스케줄러에 등록된 시간이 리턴됨
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
// 스케줄러 실행
public void run()
{
try {
sched.start();
} catch (SchedulerException e) {
e.printStackTrace();
}
}
public void stop()
{
try {
sched.shutdown();
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
public class BatchJOB implements Job {
// execute를 구현하면 Trigger가 이 메서드를 실행한다.
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDataMap map = context.getJobDetail().getJobDataMap();
String ID = map.getString("ID");
String DOMAIN = map.getString("DOMAIN");
String PARAM = map.getString("PARAM");
try
{
/*
* 비즈니스 로직 실행(ID에 requestURL등이 담겨 있다면 해당 URL로 요청을 보낸다든지...)
* 아래 소스코드는 테이블에 실행할 서비스 명이 들어 있어 context로 bean을 가져와서 실행하는 방식으로 대충 짠 코드
*/
ApplicationContext applicationContext = (ApplicationContext) context.getScheduler().getContext().get("applicationContext");
// JobExecutionContext로 ApplicationContext를 가져올 수 있다.
Object serviceBean = applicationContext.getBean(ID); // 서비스로 등록된 빈 가져옴
// Bean이 Service 인터페이스를 구현하는 경우 실행
if (serviceBean instanceof BatchService) {
((BatchService) serviceBean).execute();
} else {
// exception 처리
}
} catch (Exception e) {
e.printStackTrace();
}
}