1. 잡이란?
잡(job)은 처음부터 끝까지 독립적으로 실행할 수 있는 고유하며 순서가 지정된 여러 스텝의 목록이다. 이 정의가 너무 장황하다면 이전에 설명했던 대로 "배치로 수행할 전체 작업" 정도로 생각해도 무방할 것 같다. 잡의 특성은 다음과 같다.
1) 유일하다
- 잡은 동일한 구성으로 반복적으로 실행할 수 있다
2) 순서를 가진 여러 스탭의 목록이다
- 잡은 모든 스텝을 논리적으로 실행할 수 있도록 구성된다
- 어떤 일련의 과정을 거쳐야만 비로소 전체 작업이 달성되는 것이다
3) 처음부터 끝까지 실행 가능하다
- 외부 의존성 없이 실행할 수 있는 일련의 스텝이다
- 즉, 이전 스텝 작업의 결과를 기다리는 식으로 잡을 구성하는 것이 아니라 시작을 바로 할 수 있는 시점에 잡이 시작되도록 잡을 구성해야 한다는 것이다
4) 독립적이다
- 각 배치 잡은 외부 의존성의 영향을 받지 않고 실행할 수 있어야 한다
- 잡이 외부 의존성을 가질 수 없다는 것은 아니고, 잡이 의존성들을 관리해야 한다는 의미이다
2. 잡의 생명주기
1) JobRunner, JobLauncher
먼저 잡의 실행은 잡 러너에서 시작된다. 잡 러너는 잡 이름과 여러 파라미터를 받아들여 잡을 실행시킨다. 스프링 배치는 CommandLlineJobRunner와 JobRegistryBackgroundJobRunner라는 두 가지 잡 러너를 제공한다. CommaindLineJobRunner는 스크립트나 콘솔에서 작업을 실행할 때 사용되며, JobRegistryBackgroundJobRunner는 외부 스케줄러를 사용해 잡을 실행하기 위해 필요한 JobRegistry를 생성하는 데 사용된다.
스프링 부트 실행 시엔 기본적으로 JobLauncherCommandLineRunner를 이용해 잡을 실행시킨다(스프링 부트 2.3부터 deprecated되었으며, 대신 JobLauncerApplicationRunner가 사용된다). 이 잡 러너는 애플리케이션 컨텍스트에 등록된 Job 타입의 모든 빈을 앱이 띄워지는 시점에 실행시킨다. 잡 러너에서 잡의 실행이 시작되긴 하지만, 실제 작업을 실행시키는 것은 잡 런처(job launcher)이다. 잡 러너는 잡 실행에 필요한 JobParameters를 JobIstance에 전달하는 역할을 수행한다.
package org.springframework.boot.autoconfigure.batch;
public class JobLauncherApplicationRunner implements ApplicationRunner, Ordered, ApplicationEventPublisherAware {
protected void execute(Job job, JobParameters jobParameters)
throws JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException,
JobParametersInvalidException, JobParametersNotFoundException {
JobParameters parameters = getNextJobParameters(job, jobParameters);
JobExecution execution = this.jobLauncher.run(job, parameters);
if (this.publisher != null) {
this.publisher.publishEvent(new JobExecutionEvent(execution));
}
}
//...
}
2) JobParameters, JobInstance, JobExecution
JobParameters, JobInstance, JobExecution에 대한 기본적인 설명은 이전 포스팅을 참고하자. 배치 잡이 실행되면 JobInstance가 생성되고, 성공적으로 실행된 JobExecution이 있다면 해당 JobInstance가 완료된 것으로 간주한다. 기본적으로 JobInstance는 한 번 성공적으로 완료되면 다시 실행시킬 수 없다는 특징이 있으며, 이는 JobInstance가 잡 이름과 식별 파라미터로 식별되므로 동일한 식별 파라미터를 사용하는 잡은 한 번만 실행시킬 수 있기 때문이다. 참고로 식별 파라미터는 파라미터 집합 중 JobInstance의 식별을 위해 사용되는 파라미터를 지칭하는 것이다. 같은 식별 파라미터를 지닌 JobInstance를 여러 번 실행시키고 싶다면 식별 가능한 ID를 부여하면 되는데, 이와 관련된 내용은 아래에서 설명할 것이다.
3. 잡의 구성
아래는 간단하게 잡을 구성한 예제이다.
@EnableBatchProcessing
@SpringBootApplication
public class HelloWorldJob {
@Autowired
private JobBuilderFactory jobBuilderFactory;
@Autowired
private StepBuilderFactory stepBuilderFactory;
@Bean
public Job job() {
return this.jobBuilderFactory.get("basicJob")
.start(step1())
.build();
}
@Bean
public Step step1() {
return this.stepBuilderFactory.get("step1")
.tasklet(helloWorldTasklet())
.build();
}
@Bean
public Tasklet helloWorldTasklet() {
return (contribution, chunkContext) -> {
System.out.println("Hello, world!");
return RepeatStatus.FINISHED;
};
}
public static void main(String[] args) {
SpringApplication.run(HelloWorldJob.class, args);
}
}
@EnableBatchProcessing 애너테이션을 추가하면 배치 잡 수행에 필요한 인프라스트럭처를 제공한다. 그 후엔 JobBuilderFactory와 StepBuilderFactory를 주입받아야 한다. 두 팩토리는 각각 스프링 배치 잡과 스텝을 생성하는 데 사용되는 JobBuilder와 StepBuilder 인스턴스를 생성한다.
먼저 잡 빈을 정의해야 한다. JobBuilderFactoy를 이용해 잡의 이름을 지정하고, 실행할 스텝을 지정한 후에 JobBuilder.build()를 호출하면 실제 잡이 생성된다. 참고로 위 예제의 경우 간단한 구성을 위해 @Autowired를 통해 필드 주입을 사용했지만, 실제론 생성자 주입을 사용해야 한다.
그 후 스텝 빈을 정의한다. 잡과 마찬가지로 팩토리를 사용해서 스텝 구성을 한다. StepBuilderFactory를 이용해 스텝의 이름을 지정하고, 스텝을 정의하고 마지막으로 StepBuilder.build()를 호출해서 스텝을 생성한다. 예제의 스텝은 태스크릿을 사용하는 형태로 정의되었다.
Step과 Tasklet이 빈으로 등록되었기 때문에 메서드를 직접 호출하는 방법 외에 파라미터를 통해 객체를 전달받는 방법으로도 작성할 수 있다.
4-1. 잡 파라미터
1) 잡 파라미터 전달하기
앞서 동일한 식별 파라미터를 사용하는 잡은 한 번만 실행시킬 수 있다고 했다. 만약 동일한 식별 파라미터를 가진 잡을 두 번 이상 실행시킨다면 JobInstanceAlreadyCompleteExecution이 발생할 것이다. 이렇게 식별에 필요한 파라미터 외에도 옵션으로 줄 수 있는 파라미터가 있는데, 매번 잡을 실행할 때마다 여러 조건에 따라 런타임에 변경될 수 있는 파라미터가 여기에 해당한다.
잡 파라미터를 지정할 때 - 옵션을 주면 해당 파라미터는 옵션 파라미터가 되고, 해당 옵션 파라미터가 동일하더라도 식별 파라미터만 달라진다면 JobInstance는 실행된다. 참고로 이것은 스프링 부트의 JobLauncherCommandLineRunner를 기준으로 설명한 것이며, 어떻게 잡을 실행하느냐에 따라 파라미터 전달 방식은 달라진다.
$ java -jar demo.jar executionDate(date)=2020/12/27 -name=Michael
- executionDate는 식별 파라미터이며, name은 옵션 파라미터이다
2) 잡 파라미터 접근하기
1) ChunkContext
- Tasklet 인터페이스를 보면 execute 메서드가 두 개의 파라미터를 전달받는 것을 알 수 있다
- 첫 번째는 StepContribution로 커밋되지 않은 현재 트렌잭션에 대한 정보를 가지고 있다
- 두 번째는 ChunkContext 인스턴스로서 실행 시점의 잡 상태, 처리 중인 청크와 관련된 정보를 가지고 있다
public interface Tasklet {
@Nullable
RepeatStatus execute(StepContribution var1, ChunkContext var2) throws Exception;
}
2) 늦은 바인딩(Late binding)
- 두 번째 방법은 스프링 프레임워크에서 지원하는 의존성 주입 방법을 사용하는 것이다
- 늦은 바인딩으로 구성될 빈은 잡 스코프나 스텝 스코프를 가져야 한다
@Bean
public Step step1() {
return this.stepBuilderFactory.get("step1")
.tasklet(helloWorldTasklet(null, null))
.build();
}
@StepScope
@Bean
public Tasklet helloWorldTasklet(
@Value("#{jobParameters['name']}") String name,
@Value("#{jobParameters['fileName']}") String fileName) {
// ...
}
- 스코프를 사용하면 실행 범위에 들어갈 때까지 빈 생성을 지연시킴으로써 외부로부터 받은 잡 파라미터를 빈 생성 시점에 주입할 수 있다
4-2. 잡 파라미터 유효성 검증
http request로 읽어들인 데이터를 검증하듯이, 배치 파라미터를 검증해야 할 수도 있다. 스프링 배치의 JobParamtersValidator 인터페이스를 구현하고 잡 내에 구성하면 손쉽게 파라미터의 유효성을 검증할 수 있다.
package org.springframework.batch.core;
public interface JobParametersValidator {
void validate(@Nullable JobParameters var1) throws JobParametersInvalidException;
}
아래는 실제 JobParametersValidator를 구현해 만든 커스텀 Validator이다. 반환 타입이 void이므로 JobParametersInvalidException이 발생하지 않는다면 성공적으로 유효성 검증이 끝난 것으로 판단한다.
public class ParameterValidator implements JobParametersValidator {
@Override
public void validate(JobParameters parameters) throws JobParametersInvalidException {
String fileName = parameters.getString("fileName");
if(!StringUtils.hasText(fileName)) {
throw new JobParametersInvalidException("fileName parameter is missing");
}
else if(!StringUtils.endsWithIgnoreCase(fileName, "csv")) {
throw new JobParametersInvalidException("fileName parameter does not use csv file extension");
}
}
}
커스텀 Validator를 직접 구현하는 방법 외에도, 스프링 배치에서 지원하는 DefaultJobParametersValidator를 사용하는 방법이 있다. 사용방법은 간단한데, 필수 파라미터와 옵션 파라미터를 생성자 또는 setter로 설정해주면 된다. DefaultJobParametersValidator는 단순하게 파라미터의 존재 여부만을 검증할 수 있으므로 더욱 강력한 유효성 검증이 필요하다면 JobParametersValidator를 직접 구현해야 한다.
// 생성자 이용
DefaultJobParametersValidator validator =
new DefaultJobParametersValidator(
new String[] {"requiredParam"},
new String[] {"optionalParam1", "optionalParam2"}
);
// setter 이용
DefaultJobParametersValidator validator = new DefaultJobParametersValidator();
validator.setRequiredKeys(new String[] {"requiredParam"});
validator.setOptionalKeys(new String[] {"optionalParam1", "optionalParam2"});
validator를 여러 개 사용하려면 CompositeJobParametersValidator를 쓰면 된다.
@Bean
public CompositeJobParametersValidator validator() {
CompositeJobParametersValidator validator =
new CompositeJobParametersValidator();
DefaultJobParametersValidator defaultJobParametersValidator =
new DefaultJobParametersValidator(
new String[] {"fileName"},
new String[] {"name", "currentDate"});
defaultJobParametersValidator.afterPropertiesSet();
validator.setValidators(
Arrays.asList(new ParameterValidator(),
defaultJobParametersValidator));
return validator;
}
@Bean
public Job job() {
return this.jobBuilderFactory.get("basicJob")
.start(step1())
.validator(validator())
.build();
}
4-3. 잡 파라미터 증가시키기
JobParameterIncrementer를 사용하면 동일한 파라미터를 사용해 잡을 여러 번 실행시킬 수 있다. JobParameterIncrementer는 잡에서 사용할 파라미터를 고유하게 생성하는 역할을 한다. JobParameterIncrementer 인터페이스이고, 구현체는 스프링 배치 프레임워크가 제공한다. 기본적인 구현체인 RunIdIncrementer를 쓰면 run.id라는 long 타입 파라미터 값을 증가시킨다.
@Bean("helloJob")
public Job helloJob(Step helloStep) {
return jobBuilderFactory.get("helloJob")
.incrementer(new RunIdIncrementer())
.start(helloStep)
.build();
}
물론 JobParameterIncrementer를 직접 구현하는 것도 가능하다.
public class DailyJobTimestamper implements JobParametersIncrementer {
@Override
public JobParameters getNext(JobParameters parameters) {
return new JobParametersBuilder(parameters)
.addDate("currentDate", new Date())
.toJobParameters();
}
}
5. 잡 리스너
JobExecutionListener를 사용하면 잡의 생명주기의 여러 시점에 로직을 추가할 수 있다. JobExecutionListener는 beforeJob과 afterJob이라는 콜백 메서드를 제공한다. 알림, 초기화, 정리 등에서 이를 활용할 수 있다. 잡 리스너를 작성하는 방법엔 JobExecutionListener 인터페이스 구현과 애너테이션 기반 방식 두 가지가 있다.
- beforeJob: 잡 생명주기에서 가장 먼저 실행된다
- afterJob: 잡 생명주기에서 가장 느리게 실행된다
1) JobExecutionListener 인터페이스 구현
public class JobLoggerListener implements JobExecutionListener {
private static String START_MESSAGE = "%s is beginning execution";
private static String END_MESSAGE =
"%s has completed with the status %s";
@Override
public void beforeJob(JobExecution jobExecution) {
System.out.println(String.format(START_MESSAGE,
jobExecution.getJobInstance().getJobName()));
}
@Override
public void afterJob(JobExecution jobExecution) {
System.out.println(String.format(END_MESSAGE,
jobExecution.getJobInstance().getJobName(),
jobExecution.getStatus()));
}
}
- 잡의 완료 상태와 관계없이 호출되므로 잡의 종료 상태에 따라 동적으로 로직을 수행할 수 있다
이렇게 만든 리스너를 사용하기 위해선 JobBuilder의 listener 메서드를 호출하면 된다.
@Bean
public Job job() {
return this.jobBuilderFactory.get("basicJob")
.start(step1())
.validator(validator())
.incrementer(new DailyJobTimestamper())
.listener(new JobLoggerListener())
.build();
}
2) 애너테이션 기반
public class JobLoggerListener {
private static String START_MESSAGE = "%s is beginning execution";
private static String END_MESSAGE =
"%s has completed with the status %s";
@BeforeJob
public void beforeJob(JobExecution jobExecution) {
System.out.println(String.format(START_MESSAGE,
jobExecution.getJobInstance().getJobName()));
}
@AfterJob
public void afterJob(JobExecution jobExecution) {
System.out.println(String.format(END_MESSAGE,
jobExecution.getJobInstance().getJobName(),
jobExecution.getStatus()));
}
}
- 스프링의 @PostConstruct, @PreDestroy와 유사한 형태이다
- 인터페이스를 구현할 필요 없이 해당 애너테이션을 적용하기만 하면 잡이 시작하기 전, 끝난 후에 메서드가 호출된다
애너테이션을 사용하면 인터페이스를 직접 구현할 때와 달리 JobListenerFactoryBean을 사용해서 래핑을 해야 한다. 그래야 잡에서 리스너를 주입받을 수 있다.
@Bean
public Job job() {
return this.jobBuilderFactory.get("basicJob")
.start(step1())
.validator(validator())
.incrementer(new DailyJobTimestamper())
.listener(JobListenerFactoryBean.getListener(new JobLoggerListener()))
.build();
}
6. ExecutionContext
배치 처리는 현재 어떤 스텝이 실행되고 있는지, 해당 스텝이 처리한 레코드 개수 등의 상태를 알아야 한다. 그래야 진행중인 처리를 확인할 수 있고, 실패한 처리에 대한 조치도 취할 수 있다. 이러한 잡의 상태는 JobExecution의 ExecutionContext에 저장된다.
ExecutionContext는 키-값 쌍을 보관하는 간단한 도구이면서 동시에 배치 잡의 세션 역할을 한다. StepExecution도 ExecutionContext를 가지고 있기 때문에 ExecutionContext는 동시에 여러 개가 존재할 수 있다. 따라서 개별 스텝에 필요한 데이터와 잡 전반에 사용되는 전역 데이터를 나눠서 관리할 수 있고, 이는 곧 데이터의 사용 범위를 나눠서 지정할 수 있다는 것을 의미한다.
ExecutionContext는 안전하게 데이터를 저장할 수 있는 수단이다. 이는 스프링 배치가 잡과 스텝의 ExecutionContext를 데이터베이스에 저장해주기 때문이다. 정확히는 메타데이터의 BATCH_JOB_EXECUTION_CONTEXT 테이블에 저장된다. 메타데이터에 관련된 내용은 추후 포스팅으로 다룰 예정이다.
7. 참고
- 스프링 배치 완벽 가이드 2/e
- https://github.com/AcornPublishing/definitive-spring-batch
'Spring Batch' 카테고리의 다른 글
[Spring Batch] 스프링 배치4 - JobRepository와 메타 테이블 (0) | 2023.06.06 |
---|---|
[Spring Batch] 스프링 배치3 - 스텝(Step) (0) | 2023.04.10 |
[Spring Batch] 스프링 배치1 - 기본 구조 및 도메인 (0) | 2023.03.31 |