본문 바로가기
개발/Spring

[Spring] 스프링 배치(Spring Batch) 가이드 따라가기 (2)

by devhooney 2022. 12. 11.
728x90

스프링 배치 가이드

 

- 지난 포스팅에서 튜토리얼 보고 따라해봤는데, 이번에는 개념부터 천천히 공부해보자.

- 책을 보고 공부하려 했으나, 스프링과 부트의 배치 사용 문법이 많이 달라 이동욱님의 블로그를 보고 공부했다.

http://devhooney.tistory.com/135

 

[Spring] 스프링 배치(Spring Batch) 가이드 따라가기 (1)

스프링 배치 가이드 - 지난 포스팅에서 튜토리얼 보고 따라해봤는데, 이번에는 개념부터 천천히 공부해보자. - 책을 보고 공부하려 했으나, 스프링과 부트의 배치 사용 문법이 많이 달라 이동욱

devhooney.tistory.com

 

 

1. Next

- next()는 순차적으로 step을 연결할 때 사용된다.

- 코드를 작성한다.

@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("Step1!!!!!!");
                    return RepeatStatus.FINISHED;
                })
                .build();
    }

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

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

}

- 구성편집에서 프로그램인수(Program argument)를 requestDate=20221211를 version=1로 변경해준다.

- 그리고 실행하면 에러가 나는데, 지난번 포스팅때 만든 simpleJob에서 parameter로 requestDate를 받고있기 때문.

- 그래서 방금 작성한 배치만 실행되도록 application.yml 파일을 수정해준다.

 

spring:
  profiles:
    active: local
  batch: # 추가된 설정
    job:
      names: ${job.name:NONE}

- 이 설정은 스프링 배치가 실행될 때, 프로그램 인수로 job.name 값이 넘어오면, 해당 값과 일치하는 job만 실행하게 해준다.

- 값이 NONE으로 되어 있는데, 이건 job.name이 없을 경우에 NONE으로 처리하고, NONE으로 처리된 배치는 실행되지 않는다.

- 구성편집에서 프로그램 인수를 --job.name=stepNextJob version=2로 수정한다. 1은 전에 실행했기때문에 값을 2로 변경해줬다.

- 이렇게 하고 실행하면 잘된다.

 

 

 

 

 

 

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

- Next가 순차적으로 Step의 순서를 제어하는데, step1 에러가 발생하면 step2, 3은 실행되지 않는다.

- 분기처리가 필요할 때가 있다.

- 코드를 작성한다.

@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("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("Step2~~~~");
                    return RepeatStatus.FINISHED;
                })
                .build();
    }

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

- 위 코드는 step1이 성공하면 순차적으로 step2, 3을 진행하고, 실패하면 step1에서 step3으로 넘어간다.

- 전체 Flow를 관리하는 코드를 보면

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

- on():

  • 캐치할 ExitStatus 지정.
  • *일 경우 모든 ExitStatus
  • 분기처리가 필요할 경우 ExitStatus를 조정해야한다.

- to(): 다음으로 이동할 step 지정

- from():

  • 상태값을 보고 일치하는 상태일 경우 to()에 포함돤 step 호출
  • step1의 이벤트 캐치가 FAILED일 경우에 추가로 캐치를 하려면 from()을 써야한다.

- end(): 

  • end()는 FlowBuilder를 반환하는 end, 종료하는 end 2개가 있다.
  • on("*") 뒤에 있는 end는 FlowBuilder를 반환하는 end
  • build() 앞에 있는 end는 FlowBuilder를 종료하는 end
  • FlowBuilder를 반환하는 end 사용시 계속해서 from을 이어갈 수 있다.

 

- step1 코드를 보면

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

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

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

- contribution.setExitStatus()로 상태를 지정해줬다.

- 강제로 FAILED를 지정했기 때문에 실행하면 step2는 실행이 되지 않는다.

 

 

 

3. BatchStatus와 ExitStatus의 차이

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

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

 

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

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

- ExitStatus는 Step의 실행 후 상태를 말한다.(ExitStatus는 Enum이 아니다.)

 

 

 

 

728x90

 

 

 

4. Decide

- Decide는 Step들의 Flow속에서 분기만 담당한다.

- 코드를 작성한다.

@Slf4j
@Configuration
@RequiredArgsConstructor
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") // EVENㅇㅣ라면
                .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 진행

- decider를 Flow 사이에 넣는 로직은 아래 코드

@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") // EVEN이라면
            .to(evenStep()) // evenStep로 간다.
            .end() // builder 종료
            .build();
}

- start(): Job Flow의 첫 번째 Step을 시작한다.

- next(): start() 이후 decider를 실행한다.

- from(): decider의 상태값을 보고 일치하면 to()에 포함된 step을 호출한다.

- 분기 로직에 대한 모든 일은 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");
        }
    }
}

- OddDecider는 Step이 아니기 때문에 ExitStatus가 아닌 FlowExecutionStatus로 상태를 관리한다

- EVEN, ODD라는 상태를 생성하여 반환하였고, 이를 from().on() 에서 사용한다.

- 실행해보면

 

 

- 잘 되는걸 볼 수 있다.

 

 

 

 

- 참고

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

728x90