[Spring Batch 5] Quartz 적용하기

식빵·2024년 10월 17일
0

spring-batch-5

목록 보기
4/4

Quartz 를 잘 모르시는 분들은 아래 링크를 참고해주세요~
https://velog.io/@dailylifecoding/how-to-use-java-quartz

작성된 코드는 모두 저의 github 에서 확인 가능합니다.


🍞 Maven 의존성 추가

참고사항:

  • spring boot (ver 3.3.4) 사용
  • jdk : temurin-21 사용
<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-batch</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
  </dependency>
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
  </dependency>
  
  <!-- 자기 상황에 맞게 Database jdbc 구현체를 지정해주세요~ -->
  <!-- 저는 일한는 곳에서 Postgresql 써서 이렇게 했습니다. -->
  <dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
  </dependency>
</dependencies>



🍞 자동실행 방지 설정

spring boot 의 설정 파일인 application.yaml 을 열고 다음처럼 작성합니다.

spring:
  application:
    name: my-batch
  batch:
    jdbc:
      platform: postgresql
      initialize-schema: always
    job:
      enabled: false # 중요
  datasource:
    url: jdbc:postgresql://localhost:5432/postgres
    driver-class-name: org.postgresql.Driver
    password: postgres
    username: postgres
logging:
  level:
    root: info
  • 다른 건 몰라도 spring.batch.job.enabled: false 는 꼭 설정해주세요!
    • Bean 으로 등록된 Job자동 실행을 방지합니다.
    • Spring Boot 의 자동실행을 통한 Job 실행이 아닌,
      Quartz 에 의한 자동실행을 보기 위해서 이런 겁니다.

DBMS 가 다르신 분들은 아래 설정들을 잘 바꿔주세요.

  • spring.batch.jdbc.platform
  • spring.datasource.[url, driver-class-name, username, password]




🍞 Batch Job 설정

실행할 Batch Job 에 대한 설정을 작성합니다.

@Configuration
@RequiredArgsConstructor
public class SimpleBatchJobConfig {
	
	private final JobRepository jobRepository;
	private final PlatformTransactionManager txManager;
	
	@Bean
	public Job quartzJobConfigJob() {
		return new JobBuilder("quartzJobConfigJob", jobRepository)
			.start(quartzJobConfigStep())
			.incrementer(new RunIdIncrementer())
			.build();
	}
	
	@Bean
	public Step quartzJobConfigStep() {
		return new StepBuilder("quartzJobConfigStep", jobRepository)
			.tasklet((contribution, chunkContext) -> {
				System.out.println("quartzJobConfigStep RAN!");
				return RepeatStatus.FINISHED;
			}, txManager)
			.build()
			;
	}
}



🍞 QuartzJobBean 상속 클래스

QuartzJobBean 추상클래스는 executeInternal 라는
추상 메소드를 제공합니다. 이 메소드는 상속 클래스에서 overwrite 를 하면,
이후에 등록할 트리거에 의해서 메소드 안의 내용이 주기적으로 실행됩니다.

package coding.toast.batch.job.quartz;

import org.quartz.JobExecutionContext;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.lang.NonNull;
import org.springframework.scheduling.quartz.QuartzJobBean;

// @Component 같은 빈 등록용 어노테이션을 안하셔도 됩니다.
public class BatchScheduledJob extends QuartzJobBean {

	// 앞서 생성한 quartzJobConfigJob 빈을 DI 받습니다.
	private final Job job;
    
    // 자동 생성된 JobExplorer, JobLauncher 를 DI 받습니다.
	private final JobExplorer jobExplorer;
	private final JobLauncher jobLauncher;
	
	public BatchScheduledJob(@Qualifier("quartzJobConfigJob") Job job,
	                         JobExplorer jobExplorer,
	                         JobLauncher jobLauncher) {
		this.job = job;
		this.jobExplorer = jobExplorer;
		this.jobLauncher = jobLauncher;
	}
	
	@Override
	protected void executeInternal(@NonNull JobExecutionContext context) {
    
    	// 아주 간단한 JobLauncher 실행 코드입니다.
        // Job 생성시 사용되는 new RunIdIncrementer() 의 
        // 신규 run.id 값을 읽어오기 위해  jobExplorer 인자값을 부여하고,
        // getNextJobParameters 메소드를 호출했습니다.
        
		JobParameters jobParameters = new JobParametersBuilder(this.jobExplorer)
			.getNextJobParameters(this.job)
			.toJobParameters();
		
		try {
			this.jobLauncher.run(this.job, jobParameters);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
}

주의!
참고로 QuartzJobBean 상속 클래스는 componentScan 대상임을 알리는
애노테이션인 (ex: Component, Service 등) 붙이지 않습니다.

이 상속 클래스는 아래처럼 org.quartz.JobBuilder 에 의해서 한번 wrapping 되며,
이러면 상속 클래스 내부의 의존 인스턴스들이 자동으로 auto wire 됩니다.

@Bean
public JobDetail quartzJobDetail() {
	return org.quartz.JobBuilder
    	.newJob(BatchScheduledJob.class)
		.storeDurably()
		build();
}




🍞 Quartz 설정

이번에는 Quartz 설정을 해줍니다.

package coding.toast.batch.job.quartz._01;

import coding.toast.batch.job.quartz.BatchScheduledJob;
import lombok.RequiredArgsConstructor;
import org.quartz.JobDetail;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QuartzJobConfig {
	
	@Bean
	public JobDetail quartzJobDetail() {
		return org.quartz.JobBuilder.newJob(BatchScheduledJob.class)
			.storeDurably()
			.build();
	}
	
	@Bean
	public Trigger jobTrigger() {
		
		// 간단하게 하려면 simpleBuilder 를 사용하세요!
		SimpleScheduleBuilder simpleBuilder
			= SimpleScheduleBuilder.simpleSchedule()
					.withIntervalInSeconds(5)   // 매 5초마다
					.withRepeatCount(4);    // 최초 1회 이후 4번 반복 (= 총 5회 동작)
		
		// cron 이 편하신 분들은 아래 방법 사용.
		/* CronScheduleBuilder cronBuilder
			= CronScheduleBuilder.cronSchedule("0/5 * * * * ?");*/ // 매 5초마다.
		
		return TriggerBuilder
			.newTrigger()
			.forJob(quartzJobDetail())
			.withSchedule(simpleBuilder)
			// .withSchedule(cronBuilder)
			.build();
	}
	
}




🍞 테스트

지금까지 작성한 코드를 기반으로 Spring Boot Application 을 실행하면
Spring boot 가 자동으로 QuartzScheduler 를 생성하고
빈으로 등록한 Trigger, JobDetail 을 사용하게 됩니다.

Spring Boot 애플리케이션을 실행해서 진짜 그런지 봅시다.

로그(quartzJobConfigStep 빈에 작성한 로그)에 설정했던 대로
5초 간격으로, 총 5번 Job 이 실행된 것을 확인할 수 있습니다.




🍞 보충: ApplicationRunner 로 스케줄러 실행

이번에는 spring-boot 에 의한 자동 실행이 아닌,
저희가 등록하는 코드에서 실행하는 방법을 알아봅시다.

주의사항!
🍞 Quartz 설정 목차에서 작성한 설정 클래스는 비활성화해줘야 합니다!

// 주석처리해주세요!
// @Configuration
public class QuartzJobConfig { ...생략 ... }

기존 🍞 Batch Job 설정 목차에 있는 설정 클래스를
사용했기 여기서는 따로 스프링 배칭 설정 클래스는 또 작성하지 않겠습니다.


1. Runner Bean 생성

package coding.toast.batch.job.quartz._02;

import coding.toast.batch.job.quartz.BatchScheduledJob;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.converter.DefaultJobParametersConverter;
import org.springframework.batch.core.converter.JobParametersConverter;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.Properties;

@Slf4j
@Component
@RequiredArgsConstructor
public class BatchQuartzJobRunner implements ApplicationRunner {
	
	private final JobParametersConverter converter
    		= new DefaultJobParametersConverter();
                    
	private final Scheduler scheduler;
	
	@Override
	public void run(ApplicationArguments args) throws Exception {
    
    	// JobDetail 인스턴스 생성
		JobDetail jobDetail = quartzJobDetail();
        
        // Trigger 인스턴스 생성
		Trigger trigger = jobTrigger(jobDetail);
		
		// args 에 있는 파리미터 값들을 JobParameters 로 변환합니다.
		JobParameters jobParameter = createJobParameter(args);

		// BatchScheduledJob.executeInternal 메소드 내에서 사용하기 위해
        // JobDataMap 에 JobParameters 를 넣는 과정입니다.
		JobDataMap jobDataMap = jobDetail.getJobDataMap();
		jobDataMap.put("jobParamFromArguments", jobParameter);
        
        // 스케줄러를 실행합니다!
		scheduler.scheduleJob(jobDetail, trigger);
	}
	
	/**
	 * arguments 를 JobParameter 로 변환한다.
	 */
	private JobParameters createJobParameter(ApplicationArguments args) {
		String[] jobArguments = args.getNonOptionArgs().toArray(new String[0]);
		Properties properties = 
        	StringUtils.splitArrayElementsIntoProperties(jobArguments, "=");
		return converter.getJobParameters(properties);
	}
	
	/**
	 * Quartz 의 JobDetail 생성
	 */
	public JobDetail quartzJobDetail() {
		return org.quartz.JobBuilder
			.newJob(BatchScheduledJob.class)
			.storeDurably()
			.build();
	}
	
	/**
	 * JobTrigger 생성
	 */
	public Trigger jobTrigger(JobDetail jobDetail) {
		// 간단하게 하려면 simpleBuilder 를 사용하세요!
		SimpleScheduleBuilder simpleBuilder
			= SimpleScheduleBuilder.simpleSchedule()
			.withIntervalInSeconds(5)   // 매 5초마다
			.withRepeatCount(4);    // 최초 1회 이후 4번 반복 (= 총 5회 동작)
		
		return TriggerBuilder
			.newTrigger()
			.forJob(jobDetail)
			.withSchedule(simpleBuilder)
			.build();
	}
}

2. BatchScheduledJob 생성

기존에 작성했던 QuartzJobBean 상속 클래스 의 내용을 살짝 수정해서 작성해봤습니다.

package coding.toast.batch.job.quartz;

import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.lang.NonNull;
import org.springframework.scheduling.quartz.QuartzJobBean;

public class BatchScheduledJob extends QuartzJobBean {
	private final Job job;
	private final JobExplorer jobExplorer;
	private final JobLauncher jobLauncher;
	
	public BatchScheduledJob(@Qualifier("quartzJobConfigJob") Job job,
	                         JobExplorer jobExplorer,
	                         JobLauncher jobLauncher) {
		this.job = job;
		this.jobExplorer = jobExplorer;
		this.jobLauncher = jobLauncher;
	}
	
	@Override
	protected void executeInternal(@NonNull JobExecutionContext context) {
		JobDataMap dataMap = context.getMergedJobDataMap();
		
		JobParameters jobParameters = new JobParametersBuilder(this.jobExplorer)
			.getNextJobParameters(this.job)
			.addJobParameters((JobParameters)dataMap.get("jobParamFromArguments"))
			.toJobParameters();
		
		try {
			this.jobLauncher.run(this.job, jobParameters);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
}

스프링 부트를 실행해보면 이전과 마찬가지로 잘 실행되는 것을 알 수 있습니다.

profile
백엔드 개발자로 일하고 있는 식빵(🍞)입니다.

0개의 댓글