앞서 Quartz에 대해서 개념을 알아보았다
해당 포스팅은 Spring Boot + Quartz를 활용하여 스케줄링을 구현해보겠습니다.

1. Schduler
Job과 Trigger를 연결하여 Job을 실행시키는 역할을 수행하는 인터페이스입니다.
Spring + Quartz 사용으로 ApplicationContext에 의해 관리되는 SchedulerFactoryBean을 생성해주었다.
SchedulerFactoryBean에 JobFactory를 등록하여 Job도 컨텍스트로 관리될수 있게 설정된다.
package com.kr.quartz.config;
import com.kr.quartz.listener.MyJobListener;
import com.kr.quartz.listener.TriggerListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.scheduling.quartz.SpringBeanJobFactory;
@Slf4j
@Configuration
public class SchedulerConfig {
@Bean
public SchedulerFactoryBean schedulerFactoryBean(ApplicationContext applicationContext) {
var schedulerFactoryBean = new SchedulerFactoryBean();
var jobFactory = new SpringBeanJobFactory();
schedulerFactoryBean.setJobFactory(jobFactory); // job factory 등록
schedulerFactoryBean.setApplicationContext(applicationContext); // context 등록
schedulerFactoryBean.setOverwriteExistingJobs(false); // job 중복 허용 (덮어쓰기)
schedulerFactoryBean.setWaitForJobsToCompleteOnShutdown(true); // shutdown wait 속성
schedulerFactoryBean.setGlobalTriggerListeners(new TriggerListener());
schedulerFactoryBean.setGlobalJobListeners(new MyJobListener()); // job 리스너 등록
return schedulerFactoryBean;
}
}
2. Job
Quartz에서 실행할 작업을 정의하는 인터페이스입니다.
개발자는 자신이 실행하고자 하는 작업을 Job을 통하여 정의하며 정의된 Job은 Quartz의 설정 주기에 따라 실행이 됩니다.
package com.kr.quartz.jobs;
import lombok.extern.slf4j.Slf4j;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.springframework.stereotype.Component;
/**
* Quartz 자체의 기본 인터페이스
* Quartz 스케줄러에서 직접 인스턴스화하여 사용
* 상태 저장 불가능 (매번 새로운 인스턴스 생성)
*
*/
@Slf4j
@Component
public class MyJob implements Job { //
@Override
public void execute(JobExecutionContext jobExecutionContext) {
log.info("MyJob executing...");
log.info("Job instance hash code: {}", this.hashCode());
}
}
일반적인 Job 인터페이스를 생성하였습니다 이는 트리거에 맞춰 execute 가 실행이 됩니다.
package com.kr.quartz.jobs;
import com.kr.quartz.service.MyService;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Component;
/**
* QuartzJobBean은 스프링에서 제공하는 Quartz 통합 지원 클래스
* Spring Bean으로 관리되며, 의존성 주입 가능, 상태 저장 가능 (Spring의 싱글톤 Bean으로 관리)
*/
@Slf4j
@Component
@RequiredArgsConstructor
@DisallowConcurrentExecution // 동시 실행 방지
@NoArgsConstructor(access = AccessLevel.NONE)
public class MyQuartzJob extends QuartzJobBean {
private final MyService myService;
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
log.info("MyQuartzJob executing...");
myService.myMethod(); // 의존성 주입 확인
log.info("MyQuartzJob instance hash code: {}", this.hashCode());
try {
log.info("Thread start");
Thread.sleep(10000); // 동시 실행 방지 테스트
log.info("Thread end");
} catch (InterruptedException ignored) {}
}
}
QuartzJobBean은 Spring에서 빈 주입이 가능하게 생성해주는 Job입니다. 내부 소스를 보게되면 Job 인터페이스를 구현하였으며 execute내부에서 excuteInternal을 호출합니다.
package com.kr.quartz.jobs;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 리플렉션을 이용하여 targetMethod로 실행될수있게 설정
*/
@Slf4j
@NoArgsConstructor
public class InvokingJob {
public void run() {
log.info("InvokingJob executing...");
log.info("Job instance hash code: {}", this.hashCode());
}
}
일반적인 케이스가 아닌 Job을 invoking해서 타겟 메소드를 호출하게 하는 방식입니다.
위 3개의 개별적인 Job을 등록하였고 아래에서 해당 Job의 실행에 대해서 알아볼것 입니다.
3. JobListener
quartz에서 제공하는 인터페이스로 Job의 생명주기 동안 발생하는 이벤트를 처리합니다. 이 인터페이스를 구현함으로써 개발자는 Job이 시작되거나 완료될 때마다 특정 작업을 수행하도록 할 수 있습니다.
메서드 | 반환 값 | 설명 |
getName() | String | JobListener의 이름을 반환합니다. |
jobToBeExecuted() | void | Job이 실행되기 직전에 호출되는 메서드로, Job의 실행 준비 상황을 확인하거나 필요한 작업을 수행하는 데 사용할 수 있습니다. |
jobExecutionVetoed() | void | 다른 JobListener가 Job의 실행을 방해(veto)했을 때 호출되는 메서드입니다. |
jobWasExecuted() | void | Job의 실행이 완료된 후에 호출되는 메서드로, Job의 실행 결과를 로깅하거나 후속 작업을 수행하는 데 사용할 수 있습니다. |
package com.kr.quartz.listener;
import lombok.extern.slf4j.Slf4j;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobListener;
import org.springframework.stereotype.Component;
/**
* 로깅용으로 사용하는 job 리스너
*/
@Slf4j
public class MyJobListener implements JobListener {
@Override
public String getName() {
return getClass().getSimpleName();
}
@Override
public void jobToBeExecuted(JobExecutionContext jobExecutionContext) {
log.info("jobToBeExecuted"); // job 실행 시점
}
@Override
public void jobExecutionVetoed(JobExecutionContext jobExecutionContext) {
log.info("jobExecutionVetoed"); // 트리거 veto 실행시
}
@Override
public void jobWasExecuted(JobExecutionContext jobExecutionContext, JobExecutionException e) {
log.info("jobWasExecuted"); // job 종료 시점
}
}
위에 글로벌 설정으로 SchedulerFactoryBean에 등록한것을 볼 수 있습니다. job 전/후에 동작을 살펴볼 수있으며 트리거 중지에 이벤트가 전파됩니다.
3. TriggerListener
quartz에서 제공하는 인터페이스로 트리거의 생명주기 동안 발생하는 이벤트를 처리합니다. 이 인터페이스를 구현함으로써 개발자는 트리거가 시작되거나 완료될 때, 중지 등의 특정 작업을 수행하도록 할 수 있습니다.
package com.kr.quartz.listener;
import lombok.extern.slf4j.Slf4j;
import org.quartz.JobExecutionContext;
import org.quartz.Trigger;
import org.quartz.TriggerListener;
@Slf4j
public class MyTriggerListener implements TriggerListener {
@Override
public String getName() {
return getClass().getSimpleName();
}
@Override
public void triggerFired(Trigger trigger, JobExecutionContext jobExecutionContext) {
log.info("triggerFired"); // 트리거 수행시점
}
@Override
public boolean vetoJobExecution(Trigger trigger, JobExecutionContext jobExecutionContext) {
// 트리거 중지 여부
return false;
}
@Override
public void triggerMisfired(Trigger trigger) {
log.info("triggerMisfired"); // 트리거 수행 실패
}
@Override
public void triggerComplete(Trigger trigger, JobExecutionContext jobExecutionContext, Trigger.CompletedExecutionInstruction completedExecutionInstruction) {
log.info("triggerComplete"); // 트리거 수행 완료
}
}
4. 스케줄링 구현
package com.kr.quartz.triggers;
import com.kr.quartz.jobs.MyJob;
import com.kr.quartz.jobs.MyQuartzJob;
import lombok.extern.slf4j.Slf4j;
import org.quartz.JobBuilder;
import org.quartz.SchedulerException;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.TriggerBuilder;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class MyJobTrigger {
public MyJobTrigger(SchedulerFactoryBean schedulerFactoryBean) throws SchedulerException {
var jobDetail = JobBuilder
.newJob(MyJob.class)
.withIdentity("myJob", "group1")
.withDescription("my job description")
.requestRecovery(true)
.storeDurably(true)
.usingJobData("key", "val")
.build();
var trigger = TriggerBuilder.newTrigger()
.withIdentity("myJobTrigger", "group1")
.startNow()
.withSchedule(
SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(5)
.repeatForever()
)
.build();
schedulerFactoryBean.getScheduler().scheduleJob(jobDetail, trigger);
}
}
구현한 Job을 이용하여 JobDetail을 생성해주었으며 SimpleSchedule을 등록하였습니다.

package com.kr.quartz.triggers;
import com.kr.quartz.jobs.MyJob;
import com.kr.quartz.jobs.MyQuartzJob;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class MyQuartzJobTrigger {
public MyQuartzJobTrigger(SchedulerFactoryBean schedulerFactoryBean) throws SchedulerException {
var jobDetail = JobBuilder
.newJob(MyQuartzJob.class)
.withIdentity("myQuartzJob", "group1")
.withDescription("my job description")
.requestRecovery(true)
.storeDurably(true)
.usingJobData("key", "val")
.build();
var trigger = TriggerBuilder.newTrigger()
.withIdentity("myQuartzTrigger", "group1")
.startNow()
.withSchedule(
CronScheduleBuilder.cronSchedule("0/5 * * * * ?")
)
.build();
schedulerFactoryBean.getScheduler().scheduleJob(jobDetail, trigger);
}
}
구현한 QuartzJobBean을 이용하여 JobDetail을 생성해주었으며 cronSchedule을 등록하였습니다.

위 로그를 보면 여러 내용을 확인 할 수 있습니다. cron으로 5초단위를 실행하였으나 Job 내부소스를 보게되면 ThreadSleep 10초를 주었습니다. 일반적으로는 5초마다 새로운 스레드에 의해 실행되어야하나 아래 어노테이션으로 동시 실행을 방지했습니다.
@DisallowConcurrentExecution
해당 어노테이션에 따라 종료되지 않는 스케줄러에 의해 트리거 misfired를 확인 할 수있습니다.
package com.kr.quartz.triggers;
import com.kr.quartz.jobs.InvokingJob;
import com.kr.quartz.jobs.MyQuartzJob;
import lombok.extern.slf4j.Slf4j;
import org.quartz.JobBuilder;
import org.quartz.SchedulerException;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.TriggerBuilder;
import org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class InvokingJobTrigger {
public InvokingJobTrigger(SchedulerFactoryBean schedulerFactoryBean) throws SchedulerException, ClassNotFoundException, NoSuchMethodException {
var jobDetail = new MethodInvokingJobDetailFactoryBean();
jobDetail.setTargetObject(new InvokingJob());
jobDetail.setTargetMethod("run"); // 타겟 메소드
jobDetail.setName("invokingJob");
jobDetail.setGroup("invokingJobGroup");
jobDetail.afterPropertiesSet();
var trigger = TriggerBuilder.newTrigger()
.withIdentity("invokingJobTrigger", "group1")
.startNow()
.withSchedule(
SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(5)
.repeatForever()
)
.build();
schedulerFactoryBean.getScheduler().scheduleJob(jobDetail.getObject(), trigger);
}
}

해당 Job은 일반적인 Job과 다르게 Method ivoking을 통하여 설정된 메소드를 실행 되는것을 볼수 있습니다.
내부 소스를 보면 afterPropertiesSet에 의하여 JobDetail이 생성됩니다. Job을보게되면 MethodInvokingJob 내부 Job 클래스로 생성하게됩니다.

MethodInvokingJob을 살펴보면 invoke 메소드를 호출하게 되어있으며 우리가 설정한 타겟 메소드를 리플렉션으로 실행하는것을 확인 할 수 있습니다.


이후 getObject를 이용하여 JobDetail이 반환됩니다.

위 경우는 일반적으로 사용하는것보다 레거시하게 구성된 클래스드를 적용할 수있게 지원해주는 Job으로 보입니다.
5. Job 실행 흐름 확인해보기
1. InitializingBean

SchedulerFactory를 생성하고 해당prepareScheduler를 이용하여 내부적으로 스케줄러를 생성합니다.


2. SmartLifeCycle
SmartLifeCycle 인터페이스에 의해서 start 메소드가 실행되며 startScheduler 메소드로 실행을 위임합니다.
실제 QuartzScheduler 관련 쓰레드를 시작해서 등록된 Job들을 실행합니다.


별개로 톰캣 shutdown 될 때 stop이 실행됩니다.

3. StdSchedulerFactory
위에서 살펴보았듯이 createScheduler에 일반적으로 StdSchedulerFactory가 인자로 가게됩니다. 이후 스케줄러를 호출하는 내용을 확인 할 수 있습니다.


내부 소스를 확인해보면 instantiate를 호출하는것을 볼수있다. 소스를 따라가보면 QuartzScheduler를 호출하는것을 볼수 있다.


4. QuartzScheduler
생성자를 확인해보면 QuartzSchedulerThread를 생성하여 실행되는것을 볼수 있습니다.

5. QuartzSchedulerThread
스케줄러가 halted 상태가 아닌경우는 계속 Loop가 수행되는것을 볼수 있습니다.

수행할 트리거를 조회 한 후 JonRunShell를 생성합니다. 해당 runner를 이용하서 runInThread에서 스레드를 실행하게 됩니다.


스레스 실행되면 Job을 생성하여 excute하는것을 볼수있다.


굉장히 복잡한 구조로 되어있는 Quartz 내부를 확인해 보았다.
'Spring' 카테고리의 다른 글
Spring Boot Batch 적용하기 - 1 (1) | 2024.09.18 |
---|---|
Spring Boot + Quartz 적용하기 - 3 (1) | 2024.09.17 |
Spring Boot + Quartz 적용하기 - 1 (0) | 2024.09.15 |
Jasypt 암호화를 이용하여 정보 보호하기 (0) | 2024.04.01 |
Spring Boot Logback 설정 (0) | 2024.03.21 |