[Spring Batch, Quartz Scheduler] Spring Batch + Quartz Scheduler 연동

mrcocoball·2025년 3월 24일

Spring Framework

목록 보기
16/20

개요

이전 직장에서는 Spring Batch를 통해 배치 어플리케이션을 만들고 Kubernetes CronJob을 통해서 실행시켰었는데 이직 후 신규 프로젝트에서 Quartz Scheduler를 처음 사용해보고 Spring Batch와 연계를 해보게 되면서 관련된 내용을 정리해보게 되었다.

Spring Batch와 Quartz Scheduler

Spring Batch는 배치 작업을 위한 어플리케이션을 만드는 데 사용하는 대표적인 프로젝트이며, Quartz Scheduler는 일정 주기에 따라 작업을 실행시킬 수 있는 스케줄러이다.

사실 배치 작업을 할 때에는 1) Quartz Scheduler에서 Job을 만들어서 주기적으로 실행시키는 방법도 있고 (상당히 간단함) 2) Spring Batch를 통해 배치 어플리케이션을 만들고 크론탭 등의 스케줄러를 통해 주기적으로 실행시키는 방법도 있지만

Spring Boot Starter Quartz를 통해 Quartz Scheduler가 Spring Batch의 Job을 인식하게 만들어서 코드 및 Job의 이름으로 간편하게 Spring Batch Job을 실행시킬 수 있게 연동이 가능하다.

이 경우 한 프로젝트에서 배치, 스케줄러를 모두 관리할 수 있고 스케줄러는 스케줄러의 역할을, 배치는 배치의 역할에 집중할 수 있게 되며 멀티 모듈을 사용할 경우 배치와 스케줄러 각각으로 독자적인 배포 역시 가능해진다는 장점이 있다.

설정하기

프로젝트 구조

필자는 멀티 모듈 프로젝트 구조를 활용하여 배치 모듈과 스케줄러 모듈을 각각 분리해두었다.
그리고 스케줄러 모듈 쪽에서 배치 모듈을 의존성으로 가지고 실제 배포 시에는 스케줄러만 배포되게끔 설정하였다.

루트 프로젝트
ㄴ 배치 모듈
ㄴ 스케줄러 모듈

(필요 시) 스케줄러의 컴포넌트 스캔 대상 패키지 지정

단일 모듈 내에 배치와 스케줄러가 모두 존재한다면 상관이 없겠으나 위의 프로젝트 구조처럼 배치 모듈과 스케줄러 모듈이 분리되어 있고, 패키지 루트가 다를 경우, 스케줄러에서 배치 모듈의 Configuration을 인식할 수 있도록 명시적으로 컴포넌트 스캔 대상 패키지를 지정해줘야 한다.

@SpringBootApplication(scanBasePackages = "패키지 경로")
public class SchedulerApplication {

...

스케줄러 내 QuartJobBean 등록

기본적으로 Quartz Scheduler에서 Job으로 등록할 수 있는 것은 Quartz의 Job이며, Spring Batch의 Job과 다르다.
따라서 Quartz Scheduler에서 Spring Batch의 Job을 사용하려면 Spring 프레임워크에서 제공하는 Quartz의 Job 인터페이스의 구현체인 QuartzJobBean 추상 클래스를 상속하여 사용해야 한다.

@Slf4j
@RequiredArgsConstructor
@Component
public class SpringJobLauncher extends QuartzJobBean {

    private final JobLauncher jobLauncher;
    private final JobRegistry jobRegistry;

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        try {
        
            String jobName = context.getJobDetail().getJobDataMap().getString("jobName");
            Job job = jobRegistry.getJob(jobName);
            JobParameters jobParameters = new JobParametersBuilder()
                    .addLong("timestamp", System.currentTimeMillis())
                    .toJobParameters();

            jobLauncher.run(job, jobParameters);

        } 
        catch (Exception e) {
            log.error("error is occurred, detail : ", e);
        }
    }
}

코드 내용은 간단한데
1) Quartz Scheduler의 JobExecutionContext 내에서 JobData 내 "jobName"의 값을 가져온 뒤,
2) Spring Batch의 JobRegistry에서 검색하여 Spring Batch Job을 찾고 그것을 실행시킨다.

스케줄러 Configuration 작성

스케줄러의 Configuration을 작성한다. 이 때 중요한 것은

1) Spring Batch의 JobRegistry를 등록해주어야 한다.
2) SchedulerFactoryBean에서 JobFactory를 SpringBeanJobFactory로 지정해주어야 한다.
3) JobDetail 생성 시 newJob()에 위에서 만들었던 QuartzJobBean을 사용해야 한다.
4) QuartzJobBean의 메서드에서는 JobData 내의 "jobName"의 값을 받아야 하므로 usingJobData에 추가해야 한다.

@RequiredArgsConstructor
@Configuration
public class QuartzConfiguration {

	@Value("${크론식 관련 yaml 내 설정 키}")
    private String jobCron;

	// JobRegistry를 등록
    @Bean
    public JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor(JobRegistry jobRegistry) {
        JobRegistryBeanPostProcessor postProcessor = new JobRegistryBeanPostProcessor();
        postProcessor.setJobRegistry(jobRegistry);
        return postProcessor;
    }

	// SchedulerFactoryBean에 JobFactory를 SpringBeanJobFactory로 설정
    @Bean
    public SchedulerFactoryBean schedulerFactoryBean() {
        SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
        schedulerFactoryBean.setJobFactory(new SpringBeanJobFactory());
        return schedulerFactoryBean;
    }

    @Bean
    public Scheduler scheduler(SchedulerFactoryBean schedulerFactoryBean) throws SchedulerException {
        Scheduler scheduler = schedulerFactoryBean.getScheduler();
        scheduler.scheduleJob(sampleJobDetail(), sampleJobTrigger());
        return scheduler;
    }

    @Bean
    public Trigger sampleJobTrigger() {
        return TriggerBuilder.newTrigger()
                .withIdentity("sampleJobTrigger")
                .withSchedule(CronScheduleBuilder.cronSchedule(jobCron))
                .build();
    }

    @Bean
    public JobDetail sampleJobDetail() {
        return JobBuilder.newJob(SpringJobLauncher.class)
        		// Quartz Scheduler 상에서의 Job의 Identity
                .withIdentity("sampleJob")
                // Job Data에 jobName 값을 'sampleJob' 으로 지정 -> Spring Batch의 Job 이름과 동일해야 함
                .usingJobData("jobName", "sampleJob")
                .storeDurably()
                .build();
    }

}

이렇게 하면 sampleJobDetail은 위에서 만든 QuartzJobBean을 Job(Quartz)으로서 실행시키며, 해당 Job은 Spring Batch Configuration에서 지정했던 Job 이름을 JobData 내 "jobName" 파라미터로 넘겨받아 Spring Batch JobRegistry에 등록된 Spring Batch Job을 확인하고 실행시키게 된다.

스케줄러 application.yml 수정

스케줄러에서 배치 모듈을 의존성으로 가지고 있는 상태에서 초기에 실행시킬 경우, org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jobLauncherApplicationRunner' defined in class path resource 에러 로그가 노출이 될 수 있는데 이는 여러 개의 Spring Batch Job이 감지되었으나 자동으로 실행할 Job을 지정하지 않아서 발생하는 문제로 스케줄러에서 직접 Spring Batch Job을 실행시키려면 스케줄러의 application.yml에 다음과 같이 추가해주어야 한다.

spring:
  batch:
    job:
      enabled: false

이렇게 하면 스케줄러에서는 의존성을 가지고 있는 배치 모듈 내에 있는 Job의 이름을 통해 JobDetail로 만들 수 있게 되며, 배치 모듈을 jar 파일 등으로 배포하지 않더라도 스케줄러 하나 만으로 여러 개의 Job을 실행시킬 수 있게 된다.

주의할 점

단일 프로젝트에서 배치와 스케줄러를 모두 사용하고 있다면 상관이 없겠으나, 멀티 모듈 프로젝트처럼 여러 개의 배치 모듈이 존재하고 이를 스케줄러에서 의존성으로 가지고 있을 때 Job 이름과 Bean 이름 등이 중복되어서는 안된다.

가령, 배치 모듈 A와 배치 모듈 B가 있다고 했을 때, 배치 Configuration 클래스의 이름을 똑같이 'BatchConfiguration' 으로 지정할 경우, Bean 이름 중복 문제가 발생하며, Job 이름이 동일하게 되면 Job 이름 중복 문제가 발생한다.

profile
Backend Developer

0개의 댓글