Spring Batch Guide 시리즈는 이동욱 개발자님의 Spring Batch 가이드를 보고 학습한 내용을 정리한 글입니다.
많은 내용이 원 글과 유사할 수 있습니다. 이 점 양해바랍니다 🙏🏻
이번에는 Spring Batch의 Scope에 대해 알아보겠습니다.
여기서 Scope는 @StepScope
와 @JobScope
를 말합니다. 그리고 이 둘과 떨어질 수 없는 Job Parameter에 대해서도 함께 알아보도록 하겠습니다.
Spring Batch에서 외부 혹은 내부에서 받아서 Batch 컴포넌트에서 사용할 수 있는 파라미터를 Job Parameter라고 합니다.
Job Parameter를 사용하기 위해선 항상 Spring Batch 전용 Scope을 선언해야만 합니다. Scope에는 크게 @StepScope
과 @JobScope
, 2가지가 있습니다.
Job Parameter는 SpEL으로 선언해서 사용할 수 있습니다.
@Value("#{jobParameters[파라미터명]}")
jobParameters
외에도jobExecutionContext
,stepExecutionContext
등도 SpEL로 사용할 수 있습니다.@JobScope
에선stepExecutionContext
를 제외한 나머지 2개만 사용할 수 있습니다.
간단한 예제 코드를 통해 Scope를 살펴보도록 하겠습니다.
먼저 JobScope
그리고 StepScope
입니다.
@Slf4j
@RequiredArgsConstructor
@Configuration
public class JobScopeConfiguration {
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
@Bean
public Job scopeJob() {
return jobBuilderFactory.get("scopeJob")
.start(scopeStep1(null))
.next(scopeStep2())
.build()
;
}
@Bean
@JobScope
public Step scopeStep1(@Value("#{jobParameters[requestDate]}") String requestDate) {
return stepBuilderFactory.get("scopeStep1")
.tasklet((contribution, chunkContext) -> {
log.info(">>>>> This is scopeStep1");
log.info(">>>>> requestDate = {}", requestDate);
return RepeatStatus.FINISHED;
})
.build()
;
}
@Bean
public Step scopeStep2() {
return stepBuilderFactory.get("scoreStep2")
.tasklet(scopeStep2Tasklet(null))
.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에서 사용할 수 있습니다.
Job Parameter의 타입으로 사용할 수 있는 것으로는 Double
, Long
, Date
, String
이 있습니다.
LocalDate
나 LocalDateTime
같은 타입은 String
으로 받아서 타입 변환을 해서 사용해야 합니다.
@JobScope
나 @StepScope
가 선언된 함수가 호출되는 곳에서는 null
값을 할당하고 있습니다. 이는 Job Parameter의 할당이 어플리케이션 실행 시에 하지 않기 때문에 가능합니다.
@StepScope
& @JobScope
앞서 본 것처럼 Spring Batch는 @StepScope
와 @JobScope
라는 특별한 Bean Scope를 지원합니다.
Spring Bean의 기본 Scope은 singleton입니다. 그러나 Spring Batch 컴포넌트(Tasklet, ItemReader, ItemWriter, ItemProcessor 등)에 @StepScope
를 사용하게 되면 Spring Batch가 Spring Container를 통해 지정된 Step의 실행 시점에 해당 컴포넌트를 Spring Bean으로 생성합니다.
즉 Bean의 생성 시점을 지정된 Scope가 실행되는 시점으로 지연시킵니다.
단순히 생성 시점을 지연하는 것뿐만 아니라 해당 Scope과 동일하게 생명 주기를 가져갑니다. Job 또는 Step이 실행될 때 생성이 되고 종료되면서 함께 삭제가 됩니다.
이렇게 Bean의 생성 시점을 어플리케이션 실행 시점이 아니라 Job 또는 Step의 실행 시점으로 지연시키면서 얻는 장점은 크게 2가지가 있습니다.
첫 번째는 Job Parameter의 Late Binding이 가능합니다.
이는 Job Parameter를 StepContext 또는 JobExecutionContext 레벨에서 할당시킬 수 있음을 말합니다.
꼭 어플리케이션 실행 시점이 아니더라도 Controller나 Service와 같은 비지니스 로직 처리 단계에서 Job Parameter를 할당시킬 수 있습니다.
두 번째는 동일한 컴포넌트를 병렬 혹은 동시에 사용할 때 유용합니다.
Step안에 Tasklet이 있고, 이 Tasklet은 맴버 변수와 이 맴버 변수를 변경하는 로직이 있다고 가정해봅시다.
이 경우 @StepScope
없이 Step을 병렬로 실행시키게 되면 서로 다른 Step에서 하나의 Tasklet을 두고 마구잡이로 상태를 변경하려고 할 것입니다.
그러나 @StepScope
가 있다면 각각의 Step에서 별도의 Tasklet을 생성하고 관리하기 때문에 서로의 상태를 침범할 일이 없습니다.
Job Parameter는 @Value
어노테이션을 통해서 사용할 수 있습니다.
그래서 Job Parameter는 Step이나 Tasklet, Reader 등 Batch 컴포넌트 Bean이 생성 시점에서 호출할 수 있습니다. 그러나, 보다 정확하게 말하자면 Scope Bean을 생성할 때만 가능합니다.
즉 @StepScope
, @JobScope
Bean을 생성할 때만 Job Parameter가 생성되기 때문에 사용할 수 있습니다.
예제 코드를 통해 좀 더 자세히 알아보도록 하겠습니다.
기존에는 메서드를 통해 Bean을 생성하였는데 이번에는 직접 클래스를 활용해서 Bean을 생성해보도록 하겠습니다.
SimpleJobTasklet
클래스를 @Component
어노테이션을 선언하여 Bean으로 등록되도록 하였습니다. 여기에 @StepScope
어노테이션을 함께 선언했기 때문에 Scope가 Step인 Bean이 생성됩니다.
이처럼 메서드의 파라미터로 Job Parameter를 할당받지 않고 클래스의 멤버 변수로 Job Parameter를 할당받도록 실행해보면
이와 같이 정상적으로 Job Parameter를 받아와서 사용하는 것을 볼 수 있습니다. 이렇게 성공하는 이유는 SimpleJobTasklet
Bean이 @StepScope
로 생성되었기 때문입니다.
만약, 이 SimpleJobTasklet
Bean을 일반 singleton Bean으로 생성되도록 설정하고
Batch를 실행해보면?
SpelEvaluationException
이 발생하고 Property or field 'jobParameters' cannot be found on object of type 'org.springframework.beans.factory.config.BeanExpressionContext'
라는 에러가 발생합니다.
즉 Bean을 메서드 또는 클래스, 어떠한 방식으로 생성해도 무방하지만 Bean의 Scope는 Step이나 Job이어야 한다는 것을 알 수 있습니다.
Job Parameter를 사용하기 위해서는 @StepScope
, @JobScope
로 Bean을 생성해야 한다는 것!
그렇다면 꼭 Job Parameter만 사용해야 할까요?
Spring Boot에서 사용하던 여러 환경 변수 혹은 시스템 변수를 사용하면 안되나?
CommandLineRunner를 사용한다면 java jar application.jar -D{파라미터}
로 시스템 변수를 지정하면 되지 않나?
그럼 이번에는 Job Parameter를 사용해야 하는 이유에 대해 알아보도록 하겠습니다!
@Bean
@StepScope
public FlatFileItemReader<Partner> reader(
@Value("#{jobParameters[pathToFile]}") String pathToFile) {
FlatFileItemReader<Partner> itemReader = new FlatFileItemReader<Partner>();
itemReader.setLineMapper(lineMapper());
itemReader.setResource(new ClassPathResource(pathToFile));
return itemReader;
)
여기서 말하는 시스템 변수는
application.properties
와-D
옵션으로 실행하는 변수까지 포함합니다.
@Bean
@ConfigurationProperties(prefix = "my.prefix")
protected class JobProperties {
private String pathToFile;
/* getters, setters */
}
@Autowired
private JobProperties jobProperties;
@Bean
public FlatFileItemReader<Partner> reader() {
FlatFileItemReader<Partner> itemReader = new FlatFileItemReader<Partner>();
itemReader.setLineMapper(lineMapper());
String pathToFile = jobProperties.getPathToFile();
itemReader.setResource(new ClassPathResource(pathToFile));
return itemReader;
}
여기서 위 두 가지 방식은 몇 가지 차이점이 있습니다.
첫 번째는 시스템 변수를 사용하면 Spring Batch의 Job Parameter 관련 기능을 쓸 수 없습니다.
예를 들어, Spring Batch는 같은 Job Parameter로 같은 Job을 두 번 실행하지 않습니다. 그러나 시스템 변수를 사용하면 이러한 체크가 되지 않습니다.
또한 Spring Batch에서 자동으로 관리해주는 Parameter 관련 메타 테이블을 사용할 수 없습니다.
두 번째는 Command Line이 아닌 다른 방법으로 Job을 실행하기가 어렵습니다. 다른 방법으로 실행하기 위해서는 전역 상태(시스템 변수 혹은 환경 변수)를 동적으로 계속해서 변경시킬 수 있도록 Spring Batch를 구성해야 합니다.
동시에 여러 Job을 실행하거나 테스트 코드로 Job을 실행해야 할 때 문제가 발생할 수 있습니다.
특히 Job Parameter를 사용하지 못한다는 것은 장점 중 하나인 Late Binding을 사용할 수 없다는 것입니다!
예를 들어 웹 서버가 있고, 이 웹 서버에서 Batch를 수행한다고 가정하겠습니다.
외부에서 넘겨주는 파라미터에 따라 Batch가 다르게 작동해야 한다면, 이는 시스템 변수로는 풀어내기가 어려울 수 있을진 몰라도 Job Parameter를 사용한다면 아주 쉽게 해결할 수 있습니다.
@Slf4j
@RequiredArgsConstructor
@RestController
public class JobLauncherController {
private final JobLauncher jobLauncher;
private final Job job;
@GetMapping("/launchjob")
public String handle(@RequestParam("fileName") String fileName) throws Exception {
try {
JobParameters jobParameters = new JobParametersBuilder()
.addString("input.file.name", fileName)
.addLong("time", System.currentTileMillis())
.toJobParameters()
;
jobLauncher.run(job, jobParameters);
} catch (Exception e) {
log.info(e.getMessage());
}
return "Done";
}
}
위의 예시에서는 Controller에서 Request Parameter로 받은 값을 Job Parameter로 생성합니다.
JobParameters jobParameters = new JobParametersBuilder()
.addString("input.file.name", fileName)
.addLong("time", System.currentTileMillis())
.toJobParameters()
;
그리고 생성한 Job Parameter로 Job을 수행합니다.
jobLauncher.run(job, jobParameters);
즉 개발자가 원하는 시점에서 언제든지 Job Parameter를 생성하고 해당 Job Parameter로 Job을 수행할 수 있다는 것을 알 수 있습니다.
Job Parameter는 각각의 Batch 컴포넌트들이 사용하면 되니 변경이 심한 경우에도 쉽게 대응할 수 있습니다.
웹 서버에서 Batch를 관리하는 것은 권장하지 않습니다. 실제 운영 환경에서 어떻게 관리하는지는 이후에 다시 학습하도록 하겠습니다.
코드를 살펴보면 @Bean
과 @StepScope
를 함께 쓰는 것은 @Scope(value = "step", proxyMode = TARGET_CLASS)
로 표기하는 것과 같다고 말하고 있습니다.
그러나 이 proxyMode로 인해 문제가 발생할 수 있습니다. 그렇기에 이러한 문제를 항상 염두에 두고 사용하시길 바랍니다.
이 부분은 Spring Batch를 추가적으로 학습한 후 정리하도록 하겠습니다.
이번에는 Spring Batch에서 자주 쓰일 것으로 보이는(?) Scope와 Job Parameter에 대해 알아보았습니다.
Batch Scope(여기서는 대표적으로 @JobScope
와 @StepScope
)가 가지는 장점으로 Late Binding과 병렬 혹은 동시에 사용 시에 유용하다는 점을 살펴보았습니다.
그리고 시스템 변수가 아닌 Job Parameter를 사용함으로서 얻을 수 있는 장점, Job Parameter가 제공해주는 기능(메타 테이블과 이미 실행한 작업의 실행 불가)과 여러 방법으로 Batch를 실행하기 쉽다는 이점을 확인할 수 있었습니다.
글 가져다 배껴놓고 광고까지 붙이는건 좀 양심없는거 아닌가요