개요
Quartz는 오픈소스 잡 스케쥴링 프레임워크로 자바를 사용합니다. 단순하면서도 엄청난 유연성을 가지고 있으며 이를 통해 스케쥴링 추가와 DB 클러스터링 설정을 해보겠습니다.
@Scheduled vs Spring Batch vs Quartz
스케쥴링을 위한 기술로는 크게 3가지를 선택할 수 있습니다. 스프링 기본 기능의 @Scheduled, Spring Batch, Quartz입니다.
처음으로 @Scheduled는 스케쥴링 기본 기능만 제공하며 DB 클러스터링이 가능하지 않습니다. 외부 라이브러리인 Shed Lock을 사용하여 제어할 수 있으나 현재 java 버전에 맞지 않아서 사용이 불가합니다. 현재 이 방식을 사용하고 있었는데 INSERT 작업의 경우 2개의 서버에서 중복 작업하는 문제가 발생하여 수정이 필요했습니다.
두번째는 Spring Batch입니다. 이는 로깅/추적, 트랜잭션 관리, 작업 처리 통계, 작업 재시작, 건너뛰기, 리소스 관리가 가능하며 좀 더 대규모 레코드 환경에 적합하다고 판단하였습니다. 러닝커브가 존재하며 배치로 처리될 데이터의 양이 대량이 아니므로 우선순위에서 제외했습니다.
세번째는 Quartz입니다. 여러 개의 서버 중에 로드밸런싱을 하며, 하나의 서버가 다운되더라도 다른 서버에서 스케줄을 할 수 있는 장점이 있습니다. 또한 초기 구성에 많은 어려움이 없습니다. INSERT 작업이 있는 스케줄에 중복 작업 문제도 자연스럽게 해결됩니다.
결국 마지막으로 자바로 적당한 코드를 작성하여 스케쥴링과 DB 클러스터링이 가능한 Quartz를 선택하였습니다.
Quartz API란?
Quartz 프레임워크의 핵심은 스케쥴러입니다. 어플리케이션을 위해 런타임 환경을 관리합니다.
Quartz는 가용성(scalability)을 향상시키기 위해 멀티 쓰레드 아키텍처로 구성됩니다. 프레임워크가 시작되면 job을 실행하기 위해 스케쥴러에 사용되는 워커 쓰레드들을 초기화합니다. 따라서 프레임워크는 많은 job을 동시에 실행할 수 있습니다. 또한 프레임워크 환경을 관리하기 위해서 쓰레드풀을 사용합니다.
중요한 인터페이스 API는 다음과 같습니다.
- Scheduler: 프레임워크의 스케쥴러와 통신하기 위한 중요한 API 입니다.
- Job: 실행하고 싶은 요소에 의해 구현될 인터페이스입니다.
- JobDetail: Job들의 인스턴스를 정의합니다.
- Trigger: 주어진 Job이 수행되는 스케줄을 결정하는 요소입니다.
- JobBuilder: Job 인스턴스를 정의하는 JobDetail 인스턴스들을 만들기 위해 사용됩니다
- TriggerBuilder: Trigger 인스턴스를 정의합니다.
(중요한 인터페이스 API의 일부는 맨 아래에서 예시로 설명하겠습니다.)
Quartz 라이브러리 장점 3가지
1. 고가용성(High availability)
고가용성 애플리케이션은 높은 비율의 시간 동안 클라이언트에게 서비스를 제공할 수 있습니다. 서버가 하나 다운되더라도 다른 서버의 Job이 실행합니다.
2. 확장성(Scalability)
확장성은 애플리케이션에 하드웨어 같은 자원을 추가하거나 용량을 늘리는 것입니다. Quartz 설정이 되어있는 서버를 추가하면 자동으로 같은 클러스터 범위에서 관리됩니다.
3. 로드 밸런싱(Load Balancing)
확장성을 달성하기 위해서 클러스터에서 노드들에 작업을 분산하는 능력이 중요합니다. 작업을 분산하면 클러스터의 각 노드가 작업 부하를 분담할 수 있습니다.
현재 쿼츠는 랜덤 알고리즘 기반으로 최소한의 로드 밸런싱 기능을 제공합니다. 스케쥴러 인스턴스들은 trigger를 시작해서 job을 실행하는 권한을 가지기 위해 database lock 경쟁하여 특정 인스턴스가 작업을 시작하면 다른 인스턴스는 작업을 멈춥니다.
코드로 작성하기
- pom.xml 설정
가장 먼저 의존성에 quartz 스케쥴링을 추가합니다.
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.0</version>
</dependency>
- BaseJob.java
BaseJob.java는 QuartzJobBean을 상속하는 클래스로 executeInternal() 메서드에 스케쥴링 동작을 정의합니다. ScheduleSerivice 의존성 주입이 제대로 되지 않아 ApplicationContext에서 직접 빈을 주입하는 방식을 사용하였습니다.
public class BaseJob extends QuartzJobBean {
private SchedulerService scheDulerService;
@Override
protected void executeInternal(JobExecutionContext arg0) throws JobExecutionException {
scheDulerService = (SchedulerService) BeanUtils.getBean("SchedulerServiceImpl");
Calendar calendar = Calendar.getInstance();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
log.info("::::::: 제품 가격 설정 스케줄러 start ::::::: {}", dateFormat.format(calendar.getTime()));
scheDulerService.showPrice();
log.info("::::::: 제품 가격 설정 스케줄러 end ::::::: ");
}
}
- BeanUtils.java, ApplicationContextProvider.java
아래의 코드로 스프링 컨테이너에서 직접 빈을 호출할 수 있습니다.
public class BeanUtils {
public static Object getBean(String beanName) {
ApplicationContext applicationContext = ApplicationContextProvider.getApplicationContext();
return applicationContext.getBean(beanName);
}
}
@Component
public class ApplicationContextProvider implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
applicationContext = ctx;
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
}
- database-context.xml 설정
DB 클러스터링을 사용하기 위해서 참조할 데이터베이스 정보가 필요합니다. 따라서, 데이터베이스를 쉽게 참조할 수 있는 database-context.xml 파일에 Quartz와 관련된 설정을 추가하였습니다.
/*Oracle DB를 사용하기 위한 bean을 정의한다
SchedulerFactoryBean은 dataSource를 확인해 스케쥴링을 실행한다
2개의 WAS에 이중화되어 있는 코드와 DB는 DB 클러스터링 설정으로 한쪽에서만 실행된다 */
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="ORA_rental" />
</bean>
...
/*Quartz 스케쥴링을 위한 bean 정의한다
baseJob 클래스의 executeInternal() 메서드에 정의된 내용이 실행된다 */
<bean name="baseJob" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<property name="jobClass" value="com.starmark.adm.util.scheduler.config.BaseJob"/>
<property name="durability" value="true"/>
<property name="group" value="BLOG_GROUP"/>
</bean>
/* cronTrigger는 baseJob을 어떤 주기로 실행할지 정의한다 */
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="baseJob"/>
<property name="cronExpression" value="0/10 * * * * ?"/>
</bean>
/* SchedulerFactoryBean을 정의하며 quartzProperties에서 쿼츠 속성을 정의한다
cronTrigger bean을 참조하여 정해진 주기로 실행한다
*/
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="cronTrigger"/>
</list>
</property>
<property name="dataSource" ref="dataSource"/>
<property name="applicationContextSchedulerContextKey" value="applicationContext"/>
<property name="autoStartup" value="true"/>
<property name="overwriteExistingJobs" value="true"/>
<property name="waitForJobsToCompleteOnShutdown" value="true"/>
<property name="quartzProperties">
<props>
<prop key="org.quartz.jobStore.class">org.quartz.impl.jdbcjobstore.JobStoreTX</prop>
<prop key="org.quartz.jobStore.driverDelegateClass">org.quartz.impl.jdbcjobstore.StdJDBCDelegate</prop>
<prop key="org.quartz.jobStore.dataSource">dataSource</prop>
<prop key="org.quartz.jobStore.tablePrefix">QRTZ_</prop>
<prop key="org.quartz.jobStore.isClustered">true</prop>
<prop key="org.quartz.jobStore.clusterCheckinInterval">42300</prop>
<prop key="org.quartz.jobStore.misfireThreshold">50000</prop>
<prop key="org.quartz.scheduler.instanceId">AUTO</prop>
</props>
</property>
</bean>
quartzProperties 속성은 다음과 같습니다.
- org.quartz.jobStore.class - JobStore 클래스를 지정합니다.
- org.quartz.jobStore.driverDelegateClass - 데이터베이스 별로 다른 SQL을 이해할 수 있는 클래스를 지정합니다.
- org.quartz.jobStore.dataSource - JobStore가 사용할 데이터소스를 지정합니다.
- org.quartz.jobStore.tablePrefix - 데이터베이스에 생성된 Quartz 테이블에 주어진 접두사를 지정합니다.
- org.quartz.jobStore.isClustered - 클러스터링 기능을 사용하려면 true로 설정합니다.
- org.quartz.jobStore.clusterCheckinInterval
- 클러스터의 다른 인스턴스에 체크인하는 빈도를 설정합니다.
- 실패한 인스턴스를 감지하는 속도에 영향을 줍니다. (ms 단위)
- Quartz DB 클러스터링을 위한 테이블 추가하기
Quartz 클러스터링을 위해서는 테이블을 추가해야 합니다.
공식 사이트에서 각 DB종류에 따라 생성해야 할 테이블이 있습니다.
현재 운영 환경이 오라클 이므로 오라클 테이블을 생성하였습니다. 테이블 정보는 아래 주소에 있습니다.
https://github.com/quartznet/quartznet/blob/main/database/tables/tables_oracle.sql
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_locks
(
SCHED_NAME VARCHAR2(120) NOT NULL,
LOCK_NAME VARCHAR2(40) NOT NULL,
CONSTRAINT QRTZ_LOCKS_PK PRIMARY KEY (SCHED_NAME,LOCK_NAME)
);
이로써 DB 클러스터링이 적용된 스케쥴링이 완성되었습니다. 새로운 서버가 추가된다면 database-context.xml에 같은 datasource를 참조하도록 Quartz를 설정하여 같은 클러스터링에 묶을 수 있습니다.
Quartz 주요 특징
처음에 소개한 대로 Quartz에는 다양한 기능이 있는데 Scheduler, Jobs, Triggers, CronTrigger를 소개하겠습니다.
Scheduler
Scheduler를 이용하기 전에, 초기화가 되어야 합니다. 아래 코드로 가능합니다.
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
Scheduler의 생명주기는 SchedulerFactory로 생성되며 shutdown() 메서드 호출로 종료됩니다. 한번 생성되면 Scheduler 인터페이스는 Job과 Trigger 추가, 삭제, 조회 그리고 다른 스케줄과 관련된 동작 수행에 사용됩니다. 그러나, Scheduler는 start() 메서드가 시작되기 전까지는 어떠한 trigger도 수행하지 않습니다.
scheduler.start();
Jobs
Job은 Job 인터페이스를 구현하는 클래스입니다. 1개의 메서드를 가지고 있습니다.
public class SimpleJob implements Job {
public void execute(JobExecutionContext arg0) throws JobExecutionException {
System.out.println("This is a quartz job!");
}
}
Job의 trigger가 실행되면, 스케쥴러의 작업 스레드 중 하나가 execute() 메서드를 호출합니다.
메서드 매개변수의 JobExecutionContext 객체는 job 인스턴스를 제공합니다. 런타임 환경 정보가 있으며 스케쥴러에 대한 관리, 실행을 유발한 Trigger 관리, job의 JobDetail 객체 등이 있습니다.
JobDetail 객체는 Job이 스케쥴러에 추가될 때 Quartz에 의해 만들어집니다.
JobDetail job = JobBuilder.newJob(SimpleJob.class)
.withIdentity("myJob", "group1")
.build();
Triggers
Trigger 객체는 Job을 실행합니다. Job 스케줄을 실행하고 싶을 때, trigger를 초기화하고 스케쥴링 요구사항을 설정하기 위해 속성을 정합니다.
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("myTrigger", "group1")
.startNow()
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(40)
.repeatForever())
.build();
Trigger는 JobDataMap을 활용해서 tigger 실행과 관련된 매개변수를 Job에 전달하는데 유용합니다.
스케쥴링 종류에 따라 trigger의 종류도 달라집니다. 각 trigger는 자신만의 TriggerKey 속성이 있습니다. 일부 속성은 모든 trigger에 공통입니다.
- jobKey: tigger가 시작할 때 실행되는 job의 식별성입니다.
- startTime: tigger의 스케줄이 시작시간입니다.
- endTime: tigger의 스케쥴이 끝나는 시간입니다
Quartz는 다양한 trigger 종류가 있는데, 가장 많이 사용되는 것은 SimpleTrigger와 CronTrigger입니다.
CronTrigger
CronTrigger는 달력과 같은 시간을 기반으로 스케줄을 할 때 사용합니다. 예를 들어, 매일 금요일 12시 혹은 매일 주말 아침 9시 30분처럼 특정할 수 있습니다.
Cron 표현식은 CronTrigger 인스턴스를 설정합니다. 이런 표현식들은 7개의 하위 표현식을 가지는 String으로 만듭니다. 아래 예는 매일 8am부터 5pm까지 매 2분 job을 실행시킵니다.
CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger3", "group1")
.withSchedule(CronScheduleBuilder.cronSchedule("0 0/2 8-17 * * ?"))
.forJob("myJob", "group1")
.build();
이로써 Quartz의 특징과 사용법을 간단하게 정리했습니다. 스케쥴링과 DB 클러스터링 기능을 쉽고 빠르게 사용할 수 있는 기술로서 다양한 상황에 고려해 보시길 바랍니다.
참고
https://www.baeldung.com/quartz
https://junhyunny.github.io/spring-mvc/quartz-clustering-in-spring-mvc/
https://flylib.com/books/en/2.65.1/what_does_clustering_mean_to_quartz_.html
'문제 해결, 기술 비교 > 실무 업무 회고' 카테고리의 다른 글
할인 쿠폰 개선하기 (0) | 2022.12.28 |
---|---|
수기 업로드 작업 자동화 및 이메일 솔루션 사용 (0) | 2022.12.16 |
NICE API 일시불 취소 개발하기 (0) | 2022.12.09 |
Jenkins로 CI/CD 구축하기 (0) | 2022.06.02 |
CI 어떤 도구를 사용할까?(GitLab CI vs Jenkins vs Travis CI) (0) | 2022.05.25 |