상세 컨텐츠

본문 제목

[JAVA] Quartz - Spring boot Example

Spring/Quartz

by Chan.94 2024. 8. 3. 23:17

본문

반응형

Spring boot를 사용함으로써 Scheduler 객체를 SchedulerFactory를 통해서 생성하고 직접 시작시켜줘야 하지만 SchedulerFactoryBean을 통해서 자동으로 시작된 상태로 생성할 수 있다.

 

Quartz 스케줄러 테스트를 위한 JAVA 소스에 대하여 간단히 리뷰하도록 하겠다.

 

Quartz 개념과 용어에 대해서는 이전 포스팅을 확인하기 바란다.

Quartz - 개념


Quartz 테이블 생성

yml 설정

quartz:
  job-store-type: jdbc
    jdbc:
      initialize-schema: never
    #initialize-schema: always

테이블이 생성되지 않았다면 initialize-schema 설정을 always로 하여 자동으로 생성할 수도 있다.

always로 하면 서버가 실행될 때 drop 하고 다시 생성하게 되는 것에 유의하자.

 

always가 아닌 직접 생성해도 무방하다.

 

Quartz 테이블 생성 SQL (ORACLE)

더보기

delete from qrtz_fired_triggers;
delete from qrtz_simple_triggers;
delete from qrtz_simprop_triggers;
delete from qrtz_cron_triggers;
delete from qrtz_blob_triggers;
delete from qrtz_triggers;
delete from qrtz_job_details;
delete from qrtz_calendars;
delete from qrtz_paused_trigger_grps;
delete from qrtz_locks;
delete from qrtz_scheduler_state;

drop table qrtz_calendars;
drop table qrtz_fired_triggers;
drop table qrtz_blob_triggers;
drop table qrtz_cron_triggers;
drop table qrtz_simple_triggers;
drop table qrtz_simprop_triggers;
drop table qrtz_triggers;
drop table qrtz_job_details;
drop table qrtz_paused_trigger_grps;
drop table qrtz_locks;
drop table qrtz_scheduler_state;


CREATE TABLE qrtz_job_details
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    JOB_NAME  VARCHAR2(200) NOT NULL,
    JOB_GROUP VARCHAR2(200) NOT NULL,
    DESCRIPTION VARCHAR2(250) NULL,
    JOB_CLASS_NAME   VARCHAR2(250) NOT NULL, 
    IS_DURABLE VARCHAR2(1) NOT NULL,
    IS_NONCONCURRENT VARCHAR2(1) NOT NULL,
    IS_UPDATE_DATA VARCHAR2(1) NOT NULL,
    REQUESTS_RECOVERY VARCHAR2(1) NOT NULL,
    JOB_DATA BLOB NULL,
    CONSTRAINT QRTZ_JOB_DETAILS_PK PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
);
CREATE TABLE qrtz_triggers
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    TRIGGER_NAME VARCHAR2(200) NOT NULL,
    TRIGGER_GROUP VARCHAR2(200) NOT NULL,
    JOB_NAME  VARCHAR2(200) NOT NULL, 
    JOB_GROUP VARCHAR2(200) NOT NULL,
    DESCRIPTION VARCHAR2(250) NULL,
    NEXT_FIRE_TIME NUMBER(19) NULL,
    PREV_FIRE_TIME NUMBER(19) NULL,
    PRIORITY NUMBER(13) NULL,
    TRIGGER_STATE VARCHAR2(16) NOT NULL,
    TRIGGER_TYPE VARCHAR2(8) NOT NULL,
    START_TIME NUMBER(19) NOT NULL,
    END_TIME NUMBER(19) NULL,
    CALENDAR_NAME VARCHAR2(200) NULL,
    MISFIRE_INSTR NUMBER(2) NULL,
    JOB_DATA BLOB NULL,
    CONSTRAINT QRTZ_TRIGGERS_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    CONSTRAINT QRTZ_TRIGGER_TO_JOBS_FK FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) 
      REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP) 
);
CREATE TABLE qrtz_simple_triggers
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    TRIGGER_NAME VARCHAR2(200) NOT NULL,
    TRIGGER_GROUP VARCHAR2(200) NOT NULL,
    REPEAT_COUNT NUMBER(7) NOT NULL,
    REPEAT_INTERVAL NUMBER(12) NOT NULL,
    TIMES_TRIGGERED NUMBER(10) NOT NULL,
    CONSTRAINT QRTZ_SIMPLE_TRIG_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    CONSTRAINT QRTZ_SIMPLE_TRIG_TO_TRIG_FK FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE qrtz_cron_triggers
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    TRIGGER_NAME VARCHAR2(200) NOT NULL,
    TRIGGER_GROUP VARCHAR2(200) NOT NULL,
    CRON_EXPRESSION VARCHAR2(120) NOT NULL,
    TIME_ZONE_ID VARCHAR2(80),
    CONSTRAINT QRTZ_CRON_TRIG_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    CONSTRAINT QRTZ_CRON_TRIG_TO_TRIG_FK FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 
      REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE qrtz_simprop_triggers
  (          
    SCHED_NAME VARCHAR2(120) NOT NULL,
    TRIGGER_NAME VARCHAR2(200) NOT NULL,
    TRIGGER_GROUP VARCHAR2(200) NOT NULL,
    STR_PROP_1 VARCHAR2(512) NULL,
    STR_PROP_2 VARCHAR2(512) NULL,
    STR_PROP_3 VARCHAR2(512) NULL,
    INT_PROP_1 NUMBER(10) NULL,
    INT_PROP_2 NUMBER(10) NULL,
    LONG_PROP_1 NUMBER(19) NULL,
    LONG_PROP_2 NUMBER(19) NULL,
    DEC_PROP_1 NUMERIC(13,4) NULL,
    DEC_PROP_2 NUMERIC(13,4) NULL,
    BOOL_PROP_1 VARCHAR2(1) NULL,
    BOOL_PROP_2 VARCHAR2(1) NULL,
    TIME_ZONE_ID VARCHAR2(80) NULL,
    CONSTRAINT QRTZ_SIMPROP_TRIG_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    CONSTRAINT QRTZ_SIMPROP_TRIG_TO_TRIG_FK FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 
      REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE qrtz_blob_triggers
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    TRIGGER_NAME VARCHAR2(200) NOT NULL,
    TRIGGER_GROUP VARCHAR2(200) NOT NULL,
    BLOB_DATA BLOB NULL,
    CONSTRAINT QRTZ_BLOB_TRIG_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    CONSTRAINT QRTZ_BLOB_TRIG_TO_TRIG_FK FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 
        REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE qrtz_calendars
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    CALENDAR_NAME  VARCHAR2(200) NOT NULL, 
    CALENDAR BLOB NOT NULL,
    CONSTRAINT QRTZ_CALENDARS_PK PRIMARY KEY (SCHED_NAME,CALENDAR_NAME)
);
CREATE TABLE qrtz_paused_trigger_grps
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    TRIGGER_GROUP  VARCHAR2(200) NOT NULL, 
    CONSTRAINT QRTZ_PAUSED_TRIG_GRPS_PK PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP)
);
CREATE TABLE qrtz_fired_triggers 
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    ENTRY_ID VARCHAR2(140) NOT NULL,
    TRIGGER_NAME VARCHAR2(200) NOT NULL,
    TRIGGER_GROUP VARCHAR2(200) NOT NULL,
    INSTANCE_NAME VARCHAR2(200) NOT NULL,
    FIRED_TIME NUMBER(19) NOT NULL,
    SCHED_TIME NUMBER(19) NOT NULL,
PRIORITY NUMBER(13) NOT NULL,
    STATE VARCHAR2(16) NOT NULL,
    JOB_NAME VARCHAR2(200) NULL,
    JOB_GROUP VARCHAR2(200) NULL,
    IS_NONCONCURRENT VARCHAR2(1) NULL,
    REQUESTS_RECOVERY VARCHAR2(1) NULL,
    CONSTRAINT QRTZ_FIRED_TRIGGER_PK PRIMARY KEY (SCHED_NAME,ENTRY_ID)
);
CREATE TABLE qrtz_scheduler_state 
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    INSTANCE_NAME VARCHAR2(200) NOT NULL,
    LAST_CHECKIN_TIME NUMBER(19) NOT NULL,
    CHECKIN_INTERVAL NUMBER(13) NOT NULL,
    CONSTRAINT QRTZ_SCHEDULER_STATE_PK PRIMARY KEY (SCHED_NAME,INSTANCE_NAME)
);
CREATE TABLE qrtz_locks
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    LOCK_NAME  VARCHAR2(40) NOT NULL, 
    CONSTRAINT QRTZ_LOCKS_PK PRIMARY KEY (SCHED_NAME,LOCK_NAME)
);

create index idx_qrtz_j_req_recovery on qrtz_job_details(SCHED_NAME,REQUESTS_RECOVERY);
create index idx_qrtz_j_grp on qrtz_job_details(SCHED_NAME,JOB_GROUP);

create index idx_qrtz_t_j on qrtz_triggers(SCHED_NAME,JOB_NAME,JOB_GROUP);
create index idx_qrtz_t_jg on qrtz_triggers(SCHED_NAME,JOB_GROUP);
create index idx_qrtz_t_c on qrtz_triggers(SCHED_NAME,CALENDAR_NAME);
create index idx_qrtz_t_g on qrtz_triggers(SCHED_NAME,TRIGGER_GROUP);
create index idx_qrtz_t_state on qrtz_triggers(SCHED_NAME,TRIGGER_STATE);
create index idx_qrtz_t_n_state on qrtz_triggers(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,TRIGGER_STATE);
create index idx_qrtz_t_n_g_state on qrtz_triggers(SCHED_NAME,TRIGGER_GROUP,TRIGGER_STATE);
create index idx_qrtz_t_next_fire_time on qrtz_triggers(SCHED_NAME,NEXT_FIRE_TIME);
create index idx_qrtz_t_nft_st on qrtz_triggers(SCHED_NAME,TRIGGER_STATE,NEXT_FIRE_TIME);
create index idx_qrtz_t_nft_misfire on qrtz_triggers(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME);
create index idx_qrtz_t_nft_st_misfire on qrtz_triggers(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_STATE);
create index idx_qrtz_t_nft_st_misfire_grp on qrtz_triggers(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_GROUP,TRIGGER_STATE);

create index idx_qrtz_ft_trig_inst_name on qrtz_fired_triggers(SCHED_NAME,INSTANCE_NAME);
create index idx_qrtz_ft_inst_job_req_rcvry on qrtz_fired_triggers(SCHED_NAME,INSTANCE_NAME,REQUESTS_RECOVERY);
create index idx_qrtz_ft_j_g on qrtz_fired_triggers(SCHED_NAME,JOB_NAME,JOB_GROUP);
create index idx_qrtz_ft_jg on qrtz_fired_triggers(SCHED_NAME,JOB_GROUP);
create index idx_qrtz_ft_t_g on qrtz_fired_triggers(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP);
create index idx_qrtz_ft_tg on qrtz_fired_triggers(SCHED_NAME,TRIGGER_GROUP);

COMMIT;


application.yml

spring: 
  datasource: 
    hikari: 
      driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
      jdbc-url: jdbc:log4jdbc:oracle:thin:@//127.0.0.1:1521/XE
      username: DEVLOG
      password: devlog
  quartz:
    auto-startup: true
    job-store-type: jdbc
    jdbc:
      initialize-schema: never
    properties:
      org:
        quartz:
          dataSource:                                                #JobStore가 사용할 데이터소스
            devlogQuartzDataSource:
              provider: hikaricp                                     #default c3p0 커넥션 풀
              URL: jdbc:log4jdbc:oracle:thin:@//127.0.0.1:1521/XE
              driver: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
              user: DEVLOG
              password: devlog
          scheduler:
            instanceId: AUTO #AUTO인 경우 인스턴스 ID를 임의로 생성
          jobStore:
            class: org.quartz.impl.jdbcjobstore.JobStoreTX           
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            dataSource: devlogQuartzDataSource                       
            useProperties: false
            tablePrefix: QRTZ_ # DB에 생성된 Quartz 테이블에 주어진 접두사를 지정
            misfireThreshold: 60000 #스케줄러가 실패한 것으로 간주되기 전에 다음 실행 시간을 전달하는 트리거를 허용하는 시간
            isClustered: false # 클러스터링 기능을 사용여부
          threadPool:
            threadCount: 3

 

 

  • provider : default는 c3p0이기 때문에 hikaricp로 설정.
  • dataSource : jobStore가 사용할 dataSource
  • jobStore.dataSource : quartz.dataSource에서 선언한 dataSource

Quartz의 dataSource를 직접 선언하였기 때문에 JAVA Configuration 파일에서는 별도로 선언해 주지 않아도 된다.


QuartzConfiguration.java

import java.util.Properties;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.quartz.QuartzProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;

import com.devlog.quartz.cmm.JobsListener;
import com.devlog.quartz.cmm.TriggersListener;

@Configuration
public class QuartzConfiguration {
    @Autowired
    private TriggersListener triggersListener;

    @Autowired
    private JobsListener jobsListener;
    
    @Value("${spring.quartz.auto-startup}")
    private boolean quartzAutoStartup;
    
    /**
     * @param quartzProperties
     * @param applicationContext
     * @return
     */
    @Bean
    public SchedulerFactoryBean schedulerFactoryBean(QuartzProperties quartzProperties, ApplicationContext applicationContext) {
        SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();

        schedulerFactoryBean.setApplicationContext(applicationContext);

        Properties properties = new Properties();
        properties.putAll(quartzProperties.getProperties());

        schedulerFactoryBean.setGlobalTriggerListeners(triggersListener);
        schedulerFactoryBean.setGlobalJobListeners(jobsListener);
        schedulerFactoryBean.setOverwriteExistingJobs(true);
        schedulerFactoryBean.setQuartzProperties(properties);
        schedulerFactoryBean.setWaitForJobsToCompleteOnShutdown(true);  //서버 shutdown시 실행 중인 Job 종료를 기다릴지 여부
        schedulerFactoryBean.setSchedulerName("DevLogQuartz");
        schedulerFactoryBean.setAutoStartup(quartzAutoStartup); // 스케줄러 초기화 후 자동실행 여부 / default : true 

        return schedulerFactoryBean;
    }
}

 

스프링에서는 4.1부터 Quartz 스케줄러 생성 작업을 쉽게 해주는 SchedulerFactoryBean을 제공한다.
Quartz 2.1.4 이상이면 호환 가능하다.

 


JobsListener.java

import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;
import org.quartz.JobListener;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class JobsListener implements JobListener {

    @Override
    public String getName() {
        return "globalJob";
    }

    @Override
    public void jobToBeExecuted(JobExecutionContext context) {
        JobKey jobKey = context.getJobDetail().getKey();
        log.info("Job 수행 되기 전 jobkey : {}", jobKey);
    }

    @Override
    public void jobExecutionVetoed(JobExecutionContext context) {
        JobKey jobKey = context.getJobDetail().getKey();
        log.info("Job 중단 jobkey : {}", jobKey);
    }

    @Override
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
        JobKey jobKey = context.getJobDetail().getKey();
        log.info("Job 수행 완료 후 jobkey : {}", jobKey);
    }
}

 

Config파일에서 SchedulerFactoryBean에 등록해 준다.

Job 실행 전후로 이벤트를 받을 수 있다.


TriggersListener.java

import org.quartz.JobExecutionContext;
import org.quartz.JobKey;
import org.quartz.Trigger;
import org.quartz.TriggerListener;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class TriggersListener implements TriggerListener {

    @Override
    public String getName() {
        return "globalTrigger";
    }

    @Override
    public void triggerFired(Trigger trigger, JobExecutionContext context) {
        JobKey jobKey = trigger.getJobKey();
        log.info("Trigger 실행 {} : jobkey : {}", trigger.getStartTime(), jobKey);
    }

    /**
     * Trigger 중단여부를 확인하는 메소드
     * 
     * 결과가 true이면 JobListener JobExcutionVetoed(Job중단) 실행
     */
    @Override
    public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {
        return false;
    }

    @Override
    public void triggerMisfired(Trigger trigger) {
        JobKey jobKey = trigger.getJobKey();
        log.info("triggerMisfired at {} : jobkey : {}", trigger.getStartTime(), jobKey);
    }

    /**
     * Trigger 완료 후 실행
     */
    @Override
    public void triggerComplete(Trigger trigger, JobExecutionContext context, Trigger.CompletedExecutionInstruction triggerInstructionCode) {
        JobKey jobKey = trigger.getJobKey();
        log.info("Trigger 성공 {} : jobkey : {}", trigger.getStartTime(), jobKey);
    }
}

 

Config파일에서 SchedulerFactoryBean에 등록해 준다.
Trigger 실행 전후로 이벤트를 받을 수 있다.

 


CronJob.java

import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;

import lombok.extern.slf4j.Slf4j;

/**
 * Schedule Job 실행 Class
 *
 */
@Slf4j
public class CronJob extends QuartzJobBean {

    /**
     * 배치 실행
     */
    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        /*구현부*/
        log.error("=============================Quartz Cron Execute====================");
        log.error("JobName : {}",context.getJobDetail().getKey().getName());
        log.error("JobGroup : {}",context.getJobDetail().getKey().getGroup());
        log.error("===================================================================");
    }

}

QuartzJobBean을 상속받으면 executeInternal 메서드를 구현해야 한다.

스케줄러에 의해 Job클래스가 호출되었을 때 executeInternal 메서드가 실행된다.

 


QuartzUtils.java

import java.text.ParseException;

import org.quartz.CronExpression;
import org.quartz.CronTrigger;
import org.quartz.Job;
import org.quartz.JobDetail;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.quartz.CronTriggerFactoryBean;
import org.springframework.scheduling.quartz.JobDetailFactoryBean;

public class QuartzUtils {
    private QuartzUtils() {
        
    }
    
    /**
     * Job 생성
     * @param quartzDto - Quartz Job 정보
     * @param jobClass - Job 생성할 Class
     * @param context - ApplicationContext
     * @return JobDetail
     */
    public static JobDetail createJob(QuartzDto quartzDto, Class<? extends Job> jobClass, ApplicationContext context) {
        JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
        factoryBean.setJobClass(jobClass);
        factoryBean.setDurability(false);
        factoryBean.setApplicationContext(context);
        factoryBean.setName(quartzDto.getJobName());
        factoryBean.setGroup(quartzDto.getJobGroup());
        factoryBean.setDescription(quartzDto.getDesc());
        if (quartzDto.getJobDataMap() != null) {
            factoryBean.setJobDataMap(quartzDto.getJobDataMap());
        }

        factoryBean.afterPropertiesSet();
        return factoryBean.getObject();
    }
    
    /**
    * Trigger 생성(Cron)
    * @param quartzDto - Quartz Job 정보
    * @return Trigger
    */
   public static Trigger createTrigger(QuartzDto quartzDto) {
       String cronExpression = quartzDto.getCronExpression();
       if (!CronExpression.isValidExpression(cronExpression)) {
           throw new IllegalArgumentException("Provided expression " + cronExpression + " is not a valid cron expression");
       } else {
           return createCronTrigger(quartzDto);
       }
   }

   /**
    * CronTrigger 생성
    * @param quartzDto - Quartz Job 정보
    * @return Trigger
    */
   private static Trigger createCronTrigger(QuartzDto quartzDto) {
       CronTriggerFactoryBean factoryBean = new CronTriggerFactoryBean();
       factoryBean.setName(quartzDto.getJobName().concat("Trigger"));
       factoryBean.setGroup(quartzDto.getJobGroup());
       factoryBean.setCronExpression(quartzDto.getCronExpression());
       factoryBean.setMisfireInstruction(CronTrigger.MISFIRE_INSTRUCTION_DO_NOTHING);   //MsiFired Trigger 무시
       
       try {
           factoryBean.afterPropertiesSet();
       } catch (ParseException e) {
           e.printStackTrace();
       }
       return factoryBean.getObject();
   }
}

 


QuartzManagerService.java

import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Trigger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.stereotype.Service;

import com.devlog.quartz.cmm.QuartzDto;
import com.devlog.quartz.cmm.QuartzUtils;
import com.devlog.quartz.job.CronJob;

@Service
public class QuartzManagerService {
    
    @Autowired
    private ApplicationContext applicationContext;
    
    @Autowired
    private SchedulerFactoryBean schedulerFactoryBean;
    
    public boolean addJob(QuartzDto quartzDto) {
        JobDetail jobDetail;
        Trigger trigger;
        try {
            
            jobDetail = QuartzUtils.createJob(quartzDto, CronJob.class, applicationContext);
            trigger = QuartzUtils.createTrigger(quartzDto);
            schedulerFactoryBean.getScheduler().scheduleJob(jobDetail, trigger);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        
        return true;
    }
    
    public boolean deleteJob(QuartzDto quartzDto) {
        boolean result;
        try {
            result = schedulerFactoryBean.getScheduler().deleteJob(JobKey.jobKey(quartzDto.getJobName(), quartzDto.getJobGroup()));
        } catch (Exception e) {
            result = false;
        }
        return result;
    }
}

 

QuartzUtils를 사용하여 add, delete Job 메서드를 구현.


QuartzTestController.java

import java.util.Map;

import org.quartz.SchedulerException;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import com.devlog.quartz.cmm.QuartzDto;
import com.devlog.quartz.service.QuartzManagerService;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RequestMapping("/Quartz")
@Controller
@RequiredArgsConstructor
public class QuartzTestController {
    
    private final QuartzManagerService quartzManagerService;
    
    @RequestMapping("/AddJob")
    public ResponseEntity<Void> addJob(@RequestParam Map<String, Object> paramMap) throws SchedulerException {
        //http://127.0.0.1:28081/Quartz/AddJob?jobName=testJob&jobGroup=testJobGroup&jobDesc=test&cronExpr=0 * * * * ?
        log.error(paramMap.toString());
        
        QuartzDto quartzDto = new QuartzDto();
        quartzDto.setJobName(paramMap.get("jobName").toString());
        quartzDto.setJobGroup(paramMap.get("jobGroup").toString());
        quartzDto.setDesc(paramMap.get("jobDesc").toString());
        quartzDto.setCronExpression(paramMap.get("cronExpr").toString());
        
        quartzManagerService.addJob(quartzDto);
        
        return ResponseEntity.ok().build();
    }
    
    @RequestMapping("/DeleteJob")
    public ResponseEntity<Void> deleteJob(@RequestParam Map<String, Object> paramMap) {
        //http://127.0.0.1:28081/Quartz/DeleteJob?jobName=testJob&jobGroup=testJobGroup
        log.error(paramMap.toString());
        
        QuartzDto quartzDto = new QuartzDto();
        quartzDto.setJobName(paramMap.get("jobName").toString());
        quartzDto.setJobGroup(paramMap.get("jobGroup").toString());
        
        quartzManagerService.deleteJob(quartzDto);
        
        return ResponseEntity.ok().build();
    }
}

 

Quartz 테스트를 위해 Get방식으로 데이터를 전송받는 Controller를 만들었다.


개발자의 고민

어떤 프로그램에 Quartz에 설정된 Job이 30개가 있다고 가정하자.

근데 그중 하나의 Job을 수정해야 한다. 

간단한 문제일까?

 

하나의 서버에 Quartz(Scheduler)와 Job(Batch)이 존재하게 되면 배포는 스케줄러의 중단을 의미한다.

어떠한 고민을 해야 할까?

 

이게 정답이 아닐 수는 있겠지만 내가 한 고민이 누군가에게 도움이 됐으면 하는 바람으로 생각해야 하는 부분에 대해 작성해 보겠다.

 

  • Misfire Instructions
    어떠한 이유로 Trigger가 실행되지 못해 Job이 실행되지 않았다면 어떻게 할 것인가?
    Example에서는 테스트를 위해 무시하도록 했지만 충분한 고민이 필요할 것이다.
  • 클러스터링 / 로드밸런싱
    특정 서버에 문제가 생기더라도 전체적인 서비스에는 영향을 주지 않게 한다.
    스케줄러만을 위해 이러한 구성을 한다는 것은 수지타산이 맞지 않을 것이다. 하지만 이미 아키텍처가 구성되어 있다면 Quartz + Batch 아키텍처에 고민이 필요한 부분이다.

  • 배치 프로젝트 분리
    배치 부분은 별도의 jar 파일로 만든다.
    Quartz Job 실행 부분에서는 jar파일을 실행시키도록 구현한다.
    배치 부분만 변경이 된다면 jar파일만 수정하여 다시 배포하면 된다.

  • 기존서버와 배치서버 분리
    스펙이 부족하거나 서버의 안전성(메모리, CPU 등)을 위해 분리한 후 분리된 배치서버에는 배치 jar파일만 배포하고 Quartz Job에서는 실행하는 역할만 하는 것도 방법이 될 수 있겠다.
반응형

'Spring > Quartz' 카테고리의 다른 글

[JAVA] Quartz - 개념  (40) 2024.07.28

관련글 더보기

댓글 영역

>