1. 스텝이란?

잡이 전체적인 처리를 지칭하는 반면, 스텝은 잡의 구성 요소를 담당한다. 스텝(step)은 모든 단위 작업의 조각으로서 트랜잭션 내에서 자체적으로 입/출력을 처리한다. 즉, 스텝을 정의하자면 독립적이고 순차적으로 배치 처리를 수행하는 잡의 하위 컴포넌트라고 할 수 있다.
배치 처리는 일반적으로 데이터 처리와 관련이 있는데, 데이터 처리를 위한 태스크릿(Tasklet) 모델과 청크(Chunk) 기반 처리 모델 두 가지 유형의 처리 모델을 지원한다.
1) 태스크릿 모델
- 단일 명령만 실행하는 작업에 적합
- 삭제 SQL문 실행, 디렉터리를 정리하는 셸 스크립트, 알림 메일 전송 등
- Tasklet 인터페이스를 사용해 Tasklet.execute 메서드가 RepeatStatus.FINISHED를 반환할 때까지 트랜잭션 범위 내에서 반복적으로 실행
2) 청크 기반 처리 모델

- 대량의 데이터를 처리하는 작업에 적합
- 한번에 하나씩 레코드나 아이템을 읽어 처리한 뒤 특정 데이터 저장소에 기록하는 일을 반복
- 2~3개의 주요 컴포넌트로 구성 (ItemReader, 필수가 아닌 ItemProcessor, ItemWriter)
- 레코드를 청크 또는 레코드 그룹 단위로 처리
- 각 청크는 자체 트랜잭션으로 실행되며, 처리에 실패했다면 마지막으로 성공한 트랜잭션 이후부터 다시 시작할 수 있음
- 각 컴포넌트가 수행하는 작업은 다음과 같다
- 먼저 ItemReader는 청크 단위로 처리할 모든 레코드를 반복적으로 메모리로 읽어온다
- ItemProcessor를 구성했다면 메모리로 읽어들인 아이템이 반복적으로 ItemProcessor를 거쳐간다
- 마지막으로 ItemWriter는 물리적 쓰기를 일괄적으로(청크 단위로) 처리한다
- 즉, ItemReader와 ItemProcessor의 처리 단위는 아이템이며 ItemWriter의 처리 단위는 청크이다
2. 태스크릿 스탭
태스크릿 스탭을 만드는 방법에는 두 가지 방법이 있다. 첫 번째 방법은 Tasklet 인터페이스를 구현하는 방법이다. 두 번째 방법은 사용자가 작성한 코드를 태스크릿 스텝처럼 실행되도록 하는 것이다.
1) Tasklet 인터페이스를 구현하는 방법
execute 메서드를 구현하고 RepeatStatus 객체를 반환하여 처리 완료 이후에 스프링 배치가 어떤 일을 할 지 정의한다. 반환값은 RepeatStatus.CONTINUABLE 또는 RepeatStatus.FINISHED 중의 하나이다. CONTINUABLE은 해당 태스크릿을 다시 실행하겠다는 의미이며, FINISHED는 처리의 성공 여부와 관계없이 해당 태스크릿의 처리를 완료하고 다음 처리를 이어서 하겠다는 의미이다. Tasklet은 함수형 인터페이스이므로 람다를 이용해 작성할 수 있다.
@Bean
public Step step1() {
return this.stepBuilderFactory.get("step1")
.tasklet((contribution, chunkContext) -> {
System.out.println("Hello, World!");
return RepeatStatus.FINISHED;
})
.build();
}
2) 사용자가 작성한 코드를 태스크릿 스텝처럼 실행되도록 하기
Tasklet 인터페이스를 직접 구현하는 대신 스프링 배치가 제공하는 CallableTaskletAdatper, MethodInvokingTaskingAdaptor, SystemCommandTasklet 구현체를 사용할 수도 있다.
1) CallableTaskletAdatper
- Callable<RepeatStatus> 인터페이스의 구현체를 구성할 수 있게 해주는 어댑터
- Callable 인터페이스는 Runnable 인터페이스와 유사하게 새 스레드에서 실행된다
- 다만 값을 반환하고 체크 예외를 바깥으로 던질 수 있다는 점에서 Runnable 인터페이스와 차이가 있다
- 스텝이 실행되는 스레드가 아닌 다른 스레드에서 실행하고 싶을 때 사용한다 (but 스텝과 병렬로 실행되진 않음)
@Bean
public Step callableStep() {
return this.stepBuilderFactory.get("callableStep")
.tasklet(tasklet())
.build();
}
@Bean
public CallableTaskletAdapter tasklet() {
CallableTaskletAdapter callableTaskletAdapter =
new CallableTaskletAdapter();
callableTaskletAdapter.setCallable(callableObject());
return callableTaskletAdapter;
}
@Bean
public Callable<RepeatStatus> callableObject() {
return () -> {
System.out.println("This was executed in another thread");
return RepeatStatus.FINISHED;
};
}
2) MethodInvokingTaskingAdaptor
- 이 구현체를 사용하면 기존에 존재하던 다른 클래스 내의 메서드를 잡 내의 태스크릿처럼 실행할 수 있다
- 호출하는 메서드가 ExisStatus를 반환하지 않는 한, ExitStatus.COMPLETED를 반환한다
- ExitStatus는 잡이나 스텝 종료 시 스프링 배치로 반환되는 값이다
@Bean
public Step methodInvokingStep() {
return this.stepBuilderFactory.get("methodInvokingStep")
.tasklet(methodInvokingTasklet(null))
.build();
}
@StepScope
@Bean
public MethodInvokingTaskletAdapter methodInvokingTasklet(
@Value("#{jobParameters['message']}") String message) {
MethodInvokingTaskletAdapter methodInvokingTaskletAdapter =
new MethodInvokingTaskletAdapter();
methodInvokingTaskletAdapter.setTargetObject(service());
methodInvokingTaskletAdapter.setTargetMethod("serviceMethod");
methodInvokingTaskletAdapter.setArguments(
new String[] {message});
return methodInvokingTaskletAdapter;
}
3) SystemCommandTasklet
- 시스템 명령을 비동기로 실행할 때 사용한다
@Bean
public Step systemCommandStep() {
return this.stepBuilderFactory.get("systemCommandStep")
.tasklet(systemCommandTasklet())
.build();
}
@Bean
public SystemCommandTasklet systemCommandTasklet() {
SystemCommandTasklet systemCommandTasklet = new SystemCommandTasklet();
systemCommandTasklet.setCommand("rm -rf /tmp.txt");
systemCommandTasklet.setTimeout(5000);
systemCommandTasklet.setInterruptOnCancel(true);
return systemCommandTasklet;
}
public static void main(String[] args) {
SpringApplication.run(SystemCommandJob.class, args);
}
3. 청크 기반 스텝
1) 청크 기반 스텝
청크는 커밋 간격(commit interval)에 의해 정의된다. 커밋 간격은 간단히 말하자면 청크를 구성하는 아이템 개수를 의미한다. 예컨대 커밋 간격을 50개 아이템으로 설정했다면 잡은 50개 아이템을 읽고, 50개 아이템을 처리한 다음에, 50개 아이템을 기록한다.
다음은 기본 청크 구현 예제이다. StepBuilderFactory에서 StepBuilder 인스턴스를 생성한 후 커밋 간격을 지정한다. 청크 기반 스텝은 태스크릿 스탭과 달리 build 메서드 호출 전에 리더(ItemReader 인터페이스 구현체) 및 라이터(ItemWriter 인터페이스 구현체)를 가져온다.
@Bean
public Step step1() {
return this.stepBuilderFactory.get("step1")
.<String, String>chunk(10)
.reader(itemReader(null))
.writer(itemWriter(null))
.build();
}
@Bean
@StepScope
public FlatFileItemReader itemReader(
@Value("#{jobParameters['inputFile']}") Resource inputFile
) {
return new FlatFileItemReaderBuilder<String>()
.name("itemReader")
.resource(inputFile)
.lineMapper(new PassThroughLineMapper())
.build();
}
@Bean
@StepScope
public FlatFileItemWriter<String> itemWriter(
@Value("#{jobParameters['outputFile']}") Resource outputFile
) {
return new FlatFileItemWriterBuilder<String>()
.name("itemWriter")
.resource(outputFile)
.lineAggregator(new PassThroughLineAggregator<>())
.build();
}
2) 청크 크기 구성하기
청크 크기를 구성하는 방법엔 여러가지가 있는데, 여기선 정적인 커밋 개수 설정 방법과 CompletionPolicy 구현체 사용 방법을 알아본다. 다른 방식들은 오류 처리와 관련이 있으므로 관련 절에서 살펴볼 예정이다.
1) 정적으로 설정 (하드코딩)
다음은 하드 코딩을 통해 정적으로 청크 크기를 설정한 예제이다. 이러한 방식은 모든 청크의 크기가 동일할 때 사용하는 것이 적절하다.
@Bean
public Step chunkStep() {
return this.stepBuilderFactory.get("chunkStep")
.<String, String>chunk(1000)
.reader(itemReader())
.writer(itemWriter())
.build();
}
@Bean
public ListItemReader<String> itemReader() {
List<String> items = new ArrayList<>(100000);
for (int i = 0; i < 100000; i++) {
items.add(UUID.randomUUID().toString());
}
return new ListItemReader<>(items);
}
@Bean
public ItemWriter<String> itemWriter() {
return items -> {
for (String item : items) {
System.out.println(">> current item = " + item);
}
};
}
2) CompletionPolicy 인터페이스의 구현체
청크 크기를 정적으로 정의하는 것이 모든 상황에 적절하진 않다. 청크마다 크기가 동일하지 않을 수 있는데, 이러한 경우에 CompletionPolicy 인터페이스의 구현체를 사용할 수 있다.
CompletionPolicy 인터페이스는 청크의 완료 여부를 결정할 수 있는 결정 로직을 정의한다. 스프링 배치는 다양한 CompletionPolicy 인터페이스의 구현체를 제공한다. 기본적으로는 SimpleCompletionPolicy를 사용한다.
- SimpleCompletionPolicy: 처리된 아이템 개수가 미리 설정해둔 임곗값에 도달하면 청크 완료로 간주한다
- TimeoutTerminationPolicy: 타임아웃 값을 설정한 후, 청크 내의 처리 시간이 해당 값을 넘으면 청크 완료로 간주한다
- CompositeCompletionPolicy: 여러 정책을 포함하며, 여러 정책 중 하나라도 청크 완료라고 판단되면 청크 완료로 간주한다
CompositeCompletionPolicy를 사용하면 청크 완료 여부를 결정하는 여러 정책을 함께 구성할 수 있다. CompositeCompletionPolicy는 자신이 포함하고 있는 여러 정책 중 하나라도 청크 완료라고 판단된다면 해당 청크가 완료된 것으로 간주한다. 타임아웃만으로 청크의 완료 여부를 판단하는 TimeoutTerminationPolicy은 대부분 CompositeCompletionPolicy의 일부로 사용된다.
@Bean
public Step chunkStep() {
return this.stepBuilderFactory.get("chunkStep")
.<String, String>chunk(completionPolicy())
.reader(itemReader())
.writer(itemWriter())
.build();
}
@Bean
public ListItemReader<String> itemReader() {
List<String> items = new ArrayList<>(100000);
for (int i = 0; i < 100000; i++) {
items.add(UUID.randomUUID().toString());
}
return new ListItemReader<>(items);
}
@Bean
public ItemWriter<String> itemWriter() {
return items -> {
for (String item : items) {
System.out.println(">> current item = " + item);
}
};
}
@Bean
public CompletionPolicy completionPolicy() {
CompositeCompletionPolicy policy =
new CompositeCompletionPolicy();
policy.setPolicies(
new CompletionPolicy[] {
new TimeoutTerminationPolicy(3),
new SimpleCompletionPolicy(1000)});
return policy;
}
위와 같이 스프링 배치에서 기본적으로 제공하는 구현체를 사용할 수 있지만, CompletionPolicy를 직접 구현하는 방법도 있다. CompletionPolicy 인터페이스는 다음과 같다.
package org.springframework.batch.repeat;
public interface CompletionPolicy {
boolean isComplete(RepeatContext var1, RepeatStatus var2);
boolean isComplete(RepeatContext var1);
RepeatContext start(RepeatContext var1);
void update(RepeatContext var1);
}
- start: 청크 시작 시 해당 구현체가 필요로 하는 모든 내부 상태를 초기화한다
- update: 각 아이템이 처리될 때마다 호출되면서 내부 상태를 갱신한다
- isComplete(var1): 내부 상태를 이용해 청크 완료 여부를 판단한다
- isComplete(var1, var2): 청크 완료 여부의 상태를 기반으로 결정 로직을 수행한다
이를 실제로 구현한 예제는 다음과 같다.
public class RandomChunkSizePolicy implements CompletionPolicy {
private int chunksize;
private int totalProcessed;
private Random random = new Random();
// 청크 완료 여부의 상태를 기반으로 결정 로직을 수행한다.
@Override
public boolean isComplete(RepeatContext context,
RepeatStatus result) {
if(RepeatStatus.FINISHED == result) {
return true;
}
else {
return isComplete(context);
}
}
// 내부 상태를 이용해 청크 완료 여부를 판단한다.
@Override
public boolean isComplete(RepeatContext context) {
return this.totalProcessed >= chunksize;
}
// 청크 시작 시 해당 구현체가 필요로 하는 모든 내부 상태를 초기화한다.
@Override
public RepeatContext start(RepeatContext parent) {
this.chunksize = random.nextInt(20);
this.totalProcessed = 0;
System.out.println("The chunk size has been set to " +
this.chunksize);
return parent;
}
// 각 아이템이 처리될 때마다 호출되면서 내부 상태를 갱신한다.
@Override
public void update(RepeatContext context) {
this.totalProcessed++;
}
}
4. 스텝 리스너
시작 및 종료 이벤트를 다루는 잡 리스너와 기본적으로 동일하다. 다만 이벤트의 범위가 잡 전체가 아닌 개별 스텝으로 한정된다는 차이가 있다. 관련 인터페이스로는 스텝과 청크의 시작과 끝에서 특정 로직을 처리할 수 있게 해주는 ChunkListener와 StepExecutionListener임가 있다.
StepExecutionListener와 ChunkListener 모두 JobExecutionListener 인터페이스와 유사한 메서드를 제공한다.
- StepExecutionListener: beforeStep(void), afterStep(ExitStatus)
- afterStep은 ExitStatus를 반환하는데, 리스너가 이 ExitStatus가 잡에 전달되기 전에 수정할 수 있다
- 이 기능은 무결성을 검증하는 로직 등에서 유용하게 사용할 수 있다
- ChunkListener: beforeChunk(void), afterChunk(void)
- 잡 리스너와 마찬가지로 @BeforeStep, @AfterStep, @BeforeChunk, @AfterChunk와 같은 애너테이션을 사용할 수 있다
public class LoggingStepStartStopListener {
@BeforeStep
public void beforeStep(StepExecution stepExecution) {
System.out.println(stepExecution.getStepName() + " has begun!");
}
@AfterStep
public ExitStatus afterStep(StepExecution stepExecution) {
System.out.println(stepExecution.getStepName() + " has ended!");
return stepExecution.getExitStatus();
}
}
- 잡 리스너와 달리 래핑하지 않고 사용할 수 있다
@Bean
public Step chunkStep() {
return this.stepBuilderFactory.get("chunkStep")
.<String, String>chunk(1000)
.reader(itemReader())
.writer(itemWriter())
.listener(new LoggingStepStartStopListener())
.build();
}
5-1. 스텝 플로우의 조건로직
지금까지 소개했던 스텝 구성방식은 순차적으로 실행되는 방식이었다. 그러나 스텝을 반드시 이와 같이 처리해야 하는 것은 아니다. 동적으로 스텝 진행 순서를 결정할 수 있는 방법으로 스텝 플로우(flow)가 있다. 스프링 배치는 잡 흐름을 커스터마이징할 수 있는 방법을 제공한다.
실행할 스텝을 결정하거나 주어진 스텝을 실행하는 데엔 스프링 배치의 조건 로직(Conditional Logic)이 사용된다. 스프링 배치의 스텝은 기본적으로 JobBuilder의 next 메서드를 사용해서 지정한 순서대로 실행된다. 아래와 같이 전이(transition)을 사용하면 다른 순서로 실행할 수도 있다. 각 스텝의 실행결과에 따라 스텝의 흐름이 유동적으로 변한다.
@Bean
public Job job() {
return this.jobBuilderFactory.get("conditionalJob")
.start(firstStep())
.on("FAILED").to(failureStep()) // firstStep이 ExitStatus.FAILED를 반환하면 failureStep 실행
.from(firstStep()).on("*").to(successStep()) // 그 외의 경우 successStep 실행
.end()
.build();
}
on 메서드는 스텝의 ExitStatus를 평가해 어떤 일을 수행할지 결정할 수 있게 한다. 참고로 ExitStatus는 잡이나 스텝 종료 시 스프링 배치로 반환되는 값인 반면, BatchStatus는 잡이나 스텝의 현재 상태를 식별하는 JobExecution이나 StepExecution의 애트리뷰트다. ExitStatus 값의 실체는 문자열이므로 다음과 같이 와일드 카드를 사용할 수도 있다.
- *는 0개 이상의 문자를 일치(match)시킨다는 것을 의미
- C* -> C, COMPLETE, CORRECT 모두 일치
- ?는 1개의 문자를 일치(match)시킨다는 것을 의미
- ?AT -> CAT, KAT과는 일치 / THAT과는 불일치
ExitStatus만으로 다음 어떤 스텝을 실행할지 결정할 수는 있지만 충분하진 않다. 예를 들어 특정 레코드를 건너뛰었을 때 특정 스탭을 실행시키지 않으려고 할 수 있다. ExitStatus를 평가한다는 것은 스텝의 종료 후 상태만 보고 다음 로직을 결정한다는 것인데, 스텝 처리중의 정보는 알 수 없으므로 이런 처리가 불가능하다. JobExecutionDecider 인터페이스를 구현하면 더욱 많은 정보를 가지고 동적으로 다음 로직을 결정할 수 있다.
package org.springframework.batch.core.job.flow;
public interface JobExecutionDecider {
FlowExecutionStatus decide(JobExecution var1, @Nullable StepExecution var2);
}
JobExecution과 StepExecution을 아규먼트로 전달받아 FlowExecutionStatus(BatchStatus/ExitStatus쌍을 래핑한 객체)를 반환한다. JobExecution과 StepExecution를 이용할 수 있으므로 다음에 무엇을 수행할지 결정을 내릴 때 모든 정보를 사용할 수 있다. 관련해서 ExecutionContext에 대한 정보는 여기서 확인하자.
public class RandomDecider implements JobExecutionDecider {
private Random random = new Random();
public FlowExecutionStatus decide(JobExecution jobExecution,
StepExecution stepExecution) {
if (random.nextBoolean()) {
return new
FlowExecutionStatus(FlowExecutionStatus.COMPLETED.getName());
} else {
return new
FlowExecutionStatus(FlowExecutionStatus.FAILED.getName());
}
}
}
@Bean
public Job job() {
return this.jobBuilderFactory.get("conditionalJob")
.start(firstStep())
.next(decider())
.from(decider()).on("FAILED").to(failureStep())
.from(decider()).on("*").to(successStep())
.end()
.build();
}
@Bean
public JobExecutionDecider decider() {
return new RandomDecider();
}
5-2. 잡 종료하기
JobInstance는 성공적으로 완료되면 두 번 이상 실행될 수 없다. 따라서 스프링 배치에서 동적으로 잡을 종료한다면 사용자는 잡의 종료 상태를 알아야 한다. 종료 상태는 Completed, Failed, Stopped 세 가지로 나뉘며, 이러한 상태는 스프링 배치가 JobRepository에 저장할 BatchStatus를 판별할 때 스텝의 ExitStatus를 평가함으로써 식별된다. BatchStatus는 StepExecution이나 JobExecution 내에 보관되며, JobRepository에 저장된다.
- Completed
- 잡이 성공적으로 종료됐음을 의미
- 동일한 파라미터를 사용해 다시 실행할 수 없다
- Failed
- 잡이 성공적으로 완료되지 않았음을 의미
- 동일한 파라미터를 사용해 다시 실행할 수 있다
- Stopped
- 잡이 중단됐음을 의미, 다시 시작할 수 있다
- 중단된 위치에서 잡을 다시 시작할 수 있다
- 이 상태는 스텝 사이에 사람의 개입이 필요하거나 다른 검사나 처리가 필요한 상황에 유용하다
빌더가 제공하는 메서드에 따라 잡의 종료 상태가 결정된다.
- end(): Completed 상태로 잡 종료
@Bean
public Job job() {
return this.jobBuilderFactory.get("conditionalJob")
.start(firstStep())
.on("FAILED").end()
.from(firstStep()).on("*").to(successStep()).end()
.build();
}
- fail(): Failed 상태로 잡 종료
@Bean
public Job job() {
return this.jobBuilderFactory.get("conditionalJob")
.start(firstStep())
.on("FAILED").fail()
.from(firstStep()).on("*").to(successStep()).end()
.build();
}
- stopAndRestart(): FAILED로 종료된 잡을 재실행하면 인자로 넘긴 스텝부터 재실행
@Bean
public Job job() {
return this.jobBuilderFactory.get("conditionalJob")
.start(firstStep())
.on("FAILED").stopAndRestart(successStep())
.from(firstStep()).on("*").to(successStep()).end()
.build();
}
5-3. 플로우 외부화
스텝을 빈으로 정의할 수 있다. 스텝의 정의와 마찬가지로 스텝의 순서 또한 재사용 가능한 컴포넌트 형태로 만들 수 있다. 스프링 배치에선 스텝의 순서를 외부화하는 세 가지 방법이 있다. 첫 번째는 스텝의 시퀀스를 독자적인 플로우로 만드는 방법이다. 두 번째는 플로우 스텝(flow step)을 사용하는 방법이다. 마지막은 잡 내에서 다른 잡을 호출하는 방법이다.
1) 플로우 사용하기
// 플로우 빌더로 플로우를 정의
@Bean
public Flow preProcessingFlow() {
return new FlowBuilder<Flow>("preProcessingFlow").start(loadFileStep())
.next(loadCustomerStep())
.next(updateStartStep())
.build();
}
// 잡 빌더로 전달해 플로우를 실행
@Bean
public Job conditionalStepLogicJob() {
return this.jobBuilderFactory.get("conditionalStepLogicJob")
.start(preProcessingFlow())
.next(runBatch())
.end()
.build();
}
- 플로우 빌더로 플로우를 정의하고 ID를 부여한 후 잡에서 참조한다 (플로우를 잡 빌더로 전달해 플로우를 실행)
- JobRepository 관점에서는 플로우를 사용하는 것과 잡 내에서 스텝을 직접 구성하는 것에 차이는 없다
- 즉, 플로우의 스텝이 잡의 일부분으로 저장되어 있다 (플로우 덩어리가 개별 스텝으로 쪼개짐)
2. 플로우 스텝을 사용하기
@Bean
public Job conditionalStepLogicJob() {
return this.jobBuilderFactory.get("conditionalStepLogicJob")
.start(intializeBatch())
.next(runBatch())
.build();
}
// Flow가 아닌 Step이 반환되는 것에 주목하자 (Step으로 Flow를 래핑)
@Bean
public Step intializeBatch() {
return this.stepBuilderFactory.get("initalizeBatch")
.flow(preProcessingFlow())
.build();
}
- 플로우를 스텝으로 래핑하고 이 스텝(=플로우 스텝)을 잡 빌더로 전달한다
- JobRepository 관점에서 플로우를 사용할 때와 차이가 있다
- 플로우가 담긴 스텝을 하나의 스텝처럼 기록한다 (플로우 덩어리가 하나의 스텝으로 기록됨)
- 플로우 스텝은 플로우가 쪼개져서 저장되지 않고, 하나의 스텝으로 저장되므로 개별 스텝을 집계하지 않고 플로우의 영향을 전체적으로 확인할 수 있다 (모니터링과 리포팅에 이점이 있음)
3. 잡 내에서 다른 잡을 호출 (잡 스텝)
@Bean
public Job conditionalStepLogicJob() {
return this.jobBuilderFactory.get("conditionalStepLogicJob")
.start(intializeBatch())
.next(runBatch())
.build();
}
@Bean
public Step intializeBatch() {
return this.stepBuilderFactory.get("initalizeBatch")
.job(proProcessingJob()) // 호출할 하위 잡 지정
.parametersExtractor(new DefaultJobParametersExtractor()) // 하위 잡으로 파라미터 전달
.build();
}
- 스텝 내에서 서브 잡을 호출하는 형태를 취한다
- 서브 잡은 다른 잡과 마찬가지로 JobRepository 내에서 식별됨
- 파라미터는 직접 전달하지 않으며, 파라미터를 추출해 하위 잡으로 전달하는 클래스를 정의하면 해당 파라미터 추출기가 전달한다
- 위에서는 DefaultJobParametersExtractor를 사용했다
- 강력한 기능이지만 잡 간의 의존성이 강해진다
- 예를 들어 하나의 마스터 잡이 여러 연결된 잡을 실행하면 잡을 중지하거나 실행을 건너뛰어야 하는 상황에서 처리가 어려워질 수 있다
- 따라서 잡 스텝은 지양하자
6. 참고
- 스프링 배치 완벽 가이드 2/e
- https://github.com/AcornPublishing/definitive-spring-batch
'Spring Batch' 카테고리의 다른 글
[Spring Batch] 스프링 배치4 - JobRepository와 메타 테이블 (0) | 2023.06.06 |
---|---|
[Spring Batch] 스프링 배치2 - 잡(Job) (0) | 2023.04.07 |
[Spring Batch] 스프링 배치1 - 기본 구조 및 도메인 (0) | 2023.03.31 |
1. 스텝이란?

잡이 전체적인 처리를 지칭하는 반면, 스텝은 잡의 구성 요소를 담당한다. 스텝(step)은 모든 단위 작업의 조각으로서 트랜잭션 내에서 자체적으로 입/출력을 처리한다. 즉, 스텝을 정의하자면 독립적이고 순차적으로 배치 처리를 수행하는 잡의 하위 컴포넌트라고 할 수 있다.
배치 처리는 일반적으로 데이터 처리와 관련이 있는데, 데이터 처리를 위한 태스크릿(Tasklet) 모델과 청크(Chunk) 기반 처리 모델 두 가지 유형의 처리 모델을 지원한다.
1) 태스크릿 모델
- 단일 명령만 실행하는 작업에 적합
- 삭제 SQL문 실행, 디렉터리를 정리하는 셸 스크립트, 알림 메일 전송 등
- Tasklet 인터페이스를 사용해 Tasklet.execute 메서드가 RepeatStatus.FINISHED를 반환할 때까지 트랜잭션 범위 내에서 반복적으로 실행
2) 청크 기반 처리 모델

- 대량의 데이터를 처리하는 작업에 적합
- 한번에 하나씩 레코드나 아이템을 읽어 처리한 뒤 특정 데이터 저장소에 기록하는 일을 반복
- 2~3개의 주요 컴포넌트로 구성 (ItemReader, 필수가 아닌 ItemProcessor, ItemWriter)
- 레코드를 청크 또는 레코드 그룹 단위로 처리
- 각 청크는 자체 트랜잭션으로 실행되며, 처리에 실패했다면 마지막으로 성공한 트랜잭션 이후부터 다시 시작할 수 있음
- 각 컴포넌트가 수행하는 작업은 다음과 같다
- 먼저 ItemReader는 청크 단위로 처리할 모든 레코드를 반복적으로 메모리로 읽어온다
- ItemProcessor를 구성했다면 메모리로 읽어들인 아이템이 반복적으로 ItemProcessor를 거쳐간다
- 마지막으로 ItemWriter는 물리적 쓰기를 일괄적으로(청크 단위로) 처리한다
- 즉, ItemReader와 ItemProcessor의 처리 단위는 아이템이며 ItemWriter의 처리 단위는 청크이다
2. 태스크릿 스탭
태스크릿 스탭을 만드는 방법에는 두 가지 방법이 있다. 첫 번째 방법은 Tasklet 인터페이스를 구현하는 방법이다. 두 번째 방법은 사용자가 작성한 코드를 태스크릿 스텝처럼 실행되도록 하는 것이다.
1) Tasklet 인터페이스를 구현하는 방법
execute 메서드를 구현하고 RepeatStatus 객체를 반환하여 처리 완료 이후에 스프링 배치가 어떤 일을 할 지 정의한다. 반환값은 RepeatStatus.CONTINUABLE 또는 RepeatStatus.FINISHED 중의 하나이다. CONTINUABLE은 해당 태스크릿을 다시 실행하겠다는 의미이며, FINISHED는 처리의 성공 여부와 관계없이 해당 태스크릿의 처리를 완료하고 다음 처리를 이어서 하겠다는 의미이다. Tasklet은 함수형 인터페이스이므로 람다를 이용해 작성할 수 있다.
@Bean
public Step step1() {
return this.stepBuilderFactory.get("step1")
.tasklet((contribution, chunkContext) -> {
System.out.println("Hello, World!");
return RepeatStatus.FINISHED;
})
.build();
}
2) 사용자가 작성한 코드를 태스크릿 스텝처럼 실행되도록 하기
Tasklet 인터페이스를 직접 구현하는 대신 스프링 배치가 제공하는 CallableTaskletAdatper, MethodInvokingTaskingAdaptor, SystemCommandTasklet 구현체를 사용할 수도 있다.
1) CallableTaskletAdatper
- Callable<RepeatStatus> 인터페이스의 구현체를 구성할 수 있게 해주는 어댑터
- Callable 인터페이스는 Runnable 인터페이스와 유사하게 새 스레드에서 실행된다
- 다만 값을 반환하고 체크 예외를 바깥으로 던질 수 있다는 점에서 Runnable 인터페이스와 차이가 있다
- 스텝이 실행되는 스레드가 아닌 다른 스레드에서 실행하고 싶을 때 사용한다 (but 스텝과 병렬로 실행되진 않음)
@Bean
public Step callableStep() {
return this.stepBuilderFactory.get("callableStep")
.tasklet(tasklet())
.build();
}
@Bean
public CallableTaskletAdapter tasklet() {
CallableTaskletAdapter callableTaskletAdapter =
new CallableTaskletAdapter();
callableTaskletAdapter.setCallable(callableObject());
return callableTaskletAdapter;
}
@Bean
public Callable<RepeatStatus> callableObject() {
return () -> {
System.out.println("This was executed in another thread");
return RepeatStatus.FINISHED;
};
}
2) MethodInvokingTaskingAdaptor
- 이 구현체를 사용하면 기존에 존재하던 다른 클래스 내의 메서드를 잡 내의 태스크릿처럼 실행할 수 있다
- 호출하는 메서드가 ExisStatus를 반환하지 않는 한, ExitStatus.COMPLETED를 반환한다
- ExitStatus는 잡이나 스텝 종료 시 스프링 배치로 반환되는 값이다
@Bean
public Step methodInvokingStep() {
return this.stepBuilderFactory.get("methodInvokingStep")
.tasklet(methodInvokingTasklet(null))
.build();
}
@StepScope
@Bean
public MethodInvokingTaskletAdapter methodInvokingTasklet(
@Value("#{jobParameters['message']}") String message) {
MethodInvokingTaskletAdapter methodInvokingTaskletAdapter =
new MethodInvokingTaskletAdapter();
methodInvokingTaskletAdapter.setTargetObject(service());
methodInvokingTaskletAdapter.setTargetMethod("serviceMethod");
methodInvokingTaskletAdapter.setArguments(
new String[] {message});
return methodInvokingTaskletAdapter;
}
3) SystemCommandTasklet
- 시스템 명령을 비동기로 실행할 때 사용한다
@Bean
public Step systemCommandStep() {
return this.stepBuilderFactory.get("systemCommandStep")
.tasklet(systemCommandTasklet())
.build();
}
@Bean
public SystemCommandTasklet systemCommandTasklet() {
SystemCommandTasklet systemCommandTasklet = new SystemCommandTasklet();
systemCommandTasklet.setCommand("rm -rf /tmp.txt");
systemCommandTasklet.setTimeout(5000);
systemCommandTasklet.setInterruptOnCancel(true);
return systemCommandTasklet;
}
public static void main(String[] args) {
SpringApplication.run(SystemCommandJob.class, args);
}
3. 청크 기반 스텝
1) 청크 기반 스텝
청크는 커밋 간격(commit interval)에 의해 정의된다. 커밋 간격은 간단히 말하자면 청크를 구성하는 아이템 개수를 의미한다. 예컨대 커밋 간격을 50개 아이템으로 설정했다면 잡은 50개 아이템을 읽고, 50개 아이템을 처리한 다음에, 50개 아이템을 기록한다.
다음은 기본 청크 구현 예제이다. StepBuilderFactory에서 StepBuilder 인스턴스를 생성한 후 커밋 간격을 지정한다. 청크 기반 스텝은 태스크릿 스탭과 달리 build 메서드 호출 전에 리더(ItemReader 인터페이스 구현체) 및 라이터(ItemWriter 인터페이스 구현체)를 가져온다.
@Bean
public Step step1() {
return this.stepBuilderFactory.get("step1")
.<String, String>chunk(10)
.reader(itemReader(null))
.writer(itemWriter(null))
.build();
}
@Bean
@StepScope
public FlatFileItemReader itemReader(
@Value("#{jobParameters['inputFile']}") Resource inputFile
) {
return new FlatFileItemReaderBuilder<String>()
.name("itemReader")
.resource(inputFile)
.lineMapper(new PassThroughLineMapper())
.build();
}
@Bean
@StepScope
public FlatFileItemWriter<String> itemWriter(
@Value("#{jobParameters['outputFile']}") Resource outputFile
) {
return new FlatFileItemWriterBuilder<String>()
.name("itemWriter")
.resource(outputFile)
.lineAggregator(new PassThroughLineAggregator<>())
.build();
}
2) 청크 크기 구성하기
청크 크기를 구성하는 방법엔 여러가지가 있는데, 여기선 정적인 커밋 개수 설정 방법과 CompletionPolicy 구현체 사용 방법을 알아본다. 다른 방식들은 오류 처리와 관련이 있으므로 관련 절에서 살펴볼 예정이다.
1) 정적으로 설정 (하드코딩)
다음은 하드 코딩을 통해 정적으로 청크 크기를 설정한 예제이다. 이러한 방식은 모든 청크의 크기가 동일할 때 사용하는 것이 적절하다.
@Bean
public Step chunkStep() {
return this.stepBuilderFactory.get("chunkStep")
.<String, String>chunk(1000)
.reader(itemReader())
.writer(itemWriter())
.build();
}
@Bean
public ListItemReader<String> itemReader() {
List<String> items = new ArrayList<>(100000);
for (int i = 0; i < 100000; i++) {
items.add(UUID.randomUUID().toString());
}
return new ListItemReader<>(items);
}
@Bean
public ItemWriter<String> itemWriter() {
return items -> {
for (String item : items) {
System.out.println(">> current item = " + item);
}
};
}
2) CompletionPolicy 인터페이스의 구현체
청크 크기를 정적으로 정의하는 것이 모든 상황에 적절하진 않다. 청크마다 크기가 동일하지 않을 수 있는데, 이러한 경우에 CompletionPolicy 인터페이스의 구현체를 사용할 수 있다.
CompletionPolicy 인터페이스는 청크의 완료 여부를 결정할 수 있는 결정 로직을 정의한다. 스프링 배치는 다양한 CompletionPolicy 인터페이스의 구현체를 제공한다. 기본적으로는 SimpleCompletionPolicy를 사용한다.
- SimpleCompletionPolicy: 처리된 아이템 개수가 미리 설정해둔 임곗값에 도달하면 청크 완료로 간주한다
- TimeoutTerminationPolicy: 타임아웃 값을 설정한 후, 청크 내의 처리 시간이 해당 값을 넘으면 청크 완료로 간주한다
- CompositeCompletionPolicy: 여러 정책을 포함하며, 여러 정책 중 하나라도 청크 완료라고 판단되면 청크 완료로 간주한다
CompositeCompletionPolicy를 사용하면 청크 완료 여부를 결정하는 여러 정책을 함께 구성할 수 있다. CompositeCompletionPolicy는 자신이 포함하고 있는 여러 정책 중 하나라도 청크 완료라고 판단된다면 해당 청크가 완료된 것으로 간주한다. 타임아웃만으로 청크의 완료 여부를 판단하는 TimeoutTerminationPolicy은 대부분 CompositeCompletionPolicy의 일부로 사용된다.
@Bean
public Step chunkStep() {
return this.stepBuilderFactory.get("chunkStep")
.<String, String>chunk(completionPolicy())
.reader(itemReader())
.writer(itemWriter())
.build();
}
@Bean
public ListItemReader<String> itemReader() {
List<String> items = new ArrayList<>(100000);
for (int i = 0; i < 100000; i++) {
items.add(UUID.randomUUID().toString());
}
return new ListItemReader<>(items);
}
@Bean
public ItemWriter<String> itemWriter() {
return items -> {
for (String item : items) {
System.out.println(">> current item = " + item);
}
};
}
@Bean
public CompletionPolicy completionPolicy() {
CompositeCompletionPolicy policy =
new CompositeCompletionPolicy();
policy.setPolicies(
new CompletionPolicy[] {
new TimeoutTerminationPolicy(3),
new SimpleCompletionPolicy(1000)});
return policy;
}
위와 같이 스프링 배치에서 기본적으로 제공하는 구현체를 사용할 수 있지만, CompletionPolicy를 직접 구현하는 방법도 있다. CompletionPolicy 인터페이스는 다음과 같다.
package org.springframework.batch.repeat;
public interface CompletionPolicy {
boolean isComplete(RepeatContext var1, RepeatStatus var2);
boolean isComplete(RepeatContext var1);
RepeatContext start(RepeatContext var1);
void update(RepeatContext var1);
}
- start: 청크 시작 시 해당 구현체가 필요로 하는 모든 내부 상태를 초기화한다
- update: 각 아이템이 처리될 때마다 호출되면서 내부 상태를 갱신한다
- isComplete(var1): 내부 상태를 이용해 청크 완료 여부를 판단한다
- isComplete(var1, var2): 청크 완료 여부의 상태를 기반으로 결정 로직을 수행한다
이를 실제로 구현한 예제는 다음과 같다.
public class RandomChunkSizePolicy implements CompletionPolicy {
private int chunksize;
private int totalProcessed;
private Random random = new Random();
// 청크 완료 여부의 상태를 기반으로 결정 로직을 수행한다.
@Override
public boolean isComplete(RepeatContext context,
RepeatStatus result) {
if(RepeatStatus.FINISHED == result) {
return true;
}
else {
return isComplete(context);
}
}
// 내부 상태를 이용해 청크 완료 여부를 판단한다.
@Override
public boolean isComplete(RepeatContext context) {
return this.totalProcessed >= chunksize;
}
// 청크 시작 시 해당 구현체가 필요로 하는 모든 내부 상태를 초기화한다.
@Override
public RepeatContext start(RepeatContext parent) {
this.chunksize = random.nextInt(20);
this.totalProcessed = 0;
System.out.println("The chunk size has been set to " +
this.chunksize);
return parent;
}
// 각 아이템이 처리될 때마다 호출되면서 내부 상태를 갱신한다.
@Override
public void update(RepeatContext context) {
this.totalProcessed++;
}
}
4. 스텝 리스너
시작 및 종료 이벤트를 다루는 잡 리스너와 기본적으로 동일하다. 다만 이벤트의 범위가 잡 전체가 아닌 개별 스텝으로 한정된다는 차이가 있다. 관련 인터페이스로는 스텝과 청크의 시작과 끝에서 특정 로직을 처리할 수 있게 해주는 ChunkListener와 StepExecutionListener임가 있다.
StepExecutionListener와 ChunkListener 모두 JobExecutionListener 인터페이스와 유사한 메서드를 제공한다.
- StepExecutionListener: beforeStep(void), afterStep(ExitStatus)
- afterStep은 ExitStatus를 반환하는데, 리스너가 이 ExitStatus가 잡에 전달되기 전에 수정할 수 있다
- 이 기능은 무결성을 검증하는 로직 등에서 유용하게 사용할 수 있다
- ChunkListener: beforeChunk(void), afterChunk(void)
- 잡 리스너와 마찬가지로 @BeforeStep, @AfterStep, @BeforeChunk, @AfterChunk와 같은 애너테이션을 사용할 수 있다
public class LoggingStepStartStopListener {
@BeforeStep
public void beforeStep(StepExecution stepExecution) {
System.out.println(stepExecution.getStepName() + " has begun!");
}
@AfterStep
public ExitStatus afterStep(StepExecution stepExecution) {
System.out.println(stepExecution.getStepName() + " has ended!");
return stepExecution.getExitStatus();
}
}
- 잡 리스너와 달리 래핑하지 않고 사용할 수 있다
@Bean
public Step chunkStep() {
return this.stepBuilderFactory.get("chunkStep")
.<String, String>chunk(1000)
.reader(itemReader())
.writer(itemWriter())
.listener(new LoggingStepStartStopListener())
.build();
}
5-1. 스텝 플로우의 조건로직
지금까지 소개했던 스텝 구성방식은 순차적으로 실행되는 방식이었다. 그러나 스텝을 반드시 이와 같이 처리해야 하는 것은 아니다. 동적으로 스텝 진행 순서를 결정할 수 있는 방법으로 스텝 플로우(flow)가 있다. 스프링 배치는 잡 흐름을 커스터마이징할 수 있는 방법을 제공한다.
실행할 스텝을 결정하거나 주어진 스텝을 실행하는 데엔 스프링 배치의 조건 로직(Conditional Logic)이 사용된다. 스프링 배치의 스텝은 기본적으로 JobBuilder의 next 메서드를 사용해서 지정한 순서대로 실행된다. 아래와 같이 전이(transition)을 사용하면 다른 순서로 실행할 수도 있다. 각 스텝의 실행결과에 따라 스텝의 흐름이 유동적으로 변한다.
@Bean
public Job job() {
return this.jobBuilderFactory.get("conditionalJob")
.start(firstStep())
.on("FAILED").to(failureStep()) // firstStep이 ExitStatus.FAILED를 반환하면 failureStep 실행
.from(firstStep()).on("*").to(successStep()) // 그 외의 경우 successStep 실행
.end()
.build();
}
on 메서드는 스텝의 ExitStatus를 평가해 어떤 일을 수행할지 결정할 수 있게 한다. 참고로 ExitStatus는 잡이나 스텝 종료 시 스프링 배치로 반환되는 값인 반면, BatchStatus는 잡이나 스텝의 현재 상태를 식별하는 JobExecution이나 StepExecution의 애트리뷰트다. ExitStatus 값의 실체는 문자열이므로 다음과 같이 와일드 카드를 사용할 수도 있다.
- *는 0개 이상의 문자를 일치(match)시킨다는 것을 의미
- C* -> C, COMPLETE, CORRECT 모두 일치
- ?는 1개의 문자를 일치(match)시킨다는 것을 의미
- ?AT -> CAT, KAT과는 일치 / THAT과는 불일치
ExitStatus만으로 다음 어떤 스텝을 실행할지 결정할 수는 있지만 충분하진 않다. 예를 들어 특정 레코드를 건너뛰었을 때 특정 스탭을 실행시키지 않으려고 할 수 있다. ExitStatus를 평가한다는 것은 스텝의 종료 후 상태만 보고 다음 로직을 결정한다는 것인데, 스텝 처리중의 정보는 알 수 없으므로 이런 처리가 불가능하다. JobExecutionDecider 인터페이스를 구현하면 더욱 많은 정보를 가지고 동적으로 다음 로직을 결정할 수 있다.
package org.springframework.batch.core.job.flow;
public interface JobExecutionDecider {
FlowExecutionStatus decide(JobExecution var1, @Nullable StepExecution var2);
}
JobExecution과 StepExecution을 아규먼트로 전달받아 FlowExecutionStatus(BatchStatus/ExitStatus쌍을 래핑한 객체)를 반환한다. JobExecution과 StepExecution를 이용할 수 있으므로 다음에 무엇을 수행할지 결정을 내릴 때 모든 정보를 사용할 수 있다. 관련해서 ExecutionContext에 대한 정보는 여기서 확인하자.
public class RandomDecider implements JobExecutionDecider {
private Random random = new Random();
public FlowExecutionStatus decide(JobExecution jobExecution,
StepExecution stepExecution) {
if (random.nextBoolean()) {
return new
FlowExecutionStatus(FlowExecutionStatus.COMPLETED.getName());
} else {
return new
FlowExecutionStatus(FlowExecutionStatus.FAILED.getName());
}
}
}
@Bean
public Job job() {
return this.jobBuilderFactory.get("conditionalJob")
.start(firstStep())
.next(decider())
.from(decider()).on("FAILED").to(failureStep())
.from(decider()).on("*").to(successStep())
.end()
.build();
}
@Bean
public JobExecutionDecider decider() {
return new RandomDecider();
}
5-2. 잡 종료하기
JobInstance는 성공적으로 완료되면 두 번 이상 실행될 수 없다. 따라서 스프링 배치에서 동적으로 잡을 종료한다면 사용자는 잡의 종료 상태를 알아야 한다. 종료 상태는 Completed, Failed, Stopped 세 가지로 나뉘며, 이러한 상태는 스프링 배치가 JobRepository에 저장할 BatchStatus를 판별할 때 스텝의 ExitStatus를 평가함으로써 식별된다. BatchStatus는 StepExecution이나 JobExecution 내에 보관되며, JobRepository에 저장된다.
- Completed
- 잡이 성공적으로 종료됐음을 의미
- 동일한 파라미터를 사용해 다시 실행할 수 없다
- Failed
- 잡이 성공적으로 완료되지 않았음을 의미
- 동일한 파라미터를 사용해 다시 실행할 수 있다
- Stopped
- 잡이 중단됐음을 의미, 다시 시작할 수 있다
- 중단된 위치에서 잡을 다시 시작할 수 있다
- 이 상태는 스텝 사이에 사람의 개입이 필요하거나 다른 검사나 처리가 필요한 상황에 유용하다
빌더가 제공하는 메서드에 따라 잡의 종료 상태가 결정된다.
- end(): Completed 상태로 잡 종료
@Bean
public Job job() {
return this.jobBuilderFactory.get("conditionalJob")
.start(firstStep())
.on("FAILED").end()
.from(firstStep()).on("*").to(successStep()).end()
.build();
}
- fail(): Failed 상태로 잡 종료
@Bean
public Job job() {
return this.jobBuilderFactory.get("conditionalJob")
.start(firstStep())
.on("FAILED").fail()
.from(firstStep()).on("*").to(successStep()).end()
.build();
}
- stopAndRestart(): FAILED로 종료된 잡을 재실행하면 인자로 넘긴 스텝부터 재실행
@Bean
public Job job() {
return this.jobBuilderFactory.get("conditionalJob")
.start(firstStep())
.on("FAILED").stopAndRestart(successStep())
.from(firstStep()).on("*").to(successStep()).end()
.build();
}
5-3. 플로우 외부화
스텝을 빈으로 정의할 수 있다. 스텝의 정의와 마찬가지로 스텝의 순서 또한 재사용 가능한 컴포넌트 형태로 만들 수 있다. 스프링 배치에선 스텝의 순서를 외부화하는 세 가지 방법이 있다. 첫 번째는 스텝의 시퀀스를 독자적인 플로우로 만드는 방법이다. 두 번째는 플로우 스텝(flow step)을 사용하는 방법이다. 마지막은 잡 내에서 다른 잡을 호출하는 방법이다.
1) 플로우 사용하기
// 플로우 빌더로 플로우를 정의
@Bean
public Flow preProcessingFlow() {
return new FlowBuilder<Flow>("preProcessingFlow").start(loadFileStep())
.next(loadCustomerStep())
.next(updateStartStep())
.build();
}
// 잡 빌더로 전달해 플로우를 실행
@Bean
public Job conditionalStepLogicJob() {
return this.jobBuilderFactory.get("conditionalStepLogicJob")
.start(preProcessingFlow())
.next(runBatch())
.end()
.build();
}
- 플로우 빌더로 플로우를 정의하고 ID를 부여한 후 잡에서 참조한다 (플로우를 잡 빌더로 전달해 플로우를 실행)
- JobRepository 관점에서는 플로우를 사용하는 것과 잡 내에서 스텝을 직접 구성하는 것에 차이는 없다
- 즉, 플로우의 스텝이 잡의 일부분으로 저장되어 있다 (플로우 덩어리가 개별 스텝으로 쪼개짐)
2. 플로우 스텝을 사용하기
@Bean
public Job conditionalStepLogicJob() {
return this.jobBuilderFactory.get("conditionalStepLogicJob")
.start(intializeBatch())
.next(runBatch())
.build();
}
// Flow가 아닌 Step이 반환되는 것에 주목하자 (Step으로 Flow를 래핑)
@Bean
public Step intializeBatch() {
return this.stepBuilderFactory.get("initalizeBatch")
.flow(preProcessingFlow())
.build();
}
- 플로우를 스텝으로 래핑하고 이 스텝(=플로우 스텝)을 잡 빌더로 전달한다
- JobRepository 관점에서 플로우를 사용할 때와 차이가 있다
- 플로우가 담긴 스텝을 하나의 스텝처럼 기록한다 (플로우 덩어리가 하나의 스텝으로 기록됨)
- 플로우 스텝은 플로우가 쪼개져서 저장되지 않고, 하나의 스텝으로 저장되므로 개별 스텝을 집계하지 않고 플로우의 영향을 전체적으로 확인할 수 있다 (모니터링과 리포팅에 이점이 있음)
3. 잡 내에서 다른 잡을 호출 (잡 스텝)
@Bean
public Job conditionalStepLogicJob() {
return this.jobBuilderFactory.get("conditionalStepLogicJob")
.start(intializeBatch())
.next(runBatch())
.build();
}
@Bean
public Step intializeBatch() {
return this.stepBuilderFactory.get("initalizeBatch")
.job(proProcessingJob()) // 호출할 하위 잡 지정
.parametersExtractor(new DefaultJobParametersExtractor()) // 하위 잡으로 파라미터 전달
.build();
}
- 스텝 내에서 서브 잡을 호출하는 형태를 취한다
- 서브 잡은 다른 잡과 마찬가지로 JobRepository 내에서 식별됨
- 파라미터는 직접 전달하지 않으며, 파라미터를 추출해 하위 잡으로 전달하는 클래스를 정의하면 해당 파라미터 추출기가 전달한다
- 위에서는 DefaultJobParametersExtractor를 사용했다
- 강력한 기능이지만 잡 간의 의존성이 강해진다
- 예를 들어 하나의 마스터 잡이 여러 연결된 잡을 실행하면 잡을 중지하거나 실행을 건너뛰어야 하는 상황에서 처리가 어려워질 수 있다
- 따라서 잡 스텝은 지양하자
6. 참고
- 스프링 배치 완벽 가이드 2/e
- https://github.com/AcornPublishing/definitive-spring-batch
'Spring Batch' 카테고리의 다른 글
[Spring Batch] 스프링 배치4 - JobRepository와 메타 테이블 (0) | 2023.06.06 |
---|---|
[Spring Batch] 스프링 배치2 - 잡(Job) (0) | 2023.04.07 |
[Spring Batch] 스프링 배치1 - 기본 구조 및 도메인 (0) | 2023.03.31 |