[Spring Batch] Spring Batch Job Flow

dev-log·2021년 11월 23일
0

Spring Batch

목록 보기
5/7

Step은 실제 Batch 작업을 수행하는 역할이다.

이전에 작성한 코드를 살펴보면 Job은 코드가 거의 없다.

실제로 Batch 비지니스 로직을 처리하는 (ex: log.info()) 기능은 Step에 구현되어 있다.

이처럼 Step에서는 Batch로 실제 처리하고자 하는 기능과 설정을 모두 포한하는 장소라고 생각하면 된다.

Batch 처리 내용을 담다보니, Job 내부의 Step들간에 순서 혹은 처리 흐름을 제어할 필요가 있는데 이번엔 여러 Step들을 어떻게 관리할지에 대해서 알아본다.

1. Next

StepNextJobConfig.java

@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();
    }
}

next()는 순차적으로 Step들을 연결시킬 때 사용된다.
step1 -> step2 -> step3 순으로 하나씩 실행될 떄 next()는 좋은 방법이다.

실행 결과

이번에는 Job Parameter를 version=1 로 변경한뒤 실행해보면

stepNextJob배치가 실행된 것을 확인할 수 있다. 하지만 기존에 있던 simpleJob도 함께 실행되었다.
만약 방금 만든 stepNextJob만 실행하고 싶다면 아래와 같이 변경해야 한다.

+ 지정한 Batch Job만 실행하기

application.yml에 아래의 코드를 추가한다.

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

위의 코드의 역할은 Spring Batch가 실행될 때, Program arguments로 job.name 값이 넘어오면 해당값과 일치하는 Job만 실행하겠다는 것이다.

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

spring.batch.job.names에 NONE이 할당되면 어떤 배치도 실행하지 않겠다는 의미입니다.

즉, 혹시라도 값이 없을때 모든 배치가 실행되지 않도록 막는 역할이다.

job.name을 배치 실행시에 Program arguments로 넘겨보자.

Job Parameter를 수정했던 Program arguments 항목에 아래와 같이 코드를 입력한다.

--job.name=stepNextJob

위 사진처럼 Program arguments 항목에 위 코드를 추가하고 version=1은 이미 실행했으니 version=2로 변경해서 실행한다.
(version=1을 변경하지 않으면 Job Instance 중복 문제가 발생한다.)

실행해보면 지정한 stepNextJob만 실행된 것을 확인할 수 있다.

2. 조건별 흐름 제어 (Flow)

Next가 순차적으로 Step의 순서를 제어한다는 것을 알게 되었다.
( 앞의 step에서 오류가 나면 나머지 뒤에 있는 step들은 실행되지 못한다.)

하지만 상황에 따라 정상일땐 step B로 오류가 났을 때는 step C를 수행해야할때가 잇다.

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

실행 코드

StepNextConditionalJobConfig.java

@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를 조정해야한다.

조정하는 코드는 아래와 같다.

원하는 상황에 따라 분기로직을 작성하여 contribution.setExitStatus의 값을 변경하면 된다.

여기서 FAILED를 발생시켜 step1 -> step3를 테스트 해보자.

실행 결과

step1과 step3만 실행된 것을 확인할 수 있다.
(ExitStatus.FAILED로 인해 step2가 무시되고 실행되었다.)

코드를 수정해서 step1 -> step2 -> step3가 실행되는지 확인해보자.

주석을 걸고 다시 실행해보면

step1 -> step2 -> step3가 차례대로 수행된 것을 확인할 수 있다.

+ Batch Status vs Exit Status

Batch Status와 Exit Status의 차이를 아는 것이 중요하다.

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

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

대부분의 값들은 단어와 같은 뜻으로 해석해서 이해하면 된다.

ExitStatus는 Step의 실행 후 상태를 얘기한다.


예를 들어,

.on("FAILED").to(stepB())

위 코드에서 on 메소드가 참조하는 것은 BatchStatus 으로 생각할 수 있지만 실제 참조되는 값은 Step의 ExitStatus이다.
(ExitStatus는 Enum이 아니다.)

위 예제를 좀더 쉽게 풀이 하자면 exitCode가 FAILED로 끝나게 되면 StepB로 가라는 뜻이다.
Spring Batch는 기본적으로 ExitStatusexitCode는 Step의 BatchStatus와 같도록 설정이 되어 있다.

하지만 만약에 본인만의 커스텀한 exitCode가 필요하다면,
(즉, BatchStatus와 달라야하는 상황)

.start(step1())
    .on("FAILED")
    .end()
.from(step1())
    .on("COMPLETED WITH SKIPS")
    .to(errorPrint1())
    .end()
.from(step1())
    .on("*")
    .to(step2())
    .end()

위 step1의 실행 결과는 다음과 같이 3가지가 될 수 있다.

step1이 실패하며, 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;
        }
    }
}

위 코드를 설명하면 StepExecutionListener 에서는 먼저 Step이 성공적으로 수행되었는지 확인하고, StepExecution의 skip 횟수가 0보다 클 경우 COMPLETED WITH SKIPSexitCode를 갖는 ExitStatus를 반환한다.

3. Decide

2번에서 Step의 결과에 따라 서로 다른 Step으로 이동하는 방법을 알아봤다. 이번에는 다른 방식의 분기처리를 알아보려 한다.

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

  • Step이 담당하는 역할이 2개 이상이 된다.
    - 실제 해당 Step이 처리해야할 로직외에도 분기처리를 시키기 위해 ExitStatus 조작이 필요하다.

  • 다양한 분기 로직 처리의 어려움
    - ExitStatus를 커스텀하게 고치기 위해선 Listener를 생성하고 Job Flow에 등록하는 등 번거로움이 존재한다.

이러한 불편함을 해소하기 위해 Spring Batch에는
명확하게 Step들간의 Flow 분기만 담당하면서 다양한 분기처리가 가능한 JobExecutionDecider 이라는 타입이 따로 존재한다.

DeciderJobConfig.java

@Slf4j
@Configuration
@RequiredArgsConstructor
public class DeciderJobConfig {
    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");
            }
        }
    }
}

이 Batch의 Flow는 아래와 같다

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

decider를 Flow 사이에 넣는 로직은 위의 사진과 같다.

  • start()
    Job Flow의 첫번째 Step을 시작한다.
  • next()
    startStep이후에 decider를 실행한다.
  • from()
    from은 이벤트 리스너 역할을 한다.
    decider의 상태값을 보고 일치하는 상태라면 to()에 포함된 step을 다.

분기 로직에 대한 모든 일은 0ddDecider가 전담하고 있다.
때문에 복잡한 분기로직이 있더라도 Step과는 명확히 역할과 책임이 분리된채 진행할 수 있게 된다.

(JobExecutionDecider 인터페이스를 구현한 OddDecider)
여기서는 랜덤하게 숫자를 생성하여 홀수/짝수인지에 따라 서로 다른 상태를 반환한다.
주의할 점은 Step으로 처리하는게 아니기 때문에 ExitStatus가 아닌 FlowExecutionStatus로 상태를 관리한다는 것이다.

아주 쉽게 EVEN, ODD라는 상태를 생성하여 반환하였고, 이를 from().on() 에서 사용하는 것을 알 수 있다.

여러번 실행해보면 짝수/홀수가 나오면서 서로 다른 step(oddStep,evenStep)이 실행되는 것을 확인할 수 있다.

Reference

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

profile
배운 걸 기록하는 곳입니다.

0개의 댓글