OLD

Quartz API에 대해서

Hyowon_ 2023. 11. 28. 14:52
728x90

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();
            }

}
728x90