이번에는 Spring Batch Scope 에 대해서 알아보겠습니다. 알아볼 Scpoe 은 @StepScope 과 @JobScope 입니다. 또한 이것들과 연관이 있는 Job Parameter 에 대해서도 알아보겠습니다.
Spring Batch의 경우 외부 혹은 내부에서 파라미터를 받아 여러 Batch 컴포넌트에서 사용 할 수 있게 지원 하고 있는데요. 이 파라미터를 Job Parameter라고 합니다.
Job Parameter를 사용하기 위해선 항상 Spring Batch 전용 Scope를 선언해야 합니다. 그것들은 바로 @StepScope와 @JobScope 2가지가 있습니다. 사용법은 간단합니다. SpEL 로 선언해서 사용 할 수 있습니다.
@Value("#{jobParameters[Parameter Name]}")
주의 할 점
1. @StepScope/@JobScope 없이 @Value("#{jobParameters[...]}" )를 쓰면 null이 주입되거나 초기화 시점 오류가 납니다.
2. Batch 5.x에서는 JobBuilderFactory/StepBuilderFactory 대신 new JobBuilder(...), new StepBuilder(...) 를 사용하는 것이 표준입니다
@Bean
public Job scopeJob(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new JobBuilder("scopeJob", jobRepository)
.start(scopeStep1(jobRepository, transactionManager, null))
.next(scopeStep2(jobRepository, transactionManager))
.build();
}
@Bean
@JobScope
public Step scopeStep1(JobRepository jobRepository,
PlatformTransactionManager transactionManager,
@Value("#{jobParameters['requestDate']}") String requestDate) {
return new StepBuilder("scopeStep1", jobRepository)
.tasklet((contribution, chunkContext) -> {
log.info(">>>>> This is scopeStep1");
log.info(">>>>> requestDate = {}", requestDate);
return RepeatStatus.FINISHED;
}, transactionManager)
.build();
}
@Bean
public Step scopeStep2(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new StepBuilder("scopeStep2", jobRepository)
.tasklet(scopeStep2Tasklet(null), transactionManager)
.build();
}
@Bean
@StepScope
public Tasklet scopeStep2Tasklet(@Value("#{jobParameters['requestDate']}") String requestDate) {
return (contribution, chunkContext) -> {
log.info(">>>>> This is scopeStep2");
log.info(">>>>> requestDate = {}", requestDate);
return RepeatStatus.FINISHED;
};
}
@JobScope는 Step 선언문에서 사용 가능하고, @StepScope는 Tasklet이나 ItemReader, ItemWriter, ItemProcessor에서 사용할 수 있습니다.
이전에 Spring Batch 4.X 에서는 Job Parameter의 타입으로 사용할 수 있는 것은 Double, Long, Date, String 이 있습니다.
하지만, 이번에 Spring Batch 5.X 들어 서면서 LocalDate와 LocalDateTime을 Job Parameter의 타입으로 사용 할 수 있게 되었습니다. 다만 아쉬운건 타입을 직접 입력해야 한다는 것 입니다. 이건 잠시 뒤에 알아 보고
우선 예제 코드를 보시면 호출하는 쪽에서 null 를 할당하고 있습니다.
이는 Job Parameter의 할당이 어플리케이션 실행시에 하지 않기 때문에 가능한데요, 이게 어떤 무슨 말일까요? 자세히 보도록 하겠습니다.
@StepScope, @JobScope Spring Batch 에서 제공하는 어노테이션으로 Bean Scope 을 지원 합니다. (Spring Bean 은 기본적으로 Sigleton)
그러나 아래 소스 처럼 Spring Batch 컴포넌트 (Tasklet, ItemReader, ItemWriter, ItemProcessor 등)에 @StepScope를 사용하게 되면
@Bean
@StepScope
public Tasklet scopeStep2Tasklet(@Value("#{jobParameters['requestDate']}") String requestDate) {
return (contribution, chunkContext) -> {
log.info(">>>>> This is scopeStep2");
log.info(">>>>> requestDate = {}", requestDate);
return RepeatStatus.FINISHED;
};
}
Spring Batch 에서 Spring Container 를 통해서 Step의 실행시점에 해당 컴포넌트를 Spring Bean 으로 생성합니다. 같은 이치로 @JobScope는 Job 실행시점에 Bean이 생성합니다.
즉, Bean의 생성 시점을 지정된 Scope가 실행되는 시점으로 지연시킵니다
그렇다면, Spring Batch 왜 Bean 을 생성지점이 application 실행 시점이 아니라, Job 과 Step 실행 시점으로 지연 했을까요? 장점이 있을까요? 크게 2가지가 있습니다.
JobParameter의 Late Binding이 가능합니다.
Job Parameter가 StepExecution 또는 JobExecution 레벨에서 할당시킬 수 있습니다.
꼭 Application이 실행되는 시점이 아니더라도 Controller나 Service와 같은 비지니스 로직 처리 단계에서 Job Parameter를 할당시킬 수 있습니다.
동일한 컴포넌트를 병렬 혹은 동시에 사용할때 유용합니다.
Step 안에 Tasklet이 있고, 이 Tasklet은 멤버 변수와 이 멤버 변수를 변경하는 로직이 있다고 가정해봅시다.
이 경우 @StepScope 없이 Step을 병렬로 실행시키게 되면 서로 다른 Step에서 하나의 Tasklet을 두고 마구잡이로 상태를 변경하게 될 것 입니다.
하지만 @StepScope가 있다면 각각의 Step에서 별도의 Tasklet을 생성하고 관리하기 때문에 서로의 상태를 침범할 일이 없습니다.
Job Parameters는 @Value를 통해서 사용이 가능합니다. 그에 따라서 다소 오해가 있을 수도 있습니다. 정확히 알아야 할 것은 Job Parameters는 Step이나, Tasklet, Reader 등 Batch 컴포넌트 Bean의 생성 시점 즉, Scope Bean을 생성할때만 사용이 가능합니다. 따라서 @StepScope, @JobScope Bean을 생성할때만 Job Parameters가 생성되기 때문에 사용할 수 있습니다.
아래 예시를 보면, 메소드를 통해서 Bean을 생성하지 않고, 클래스에서 직접 Bean 을 생성해보겠습니다.
Job과 Step의 코드에서 @Bean과 @Value("#{jobParameters[파라미터명]}")를 제거하고 SimpleJobTasklet을 생성자 DI로 받도록 합니다.
@Component
@Slf4j
@StepScope
public class SimpleTasklet implements Tasklet {
@Value("#{jobParameters['requestDate']}")
String requestDate;
public SimpleTasklet() {
log.info(">>>>> Create Tasklet");
}
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
log.info(">>>>> This is scopeStep1");
log.info(">>>>> requestDate = {}", requestDate);
return RepeatStatus.FINISHED;
}
}
@Configuration
@Slf4j
@RequiredArgsConstructor
public class SimpleJobConfiguration {
private final SimpleTasklet simpleTasklet;
@Bean
public Job simpleJob(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new JobBuilder("simpleTaskletJob", jobRepository)
.start(simpleTaskletStep1(jobRepository, transactionManager))
.build();
}
public Step simpleTaskletStep1(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
log.info(">>>>> This is simpleStep1");
return new StepBuilder("simpleTaskletStep1", jobRepository)
.tasklet(simpleTasklet, transactionManager)
.build();
}
}
SimpleJobTasklet은 위와 같이 @Component와 @StepScope로 Scope가 Step인 Bean으로 생성합니다. 이 상태에서 @Value("#{jobParameters[파라미터명]}를 Tasklet의 변수로 할당합니다.
이렇게해서 메소드의 파라미터로 JobParameter를 할당받지 않고, 클래스의 멤버 변수로 JobParameter를 할당 받도록 해도 실행해보겠습니다.

보시는 것 처럼 정상적으로 JobParameter를 받아 사용할 수 있습니다.
이는 SimpleJobTasklet Bean이 @StepScope로 생성되었기 때문입니다.
이제는 소스코드에서 @StepScope 을 주석 처리하고 실행 해보겠습니다.

결과를 보면, 위와같이 'jobParameters' cannot be found 에러가 발생합니다.
이를 통해서 알 수 있는건 Bean을 메소드나 클래스 어느 것을 통해서 생성해도 무방하나 Bean의 Scope는 Step이나 Job이어야 한다는 것을 알 수 있습니다.
즉, JobParameters를 사용하기 위해선 꼭 @StepScope, @JobScope로 Bean을 생성해야한다는 것을 잊지마세요.
Reference
https://jojoldu.tistory.com/330
https://docs.spring.io/spring-batch/reference/step/late-binding.html
Source Code
https://github.com/trustonlyyou/batch-guide