Spring Boot Quartz를 활용한 동적 스케줄링 및 DB 영속화 구현

Quartz는 Java 환경에서 널리 사용되는 강력한 작업 스케줄링 프레임워크입니다. Spring Boot와 통합하면 단순한 반복 작업을 넘어, 실행 중에 스케줄을 변경하거나 작업 상태를 데이터베이스에 저장하여 서버 재시작 후에도 작업을 유지하는 영속화 기능을 쉽게 구현할 수 있습니다.

1. 의존성 설정

Spring Boot 프로젝트에서 Quartz를 사용하기 위해 다음의 의존성을 pom.xml에 추가합니다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
</dependency>

2. 사용자 정의 커넥션 프로바이더 구현

Quartz가 기존 애플리케이션의 커넥션 풀을 활용하도록 ConnectionProvider 인터페이스를 구현합니다. 아래 예시는 리플렉션을 통해 기존에 설정된 데이터소스를 참조하는 방식을 보여줍니다.

package com.example.config;

import org.quartz.utils.ConnectionProvider;
import java.sql.Connection;
import java.sql.SQLException;
import com.mchange.v2.c3p0.ComboPooledDataSource;

public class CustomQuartzConnectionProvider implements ConnectionProvider {

    private ComboPooledDataSource customDataSource;

    // Quartz 설정에 의해 자동 주입되는 속성들
    public String driver;
    public String URL;
    public String user;
    public String password;
    public int maxConnections;

    @Override
    public Connection getConnection() throws SQLException {
        return DataSourceManager.getInstance().getDataSource().getConnection();
    }

    @Override
    public void shutdown() throws SQLException {
        // 커넥션 풀 종료 로직
    }

    @Override
    public void initialize() throws SQLException {
        // 이미 초기화된 데이터소스를 사용하므로 추가 로직 생략 가능
    }
}

3. Quartz 속성 설정

application.yml 또는 application.properties 파일에 Quartz의 동작 방식과 JDBC 기반의 영속성 설정을 정의합니다.

# Quartz 기본 설정
org.quartz.scheduler.instanceName=CustomScheduler
org.quartz.scheduler.instanceId=AUTO
org.quartz.threadPool.threadCount=5

# JobStore 설정 (데이터베이스 영속화)
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix=QRTZ_
org.quartz.jobStore.isClustered=true
org.quartz.jobStore.dataSource=myDs

# 데이터소스 연결 설정
org.quartz.dataSource.myDs.connectionProvider.class=com.example.config.CustomQuartzConnectionProvider
org.quartz.dataSource.myDs.driver=com.mysql.cj.jdbc.Driver
org.quartz.dataSource.myDs.URL=jdbc:mysql://localhost:3306/quartz_db
org.quartz.dataSource.myDs.user=admin
org.quartz.dataSource.myDs.password=pass123

4. 스케줄러 빈(Bean) 설정

설정 파일의 정보를 바탕으로 SchedulerFactoryBean을 구성하여 Spring 컨텍스트에 등록합니다.

@Configuration
public class QuartzSchedulerConfig {

    @Bean
    public Properties quartzProperties() {
        Properties props = new Properties();
        // YAML이나 Properties에서 값을 읽어와 설정
        return props;
    }

    @Bean
    public Scheduler scheduler(QuartzJobFactory jobFactory) throws Exception {
        SchedulerFactory schedulerFactory = new StdSchedulerFactory(quartzProperties());
        Scheduler scheduler = schedulerFactory.getScheduler();
        // Spring Bean 주입을 위한 커스텀 팩토리 설정
        scheduler.setJobFactory(jobFactory);
        scheduler.start();
        return scheduler;
    }
}

5. 작업 관리를 위한 데이터 모델

작업의 이름, 그룹, 실행 클래스명, Cron 표현식 등을 관리하기 위한 엔티티를 정의합니다.

@Data
public class ScheduleRequest {
    private String taskName;
    private String taskGroup;
    private String cronExpression;
    private String targetClassName;
    private String description;
    private Map<String, Object> params;
}

6. 동적 작업 제어 서비스

Scheduler 인터페이스를 사용하여 런타임에 작업을 추가, 수정, 삭제하는 기능을 구현합니다.

@Service
@RequiredArgsConstructor
public class DynamicJobService {

    private final Scheduler scheduler;

    public void registerNewJob(ScheduleRequest request) throws Exception {
        JobKey jobKey = JobKey.jobKey(request.getTaskName(), request.getTaskGroup());
        
        if (scheduler.checkExists(jobKey)) {
            throw new RuntimeException("이미 존재하는 작업입니다.");
        }

        Class<? extends Job> jobClass = (Class<? extends Job>) Class.forName(request.getTargetClassName());
        JobDetail jobDetail = JobBuilder.newJob(jobClass)
                .withIdentity(jobKey)
                .withDescription(request.getDescription())
                .build();

        if (request.getParams() != null) {
            jobDetail.getJobDataMap().putAll(request.getParams());
        }

        CronTrigger trigger = TriggerBuilder.newTrigger()
                .withIdentity(request.getTaskName(), request.getTaskGroup())
                .withSchedule(CronScheduleBuilder.cronSchedule(request.getCronExpression()))
                .build();

        scheduler.scheduleJob(jobDetail, trigger);
    }

    public void updateJobSchedule(ScheduleRequest request) throws Exception {
        TriggerKey triggerKey = TriggerKey.triggerKey(request.getTaskName(), request.getTaskGroup());
        CronTrigger oldTrigger = (CronTrigger) scheduler.getTrigger(triggerKey);
        
        CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(request.getCronExpression());
        CronTrigger newTrigger = oldTrigger.getTriggerBuilder()
                .withSchedule(scheduleBuilder)
                .build();

        scheduler.rescheduleJob(triggerKey, newTrigger);
    }

    public void terminateJob(String name, String group) throws Exception {
        scheduler.deleteJob(JobKey.jobKey(name, group));
    }
}

7. Job 내 의존성 주입 활성화

기본적으로 Quartz는 Job 인스턴스를 직접 생성하기 때문에 Spring의 @Autowired가 동작하지 않습니다. 이를 해결하기 위해 AdaptableJobFactory를 상속받아 커스텀 팩토리를 만듭니다.

@Component
public class QuartzJobFactory extends AdaptableJobFactory {

    @Autowired
    private AutowireCapableBeanFactory beanFactory;

    @Override
    protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
        Object jobInstance = super.createJobInstance(bundle);
        // 생성된 Job 인스턴스에 Spring Bean 주입
        beanFactory.autowireBean(jobInstance);
        return jobInstance;
    }
}

8. DB 작업 상태 조회 쿼리

현재 스케줄링된 작업들의 상세 상태와 실행 정보를 확인하려면 Quartz 시스템 테이블을 조인하여 조회합니다.

SELECT 
    JD.JOB_NAME, 
    JD.JOB_GROUP, 
    JD.JOB_CLASS_NAME, 
    T.TRIGGER_STATE, 
    CT.CRON_EXPRESSION, 
    T.NEXT_FIRE_TIME
FROM QRTZ_JOB_DETAILS JD
JOIN QRTZ_TRIGGERS T ON JD.JOB_NAME = T.JOB_NAME AND JD.JOB_GROUP = T.JOB_GROUP
JOIN QRTZ_CRON_TRIGGERS CT ON T.TRIGGER_NAME = CT.TRIGGER_NAME AND T.TRIGGER_GROUP = CT.TRIGGER_GROUP;

태그: Spring Boot Quartz Scheduling JDBC java

8월 13일 16:46에 게시됨