pooney
article thumbnail

 

안녕하세요 오늘은 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를 사용하는 방법은 제가 작성한 글을 참고 해주세요. 

 

 

 

https://pooney.tistory.com/96

 

Spring boot - Spring Batch란?

안녕하세요 오늘은 많이 사용하는 Spring Batch를 설명해 드릴려고 합니다. Batch를 사용하기 위해선 스케줄러를 같이 사용하는데 대표적으로 아래와 같습니다 쉽게 어노테이션으로 사용가능 한 @Sch

pooney.tistory.com

 

 

 

 

 

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는 아래와 같습니다 

  1. createJob1 , createJob2 Bean 등록
  2. QuartzSerivce의 addJob()을 통해  QuartzBatchJob을 등록 
  3. QuartzBatchJob이 동작할때 전달받은 JobDataMap에서 JOB_NAME을 꺼냄
  4. 해당 JOB_NAME을 가지고 BeanUtil을 통해 Bean(Job)을 가지고 오고 해당 Bean을 JobLauncher를 통해 실행 
  5. 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를 동작시키는 배치를 만들어 보았는데요 많이 부족한점이 보일 수 있는데 저도 공부 목적으로 만든 것이라 이해해 주시면 감사하겠습니다. 

 

 

 

 

 

 

profile

pooney

@pooney

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!