오늘은 Spring Batch에 대한 간략한 소개 및 Job 생성에 대해 소개해보도록 하겠다.
Spring Batch
의 이론적인 개념에 대해 살펴보면 아래와 같다.
Spring Batch :
Enterprise System의 운영에 있어 대용량 일괄처리의 편의를 위해 설계된 가볍고 포괄적인 Batch Framework
Spring Batch
는 보통 위 개념과 같이 일관되고 반복적인 작업
, 트랜잭션 관리
, Logging/Trace 모니터링
, 트랜잭션 관리
등의 용도로 사용되며 대용량 레코드에 대한 처리 방식으로 많은 Enterprise에서 활용하고 있다.
Spring Batch는 그 자체만으로 Job을 실행할 수 없다. Configuration Bean을 통해 해당 Job이 실행될 경우에 어떤 일을 처리할지 세팅할 뿐 이를 실행시켜주기 위해선 Batch Scheduler
가 필요하다.
이러한 역할을 하는 Scheduler는 여러가지가 있다. 대표적으로는 Quertz Scheduler
, Jenkins Scheduler
가 있으며 Scheduler에 대한 더 깊은 얘기는 너무 길어질 것 같으니.. 다음 기회에 작성해보도록 하겠다.
JobRepository
는 배치를 수행하는데 필요한 전반적인 Object를 모두 포함하고 있다. (Job, JobLauncher, Step, 등)
그리고 이를 통해 배치 수행과 관련된 수치 데이터와 Job의 Status를 유지/관리
하며 배치와 관련된 CRUD 처리를 하는 역할
을 수행한다.
Job
은 배치 처리 과정을 하나의 단위로 만들어 놓은 객체이다.
배치처리 과정에 있어 최상단 Object라고 볼 수 있다.
JobLauncher
는 말그대로 Job을 실행시키는 역할을 담당한다.
Job & JobParameters를 param으로 받고 배치 수행 후, JobExecution을 반환한다.
Spring Batch에서는 JobLauncherApplicationRunner 클래스가 자동으로 JobLauncher를 실행하기 때문에 우리가 직접 컨트롤할 일은 없다고 볼 수 있다.
Step
은 Job을 구성하는 독립적인 작업 단위이다. 실제 배치가 실행되는 처리를 정의하고 제어하는데 필요한 모든 정보를 가지고 있는 Object라고 볼 수 있다.
Step은 Tasklet
/ Chunk
기반으로 수행되며 이는 유저가 선택해서 사용하게 된다.
Tasklet vs Chunk
Tasklet: 한 가지 이상의 CRUD가 발생(=비즈니스 로직)하는 task에 대해 일괄적으로 처리하는 경우, 채택한다. (복잡한 로직을 수행해야 하는 job일 경우, 채택)
Chunk: chunk 단위로 처리할 모든 record를 쭉 읽어들인 후, 모두 읽어들이는데 성공하면 한번에 Write하는 방식 (대용량 데이터에 대해 단순 처리할 경우, 채택)
ItemReader
는 말 그대로 배치를 수행하는데 있어 대상이 되는 데이터를 읽어들이는데 사용되는 Object이다. 데이터를 읽어들이는 방법에는 DB Connection을 통해 불러올 수도 있지만 File I/O를 통해 불러오기도 한다.
ItemProcessor
는 Read와 Write의 중간 단계에서 가공(처리) 역할을 수행한다.
그리고 이 단계는 필수가 아니기도 하다. Read와 Write만으로 원하는 기능을 수행할 수 있다면 ItemProcessor는 과감히 Skip해도 된다.
ItemWriter
는 Batch의 마지막 단계이다. 말 그대로 우리가 처리하고자 하는 데이터에 대해 최종적으로 가공된 데이터를 출력하는 역할을 수행한다.
우리는 ItemWriter를 통해 다시 DB에 해당 내용을 저장
할 수도 있고 해당 데이터를 다른 프로젝트로 API 호출
시킬 수도, Kafka를 통해 msg 발행
시킬 수도 있다.
그럼 간단히 Spring Batch의 용어 정리도 해봤겠다 이제 소스코드를 만들어보기로 하자.
우선, 나는 Spring Initializr
(https://start.spring.io)를 통해 SpringBoot 프로젝트를 하나 만들었으며 기본 Spec은 아래와 같다.
@SpringBootApplication
@EnableBatchProcessing
public class JobPracticeApplication {
public static void main(String[] args) {
SpringApplication.run(JobPracticeApplication.class, args);
}
}
프로젝트 생성 시, 최초 메인 Application 클래스에 위와 같이 @EnableBatchProcessing
애노테이션을 추가해준다.
@EnableBatchProcessing
: 배치 기능 활성화
특정 Job을 수행하기 위한 클래스 파일을 생성하도록 하자.
@Slf4j
@Configuration
@RequiredArgsConstructor
public class MyFirstJobConfiguration {
...
}
애노테이션 종류
@Configuration: 해당 클래스가 설정(=Configuration)을 위해 사용되는 Bean임을 명시
@RequiredArgsConstructor: 의존성 주입에 있어 @Autowired를 권장하지 않으며 해당 애노테이션을 통해 생성자 주입
Spring Batch 5.0 부터 JobBuilderFactory
는 deprecated되었다.
https://docs.spring.io/spring-batch/docs/current/api/deprecated-list.html
이로 인해 기존에 활용하던 방식은 사용하기 어렵게 되었다.
그래서 JobRepository
를 활용하는 방식으로 바꿔줄 필요가 있다.
(AS-IS)
...
@Bean
public Job myFirstJob(Step step) {
return this.jobBuilderFactory.get("myFirstJob")
.start(myFirstStep)
.build();
}
...
(TO-BE)
...
@Bean
public Job myFirstJob(JobRepository jobRepository){
return new JobBuilder("myFirstJob", jobRepository)
.start(myFirstStep(jobRepository))
.build();
}
...
예제 속, job은 단일 Step으로 구성하도록 하겠다.
이제 Job에 포함되는 Step을 생성해야 한다.
StepBuilderFactory
역시 Spring Batch 5.0이상부터 deprecated되어 다른 방식으로 처리해야 한다.
(AS-IS)
...
public Step myFirstStep() {
return stepBuilderFactory.get("myFirstStep")
.<String, String>chunk(1000)
.reader(itemReader())
.writer(itemWriter())
.build();
}
...
(TO-BE)
...
@Bean
public Step myFirstStep(JobRepository jobRepository){
return new StepBuilder("myFirstStep",jobRepository)
.<String, String>chunk(1000,transactionManager)
.reader(itemReader())
.writer(itemWriter())
.build();
}
...
Step은 chunk 기반으로 동작하도록 구성하였고 한번에 들어올 수 있는 chunk size는 1000으로 설정했다.
다음으로 Batch를 통해 처리하고자 하는 데이터를 Read하는 ItemReader 부분을 만들어보자.
...
@Bean
public ItemReader<String> itemReader(){
return new ItemReader<String>() {
@Override
public String read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException {
return "Read OK";
}
};
}
...
위와 같이 구성할 수 있고 실질적으로 DB 통신
/ File I/O
를 통해 처리할 경우, 이 부분에 로직을 구현하면 된다.
(아래는 MyBatis를 활용하는 경우, 예시이다.)
...
public MyBatisCursorItemReader<Object> itemReader() {
String strQueryId = "쿼리 경로 기재";
return new MyBatisCursorItemReaderBuilder<Object>()
.sqlSessionFactory(sqlSessionFactory)
.queryId(strQueryId)
.parameterValues(parameterValues)
.saveState(false)
.build();
}
...
마지막으로 배치를 통해 처리된 데이터에 대한 출력을 담당하는 ItemWriter 부분이다.
...
@Bean
public ItemWriter<String> itemWriter(){
return strList -> {
strList.forEach(
str -> log.info("str: {}", str)
);
};
}
...
ItemReader에서 처리한 데이터가 String List형태로 담겨 들어오면 해당 str를 로그 출력하는 형태로 간단히 구성해보았다.
실질적으로 활용하게 되다면 Writer 부분에서 해당 데이터가 필요한 곳으로 API 전송을 할 수도 있고 DB에 가공된 데이터를 저장할 수도 있겠다.
회사에서 Spring Batch를 사용할 기회가 있어서 이번 기회를 통해 Spring Batch에 대해 조금 더 알아보는 시간을 가져보게 되었다. 아직 Spring Batch에 대해 완벽히 알고 있다고 얘기할 순 없지만 계속 사용하고 이것저것 바꿔가며 활용하다보면 보다 나은 서비스를 만들 수 있을 것 같다.