Spring Batch 시리즈를 포스팅하고 있습니다. 기준이 되는 버전은 5.x 버전입니다.
1. Spring Batch 소개
2. Job과 Step의 구조 및 생성 - 현재 포스팅
3. Job과 Step의 실행 과정
4. Job의 흐름 제어 - Flow
5. FlowJob의 실행 과정
6. 청크 기반 프로세싱의 구조 및 생성
7. 청크 기반 프로세싱의 진행 과정
8. Flat File, JSON 형식 파일 읽기 및 쓰기
9. DB 데이터 읽기 및 쓰기
10. Step의 예외 제어하기(스킵, 재시도)
11. 배치 작업 확장하기
이번에는 Spring Batch에서 제공하는 `Job`과 `Step`의 기본 구현이 어떤 구조로 되어있는지 살펴보고, 이를 생성할 수 있는 각각의 빌더 클래스들(`JobBuilder`, `StepBuilder`)을 살펴볼 것이다.
그리고 `Job`과 `Step`각각의 빌더 클래스에서 설정할 수 있는 공통 속성들과, 공통 속성들을 설정할 수 있는 각 빌더 클래스의 API에 대해서 상세하게 알아볼 것이다.
Job의 구조와 생성
`Job` 인터페이스는 위와 같이 기본 구현되어 있다.
`AbstractJob`에서 공통 기능들을 구현해 놓았고, 각 구현체별로 다르게 작동하는 부분은 추상 메서드로 정의하여 각 구현체에 구현되어 있다.
`SimpleJob`은 구성된 `Step`들을 순차적으로 실행시키는 가장 기본적인 `Job`이다.
`FlowJob`은 `Flow`에 `Job`의 실행을 위임시키는데, `Flow`는 분기문 처럼 `Step`의 제어 흐름을 정의할 수 있는 객체이다. 따라서 순차적으로 실행시킬수도, 특정한 조건에 따라 실행시킬 `Step`을 정할 수도 있다.
`GroupAwareJob`은 여러 `Job`들을 그룹으로 묶기 위해 특정 `Job`에 그룹 이름을 부여할 수 있도록 하는 일종의 `Wrapper` 구현체로, 사실 위임되는 `Job` 구현체는 `FlowJob`, `SimpleJob` 중 하나이다.
그렇다면 실제 `Job`을 어떻게 만들 수 있는지, `JobBuilder` 클래스부터 살펴보자.
JobBuilder
`JobBuilder`는 다양한 종류의 `Job`을 생성하도록 편의 객체이다.
Spring Batch에 기본 구현되어있는 `Job` 인터페이스 구현체는 `Step`을 기준으로 작동하는 `SimpleJob`, `Flow`를 기준으로 작동하는 `FlowJob`이 있다. 두 구현체는 각각 `SimpleJobBuilder`, `FlowJobBuilder`를 통해 생성할 수 있는데, 이 빌더를 `JobBuilder`에서 얻을 수 있다.
public class JobBuilder extends JobBuilderHelper<JobBuilder> {
public JobBuilder(String name, JobRepository jobRepository) {
super(name, jobRepository);
}
public SimpleJobBuilder start(Step step) {
return new SimpleJobBuilder(this).start(step);
}
public JobFlowBuilder start(Flow flow) {
return new FlowJobBuilder(this).start(flow);
}
public JobFlowBuilder start(JobExecutionDecider decider) {
return new FlowJobBuilder(this).start(decider);
}
public JobFlowBuilder flow(Step step) {
return new FlowJobBuilder(this).start(step);
}
}
구현을 보면 start 메서드가 오버로딩 되어있는 것을 볼 수 있는데, `Step` 타입, `Flow` 타입에 따라 각각의 다른 빌더를 반환한다. (우선 `FlowJobBuilder`와 `JobFlowBuilder`는 같다고 생각만 하자. 추후에 `Flow`에 대해서 자세하게 다룰 예정이다.)
반환된 `SimpleJobBuilder`, `FlowJobBuilder`에서 각각 추가적으로 `Job`을 구성한 후, `build` 메서드를 호출하면 각 구현체에 맞는 `Job`을 반환한다. `SimpleJobBuilder`의 `build`를 간단하게 살펴보자.
public Job build() {
if (builder != null) {
return builder.end().build();
}
SimpleJob job = new SimpleJob(getName());
super.enhance(job);
job.setSteps(steps);
try {
job.afterPropertiesSet();
}
catch (Exception e) {
throw new JobBuilderException(e);
}
return job;
}
보면 `SimpleJob`을 만들어서 전/후처리 한 다음 반환하는 것을 볼 수 있다. (`enhance` 메서드를 호출한다는 점을 일단 알아두자. 아래에서 다룰 것이다)
JobBuilderHelper
`JobBuilder`, `SimpleJobBuilder`, `FlowJobBuilder` 모두 `JobBuilderHelper`추상 클래스의 구현체이다. `JobBuilderHelper`는 추상클래스로, 모든 유형의 `Job`에서 사용하는 공통 속성을 설정할 수 있도록 도와준다.
public abstract class JobBuilderHelper<B extends JobBuilderHelper<B>> {
...
private final CommonJobProperties properties;
...
public B validator(JobParametersValidator jobParametersValidator) {
properties.jobParametersValidator = jobParametersValidator;
...
}
public B incrementer(JobParametersIncrementer jobParametersIncrementer) {
properties.jobParametersIncrementer = jobParametersIncrementer;
...
}
...
public static class CommonJobProperties {
private Set<JobExecutionListener> jobExecutionListeners = new LinkedHashSet<>();
private boolean restartable = true;
private JobRepository jobRepository;
private BatchJobObservationConvention observationConvention = new DefaultBatchJobObservationConvention();
private ObservationRegistry observationRegistry;
private MeterRegistry meterRegistry;
private JobParametersIncrementer jobParametersIncrementer;
private JobParametersValidator jobParametersValidator;
...
}
}
`JobBuilderHelper`를 간략하게 추려봤다. `CommonJobProperties`를 멤버로 가지고 있는데, 내부 정적 클래스로 정의되어 있다. `CommonJobProperties`가 가지고 있는 멤버들이 `Job`에서 공통적으로 사용하는 속성들이다. `JobBuilderHelper` 클래스는 `validator`, `incremeter` 등등의 메서드를 통해 공통 속성들을 정의할 수 있도록 정의된 추상 클래스이다. (각 공통 속성들이 어떤 역할을 하는지는 아래에서 다룰 예정이다)
`SimpleJobBuilder`, `FlowJobBuilder` 모두 이 `JobBuilderHelper`를 구현하고 있기 때문에, `Job`을 만들 때 공통 속성들을 설정해줄 수 있다. 한번 실제 예시를 보자.
@Bean
fun membershipJob(): Job {
return JobBuilder("membership-job", jobRepository)
.start(membershipUpdateStep())
.next(notifyStep())
.validator {
if (it?.getLong("key") == null)
throw JobParametersInvalidException("필수 파라미터가 없습니다.")
}
.incrementer {
JobParametersBuilder()
.addLong("key", it!!.getLong("key")!! + 1)
.toJobParameters()
}
.build()
}
이전 포스팅에서 간단하게 만들어봤던 예제이다. 보면 `Job`을 생성할 때 `validator`, `incremeter` 메서드를 통해 공통 속성들을 설정할 수 있는 것을 볼 수 있다.
이렇게 `JobBuilder`로부터 설정된 공통 속성들은, 실제 `build` 메서드를 호출할 때 모두 설정된다. 앞에서 `SimpleJobBuilder`의 `build` 메서드 구현을 봤는데, 중간에 부모인 `JobBuilderHelper`에 정의된 `enhance`를 호출하는 부분이 바로 이 공통 속성들을 설정하는 부분이자. 한번 `enhance` 메서드의 구현을 살펴보자.
protected void enhance(AbstractJob job) {
job.setJobRepository(properties.getJobRepository());
JobParametersIncrementer jobParametersIncrementer = properties.getJobParametersIncrementer();
if (jobParametersIncrementer != null) {
job.setJobParametersIncrementer(jobParametersIncrementer);
}
JobParametersValidator jobParametersValidator = properties.getJobParametersValidator();
if (jobParametersValidator != null) {
job.setJobParametersValidator(jobParametersValidator);
}
BatchJobObservationConvention observationConvention = properties.getObservationConvention();
if (observationConvention != null) {
job.setObservationConvention(observationConvention);
}
ObservationRegistry observationRegistry = properties.getObservationRegistry();
if (observationRegistry != null) {
job.setObservationRegistry(observationRegistry);
}
MeterRegistry meterRegistry = properties.getMeterRegistry();
if (meterRegistry != null) {
job.setMeterRegistry(meterRegistry);
}
Boolean restartable = properties.getRestartable();
if (restartable != null) {
job.setRestartable(restartable);
}
List<JobExecutionListener> listeners = properties.getJobExecutionListeners();
if (!listeners.isEmpty()) {
job.setJobExecutionListeners(listeners.toArray(new JobExecutionListener[0]));
}
}
`properties`에서 공통 속성들을 꺼내서 `job`에 설정하고 있음을 볼 수 있다.
Step의 구조와 생성
`Step` 인터페이스는 위와 같이 기본 구현되어 있다. `Step` 역시 `Job`처럼 여러 종류가 있고, 공통 기능들은 `AbstractStep`에 구현되어 있는 방식으로 되어 있다.
`TaskletStep`은 `Tasklet`을 호출하여 `Step`을 진행하는 구현체이다.
`FlowStep`은 `Step`의 진행을 `Flow`에 위임시키는 구현체이다.
`JobStep`은 `Step`의 진행을 또 다른 `Job`에 위임시키는 구현체이다. 보통 `Step`의 로직이 크고 복잡한 경우 `Job`을 통해 이를 모듈화하기 위해 사용하거나, 여러 `Job` 간에 종속성이 있는 경우 이를 관리하기 위해 사용한다. 단순하게 `Step` 내부에서 `Job`을 실행시킨다고 보면 된다.
한번 `Step`을 생성하는 `StepBuilder`를 살펴보자.
StepBuilder
`JobBuilder`와 구조가 완전히 똑같다. 다양한 `Step`을 생성할 수 있도록 제공되는 편의 객체이다. 간략하게 소스코드를 살펴보자.
public class StepBuilder extends StepBuilderHelper<StepBuilder> {
...
public TaskletStepBuilder tasklet(Tasklet tasklet, PlatformTransactionManager transactionManager) {
return new TaskletStepBuilder(this).tasklet(tasklet, transactionManager);
}
public <I, O> SimpleStepBuilder<I, O> chunk(int chunkSize, PlatformTransactionManager transactionManager) {
return new SimpleStepBuilder<I, O>(this).transactionManager(transactionManager).chunk(chunkSize);
}
public PartitionStepBuilder partitioner(String stepName, Partitioner partitioner) {
return new PartitionStepBuilder(this).partitioner(stepName, partitioner);
}
public JobStepBuilder job(Job job) {
return new JobStepBuilder(this).job(job);
}
public FlowStepBuilder flow(Flow flow) {
return new FlowStepBuilder(this).flow(flow);
}
...
}
각 메서드별로 다양한 타입의 빌더 클래스를 제공한다. `JobBuilder`가 메서드 오버로딩을 통해 파라미터의 타입에 따라 `SimpleJobBuilder`, `FlowJobBuilder`를 반환한 것처럼, `Step` 역시 파라미터에 따라 다향한 `Step`의 구현체를 빌드할 수 있도록 빌더 구현체를 반환하고 있다.
각 `Job`과 `Step`구현체에 대해서는 다음 섹션에서 자세히 알아볼 것이다.
StepBuilderHelper
`Step`을 생성할 수 있는 여러 빌더 클래스들은 `Job`과 마찬가지로 `StepBuilderHelper`를 상속받는다. 이는 `JobBuilderHelper`처럼 `Step`을 생성하기 위해 공통적으로 사용하는 속성들을 `properties`에 저장하고, 이를 `enhance` 메서드를 통해 설정할 수 있도록 설계되어 있다. `JobBuilderHelper`와 구조가 정말 동일하기 때문에, 간략하게 코드만 보고 넘어가겠다.
public abstract class StepBuilderHelper<B extends StepBuilderHelper<B>> {
...
protected final CommonStepProperties properties;
...
protected void enhance(AbstractStep step) {
step.setJobRepository(properties.getJobRepository());
BatchStepObservationConvention observationConvention = properties.getObservationConvention();
if (observationConvention != null) {
step.setObservationConvention(observationConvention);
}
ObservationRegistry observationRegistry = properties.getObservationRegistry();
if (observationRegistry != null) {
step.setObservationRegistry(observationRegistry);
}
MeterRegistry meterRegistry = properties.getMeterRegistry();
if (meterRegistry != null) {
step.setMeterRegistry(meterRegistry);
}
Boolean allowStartIfComplete = properties.allowStartIfComplete;
if (allowStartIfComplete != null) {
step.setAllowStartIfComplete(allowStartIfComplete);
}
step.setStartLimit(properties.startLimit);
List<StepExecutionListener> listeners = properties.stepExecutionListeners;
if (!listeners.isEmpty()) {
step.setStepExecutionListeners(listeners.toArray(new StepExecutionListener[0]));
}
}
...
public static class CommonStepProperties {
private List<StepExecutionListener> stepExecutionListeners = new ArrayList<>();
private int startLimit = Integer.MAX_VALUE;
private Boolean allowStartIfComplete;
private JobRepository jobRepository;
private BatchStepObservationConvention observationConvention = new DefaultBatchStepObservationConvention();
private ObservationRegistry observationRegistry = ObservationRegistry.NOOP;
private MeterRegistry meterRegistry = Metrics.globalRegistry;
...
}
}
`Step`도 한번 실제 생성하는 예제를 살펴보자.
@Bean
fun notifyStep(): Step {
return StepBuilder("notify-step", jobRepository)
.tasklet(transactionManager) { contribution, chunkContext ->
log.info("notify...")
RepeatStatus.FINISHED
}
.allowStartIfComplete(true)
.taskExecutor(SimpleAsyncTaskExecutor())
.build()
}
`tasklet` 메서드를 통해 `TaskletStepBuilder`를 받은 다음, `StepBuilderHelper`에 정의된 메서드를 통해 공통 속성들을 설정했다.
JobBuilder API
그러면 본격적으로 `JobBuilder`(정확히는 `JobBuilderHelper`)를 통해 설정할 수 있는 공통 속성들이 각각 어떤 기능을 수행하는지 알아보자.
validator - JobParameteresValidator
`validator` 메서드를 통해 `JobParameteresValidator`를 설정할 수 있다.
@FunctionalInterface
public interface JobParametersValidator {
void validate(@Nullable JobParameters parameters) throws JobParametersInvalidException;
}
`validate`라는 메서드 하나만 가지고 있는 SAM 인터페이스며, `Job`을 실행하기 전 `JobParameters`를 검증하는 역할을 수행한다. 추후에 알아볼 `JobLauncher`의 `run`메서드에서 실행된다.
JobBuilder("membership-job", jobRepository)
.start(membershipUpdateStep())
.next(notifyStep())
.validator {
if (it?.getLong("key") == null)
throw JobParametersInvalidException("필수 파라미터가 없습니다.")
}
위와 같이 개발자가 직접 람다형식으로 구현해도 되며, 다른 파일에 구현체를 만들어도 된다. Spring Batch에서 제공하는 기본 구현으로는 `CompositeJobParametersValidator`와, `DefaultJobParametersValidator`가 있다.
`CompositeJobParametersValidator`는 이름 그대로 복수의 `JobParameteresValidator`를 합성하여 주입된 순서대로 실행시키는 간단한 구현체이다. 멤버로 `List<JobParametersValidator>`를 가지고 있으며, 해당 멤버를 순회하며 검증한다.
`DefaultJobParametersValidator`는 필수와 선택적 파라미터를 검증하는 구현체이다. 한번 코드를 살펴보자.
public class DefaultJobParametersValidator implements JobParametersValidator, InitializingBean {
...
private Collection<String> requiredKeys;
private Collection<String> optionalKeys;
...
@Override
public void validate(@Nullable JobParameters parameters) throws JobParametersInvalidException {
if (parameters == null) {
throw new JobParametersInvalidException("The JobParameters can not be null");
}
Set<String> keys = parameters.getParameters().keySet();
if (!optionalKeys.isEmpty()) {
Collection<String> missingKeys = new HashSet<>();
for (String key : keys) {
if (!optionalKeys.contains(key) && !requiredKeys.contains(key)) {
missingKeys.add(key);
}
}
if (!missingKeys.isEmpty()) {
logger.warn("The JobParameters contains keys that are not explicitly optional or required: " + missingKeys);
}
}
Collection<String> missingKeys = new HashSet<>();
for (String key : requiredKeys) {
if (!keys.contains(key)) {
missingKeys.add(key);
}
}
if (!missingKeys.isEmpty()) {
throw new JobParametersInvalidException("The JobParameters do not contain required keys: " + missingKeys);
}
}
...
}
`Collection<String>` 타입의 `requiredKeys`, `optionalKeys`를 멤버로 가지고 있으며, 멤버 변수명 그대로 각각 `JobParameters`에 필수로 있어야하는 파라미터의 key, 선택적으로 있어야 하는 파라미터의 key를 저장하는 필드이다.
`validate`메서드에서는 `JobParameters`에 필수 파라미터가 없다면 `JobParametersInvalidException`을 던진다. 앞서서 선택적으로 있어야 하는 파라미터가 없는 경우에는 로그만 남긴다.
incrementer - JobParametersIncrementer
`incrementer`메서드를 통해 `JobParametersIncrementer`를 설정할 수 있다.
`JobParametersIncrementer`는 이전 `Job`에서 사용했던 파라미터의 값을 증가시키는 역할을 한다. 저번 포스팅에서 `JobParameters`에 대해 설명했을 때, `JobInstance`의 유일성에 대해서 다뤘었다. Spring Batch에서는 `JobInstance`의 유일성을 보장하기 위해, 같은 `JobParameters`로 동일한 `Job`을 실행하지 못한다. (참고)
하지만 반복적으로 수행해야 하는 배치 작업인 경우에는 일일이 파라미터의 값을 다르게 설정해서 실행해야하는 불편함이 있다. 또한 실행마다 이전 파라미터의 값을 사용해서 값을 자동으로 변경해야 하는 경우도 있을 것이다. 이런 경우에는 `JobParametersIncrementer`를 사용하는 것을 고려해볼 수 있다.
@FunctionalInterface
public interface JobParametersIncrementer {
JobParameters getNext(@Nullable JobParameters parameters);
}
`JobParameteresValidator` 처럼 SAM 인터페이스로 선언되어 있다.
JobBuilder("membership-job", jobRepository)
.start(membershipUpdateStep())
.next(notifyStep())
.incrementer {
JobParametersBuilder()
.addLong("key", it!!.getLong("key")!! + 1)
.toJobParameters()
}
.build()
따라서 위와 같이 사용 시점에 람다를 통해 간단하게 구현할 수도 있다.
기본 구현으로는 `RunIdIncrementer`가 있는데, `Long`타입의 특정 파라미터를 1씩 올려주는 구현체이다.
public class RunIdIncrementer implements JobParametersIncrementer {
private static final String RUN_ID_KEY = "run.id";
private String key = RUN_ID_KEY;
public void setKey(String key) {
this.key = key;
}
@Override
public JobParameters getNext(@Nullable JobParameters parameters) {
JobParameters params = (parameters == null) ? new JobParameters() : parameters;
JobParameter<?> runIdParameter = params.getParameters().get(this.key);
long id = 1;
if (runIdParameter != null) {
try {
id = Long.parseLong(runIdParameter.getValue().toString()) + 1;
}
catch (NumberFormatException exception) {
throw new IllegalArgumentException("Invalid value for parameter " + this.key, exception);
}
}
return new JobParametersBuilder(params).addLong(this.key, id).toJobParameters();
}
}
`key` 멤버 변수와 일치하는 파라미터를 얻은 다음 해당 파라미터에 1을 추가해서 반환한다. `Long`타입이 아닌 경우에는 예외기 발생하기 때문에 무조건 `Long` 타입인 파라미터의 이름만을 `key`로 설정해야 한다.
listener - JobExecutionListener
`listener`메서드를 를 통해 `JobExecutionListener`을 설정할 수 있다. `JobExecutionListener`는 `Job`의 시작과 종료시점에 실행할 콜백을 제공하는 역할을 한다.
public interface JobExecutionListener {
default void beforeJob(JobExecution jobExecution) {}
default void afterJob(JobExecution jobExecution) {}
}
`Job`이 실행되기 전에 호출되는 `beforeJob`, 실행 후에 호출되는 `afterJob`이 정의되어 있다.
만약 멀티스레드를 사용하는 배치 작업이라면, 구현 시에 스레드 세이프하게 구현해야 상태를 유지할 수 있다. 만약 단일 스레드를 사용하는 배치 작업이라면, Spring Bean으로 등록해 싱글톤으로 사용하는 것을 권장한다고 한다.
기본 구현으로는 `CompositeJobExecutionListener`가 있으며, 여러 리스너를 복합하여 각각 위임하는 구현체이다.
public class CompositeJobExecutionListener implements JobExecutionListener {
private final OrderedComposite<JobExecutionListener> listeners = new OrderedComposite<>();
public void setListeners(List<? extends JobExecutionListener> listeners) {
this.listeners.setItems(listeners);
}
public void register(JobExecutionListener jobExecutionListener) {
listeners.add(jobExecutionListener);
}
@Override
public void afterJob(JobExecution jobExecution) {
for (Iterator<JobExecutionListener> iterator = listeners.reverse(); iterator.hasNext();) {
JobExecutionListener listener = iterator.next();
listener.afterJob(jobExecution);
}
}
@Override
public void beforeJob(JobExecution jobExecution) {
for (Iterator<JobExecutionListener> iterator = listeners.iterator(); iterator.hasNext();) {
JobExecutionListener listener = iterator.next();
listener.beforeJob(jobExecution);
}
}
}
`beforeJob`의 경우에는 `listners`를 순서대로, `afterJob`의 경우에는 역순으로 호출한다는 점을 주의하면 될 거 같다.
preventRestart
`Job`의 재시작을 방지하도록 설정한다.
`Job`은 무조건 성공한다는 보장이 없다. `Step`에서 중간에 예외가 발생하는 경우에는 당연히 전체 `Job`이 실패한다. 따라서 동일한 `JobParameters`로 `Job`을 실행하는 경우에는 이전에 실행했던 `JobExecution` 객체를 불러와서, 진행 상태를 파악한 후 실패 지점부터 다시 실행하게 된다.
하지만 같은 `JobParameters`를 가진 `Job`이 무조건 재시작되면 안되는 경우에는 `preventRestart`로 방지할 수 있다.
JobBuilder("membership-job", jobRepository)
.start(membershipUpdateStep())
.next(notifyStep())
.preventRestart()
.build()
사용은 메서드를 호출하기만 하면 된다.
StepBuilder API
이번에는 `StepBuilder`(정확히는 `StepBuilderHelper`)를 통해 설정할 수 있는 공통 속성들이 각각 어떤 기능을 수행하는지 알아보자.
startLimit
`Step`의 실행 횟수를 제한한다. `Step`에서 예외가 발생해서 `Job`을 재실행하면 역시 예외가 발생한 `Step`이 다시 시작된다. 다시 실행할 횟수를 제한하고 싶은 경우에는 `startLimit`을 통해 횟수를 제한할 수 있다.
StepBuilder("membership-update-step", jobRepository)
.tasklet(transactionManager) { contribution, chunkContext ->
log.info("membership update...")
RepeatStatus.FINISHED
}.startLimit(10)
.build()
사용은 위와 같이 하면 된다.
제한 횟수가 모든 `JobInstance` 전반적으로 적용되는 것이 아니다. 예를 들어 횟수를 5번으로 제한했다고 했을 때, key라는 파라미터에 1을 설정한 `JobInstance`에서 해당 `Step`이 5번 실행되었다고 해도, 파라미터를 2로 설정한 `Job`을 이후에 돌렸을 때는 실행 횟수가 0이다. `StepExecution`을 기준으로 적용된다.
`StepExecution`은 고유한 `JobInstance`에서 `Step`이 실행될 때 생기기 때문에, 전체 `JobInstance`를 통들어서 횟수가 제한이 걸리지 않는다. 즉, 다른 파라미터로 다른 `JobInstance`를 만들어서 `Step`을 실행한다면 이전에 다른 `JobInstance`에서 `Step`을 실행했더라도 다른 `StepExecution`이기 때문에 실행횟수가 0인 상태인 것이다.
그리고 주의할 점은 앞 포스팅에서 설명했던 `Tasklet`의 `RepeatStatus`와 관련된 사항이다. `RepeatStatus`가 `CONTINUABLE`으로 반환되는 경우 `Step`에서 계속 `Tasklet`을 실행시키는데, 이 `Tasklet`의 실행 횟수는 `startLimit`과 전혀 상관 없다. 이미 `Step`이 실행한 후 그 내부에서 `Tasklet`을 반복적으로 실행하는 것이기 때문에 `Tasklet`이 제한 횟수만큼 실행되었다고 종료되지 않는다. `Step`은 한번 실행된 것일 뿐이다.
listener - StepExecutionListener
`JobExecutionListener`와 동일하다. `Step`의 실행 전후에 호출되는 콜백을 설정할 수 있다. 사용 방법도 `Job`과 동일하니 더 서술하지는 않겠다.
allowStartIfComplete
`Job`을 재실행할 때, `Step`이 완료되었더라도 시작하도록 설정한다.
실행에 실패한 `Job`을 재실행하면, 이전에 성공했던 `Step`은 실행하지 않는다. 실패한 `Step`부터 재시작된다. 하지만 `allowStartIfComplete`를 사용하면 성공한 `Step`이라도 다시 시작한다.
StepBuilder("notify-step", jobRepository)
.tasklet(transactionManager) { contribution, chunkContext ->
log.info("notify...")
RepeatStatus.FINISHED
}
.allowStartIfComplete(true)
.build()
위와 같이 `Boolean` 타입을 인자로 주어 설정할 수 있다. 설정하지 않으면 기본값은 false 이다.
정리
이번에는 `Job`과 `Step`의 기본 구현들이 어떤 구조로 이루어져 있는지, 각각 생성할 때 어떤 방식으로 생성하고 생성자 클래스들이 어떻게 구현되어 있는지 살펴보았다. 그리고 `JobBuilderHelper`, `StepBuilderHelper`에서 제공하는 API들을 상세하게 알아보았다.
다음 포스팅에서는 배치 작업이 어떻게 실행되는지, 어떤 흐름으로 진행되는지 전반적인 과정에 대해서 살펴볼 것이다. 이 과정에서 이번에 다뤘던 `JobExecutionListener`, `JobParametersValidator`와 같은 구현체들이 어느 시점에 호출되어 그 기능을 수행하는 지 살펴볼 수 있을 것이다.