BackEnd/Spring Batch

[Spring] Spring Batch 사용해보기(1) - 환경구성, 기본구현

Wonol 2022. 9. 8. 14:45
반응형

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

 

[Spring] Spring Batch 정리

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

dev-jwblog.tistory.com


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 을 실행할 수도 있습니다.

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

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


참고

- 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

반응형