[Java] Quartz 사용법

식빵·2023년 8월 29일
0

Java Lab

목록 보기
13/29
post-thumbnail

참고사항

  1. 이번 프로젝트에서 Quartz 를 사용하게 되어서 테스트 환경을 구축하고
    테스트 코드를 작성해봤습니다. 이 글은 해당 코드에 대한 정보 공유가 목적입니다.
  1. 아주 간단한 테스트를 위한 것이므로 Junit 은 사용하지 않았습니다.
  1. 작성된 코드들은 제 github에서도 확인 가능합니다.

Maven - pom.xml 설정

Maven 프로젝트 생성하고, 아래와 같이 의존성을 추가했습니다.

<dependencies>
        <!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz -->
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.3.2</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz-jobs -->
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz-jobs</artifactId>
            <version>2.3.2</version>
        </dependency>

  		<!-- 
			quartz 의 transitive dependency 목록 중에서
 			slf4j-api 1.7.7 가 있는데, 더 최신 버전을 쓰고 싶어서 
			2 버전대로 올립니다.
		-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>2.0.7</version>
        </dependency>

  		<!-- slf4j 의 구현체를 하나 dependecy 로 넣습니다. logback 사용했습니다. -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.4.8</version>
        </dependency>

</dependencies>
  • Quartz 버전은 회사에서 사용하는 버전 2.3.2 로 맞췄습니다.
  • logback 은 logback.xml 파일을 통해서 custom 하게 세팅할 수 있습니다.
    • 그 방법은 맨 아래 참고 - 1. logback 설정 목차를 참고하시기 바랍니다.
  • 참고로 jdk 는 17 버전을 사용했습니다.





일반적인 사용법


테스트 Main 클래스

package coding.toast;

import coding.toast.job.SampleJob;
import org.quartz.*;
import org.quartz.core.jmx.JobDataMapSupport;
import org.quartz.impl.StdSchedulerFactory;

import java.util.Map;
import java.util.Properties;

/**
 * 테스트 코드를 실행해볼 Main 클래스
 */
public class Main {
    public static void main(String[] args) {

        Scheduler scheduler = null;
        try {

            // Scheduler 의 설정값으로 사용할 Properties 를 생성합니다.
            // http://www.quartz-scheduler.org/documentation/quartz-2.3.0/configuration/ 참고
            // 참고: 사실 이미 디폴트 세팅값이 있어서 굳이 이렇게 세팅하지 않아도 됩니다.
            // quartz-2.3.2.jar!/org/quartz/quartz.properties 경로에 기본 설정값들이 이미 있습니다.
            Properties properties = new Properties();
            properties.setProperty("org.quartz.scheduler.instanceName", "CodingToast_Scheduler");
            properties.setProperty("org.quartz.threadPool.threadCount", "15");
            properties.setProperty("org.quartz.threadPool.threadPriority", "4");
            properties.setProperty("org.quartz.jobStore.class", "org.quartz.simpl.RAMJobStore");

            // 스케줄러를 생성합니다. 참고로 생성했다고 스케줄러가 실제 동작 상태에 들어간 게 아닙니다.
            scheduler = new StdSchedulerFactory(properties).getScheduler();

            // 스케줄러가 수행해야 할 일(= JobDetail instance)에서 사용할 DataMap 을 생성합니다.
            JobDataMap jobDataMap = JobDataMapSupport
                    .newJobDataMap(Map.of(
                            "param1", "value1",
                            "param2", "value2"
                    ));

            // JobBuilder 를 통해서 스케줄러에 줄 파리미터인 JobDetail 을 생성합니다.
            JobDetail jobDetail = JobBuilder.newJob()
                    .ofType(SampleJob.class) // 반드시 세팅해줘야 한다!
                    .withIdentity("jobDetail1", "jobGroup")  // 스케줄러가 JobDetail 을 다른 JobDetail 들과 구별하기 위한 일종의 아이디를 제공합니다.
                    .setJobData(jobDataMap) // 앞서 생성한 DataMap 을 넘겨줍니다.
                    .usingJobData("param3", "value3") // DataMap 에 추가적인 값들을 넣어줍니다.
                    .withDescription("a simple quartz job") // 이 일(JobDetail)에 대한 설명을 작성해줍니다.
                    .build();


            // 상세하게 "어떤 것"(= JobDetail) 을 할지를 지정했습니다만,
            // 정작 중요한 "언제할 지" 가 결정되지 않은 상태입니다.
            // 이를 위해서 Trigger 인스턴스를 생성합니다.
            Trigger trigger = TriggerBuilder.newTrigger()
                    .withIdentity("trigger1", "triggerGroup")
                    // .usingJobData(jobDataMap)  // JobDetail 과 마찬가지로 DataMap 을 사용할 수 있습니다.
                    // 만약에 같은 시간에 수행해야될 Trigger 가 존재한다면, 
                    // 그때 우선순위를 어떻게 할지 지정합니다.
					// .withPriority()

					// cron 을 통한 주기를 줍니다.
                    // https://www.freeformatter.com/cron-expression-generator-quartz.html 참고
                    .withSchedule(CronScheduleBuilder.cronSchedule("0/2 * * * * ? *"))
                    
                    // cron 이 너무 어렵다면 아래처럼 설정할 수도 있습니다.
                    // .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                    // .withIntervalInSeconds(1).repeatForever())
                    
                    .build(); // 빌드하여 Trigger 인스턴스 생성


            // "언제(=trigger)", "무엇(=jobDetail)"을 할지를 스케줄러에게 알려줍니다.
            scheduler.scheduleJob(jobDetail, trigger);

            // start 를 호출해야 스케줄러가 standby mode 에 들어가서
            // 주입된, 그리고 주입될 Job 들을 실행할 수 있는 상태가 됩니다.
            scheduler.start();


            // 10초 정도만 스케줄러를 실행시키겠습니다.
            Thread.sleep(10000);


        } catch (SchedulerException | InterruptedException e) {
            // 테스트니까 에러는 크게 신경쓰지 않겠습니다.
            e.printStackTrace();
        } finally {
            // scheduler 는 shutdown 해야만 프로그램이 종료됩니다.
            // 참고로 scheduler 는 데몬 쓰레드가 아닌 user 쓰레드이기 때문에 shutdown 을 안 하면 프로그램이 종료되지 않습니다.
            if (scheduler != null) try {
                scheduler.shutdown();
            } catch (SchedulerException e) {
                e.printStackTrace();
            }
        }
    }
}

참고

사실 Quartz 는 이미 Scheduler 디폴트 세팅값이 있어서 굳이 Properties 인스턴스를
생성해서 생성자에 주입할 필요는 없습니다. 해당 기본 세팅값들은
quartz-2.3.2.jar!/org/quartz/quartz.properties 경로에 있습니다.
그리고 이 기본 설정값을 사용하는 default scheduler 를 사용하고 싶다면
아래 팩토리 메소드를 호출하세요.

Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();



org.quartz.Job 구현체 생성

구체적으로 Quartz 에게 어떤 일을 시킬지를 결정하려면
저희가 직접 org.quartz.Job 를 구현한 클래스를 생성해야 합니다.
아래 예시는 아주 심플한 구현입니다.

package coding.toast.job;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


public class SampleJob implements Job {

    private static final Logger log
    	= LoggerFactory.getLogger(SampleJob.class);
    
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // Trigger 와 JobDetail 양쪽에서 설정한 DataMap 을 합쳐서 보여준다.
        // 참고로 가져와서 setting 하는 건 안된다.
 		context.getMergedJobDataMap()
        	.forEach((key, val) -> log.info("key: {} / value: {}", key, val));

        // 세팅했던 Quartz 핵심 도메인을 조회한다.
        //System.out.println(context.getJobDetail());
        //System.out.println(context.getTrigger());
    }
}





Listener 등록 테스트 코드


Listener 를 사용하면 Job, Trigger 가 수행하기 전/후에 대하여
추가적인 동작(ex: 모니터링, 로깅 등)을 지정할 수 있습니다.
등록 방법에 대해서는 코드를 통해서 확인해보시기 바랍니다.


테스트 Main 클래스

package coding.toast;

import coding.toast.job.SampleJob;
import coding.toast.listener.MyJobListener;
import coding.toast.listener.MyTriggerListener;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

import java.util.Map;
import java.util.Properties;

import static org.quartz.CronScheduleBuilder.cronSchedule;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;
import static org.quartz.core.jmx.JobDataMapSupport.newJobDataMap;


public class TestJobListenerMain {
    public static void main(String[] args) {

        Scheduler scheduler = null;
        try {

            // Scheduler 의 설정값으로 사용할 Properties 를 생성합니다.
            // http://www.quartz-scheduler.org/documentation/quartz-2.3.0/configuration/ 참고
            Properties properties = new Properties();
            properties.setProperty("org.quartz.scheduler.instanceName", "CodingToast_Scheduler");
            properties.setProperty("org.quartz.threadPool.threadCount", "15");
            properties.setProperty("org.quartz.threadPool.threadPriority", "4");
            properties.setProperty("org.quartz.jobStore.class", "org.quartz.simpl.RAMJobStore");

            // 스케줄러를 생성합니다. 참고로 생성했다고 스케줄러가 실제 동작 상태에 들어간 게 아닙니다.
            scheduler = new StdSchedulerFactory(properties).getScheduler();

            // 스케줄러가 수행해야 할 일(= JobDetail instance)에서 사용할 DataMap 을 생성합니다.
            JobDataMap jobDataMap = newJobDataMap(
            	Map.of("param1", "value1", "param2", "value2")
			);

            // JobBuilder 를 통해서 스케줄러에 줄 파리미터인 JobDetail 을 생성합니다.
            JobDetail jobDetail = newJob()
                    .ofType(SampleJob.class)
                    .withIdentity("jobDetail-1", "jobGroup")
                    .setJobData(jobDataMap)
                    .build();

            // Trigger 생성
            Trigger trigger = newTrigger()
                    .withIdentity("trigger-1", "triggerGroup")
                    .withSchedule(cronSchedule("0/2 * * * * ? *"))
                    .build();


            // 스케줄러에 특정 시점에 수행할 작업을 등록
            scheduler.scheduleJob(jobDetail, trigger);


            // ** 리스너 등록합니다!
            ListenerManager listenerManager = scheduler.getListenerManager();
            listenerManager.addJobListener(new MyJobListener());
            listenerManager.addTriggerListener(new MyTriggerListener());

            scheduler.start();
            Thread.sleep(10000);

        } catch (SchedulerException | InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (scheduler != null) try {
                scheduler.shutdown();
            } catch (SchedulerException e) {
                e.printStackTrace();
            }
        }
    }
}



JobListener 구현체

  • job 실행을 모니터링할 Listener 를 생성합니다.
package coding.toast.listener;

import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyJobListener implements JobListener {
    public static final Logger log = LoggerFactory.getLogger(MyJobListener.class);

    @Override
    public String getName() {
        return this.getClass().getSimpleName();
    }

    @Override
    public void jobToBeExecuted(JobExecutionContext context) {
        log.info("============= [JOB Listener] - jobToBeExecuted =============");
    }

    @Override
    public void jobExecutionVetoed(JobExecutionContext context) {
        log.info("============= [JOB Listener] - jobExecutionVetoed =============");
    }

    @Override
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
        log.info("============= [JOB Listener] - jobWasExecuted =============");
    }
}



TriggerListener 구현체

trigger 실행을 모니터링할 Listener 를 생성합니다.

package coding.toast.listener;

import org.quartz.JobExecutionContext;
import org.quartz.Trigger;
import org.quartz.TriggerListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyTriggerListener implements TriggerListener {
    public static final Logger log = LoggerFactory.getLogger(MyTriggerListener.class);

    @Override
    public String getName() {
        return this.getClass().getSimpleName();
    }

    @Override
    public void triggerFired(Trigger trigger, JobExecutionContext context) {
        log.info("==================== [TRIGGER Listener] - triggerFired ====================");
    }

    @Override
    public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {
        log.info("==================== [TRIGGER Listener] - vetoJobExecution ====================");
        return false;
    }

    @Override
    public void triggerMisfired(Trigger trigger) {
        log.info("==================== [TRIGGER Listener] - triggerMisfired ====================");
    }

    @Override
    public void triggerComplete(Trigger trigger, JobExecutionContext context, Trigger.CompletedExecutionInstruction triggerInstructionCode) {
        log.info("==================== [TRIGGER Listener] - triggerComplete ====================");
    }
}



참고: Listener.vetoJobExecution 메소드

이 메소드의 return 값을 통해서 Job 의 실제 여부를 결정합니다.

  • true(=Job 실행금지), false(=Job 실행승인)입니다.
    • 반환값이 true(=Job 실행철회) 이면 JobListener.jobExecutionVetoed 메소드만 실행
    • false(=Job 실행승인) 이면 jobToBeExecuted, jobWasExecuted 메소드만 실행
  • 메소드 이름을 직역하면 잡 실행(JobExecution) 을 금지(veto)한다 입니다.
  • Job 을 시행에 앞서 유효성 검사 수행 시에 유용합니다.

return false 의 경우

  • 빨간선으로 각 Job 실행을 구분했습니다.
  • Quartz Scheduler 는 하나의 Job 에 대해서는 같은 Worker Thread 를 사용하는걸
    확인할 수 있습니다.

return true 의 경우




One Job ⇿ Multiple Trigger

Quartz 는 하나의 Job 에 여러 Trigger 를 등록할 수 있습니다.
하지만 반대로 하나의 Trigger 에 여러 Job 은 매칭시킬 수 없습니다 (불편🤔).

테스트 Main 클래스

package coding.toast;

import coding.toast.job.SampleJob;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

import java.util.Map;
import java.util.Properties;
import java.util.Set;

import static org.quartz.CronScheduleBuilder.cronSchedule;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;
import static org.quartz.core.jmx.JobDataMapSupport.newJobDataMap;


public class TestOneJobBindWithMultipleTriggerMain {
    public static void main(String[] args) {

        Scheduler scheduler = null;
        try {

            // http://www.quartz-scheduler.org/documentation/quartz-2.3.0/configuration/ 참고
            Properties properties = new Properties();
            properties.setProperty("org.quartz.scheduler.instanceName", "CodingToast_Scheduler");
            properties.setProperty("org.quartz.threadPool.threadCount", "4"); // thread pool size
            properties.setProperty("org.quartz.threadPool.threadPriority", "4");
            properties.setProperty("org.quartz.jobStore.class", "org.quartz.simpl.RAMJobStore");

            // 스케줄러를 생성합니다. 참고로 생성했다고 스케줄러가 실제 동작 상태에 들어간 게 아닙니다.
            scheduler = new StdSchedulerFactory(properties).getScheduler();

            // 스케줄러가 수행해야 할 일(= JobDetail instance)에서 사용할 DataMap 을 생성합니다.
            JobDataMap jobDataMap = newJobDataMap(Map.of("param1", "value1"));


            //하나의 Job 에 여러 Trigger 걸기
            JobDetail jobDetail = newJob()
                    .ofType(SampleJob.class)
                    .withIdentity("jobDetail-1")
                    .setJobData(jobDataMap)
                    .build();


            // 1 초에 한번
            Trigger trigger = newTrigger()
                    .withIdentity("trigger-1-sec", "triggerGroup")
                    .withSchedule(cronSchedule("0/1 * * * * ? *"))
                    .build();

            // 3 초에 한번
            Trigger trigger2 = newTrigger()
                    .withIdentity("trigger-3-sec", "triggerGroup")
                    .withSchedule(cronSchedule("0/3 * * * * ? *"))
                    .build();


            // 하나의 Job 에 Trigger 를 여러개 넣을 수 있는 방법들
            // scheduler.scheduleJob(jobDetail, Set.of(trigger, trigger2), false); // 방법 1
            scheduler.scheduleJobs(Map.of(jobDetail, Set.of(trigger, trigger2)), false); // 방법 2

            // 스케줄러 시작
            scheduler.start();
            Thread.sleep(9000);

        } catch (SchedulerException | InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (scheduler != null) try {scheduler.shutdown();} catch (SchedulerException e) {e.printStackTrace();}
        }
    }
}




딱 한번만 실행하기

이때는 SimpleTrigger 를 생성해서 scheduler.scheduleJob 의 파라미터로
넘겨주면 끝입니다.

package coding.toast;

import coding.toast.job.SampleJob;
import org.quartz.*;
import org.quartz.core.jmx.JobDataMapSupport;
import org.quartz.impl.StdSchedulerFactory;

import java.util.Map;
import java.util.Properties;


public class TestExecuteOnlyOnceMain {
    public static void main(String[] args) {

        Scheduler scheduler = null;
        try {

            Properties properties = new Properties();
            properties.setProperty("org.quartz.scheduler.instanceName", "coding-toast");

            scheduler = new StdSchedulerFactory(properties).getScheduler();

            JobDetail jobDetail = JobBuilder.newJob()
                    .ofType(SampleJob.class)
                    .withIdentity("attempt-only-once", "jobGroup")
                    .build();

			// SimpleScheduleBuilder.simpleSchedule() 를 사용하면 SimpleTrigger
            // 인스턴스가 생성됩니다. SimpleTrigger 를 사용하면 
            // 스케줄러에서 딱 한번만 실행하고 끝낼 수 있습니다.
            SimpleTrigger trigger = TriggerBuilder.newTrigger()
                    .withIdentity("trigger1", "triggerGroup")
                    .withSchedule(SimpleScheduleBuilder.simpleSchedule())
                    .build();


            scheduler.scheduleJob(jobDetail, trigger);
            scheduler.start();
            Thread.sleep(10000); // 10 초 동안에 딱 한번만 실행됩니다.

        } catch (SchedulerException | InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (scheduler != null) try {
                scheduler.shutdown();
            } catch (SchedulerException e) {
                e.printStackTrace();
            }
        }
    }
}




참고


1. logback 설정

위와 같이 resources 디렉토리 하단에 logback.xml 이라는 파일을 생성하고,
아래와 같이 내용을 작성해주면 됩니다.

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %magenta(%-4relative) --- [ %thread{10} ] %cyan(%logger{20}) : %msg%n
            </pattern>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>



profile
백엔드를 계속 배우고 있는 개발자입니다 😊

0개의 댓글