본문 바로가기
Practice/SpringBatch

Day 3. Spring Batch Job 흐름 정의

반응형

Spring-Batch의 기본 개념을 정리하고, 간단한 실습 환경부터 연습해보는 포스팅입니다

참고

Spring-Batch 공식문서(4.3.3 기준)

https://docs.spring.io/spring-batch/docs/4.3.3/reference/html/

 

Spring Batch - Reference Documentation

Welcome to the Spring Batch reference documentation! This documentation is also available as single html and pdf documents. The reference documentation is divided into several sections: The following appendices are available: Lucas Ward, Dave Syer, Thomas

docs.spring.io

 

 

이동욱님의 블로그

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

 

1. Spring Batch 가이드 - 배치 어플리케이션이란?

Spring Batch In Action이 2011년 이후 개정판이 나오지도 않고 (2019.03 기준), 한글 번역판도 없고, 국내 Spring Batch 글 대부분이 튜토리얼이거나 공식 문서 중 일부분을 짧게 번역한 내용들이라 대용량 시

jojoldu.tistory.com


1. Next method

  • 순차적으로 Step을 실행시킬 때 사용하는 메서드입니다
  • 예제 코드
    // Job
    
    @Slf4j
    @Configuration
    @RequiredArgsConstructor
    public class SimpleNextJobConfiguration {
    
        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();
        }
    
    }

    • "stepNextJob"이라는 jobName을 가진 Job아래 "step1", "step1", "step1" 순차적으로 Step을 실행 시킵니다
    • 위에서 새로 정의한 "stepNextJob" 만 실행시키려면 추가 셋팅을 해줘야합니다
      • 특정 Job만 실행 시키기
        • application.yml에 config 정의
          spring.batch.job.names: ${job.name:NONE}​
          • Spring Batch가 실행될때, Program arguments로 job.name 값이 넘어오면 해당 값과 일치하는 Job만 실행
          • job.name 값이 있으면 할당하고 없다면 NONE(아무런 배치도 실행하지 않음)을 할당합니다
        • program arguments에 job.name 추가
          --job.name=stepNextJob
      • 실제 운영 환경에서는 
        java -jar batch-application.jar --job.name=simpleJob​
        와 같이 jvm 옵션을 통해 실행시킵니다
    • 참고로 위와 같이 JobParameters 없이 정의 Step들은 다시 실행시키면 Exception이 발생하는 것이 아닌 EXIT_CODE가 NOOP인 "All steps already completed or no steps configured for this job" msg가 나온다
      ( 실행은 안된다 )

 

2. 조건별 흐름 제어

  • 위에서 본 Next로 흐름 제어는 앞 선 Step에서 오류가 나면 중단, 정상적으로 실행됐을때만 다음 Step을 수행했습니다
  • 이와 다르게 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") // conditionalJobStep1이 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 -> step3
      • 생성된 job의 start method를 통해 conditionalStep1을 실행
      • on 메서드를 통해 ExitStatus.FAILED인 경우를 정의
      • to 메서드를 통해 on 메서드에서 정의된 상황이 발생한 경우 보낼 곳(conditionalStep3)을 정의
      • on 메서드를 통해 * 모든 경우를 정의
      • end 메서드를 통해 해당 Job을 종료시킵니다
    • step1 성공 시나리오: step1 -> step2 -> step3
      • 생성된 job의 start method를 통해 conditionalStep1을 실행
      • on 메서드를 통해 ExitStatus.FAILED인 경우를 정의가 됐지만 conditionalStep1은 성공했기 때문에 to 메서드로 정의된 곳으로 넘어가지 않습니다
      • 다시 from 메서드로 정의된 Step의 ExitStatus 코드를 보고
      • on 메서드로 정의된 내용(*), 모든 경우에 따라 하위 메서드들을 순차적으로 실행시킵니다
  • 주요 메서드 설명
    • on()
      • 캐치할 ExitStatus 지정, BatchStatus가 아닌 ExitStatus 코드를 이용해 분기처리를 한다
      • * 일 경우 모든 ExitStatus가 지정된다
    • to()
      • 다음으로 이동할 Step 정의
    • from()
      • 일종의 이벤트 리스너
      • on(), to()의 흐름을 이용해 발생하는 ExitStatus를 추가적으로 캐치할 때 사용
    • end()
      • FlowBuilder()를 반환하는 end와 FlowBuilder()를 종료하는 end 두 가지로 나뉜다
      • 정의된 이벤트 흐름(on -> to)을 마무리하는 end는 FlowBuilder()를 반환, 반환된 FlowBuilder()를 통해 Job 흐름을 이어나갈 수 있다
      • Job.Build() 앞에 있는 end는 FlowBuilder()를 종료하는 end, Job의 흐름을 종료한다
  • BatchStatus vs ExitStatus
    • BatchStatus ( 4.3.3 기준 )
      • Job 또는 Step 의 실행 결과를 Spring에서 기록할 때 사용하는 Enum
      • org.springframework.batch.core.BatchStatus
      • COMPLETED, STARTING, STARTED, STOPPING, STOPPED, FAILED, ABANDONED, UNKNOWN
    • ExitStatus
      • ExitStatus는 Step의 실행 후 상태
      • org.springframework.batch.core.ExitStatus
      • UNKNOWN, EXECUTING, COMPLETED, NOOP, FAILED, STOPPED

 

3. Decide

  • 위에서 조건분기를 태웠던 예제에는 2가지 문제점이 있습니다
    • Step이 담당하는 역할이 2개 이상이 됩니다.
      • 실제 해당 Step이 처리해야할 로직외에도 분기처리를 시키기 위해 ExitStatus 조작이 필요
    • 다양한 분기 로직 처리의 어려움
      • ExitStatus를 커스텀하게 고치기 위해선 Listener를 생성하고 Job Flow에 등록하는 등 번거로움이 존재
  • 분기만 담당하는 타입을 만들어 처리하면 Step이 담당했던 분기라는 역할을 추출할 수 있고 어려웠던 분기 로직 처리를 간단하게 만들 수 있습니다
  • JobExecutionDecider interface를 구현해 분리를 담당하는 로직만 분리할 수 있습니다
  • 예제 코드
    // JobExecutionDecider
    
    @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") // 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");
                }
            }
        }
    }
    • "deciderJob" 이름을 갖는 Job 생성
    • "startStep" step을 실행시키고 정상적으로 수행되면 decider() 를 수행
    • 해당 JobExecutionDecider는 랜덤하게 추출되는 숫자를 이용하여 홀수/짝수를 기준으로 FlowExecutionStatus를 반환
    • from(decider())를 통해 "ODD" ExitStatus라면 oddStep()을 수행
    • from(decider())를 통해 "EVEN" ExitStatus라면 evenStep()을 수행
  • 실행 결과

  • 참고
    • 이미 실행된 Step만 Step already complete or not restartable, so no action to execute 를 발생시키며 Skip한다
    • 즉 동일한 JobParameters로 실행시켰을시 정의된 Decider는 계속 실행되며 각 Step들은 한번 실행이 된 후 다시 실행되지 않는다
728x90
반응형

'Practice > SpringBatch' 카테고리의 다른 글

Day 5. Spring Batch Chunk  (0) 2021.10.07
Day 4. Spring Batch Scope  (0) 2021.09.09
Day 2. SpringBatch Job 메타 테이블이란?  (0) 2021.09.04
Day 1. Spring-Batch 환경 설정  (0) 2021.09.01