Spring Batch Guide - 04. Spring Batch Job Flow

이준영·2021년 1월 12일
0

Spring Batch Guide

목록 보기
4/5
post-thumbnail

Spring Batch Guide 시리즈는 이동욱 개발자님의 Spring Batch 가이드를 보고 학습한 내용을 정리한 글입니다.

많은 내용이 원 글과 유사할 수 있습니다. 이 점 양해바랍니다 🙏🏻

앞서 Spring Batch의 Job을 구성하는 데는 Step이 있다고 했습니다. Step실제 Batch 작업을 수행하는 역할을 합니다.

Batch의 비지니스 로직(log.info())을 처리하는 기능은 Step에 구현되어 있습니다. 즉, Batch로 실제 처리하고자 하는 기능과 설정을 모두 포함하는 장소Step이라고 할 수 있습니다.

이처럼 Step은 처리하고자 하는 기능을, 그리고 Job은 이런 Step의 처리 흐름을 제어하는 역할을 합니다.

그럼 이번에는 Job에서 Step의 흐름을 어떻게 제어하는 지에 대해 알아보도록 하겠습니다.

1. Next

next는 앞서 simpleJob을 작성하면서 사용하였습니다.

그럼 이번에는 StepNextJobConfiguration 클래스를 통해 Job을 만들어보도록 하겠습니다.

@Slf4j
@RequiredArgsConstructor
@Configuration
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을 연결시킬 때 사용됩니다. stepNextJob은 Step1 -> Step2 -> Step3 순서로 순차적으로 Step을 실행시키는 Job입니다.

그럼 직접 실행을 해서 호출 순서를 확인하도록 하겠습니다. 이번에는 Job Parameter를 version=1로 변경하고 실행해보겠습니다.

어플리케이션을 실행해보면 아래와 같이 동작하는 것을 확인할 수 있습니다.

그런데 잘 살펴보면 stepNextJob도 잘 수행됐지만 기존의 simpleJob도 같이 수행된 것을 볼 수 있습니다.

만약 stepNextJob만 실행하고 싶을 경우 위와 같은 상황은 문제를 야기할 수 있습니다.

1. 지정한 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을 할당하겠다'는 의미입니다. 그래서 job.name의 값이 없다면 모든 배치가 실행되지 않도록 막는 역할을 합니다.

그럼 이 job.name을 배치 실행 시에 Program arguments로 넘기도록 실행 환경을 수정하겠습니다.

version은 1을 이미 수행했으므로 2로 변경해야 합니다. 그래야지 Job Instance의 Job Parameter 중복이 발생하지 않습니다.

이렇게 실행 환경을 설정하고 다시 실행해보겠습니다.

job.name 값으로 지정한 stepNextJob만 실행된 것을 확인할 수 있습니다.

앞으로 이런 방식으로 필요한 Job만 실행하면 될 것입니다!

실제 운영 환경에서는 java -jar batch-application.jar --job.name=simpleJob과 같이 배치를 실행합니다.

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

앞에서 살펴본 next는 Step을 순차적으로 진행하도록 제어한다는 것을 알게 되었습니다.

하지만 여기서 중요한 점은 앞에 Step이 실패하게 되면 그 이후에 위치하는 Step은 모두 실행되지 못한다는 점입니다.

그러나 상황에 따라서 정상일 경우 Step B를, 실패했을 경우 Step C를 실행해야 하는 경우가 있습니다.

Spring Batch의 Job는 이런 상황을 대비해서 실행 상태에 따라 Step이 실행되도록 작성할 수 있습니다.

StepNextConditionalJobConfiguration 클래스를 통해 해당하는 Job을 만들어보도록 하겠습니다.

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

    @Bean
    public Job stepNextConditionalJob() {
        // @formatter:off
        return jobBuilderFactory.get("stepNextConditionalJob")
                .start(conditionalJobStep1())
                    .on("FAILED")
                    .to(conditionalJobStep3())
                    .on("*")
                    .end()
                .from(conditionalJobStep1())
                    .on("*")
                    .to(conditionalJobStep2())
                    .next(conditionalJobStep3())
                    .on("*")
                    .end()
                .end()
                .build();
        // @formatter:on
    }

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

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

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

위 Job은 2가지의 시나리오를 가지고 있습니다.

  • Step1 실패 시나리오: step1 -> step3

  • Step1 성공 시나리오: step1 -> step2 -> step3

이렇게 전체 Flow를 관리하는 Job 코드가 바로 아래와 같습니다.

  • 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로 실행되는 flow를 테스트해보겠습니다.

물론, 마찬가지로 job.nameversion을 실행 환경에서 수정해주어야 합니다.

실행한 결과는 아래와 같습니다.

ExitStatus.FAILED로 인해 step1과 step3만 실행된 것을 확인할 수 있습니다.

그럼 이번에는 정상적으로 실행되어 step1 -> step2 -> step3로 실행되는 flow를 테스트해보겠습니다.

한 줄의 코드만 주석으로 처리하고 실행하면 됩니다.

실행한 결과는 다음과 같습니다.

flow 대로 step1 -> step2 -> step3가 순서대로 실행됨을 확인할 수 있습니다.

2. Batch Status vs Exit Status

앞에서 BatchStatusExitStatus에 대해 잠깐 언급했었는데 이 둘의 차이점을 아는 것이 중요합니다.

BatchStatusJob 또는 Step의 실행 결과를 Spring에 기록할 때 사용하는 Enum입니다.

BatchStatus의 값으로는 COMPLETED, STARTING, STARTED, STOPPING, STOPPED, FAILED, ABANDONED, UNKNOWN이 있습니다. 각 값들의 역할은 단어의 뜻과 거의 동일합니다.

그러나 아래와 같은 코드에서 on() 메서드가 참조하는 것은 BatchStatus가 아니라 Step의 ExitStatus입니다.

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

ExitStatusStep의 실행 후 상태를 말합니다(ExitStatus는 Enum이 아닙니다).

앞서 소개한 코드인 on("FAILED").to(stepB()) 코드는 exitCode FAILED로 끝나게 되면 stepB로 가라는 뜻입니다.

Spring Batch에서 기본적으로 ExitStatusexitCode는 Step의 BatchStatus와 같도록 설정이 되어있습니다.

만약 본인만의 커스텀한 exitCode가 필요한 상황이라면?

(이 상황은 BatchStatusexitCode가 달라야 하는 상황입니다)

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

위 step1의 flow는 3가지로 볼 수 있습니다.

  • step1이 실패하면 Job 또한 실패하게 됩니다.

  • step1이 성공적으로 실행되어 step2가 수행됩니다.

  • step1이 성공적으로 실행되며 COMPLETED WITH SKIPSexitCode로 종료하게 됩니다.

하지만 위 코드에서 COMPLETED WITH SKIPS와 같은 exitCodeExitStatus에 존재하지 않는 코드입니다. 만약 이 로직이 정상적으로 실행되기 위해서는 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.getSkipCound() > 0) {
            return new ExitStatus("COMPLETED WITH SKIPS");
        }
        else {
            return null;
        }
    }
}

해당 코드는 StepExecutionListener에서 먼저 Step이 성공적으로 수행되었는지 확인하고 StepExecution의 skip 횟수가 0보다 클 경우 COMPLETE WITH SKIPSexitCode를 갖는 ExitStatus를 반환합니다.

3. Decide

이번에는 지금까지 소개한 방법과 다른 분기 처리를 알아보도록 하겠습니다.

앞서 설명한 방법들은 2가지의 문제가 존재합니다.

  • Step이 담당하는 역할이 2개 이상이다!

    • 실제로 Step이 처리해야 할 로직 외에도 분기 처리를 하기 위해 ExitStatus 조작이 필요합니다.
  • 다양한 분기 로직 처리가 어렵다!

    • ExitStatus를 커스텀하게 고치기 위해서는 Listener를 생성하고 Job Flow에 등록하는 등의 번거로움이 존재합니다.

이런 단점을 해결해줄 수 있는, 명확하게 Step간의 Flow 분기만 담당하면서 다양한 분기 처리가 가능한 타입이 Spring Batch에는 존재합니다.

Flow 속에서 분기만 담당하는데 이를 JobExecutionDecider라고 하고 이를 사용해서 실습 코드를 작성해보도록 하겠습니다.

DeciderJobConfiguration 클래스에 JobExecutionDecider를 사용한 Job을 만들어보도록 하겠습니다.

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

    @Bean
    public Job deciderJob() {
        // @formatter:off
        return jobBuilderFactory.get("deciderJob")
                .start(startStep())
                .next(decider()) // 홀수, 짝수 구분
                .from(decider()) // decider의 상태
                    .on("ODD") // 상태가 ODD
                    .to(oddStep()) // oddStep으로 이동
                .from(decider()) // decider의 상태
                    .on("EVEN") // 상태가 EVEN
                    .to(evenStep()) // evenStep으로 이동
                .end() // builder 종료
                .build();
        // @formatter:on
    }

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

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

    @Bean
    public Step evenStep() {
        return stepBuilderFactory.get("evenStep")
                .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");
            }
        }
    }
}

deciderJob의 Flow는 다음과 같습니다.

  • startStep -> oddDecider에서 홀수인지 확인 -> oddStep 실행

  • startStep -> oddDecider에서 짝수인지 확인 -> evenStep 실행

decider를 Flow 사이에 넣는 로직은 아래와 같습니다.

  • start()
    • Job Flow의 첫 번재 step을 시작합니다.
  • next()
    • startStep 이후에 decider를 실행합니다.
  • from()
    • 마찬가지로 이벤트 리스너 역할을 합니다.

    • decider의 상태 값을 보고 일치하는 상태이면 to()에 포함된 step을 호출합니다.

해당 코드에서 분기와 관련된 모든 처리는 OddDecider 인스턴스가 전담하고 있습니다.

그 덕분에 복잡한 분기가 존재하더라도 step과는 명확히 역할과 책임이 분리된 채 진행이 가능하게 되었습니다.

Decider의 구현체는 다음과 같습니다.

OddDeciderJobExecutionDecider 인터페이스를 구현한 구현체입니다.

decide 메서드는 랜덤하게 숫자를 생성하고 해당 숫자가 홀수인지 짝수인지에 따라 각기 다른 상태를 반환합니다.

주의할 점은 step으로 처리되는 것이 아니기 때문에 ExitStatus가 아닌 FlowExecutionStatus로 상태를 관리합니다.

그럼 이 코드를 한 번 실행해보겠습니다.

홀수, 짝수에 따라서 각기 다른 step이 실행되는 것을 확인할 수 있었습니다!

정리하며

이번에는 Spring Batch Job 구성 시 어떻게 Job Flow를 구성하면 되는지에 대해 간단하게 살펴보았습니다.

job.name으로 원하는 Job만 선택해서 실행하는 방법, Flow에 분기를 주고 그에 따라 step을 실행하는 방법, 그리고 여기서 조금 더 개선해서 JobExecutionDecider 인터페이스를 구현한 Decider를 통해 Flow를 분기하는 방법에 대해 알아봤습니다.

다음에는 Spring Batch에서 가장 중요한 개념인 Scope에 대해 진행하겠습니다.

참고 자료

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

profile
growing up 🐥

0개의 댓글