Spring boot - Spring Batch + Quartz 실행
안녕하세요 오늘은 Spring Batch 와 스케줄라 Quartz를 결합하여 사용하는 방법을 알려 드릴려고 합니다.
보통 Batch , Quartz를 각 각 사용하는 방법은 쉽게 찾을 수 있지만 그 두개를 결합하여 사용하는 방법은 찾기가 힘들어 많은 검색을 통한 Batch + Quartz 결합 방법을 알려드리고자 합니다. 이글을 보시기전에 제가 작성한 Batch와 Quartz를 보고 오시는 것을 추천드립니다. 그것을 기반으로 진행할 것이기 때문입니다.
Spring Batch + Quartz
예전에 작성한 Quartz 구성에서 빨간색 부분이 Batch를 사용하기 위해서 생성된 class 입니다.
build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-batch'
implementation "org.springframework.boot:spring-boot-starter-quartz"
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'mysql:mysql-connector-java'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
quartz.properties
org.quartz.scheduler.instanceName=pooney
org.quartz.scheduler.instanceId=AUTO
org.quartz.scheduler.rmi.export=false
org.quartz.scheduler.rmi.proxy=false
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount=3
org.quartz.context.key.QuartzTopic=QuartzPorperties
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix=QRTZ_
org.quartz.jobStore.isClustered=true
org.quartz.jobStore.dataSource = pooney
org.quartz.dataSource.pooney.provider=hikaricp
org.quartz.dataSource.pooney.driver = com.mysql.cj.jdbc.Driver
org.quartz.dataSource.pooney.URL = jdbc:mysql://localhost:3306/batch?serverTimezone=UTC&characterEncoding=UTF-8
org.quartz.dataSource.pooney.user = root
org.quartz.dataSource.pooney.password = 1234
org.quartz.dataSource.pooney.maxConnections = 30
application.yml
spring:
jpa:
hibernate:
ddl-auto: create
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
use-new-id-generator-mappings: false
show-sql: true
properties:
hibernate.format_sql: true
datasource:
url: jdbc:mysql://localhost:3306/batch?serverTimezone=UTC&characterEncoding=UTF-8
username: root
password: 1234
driver-class-name: com.mysql.cj.jdbc.Driver
Quartz 구성
QuartzConfig
@Configuration
@RequiredArgsConstructor
@Slf4j
public class QuartzConfig {
private final DataSource dataSource;
private final ApplicationContext applicationContext;
private final PlatformTransactionManager platformTransactionManager;
@Bean
public SchedulerFactoryBean schedulerFactoryBean() {
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
AutoWiringSpringBeanJobFactory autoWiringSpringBeanJobFactory = new AutoWiringSpringBeanJobFactory();
autoWiringSpringBeanJobFactory.setApplicationContext(applicationContext);
schedulerFactoryBean.setJobFactory(autoWiringSpringBeanJobFactory);
schedulerFactoryBean.setDataSource(dataSource);
schedulerFactoryBean.setOverwriteExistingJobs(true);
schedulerFactoryBean.setAutoStartup(true);
schedulerFactoryBean.setTransactionManager(platformTransactionManager);
schedulerFactoryBean.setQuartzProperties(quartzProperties());
return schedulerFactoryBean;
}
private Properties quartzProperties() {
PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
propertiesFactoryBean.setLocation(new ClassPathResource("quartz.properties"));
Properties properties = null;
try {
propertiesFactoryBean.afterPropertiesSet();
properties = propertiesFactoryBean.getObject();
} catch (IOException e) {
log.error("quartzProperties parse error : {}", e);
}
return properties;
}
}
QuartzJobListener
@Slf4j
public class QuartzJobListner implements JobListener {
@Override
public String getName() {
return this.getClass().getName();
}
@Override
public void jobToBeExecuted(JobExecutionContext context) {
log.info("Job 수행 되기 전");
}
@Override
public void jobExecutionVetoed(JobExecutionContext context) {
log.info("Job 중단");
}
@Override
public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
log.info("Job 수행 완료 후");
}
}
QuartzTriggerListener
@Slf4j
public class QuartzTriggerListener implements TriggerListener {
@Override
public String getName() {
return this.getClass().getName();
}
@Override
public void triggerFired(Trigger trigger, JobExecutionContext context) {
log.info("Trigger 실행");
}
/**
* @Content : 결과가 true이면 JobListener jobExecutionVetoed(JOB중단) 실행
*/
@Override
public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {
log.info("Trigger 상태 체크");
JobDataMap map = context.getJobDetail().getJobDataMap();
int executeCount = 1;
if (map.containsKey("executeCount")) {
executeCount = (int) map.get("executeCount");
}
return executeCount >= 2;
}
@Override
public void triggerMisfired(Trigger trigger) {
}
@Override
public void triggerComplete(Trigger trigger, JobExecutionContext context, Trigger.CompletedExecutionInstruction triggerInstructionCode) {
log.info("Trigger 성공");
}
}
BeanUtil
@Component
@RequiredArgsConstructor
public class BeanUtil {
private final ApplicationContext applicationContext;
public Object getBean(String name){
return applicationContext.getBean(name);
}
}
BeanUtil의 역할은 등록한 Bean 이름을 주면 Context에서 해당 하는 Bean을 찾아주는 역할을 수행합니다. 여기서 Bean 은 Job이 되겠습니다.
QuartzBatchJob
@Component
public class QuartzBatchJob extends QuartzJob {
@Autowired
private JobLauncher jobLauncher;
@Autowired
private BeanUtil beanUtil;
@SneakyThrows
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
//전달받은 JodDataMap에서 Job이름을 꺼내오고 그 Job이름으로 context에서 bean을 가져온다
Job job = (Job) beanUtil.getBean((String) jobDataMap.get(QuartzService.JOB_ANME));
JobParameters jobParameters = new JobParametersBuilder()
.addDate("curDate", new Date())
.toJobParameters();
jobLauncher.run(job, jobParameters);
}
}
Quartz가 동작시키는 Job은 QuartzBatchJob 하나로 구성되어 있습니다. JobDataMap으로 동작시킬 JOB_NAME을 넘기면 해당 JOB_NAME을 가지고 Context에서 동작 시킬 Job(Bean) 을 찾고 JobLauncher을 통해 해당 Job을 실행 시킵니다. 이렇게 구성하면 JOB_NAME만 잘넘겨준다면 QuartzBatchJob하나의 Job으로 동작시키고자 하는 Spring Batch Job을 실행시킬 수 있습니다.
QuartzService
@Slf4j
@Configuration
@RequiredArgsConstructor
public class QuartzService {
private final Scheduler scheduler;
public static final String JOB_ANME = "JOB_NAME";
@PostConstruct
public void init() {
try {
scheduler.clear();
scheduler.getListenerManager().addJobListener(new QuartzJobListner());
scheduler.getListenerManager().addTriggerListener(new QuartzTriggerListener());
// addJob(QuartzJob.class, "QuartzJob", "Quartz Job 입니다", paramsMap, "0/1 * * * * ?");
addJob(QuartzBatchJob.class, "createJob1", "createJob1 입니다", null , "0/1 * * * * ?");
addJob(QuartzBatchJob.class, "createJob2", "createJob2 입니다", null , "0/1 * * * * ?");
} catch (Exception e){
log.error("addJob error : {}", e);
}
}
//Job 추가
public <T extends Job> void addJob(Class<? extends Job> job ,String name, String dsec, Map paramsMap, String cron) throws SchedulerException {
JobDetail jobDetail = buildJobDetail(job,name,dsec,paramsMap);
Trigger trigger = buildCronTrigger(cron);
if(scheduler.checkExists(jobDetail.getKey())) scheduler.deleteJob(jobDetail.getKey());
scheduler.scheduleJob(jobDetail,trigger);
}
//JobDetail 생성
public <T extends Job> JobDetail buildJobDetail(Class<? extends Job> job, String name, String desc, Map paramsMap) {
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put(JOB_ANME, name);
jobDataMap.put("executeCount", 1);
return JobBuilder
.newJob(job)
.withIdentity(name)
.withDescription(desc)
.usingJobData(jobDataMap)
.build();
}
//Trigger 생성
private Trigger buildCronTrigger(String cronExp) {
return TriggerBuilder.newTrigger()
.withSchedule(CronScheduleBuilder.cronSchedule(cronExp))
.build();
}
}
Spring Batch Job 구성
여기서는 Quartz와 Spring Batch를 결합하는 방법을 소개하는거라 Job은 간단하게 Tasklet으로 구성하고 있습니다. [Reader,Writer,Processor] , JobParameter를 사용하는 방법은 제가 작성한 글을 참고 해주세요.
BatchJob
@Slf4j
@Configuration
@RequiredArgsConstructor
public class BatchJob {
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
@Bean(name = "createJob1")
public Job createJob1(){
return jobBuilderFactory.get("createJob1")
.start(createJob1_Step1())
.build();
}
public Step createJob1_Step1() {
return stepBuilderFactory.get("createJob1_Step1")
.tasklet(new Tasklet() {
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
log.info("createJob1_Step1 start!!!!");
return RepeatStatus.FINISHED;
}
}).build();
}
}
BatchJob2
@Slf4j
@Configuration
@RequiredArgsConstructor
public class BatchJob2 {
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
@Bean(name = "createJob2")
public Job createJob2(){
return jobBuilderFactory.get("createJob2")
.start(createJob2_Step1())
.build();
}
public Step createJob2_Step1() {
return stepBuilderFactory.get("createJob2_Step1")
.tasklet(new Tasklet() {
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
log.info("createJob2_Step1 start!!!!");
return RepeatStatus.FINISHED;
}
}).build();
}
}
전체적인 Flow는 아래와 같습니다
- createJob1 , createJob2 Bean 등록
- QuartzSerivce의 addJob()을 통해 QuartzBatchJob을 등록
- QuartzBatchJob이 동작할때 전달받은 JobDataMap에서 JOB_NAME을 꺼냄
- 해당 JOB_NAME을 가지고 BeanUtil을 통해 Bean(Job)을 가지고 오고 해당 Bean을 JobLauncher를 통해 실행
- createJob1 , createJob2 동작
결과 화면을 보시면 1초마다 Worker 스레드가 할당되어 creatJob1, creatJob2가 동작하는 것을 확인 하 실 수 있습니다.
아래에 Quartz, Batch 테이블을 보시면 정상적으로 Trigger, Job이 등록 된 것을 확인 할 수 있습니다. 참고로 Batch 테이블은 JOB_INSTANCE_ID, JOB_EXCUTION_ID를 추적하면서 보시면 됩니다.
batch_job_instance
batch_job_executon
batch_job_exction_params
qrtz_cron_triggers
qrtz_job_detatils
이처럼 Spring Batch + Quartz를 결합하여 1초마다 스케줄러가 createJob1, createJob2를 동작시키는 배치를 만들어 보았는데요 많이 부족한점이 보일 수 있는데 저도 공부 목적으로 만든 것이라 이해해 주시면 감사하겠습니다.