Spring Batch

2. spring Batch Job Flow

wellbell 2021. 6. 20. 21:40

이글을 창천 향로님의 블로그의 글을 따라한 내용입니다.

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

 

4. Spring Batch 가이드 - Spring Batch Job Flow

자 이번 시간부터 본격적으로 실전에서 사용할 수 있는 Spring Batch 내용들을 배워보겠습니다. 작업한 모든 코드는 Github에 있으니 참고하시면 됩니다. 앞서 Spring Batch의 Job을 구성하는데는 Step이

jojoldu.tistory.com

 

앞서 Spring Batch의 Job을 구성하는데는 Step 존재하는데 Step은 실제 Batch 작업을 수행하는 역할을 합니다.

- Step에서는 Batch로 실제 처리하고자 하는 기능과 설정을 모두 포함하는 장소

 

1. Next

next() 순차적으로 Step들 연결시킬때 사용됩니다.
step1 -> step2 -> stpe3 순으로 하나씩 실행시킬때 next() 는 좋은 방법

@Slf4j
@Configuration
@RequiredArgsConstructor
public class StepNextJobConfiguration {

    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;

    @Bean
    public Job stepNextJob() {
        return jobBuilderFactory.get("stepNextJob")
                .start(step1())
                .next(step2())
                .next(step3())
                .build();
    }

    @Bean
    public Step step1() {
        return stepBuilderFactory.get("step1")
                .tasklet((contribution, chunkContext) -> {
                    log.info(">>>>> This is Step1");
                    return RepeatStatus.FINISHED;
                })
                .build();
    }

    @Bean
    public Step step2() {
        return stepBuilderFactory.get("step2")
                .tasklet((contribution, chunkContext) -> {
                    log.info(">>>>> This is Step2");
                    return RepeatStatus.FINISHED;
                })
                .build();
    }

    @Bean
    public Step step3() {
        return stepBuilderFactory.get("step3")
                .tasklet((contribution, chunkContext) -> {
                    log.info(">>>>> This is Step3");
                    return RepeatStatus.FINISHED;
                })
                .build();
    }
}

 

application.yml에 추가

Spring Batch가 실행될때, Program arguments로 job.name 값이 넘어오면 해당 값과 일치하는 Job만 실행

spring.batch.job.names: ${job.name:NONE}

${job.name:NONE}을 보면 :를 사이에 두고 좌측에 job.name이, 우측에 NONE이 있는데요.
이 코드의 의미는 job.name이 있으면 job.name값을 할당하고, 없으면 NONE을 할당하겠다는 의미

- NONE이 할당되면 어떤 배치도 실행하지 않겠다는 의미

 

2. 조건별 흐름 제어

Next의 경우 앞의 step에서 오류가 나면 나머지 뒤에 있는 step 들은 실행되지 못하게 됩니다.

하지만 상황에 따라 정상일때는 Step B로, 오류가 났을때는 Step C로 수행해야할때가 있습니다.

이럴 경우를 대비해 Spring Batch Job에서는 조건별로 Step을 사용할 수 있습니다.

@Slf4j
@Configuration
@RequiredArgsConstructor
public class StepNextConditionalJobConfiguration {

    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;

    @Bean
    public Job stepNextConditionalJob() {
        return jobBuilderFactory.get("stepNextConditionalJob")
                .start(conditionalJobStep1())
                    .on("FAILED") // FAILED 일 경우
                    .to(conditionalJobStep3()) // step3으로 이동한다.
                    .on("*") // step3의 결과 관계 없이 
                    .end() // step3으로 이동하면 Flow가 종료한다.
                .from(conditionalJobStep1()) // step1로부터
                    .on("*") // FAILED 외에 모든 경우
                    .to(conditionalJobStep2()) // step2로 이동한다.
                    .next(conditionalJobStep3()) // step2가 정상 종료되면 step3으로 이동한다.
                    .on("*") // step3의 결과 관계 없이 
                    .end() // step3으로 이동하면 Flow가 종료한다.
                .end() // Job 종료
                .build();
    }

    @Bean
    public Step conditionalJobStep1() {
        return stepBuilderFactory.get("step1")
                .tasklet((contribution, chunkContext) -> {
                    log.info(">>>>> This is stepNextConditionalJob Step1");

                    /**
                        ExitStatus를 FAILED로 지정한다.
                        해당 status를 보고 flow가 진행된다.
                    **/
                    contribution.setExitStatus(ExitStatus.FAILED);

                    return RepeatStatus.FINISHED;
                })
                .build();
    }

    @Bean
    public Step conditionalJobStep2() {
        return stepBuilderFactory.get("conditionalJobStep2")
                .tasklet((contribution, chunkContext) -> {
                    log.info(">>>>> This is stepNextConditionalJob Step2");
                    return RepeatStatus.FINISHED;
                })
                .build();
    }

    @Bean
    public Step conditionalJobStep3() {
        return stepBuilderFactory.get("conditionalJobStep3")
                .tasklet((contribution, chunkContext) -> {
                    log.info(">>>>> This is stepNextConditionalJob Step3");
                    return RepeatStatus.FINISHED;
                })
                .build();
    }
}

 

위의 코드 시나리오는 step1이 실패하냐 성공하냐에 따라 시나리오가 달라지는데요.

  • step1 실패 시나리오: step1 -> step3
  • step1 성공 시나리오: step1 -> step2 -> step3
  • .on()
    • 캐치할 ExitStatus 지정
    • * 일 경우 모든 ExitStatus가 지정된다.
  • to()
    • 다음으로 이동할 Step 지정
  • from()
    • 일종의 이벤트 리스너 역할
    • 상태값을 보고 일치하는 상태라면 to()에 포함된 step을 호출합니다.
    • step1의 이벤트 캐치가 FAILED로 되있는 상태에서 추가로 이벤트 캐치하려면 from을 써야만 함
  • end()
    • end는 FlowBuilder를 반환하는 end와 FlowBuilder를 종료하는 end 2개가 있음
    • on("*")뒤에 있는 end는 FlowBuilder를 반환하는 end
    • build() 앞에 있는 end는 FlowBuilder를 종료하는 end
    • FlowBuilder를 반환하는 end 사용시 계속해서 from을 이어갈 수 있음

on이 캐치하는 상태값이 BatchStatus가 아닌 ExitStatus라는 점

- 분기처리를 위해 상태값 조정이 필요하시다면 ExitStatus를 조정해야함

 

3. Batch Status vs. Exit Status

BatchStatus는 Job 또는 Step 의 실행 결과를 Spring에서 기록할 때 사용하는 Enum

- BatchStatus로 사용 되는 값은 COMPLETED, STARTING, STARTED, STOPPING, STOPPED, FAILED, ABANDONED, UNKNOWN

 

ExitStatus는 Step의 실행 후 상태

 

Spring Batch는 기본적으로 ExitStatus의 exitCode는 Step의 BatchStatus와 같도록 설정이 되어 있습니다.
하지만 만약에 본인만의 커스텀한 exitCode가 필요하다면 exitCode를 반환하는 별도의 로직이 필요

.start(step1())
    .on("FAILED")
    .end()
.from(step1())
    .on("COMPLETED WITH SKIPS")
    .to(errorPrint1())
    .end()
.from(step1())
    .on("*")
    .to(step2())
    .end()
  • tep1이 실패하며, Job 또한 실패하게 된다.
  • step1이 성공적으로 수행되어 step2가 수행된다.
  • step1이 성공적으로 완료되며, COMPLETED WITH SKIPS의 exit 코드로 종료 된다.

COMPLETED WITH SKIPS는 ExitStatus에는 없는 코드 - 하는대로 처리되기 위해서는 COMPLETED WITH SKIPS exitCode를 반환하는 별도의 로직이 필요

public class SkipCheckingListener extends StepExecutionListenerSupport {

    public ExitStatus afterStep(StepExecution stepExecution) {
        String exitCode = stepExecution.getExitStatus().getExitCode();
        if (!exitCode.equals(ExitStatus.FAILED.getExitCode()) && 
              stepExecution.getSkipCount() > 0) {
            return new ExitStatus("COMPLETED WITH SKIPS");
        }
        else {
            return null;
        }
    }
}

Step이 성공적으로 수행되었는지 확인하고, StepExecution의 skip 횟수가 0보다 클 경우 COMPLETED WITH SKIPS 의 exitCode를 갖는 ExitStatus를 반환

 

4. Decide

위에서 진행했던 방식은 2가지 문제가 있습니다.

  • Step이 담당하는 역할이 2개 이상이 됩니다.
    • 실제 해당 Step이 처리해야할 로직외에도 분기처리를 시키기 위해 ExitStatus 조작이 필요합니다.
  • 다양한 분기 로직 처리의 어려움
    • ExitStatus를 커스텀하게 고치기 위해선 Listener를 생성하고 Job Flow에 등록하는 등 번거로움이 존재합니다.

JobExecutionDecider : Spring Batch에서는 Step들의 Flow속에서 분기만 담당하는 타입

 

@Slf4j
@Configuration
@RequiredArgsConstructor
public class DeciderJobConfiguration {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;

    @Bean
    public Job deciderJob() {
        return jobBuilderFactory.get("deciderJob")
                .start(startStep())
                .next(decider()) // 홀수 | 짝수 구분
                .from(decider()) // decider의 상태가
                .on("ODD") // ODD라면
                .to(oddStep()) // oddStep로 간다.
                .from(decider()) // decider의 상태가
                .on("EVEN") // ODD라면
                .to(evenStep()) // evenStep로 간다.
                .end() // builder 종료
                .build();
    }

    @Bean
    public Step startStep() {
        return stepBuilderFactory.get("startStep")
                .tasklet((contribution, chunkContext) -> {
                    log.info(">>>>> Start!");
                    return RepeatStatus.FINISHED;
                })
                .build();
    }

    @Bean
    public Step evenStep() {
        return stepBuilderFactory.get("evenStep")
                .tasklet((contribution, chunkContext) -> {
                    log.info(">>>>> 짝수입니다.");
                    return RepeatStatus.FINISHED;
                })
                .build();
    }

    @Bean
    public Step oddStep() {
        return stepBuilderFactory.get("oddStep")
                .tasklet((contribution, chunkContext) -> {
                    log.info(">>>>> 홀수입니다.");
                    return RepeatStatus.FINISHED;
                })
                .build();
    }

    @Bean
    public JobExecutionDecider decider() {
        return new OddDecider();
    }

    public static class OddDecider implements JobExecutionDecider {

        @Override
        public FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) {
            Random rand = new Random();

            int randomNumber = rand.nextInt(50) + 1;
            log.info("랜덤숫자: {}", randomNumber);

            if(randomNumber % 2 == 0) {
                return new FlowExecutionStatus("EVEN");
            } else {
                return new FlowExecutionStatus("ODD");
            }
        }
    }
}

flow: startStep -> oddDecider에서 홀수 인지 짝수인지 구분 -> oddStep or evenStep 진행

  • start()
    • Job Flow의 첫번째 Step을 시작합니다.
  • next()
    • startStep 이후에 decider를 실행합니다.
  • from()
    • 4-2와 마찬가지로 from은 이벤트 리스너 역할을 합니다.
    • decider의 상태값을 보고 일치하는 상태라면 to()에 포함된 step 를 호출합니다.

분기 로직에 대한 모든 일은 OddDecider가 전담 -  Step과는 명확히 역할과 책임이 분리된채 진행

Reference

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