Quartz Scheduler 클러스터링 구성

중고 개발자
중고 개발자
Published in
10 min readMay 30, 2021
QUARTZ

Quartz는 Job Scheduling 라이브러리다. Java Application에서 동작할 수 있고, stand-alone 방식부터 클러스터 방식까지 지원한다. 단순하거나 복잡한 스케줄을 생성하는데 사용할 수 있으며, 작업은 표준 Java 컴포넌트로 정의된다. 프로그램에 특정 시점에 수행이 필요한 작업이 있는 경우, 혹은 반복적인 작업이 있는 경우에 사용하기 편리하다.

도입 이유

Quartz는 Spring Batch와 같이 사용되는 경우가 많은 것 같다. Spring Batch는 대용량의 데이터를 처리하는데 조금 더 적합해 보이고, 내가 작업하려는 내용은 비교적 단순한 작업이어서 Quartz만 사용했다. 대용량 데이터 처리나 작업 기록 관리, 대시보드 등의 기능은 필요 없었고, 하루 한번 수행하는 메인터넌스 작업 및 주기적으로 수행되는 반복 작업 등 Scheduler의 기능만 필요했기 때문에 Quartz가 적합했다.

기능

런타임 환경

  • Quartz는 다른 애플리케이션에 내장되어 실행할 수도 있고, 애플리케이션 서버에 인스턴스화 하여 실행할 수도 있다. JVM 내에서 stand-alone 프로그램으로 실행할 수 있으며, stand-alone 프로그램들의 클러스터로 인스턴스화할 수 있다.

작업 스케줄링

  • 작업(Job)은 트리거(Trigger)가 발포(Fire)되면 스케줄 된다. 트리거는 특정 시점 혹은 반복 주기에 의해 생성할 수 있다. 작업과 트리거는 이름과 그룹명으로 구성한다. 작업은 스케줄에 한번 등록되지만, 여러 트리거에 의해 등록될 수 있다.

작업 실행

  • 작업은 Java 클래스로 구현된다. 작업 클래스는 Quartz에 의해 인스턴스화될 수 있고, Spring과 같은 프레임워크에 의해서도 인스턴스화될 수 있다.

작업 지속성

  • 작업의 저장소로 다양한 메커니즘을 제공하며, 관계형 데이터베이스도 사용할 수 있고 인 메모리 방식도 사용할 수 있다. 관계형 데이터베이스를 사용하면 작업과 트리거 등 스케줄 정보를 영구 보관할 수 있다. 인 메모리 방식은 외부 데이터베이스를 연계하지 않아도 되지만 시스템 장애 발생 시 스케줄 정보를 존속할 수 없다.

트랜잭션

  • Quartz는 JTA 트랜잭션에 참여할 수 있다. 작업이 JTA 트랜잭션 내에서 자동으로 수행되도록 JTA 트랜잭션을 관리할 수 있다.

클러스터링

  • Quartz 내장 클러스터링 기능은 로드 밸런싱 및 페일 오버를 지원한다. 이 기능은 인 메모리 방식이 아닌 관계형 데이터베이스 방식에 의존한다.

리스너 & 플러그인

  • 애플리케이션은 리스너를 구현하여 작업 및 트리거의 동작을 모니터링하거나 제어하는 스케줄링 이벤트를 잡을 수 있다. 또한 플러그인을 사용하여 작업 실행 기록을 유지하는 등 부가 기능을 추가할 수 있다.

스케줄러 구성

애플리케이션은 Spring을 사용하지만, Quartz의 각 개별 컴포넌트는 Quartz 혹은 Spring에 의해서 구성할 수 있다. 여기서는 Spring으로 구성하였다.

Scheduler 인터페이스는 작업 스케줄러와 인터페이스하는 메인 API 이다. Scheduler는 SchedulerFactory로 인스턴스화되며, 인스턴스가 생성되면 작업과 트리거를 등록할 수 있다.

Spring SchedulerFactoryBean 구성
Spring의 SchedulerFactoryBean을 통해 Bean 스타일을 사용하여 Scheduler 구성을 할 수 있다. ApplicationContext 내에서 생명 주기를 관리하며, Scheduler를 의존 주입을 위한 Bean으로 노출한다.

@Bean
public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) {
log.debug("Setting SchedulerFactoryBean");
SchedulerFactoryBean schedulerFactory = new SchedulerFactoryBean();
schedulerFactory.setDataSource(dataSource);
schedulerFactory.setOverwriteExistingJobs(true);
schedulerFactory.setJobFactory(this.springBeanJobFactory());
schedulerFactory.setQuartzProperties(this.quartzProperties());
return schedulerFactory;
}

SpringBeanJobFactory 구성
SpringBeanJobFactory는 인스턴스를 생성하는 동안, 작업 Bean에 속성으로서 스케줄러 컨텍스트나 작업 데이터 및 트리거를 주입하는 기능을 제공한다. 단, ApplicationContext의 Bean 참조를 주입하는 기능이 없다. 하단 출처의 블로그를 참조하여 아래와 같이 Auto Wiring 기능을 추가하였다.

@Bean
public SpringBeanJobFactory springBeanJobFactory() {
log.debug("Configuring SpringBeanJobFactory");
AutoWiringSpringBeanJobFactory jobFactory = new AutoWiringSpringBeanJobFactory();
jobFactory.setApplicationContext(this.applicationContext);
return jobFactory;
}

Properties 구성
Quartz의 Scheduler와 JobStore에 대한 상세 설정을 아래와 같이 구성하였다. 각 설정 정보는 공식 문서에 자세히 설명되어 있다. 설정 정보 중에서 org.quartz.jobStore.isClustered 값을 true 로 설정하여 쉽게 클러스터링 구성을 할 수 있다. (클러스터링 구성 시 인스턴스 이름과 ID도 필수로 설정해주어야 하며, 데이터베이스도 연계해주어야 한다.)

private Properties quartzProperties() {
Properties props = new Properties();
props.put("org.quartz.scheduler.instanceName", "TestScheduler");
props.put("org.quartz.scheduler.instanceId", "AUTO");
props.put("org.quartz.jobStore.driverDelegateClass", "org.quartz.impl.jdbcjobstore.StdJDBCDelegate");
props.put("org.quartz.jobStore.useProperties", "true");
props.put("org.quartz.jobStore.misfireThreshold", "60000");
props.put("org.quartz.jobStore.isClustered", "true");
return props;
}

스키마 생성
작업과 트리거 등 스케줄과 관련된 정보를 관리하는 스키마를 생성할 수 있다. 작업 저장소를 인 메모리를 사용하거나 작업 정보를 관리할 필요가 없는 경우는 생성할 필요가 없을 수도 있지만, 간단히 작업 정보 관리가 필요하거나 클러스터링으로 구성하는 경우에는 스키마 생성이 필요하다.

스키마 생성을 위해 Quartz의 소스 코드 내부에 DB 별로 테이블을 생성하는 sql 파일이 포함되어 있다. 해당 sql 파일을 직접 수기로 실행하여 DB 내에 테이블을 생성할 수도 있지만, 애플리케이션 기동 시에 자동으로 스키마가 생성되도록 설정 값을 넣어줄 수 있다.

다음은 로컬 환경 DB로 H2 인메모리 DB를 사용했을 때, 스키마 자동 생성을 위해 로컬 환경의 properties 파일(application-local.yml)을 설정한 내용이다. 해당 설정은 Quartz가 아닌 Spring에서 제공하는 설정이다.

spring:
quartz:
jdbc:
initialize-schema: always

MySQL을 사용하는 경우 아래와 같이 comment-prefix 설정을 ‘#’으로 두어야 스키마 생성이 가능하다. Quartz 2.3.1 버전을 사용하는데, MySQL 스키마를 생성하는 sql 파일에서 주석이 ‘--’가 아니라 ‘#’으로 되어 있다.

spring:
quartz:
jdbc:
initialize-schema: always
comment-prefix: #

단, 위와 같이 스키마를 자동 생성할 경우, 애플리케이션이 셧다운되어 다시 기동되거나 재배포가 있을 시에 기존 테이블들을 삭제하고 다시 생성한다. 테이블을 삭제하는 것이 부담된다면 staging 및 production 환경에서는 수기로 테이블을 생성하고 위의 설정 정보를 넣지 않으면 된다.

작업 덮어쓰기 설정
위에서 SchedulerFactoryBean 설정하는 부분을 보면 아래와 같이 이미 존재하는 작업에 덮어쓰기가 가능하게 하는 설정이 있다. 만약에 애플리케이션이 셧다운되어 재 기동되거나 애플리케이션이 확장되는 경우, 스케줄러에 동일한 작업을 다시 등록하여도 해당 부분이 반영되는지 테스트를 해보았다. 테스트한 결과, 작업 등록이 실패되고 애플리케이션이 종료되는 현상이 발생하였다.

schedulerFactory.setOverwriteExistingJobs(true);

원인을 찾아보니 해당 부분이 사용하고 있는 Spring Boot 버전에 반영이 안되어 있었다. 애플리케이션이 재기동되거나 확장되면 동일한 작업이 다시 스케줄러에 등록될 수 밖에 없으므로, 해당 문제를 해결해야 했다. 다행히 Quartz의 스케줄러 API에서 작업 재등록이 가능하도록 메소드를 지원하고 있어서, 결국 로직 내에서 동일한 작업을 재등록할 수 있도록 구현하였다.

결론

위와 같이 Quartz Scheduler를 클러스터링 구성하여 컨테이너 환경에서 테스트 하였다. 애플리케이션의 Replica를 3개로 Scale Up 하여, 작업 실행 로그를 확인한 결과, 스케줄 된 작업이 3개의 애플리케이션에서 랜덤하게 분산되어 실행되었다. Scale Down 하여도 다른 애플리케이션에서 인스턴스가 shutdown 된 것을 감지하여 페일 오버 기능도 작동하는 것을 확인하였다.

몇 가지의 간단한 설정만으로 클러스터링 구성이 가능하기 때문에, 앞으로도 다른 업무에서 Quartz를 통해 작업 스케줄러를 쉽게 적용할 수 있을 것 같다. 다음엔 모니터링 결과 알림과 같은 작업을 구현하여 스케줄러에 적용해보려고 한다.

--

--