JaeWon's Devlog
article thumbnail
반응형

최근에 Spring Batch 에 대해 정리 를 하였는데, 이를 간단하게 사용하는 것도 정리하고자 합니다.

 

[Spring] Spring Batch 정리

회사에서 업무를 하나씩 배우고 맡게 되면서, Spring Batch 를 사용하고 있는 배치서버를 맡게 되었습니다. 이전 스케줄링 관련 개발을 할 때는 Crontab, Quartz 등을 사용하였는데, 여기서는 Spring Batch

dev-jwblog.tistory.com


1. 1. Spring Batch 프로젝트 구성

- 개발 환경은 아래와 같습니다.

  • IDE : Intelli J 2022.02
  • SpringBoot : 2.7.3
  • java : JDK 11
  • DB : MySQL

1.1. 1-1. 프로젝트 생성

- 인텔리제이를 통해서 프로젝트를 생성합니다.

- 프로젝트에서 필요한 Dependency 를 선택합니다.

- 프로젝트가 생성되었다면, build.gradle 은 아래와 같이 생성됩니다.

<java />
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. 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 파일을 수정합니다.

<java />
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. 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. 2-1. SpringbatchApplication.java

- SpringBatch 를 사용하기 위해서는 0000Application.java (여기서는 SpringbatchApplication.java) 에서 SpringBatch 기능을 활성화가 필요합니다.

- @EnableBatchProcessing 어노테이션을 추가합니다.

<java />
@EnableBatchProcessing // 배치 기능 활성 @SpringBootApplication public class SpringbatchApplication { public static void main(String[] args) { SpringApplication.run(SpringbatchApplication.class, args); } }

- 해당 어노테이션을 추가해야만 스프링 배치의 기능들을 사용할 수 있습니다.

만약 해당 어노테이션을 선언하지 않는다면, Spring Batch 기능들을 사용할 수 없어 필수적으로 선언해야만 합니다.

2.2. 2-2. SimpleBatchJob.java

- 스프링 배치에서 실행시키기 위한 Job 을 모아두는 패키지를 생성하고 아래에 SimpleBatchJob 클래스를 생성합니다.

<java />
@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. 3. MultiStepBatchJob 생성하기

- 2번에서 간단하게 배치를 실행하는 Job 을 생성해보았다면, 여기서는 여러 Step 을 하는 배치를 작성해보겠습니다.

- 동일하게 Job 패키지 아래에 MultiStepBatchJob 클래스를 생성합니다.

<java />
@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. 4. FlowStepBatchJob 생성하기

- Next 를 통해서 순차적으로 Step 을 제어하지만, 만약 앞의 Step 에서 오류가 발생한다면 나머지 뒤에 있는 Step 들은 실행되지 못 합니다.

- 이러한 경우 Flow 를 통해 분기처리하여 Step 을 실행할 수도 있습니다.

https://jojoldu.tistory.com/328?category=902551

- 동일하게 Job 패키지 아래에 FlowStepBatchJob 클래스를 생성합니다.

<java />
@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. 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 등 더욱 활용하여 사용해보도록 하겠습니다.


참고

- https://jojoldu.tistory.com/325?category=902551  

https://khj93.tistory.com/entry/Spring-Batch%EB%9E%80-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B3%A0-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0

반응형
profile

JaeWon's Devlog

@Wonol

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!