[Spring] Spring Batch 사용해보기(1) - 환경구성, 기본구현
최근에 Spring Batch 에 대해 정리를 하였는데, 이를 간단하게 사용하는 것도 정리하고자 합니다.
1. Spring Batch 프로젝트 구성
- 개발 환경은 아래와 같습니다.
- IDE : Intelli J 2022.02
- SpringBoot : 2.7.3
- java : JDK 11
- DB : MySQL
1-1. 프로젝트 생성
- 인텔리제이를 통해서 프로젝트를 생성합니다.
- 프로젝트에서 필요한 Dependency 를 선택합니다.
- 프로젝트가 생성되었다면, build.gradle 은 아래와 같이 생성됩니다.
plugins {
id 'org.springframework.boot' version '2.7.3'
id 'io.spring.dependency-management' version '1.0.13.RELEASE'
id 'java'
}
group = 'com.study'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-batch'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'mysql:mysql-connector-java'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.batch:spring-batch-test'
}
tasks.named('test') {
useJUnitPlatform()
}
1-2. MySQL 환경 구성하기
- Spring Batch 를 사용하기 위해서는 DB 를 통해 Meta Data Table 이 생성되어야만 합니다. (해당 내용은 이전 글 참고 부탁드립니다.)
- 기본적으로 H2 DB 를 사용하는 경우에는 해당 테이블들을 Spring Boot 가 프로젝트 실행 시에 자동으로 생성해주지만, MySQL 이나 Oracle 과 같은 DB 를 사용할 때는 생성해주지 않아 개발자가 직접 생성해야만 합니다.
- 해당 테이블의 정보는 Spring Batch Dependency 에 스키마가 존재하여, 이를 복사하여 그대로 CREATE 하시면 됩니다.(여기서는 schema-mysql.sql 을 사용합니다.)
본인의 IDE에서 파일 검색으로 "schema-"를 해보시면 메타 테이블들의 스키마가 DBMS에 맞춰 각각 존재하는것을 볼 수 있습니다.
- 메타 데이터 테이블들을 생성하였다면, 다음은 MySQL 을 사용하기 위해 application.yml 파일을 수정합니다.
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/springboot-jpa?serverTimezone=UTC&characterEncoding=UTF-8
username: 유저네임
password: 패스워드
2. Simple Batch Job 생성하기
- Spring Batch 에서의 Job 은 여러 가지 Step 의 모음으로 구성되며, Job 은 순차적인 Step 을 수행하며 Batch 를 수행하게 됩니다.
- Step 은 Tasklet , Chunk 두 가지 방식을 지원합니다.
- Spring Batch 에서 Job 은 하나의 배치 작업 단위입니다.
- Job 안에는 여러 Step 이 존재할 수 있고, Step 안에는 Tasklet 또는 Reader, Processor, Writer 묶음이 존재합니다.
2-1. SpringbatchApplication.java
- SpringBatch 를 사용하기 위해서는 0000Application.java (여기서는 SpringbatchApplication.java) 에서 SpringBatch 기능을 활성화가 필요합니다.
- @EnableBatchProcessing 어노테이션을 추가합니다.
@EnableBatchProcessing // 배치 기능 활성
@SpringBootApplication
public class SpringbatchApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbatchApplication.class, args);
}
}
- 해당 어노테이션을 추가해야만 스프링 배치의 기능들을 사용할 수 있습니다.
만약 해당 어노테이션을 선언하지 않는다면, Spring Batch 기능들을 사용할 수 없어 필수적으로 선언해야만 합니다.
2-2. SimpleBatchJob.java
- 스프링 배치에서 실행시키기 위한 Job 을 모아두는 패키지를 생성하고 아래에 SimpleBatchJob 클래스를 생성합니다.
@Slf4j
@RequiredArgsConstructor
@Configuration
public class SimpleJobConfiguration {
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
@Bean
public Job simpleJob() {
return jobBuilderFactory.get("simpleJob")
.start(simpleStep1())
.build();
}
@Bean
public Step simpleStep1() {
return stepBuilderFactory.get("simpleStep1")
.tasklet((contribution, chunkContext) -> {
log.info(">>>>> This is Step1");
return RepeatStatus.FINISHED;
})
.build();
}
}
- @Configuration
- Spring Batch 에서 모든 Job은 @Configuration 을 등록해서 사용해야 합니다. - jobBuilderFactory.get("simpleJob")
- simpleJob 이란 이름의 Batch Job 을 생성합니다.
- job의 이름은 별도로 지정하지 않고, 이렇게 Builder 를 통해 지정합니다. - stepBuilderFactory.get("simpleStep1")
- simpleStep1 이란 이름의 Batch Step 을 생성합니다.
- jobBuilderFactory.get("simpleJob")와 마찬가지로 Builder를 통해 이름을 지정합니다. - tasklet((contribution, chunkContext))
- Step 안에서 수행될 기능들을 작성합니다.
- Tasklet은 Step안에서 단일로 수행될 커스텀한 기능들을 선언할 때 사용합니다.
- 프로젝트를 실행하여 결과를 확인해보겠습니다.
- "This is simpleStep1" 로그 확인을 통해 배치가 정상적으로 수행된 것을 확인하실 수 있습니다.
3. MultiStepBatchJob 생성하기
- 2번에서 간단하게 배치를 실행하는 Job 을 생성해보았다면, 여기서는 여러 Step 을 하는 배치를 작성해보겠습니다.
- 동일하게 Job 패키지 아래에 MultiStepBatchJob 클래스를 생성합니다.
@Slf4j
@RequiredArgsConstructor
@Configuration
public class MultiStepBatchJob {
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
@Bean
public Job ExampleJob(){
return jobBuilderFactory.get("multiStepJob")
.start(startStep())
.next(nextStep())
.next(lastStep())
.build();
}
@Bean
public Step startStep() {
return stepBuilderFactory.get("startStep")
.tasklet((contribution, chunkContext) -> {
log.info("MultiStepJob Start Step!");
return RepeatStatus.FINISHED;
})
.build();
}
@Bean
public Step nextStep(){
return stepBuilderFactory.get("nextStep")
.tasklet((contribution, chunkContext) -> {
log.info("MultiStepJob Next Step!");
return RepeatStatus.FINISHED;
})
.build();
}
@Bean
public Step lastStep(){
return stepBuilderFactory.get("lastStep")
.tasklet((contribution, chunkContext) -> {
log.info("MultiStepJob Last Step!!");
return RepeatStatus.FINISHED;
})
.build();
}
}
- 해당 Job 을 실행한다면 start -> next -> last 순으로 Step 이 동작합니다.
- 프로젝트를 실행하여 확인해보겠습니다.
- 정상적으로 동작하는 것을 확인하실 수 있습니다.
4. FlowStepBatchJob 생성하기
- Next 를 통해서 순차적으로 Step 을 제어하지만, 만약 앞의 Step 에서 오류가 발생한다면 나머지 뒤에 있는 Step 들은 실행되지 못 합니다.
- 이러한 경우 Flow 를 통해 분기처리하여 Step 을 실행할 수도 있습니다.
- 동일하게 Job 패키지 아래에 FlowStepBatchJob 클래스를 생성합니다.
@Slf4j
@Configuration
@RequiredArgsConstructor
public class FlowStepBatchJob {
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
@Bean
public Job ExampleJob(){
return jobBuilderFactory.get("flowStepJob")
.start(startStep())
.on("FAILED") // startStep 의 상태가 FAILED 인 경우
.to(failOverStep()) // failOverStep() Step 실행
.on("*") // failOverStep의 상태 관계 없이 모두
.end() // step 종료
.from(startStep()) // startStep 으로 부터
.on("COMPLETED") // startStep 의 상태가 COMPLETED 인 경우
.to(successOverStep()) // successOverStep() Step 실행
.next(finishOverStep())// successOverStep 다음 실행
.on("*") // successOverStep 의 상태 관계 없이 모두
.end() // Step 종료
.from(startStep()) // startStep 으로 부터
.on("*") // startStep 의 상태 관계 없이 모두
.to(allOverStep()) // allOverStep() Step 실행
.on("*") // allOverStep 의 상태 관게 없이 모두
.end() // Step 종료
.end() // Job 종료
.build();
}
@Bean
public Step startStep() {
return stepBuilderFactory.get("startStep")
.tasklet((contribution, chunkContext) -> {
log.info("FlowStepJob Start Step!");
/**
* ExitStatus 를 FAILED 로 지정하여 테스트
* 정상 Step 테스트 위해서는 ExitStatus.COMPLETED 로 변경
* 기본적으로 미선언시 COMPLETED 이므로, 필요 테스트 시 UNKNOWN 같은 타입으로 테스트
*/
//contribution.setExitStatus(ExitStatus.FAILED);
//contribution.setExitStatus(ExitStatus.COMPLETED);
contribution.setExitStatus(ExitStatus.UNKNOWN);
return RepeatStatus.FINISHED;
})
.build();
}
@Bean
public Step failOverStep(){
return stepBuilderFactory.get("failOverStep")
.tasklet((contribution, chunkContext) -> {
log.info("FlowStepJob FailOver Step! -> startStep 이 FAILED 이면 실행됌");
return RepeatStatus.FINISHED;
})
.build();
}
@Bean
public Step successOverStep(){
return stepBuilderFactory.get("successOverStep")
.tasklet((contribution, chunkContext) -> {
log.info("FlowStepJob successOverStep Step! -> startStep 이 COMPLETED 이면 실행됌");
return RepeatStatus.FINISHED;
})
.build();
}
@Bean
public Step finishOverStep(){
return stepBuilderFactory.get("finishOverStep")
.tasklet((contribution, chunkContext) -> {
log.info("FlowStepJob finishOverStep Step! -> successOverStep 이후 실행됌");
return RepeatStatus.FINISHED;
})
.build();
}
@Bean
public Step allOverStep(){
return stepBuilderFactory.get("allOverStep")
.tasklet((contribution, chunkContext) -> {
log.info("FlowStepJob allOverStep Step! -> startStep 상태 상관 없이 실행됌");
return RepeatStatus.FINISHED;
})
.build();
}
}
- 위 코드 시나리오는 startStep 이 실패하냐 성공하냐에 따라 다음 시나리오가 달라집니다.
- startStep FAILED 인 경우 : startStep -> failOverStep
- startStep COMPLETED 인 경우 : startStep -> successOverStep -> finishOverStep
- startStep 이외의 경우 : startStep -> allOverStep
- 이런 전체 Flow 를 관리하는 코드가 다음과 같습니다.
- .on()
- Step의 ExitStatus 상태를 캐치할 상태 입력
- * 인 경우 모든 상태이고, 이외에는 "FAILED", "COMPLETED" 와 같이 입력 - .to()
- 다음으로 이동할 Step 작성 - .from()
- 일종의 이벤트 리스너 역할
- 상태값을 보고 일치하는 상태라면 to() 에 포함된 step 을 호출
- 예를들어, startStep 의 이벤트 캐치가 FAILED 로 되있는 상태에서 추가로 이벤트 캐치를 하고자 하면 from 을 사용해야 함. - .end()
- FlowBuilder 를 반환하는 end 와 FlowBuild 를 종료하는 end 2가지 종류가 존재
- on("*") 뒤에 있는 end 는 FlowBuilder 를 반환하는 end
- build() 앞에 있는 end 는 FlowBuilder 를 종료하는 end
참고!!!
.on() 가 캐치하는 상태값은 BatchStatus 가 아닌 ExitStatus 입니다.
그렇기 때문에 분기처리를 하기 위해서는 ExitStatus 를 수정해야만 합니다.
수정하는 코드는 아래와 같습니다.
- 이렇게 원하는 상황에 따라 분기로직을 작성하시면 됩니다.
- 각 상태에 따라 테스트를 진행해보겠습니다.
- FAILED
- startStep -> failOverStep
- COMPLETED
- startStep -> successOverStep -> finsihOverStep
- 이외
- startStep -> allOverStep
4-1. BatchStatus VS ExitStatus
- Flow 제어 관련해서 BatchStatus 와 ExitStatus 의 차이도 중요합니다.
- BatchStatus 는 Job 또는 Step 의 실행 결과를 Spring 에서 기록할 때 사용하는 Enum 입니다.
- 예를 들어 .on("FAILED").to(nextStep()) 인 경우에는 on 메소드가 참조하는 것은 BatchStatus 가 아닌 Step 의 ExitStatus 입니다.
- ExitStatus 는 Step 의 실행 후 상태를 말합니다.
- Existatus 는 Enum이 아닙니다.
- 예를 들어 .on("FAILED").to(nextStep()) 인 경우에는 ExitStatus 가 FAILED 인 경우 nextStep 으로 이동해라 입니다.
- 이번 글에서는 간단하게 SpringBatch 를 사용해보았습니다, 다음 글에서는 Step, Scope 등 더욱 활용하여 사용해보도록 하겠습니다.
참고