오늘 하루에 집중하자
  • [Spring Batch] 7. SimpleJob 개념 및 API
    2024년 11월 02일 20시 50분 08초에 업로드 된 글입니다.
    작성자: nickhealthy

    SimpleJob 기본 개념 및 흐름


    기본 개념

    • SimpleJob은 Step을 실행시키는 Job의 구현체로서 SimpleJobBuilder에 의해 생성된다.
      • 스프링 배치에서 제공하는 표준 구현체이며, API를 설정할 때 STEP 타입의 객체만 들어올 수 있다.
    • 여러 단계의 Step으로 구성할 수 있으며 Step을 순차적으로 실행시킨다.
    • 모든 Step의 실행이 성공적으로 완료되어야 Job이 성공적으로 완료된다.
    • 맨 마지막에 실행한 Step의 BatchStatus가 Job의 최종 BatchStatus가 된다.

     

    흐름

    CASE 1.

    SimpleJob은 모든 Step이 COMPLETED 상태로 완료되어야 SimpleJob도 성공적으로 수행될 수 있다. 

     

     

    CASE 2.

    SimpleJob은 중간 Step이 FAILED 시 그 이후에 Step은 실행하지 않고, 실패 처리하게 된다.

     

    SimpleJob의 API 설명

    SimpleJob에는 다음과 같은 API들이 있는데 API 개념들에 대해서 한번 짚고 넘어가려고 한다.(실제론 더 많음)

    • `jobBuilderFactory.get("JobName")`: JobBuilder를 생성하는 팩토리, Job의 이름을 매개변수로 받는다.
    • `start(step())`: 처음 실행할 Step 설정, 최초 한번만 설정하게 된다. 이 메서드를 실행하게 되면 SimpleJobBuilder를 반환한다.
    • `next(step())`: 다음에 실행할 Step 설정, 횟수는 제한이 없으며 모든 `next()`의 Step이 종료되면 Job이 종료된다.
    • `incrementer(JobParametersIncrementer)`: JobParameter의 값을 자동으로 증가해주는 JobParameterIncrementer을 설정한다.
    • `preventRestart()`: Job의 재시작 가능 여부를 설정한다. 기본 값은 `true`이다.
    • `validator(JobParameterValidator)`: JobParameter를 실행하기 전에 올바른 구성이 되었는지 검증하는 JobParametersValidator를 설정한다.
    • `listener(JobExecutionListener)`: Job 라이프사이클의 특정 시점에 콜백을 제공받도록 JobExecutionListener를 설정한다.
    • `build()`: SimpleJob을 생성한다.
    public Job batchJob() {
        return jobBuilderFactory.get("JobName")
                .start(step())
                .next(step())
                .incrementer(JobParametersIncrementer)
                .preventRestart()
                .validator(JobParametersValidator)
                .listener(JobExecutionListener)
                .build();
    }

     

     

    SimpleJob이 생성되는 방식을 도식화하면 다음과 같다.

    JobBuilderFactory > JobBuilder > SimpleJobBuilder > SimpleJob 순으로 생성된다.

     

    SimpleJob 흐름을 디버깅으로 실습

    실습을 통해 `SimpleJob`이 어떻게 설정되고 동작하는지 자세히 알아보자

     

    [SimpleJobConfiguration] - 기본 코드

    package io.spring.batch.helloworld.ch4.simpleJob;
    
    import lombok.RequiredArgsConstructor;
    import org.springframework.batch.core.Job;
    import org.springframework.batch.core.Step;
    import org.springframework.batch.core.StepContribution;
    import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
    import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
    import org.springframework.batch.core.launch.support.RunIdIncrementer;
    import org.springframework.batch.core.scope.context.ChunkContext;
    import org.springframework.batch.core.step.tasklet.Tasklet;
    import org.springframework.batch.repeat.RepeatStatus;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    @RequiredArgsConstructor
    public class SimpleJobConfiguration {
    
        private final JobBuilderFactory jobBuilderFactory;
        private final StepBuilderFactory stepBuilderFactory;
    
        @Bean
        public Job batchJob() {
            return jobBuilderFactory.get("SimpleJobConfiguration")
                    .incrementer(new RunIdIncrementer())
                    .start(step1())
                    .next(step2())
                    .next(step3())
                    .build();
        }
    
        @Bean
        public Step step1() {
            return stepBuilderFactory.get("step1")
                    .tasklet(new Tasklet() {
                        @Override
                        public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
                            System.out.println("STEP1 HAS EXECUTED!");
                            return RepeatStatus.FINISHED;
                        }
                    }).build();
        }
    
        @Bean
        public Step step2() {
            return stepBuilderFactory.get("step2")
                    .tasklet(new Tasklet() {
                        @Override
                        public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
                            System.out.println("STEP2 HAS EXECUTED!");
                            return RepeatStatus.FINISHED;
                        }
                    }).build();
        }
    
        @Bean
        public Step step3() {
            return stepBuilderFactory.get("step3")
                    .tasklet(new Tasklet() {
                        @Override
                        public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
                            System.out.println("STEP3 HAS EXECUTED!");
                            return RepeatStatus.FINISHED;
                        }
                    }).build();
        }
    }

     

     

    앞의 포스터에서도 언급했듯이 API  사용에 따라 JobBuilder가 하위 Builder를 생성하게 된다.

    여기서는 SimpleJobBuilder를 생성하게 된다.

    JobBuilder

     

    그리고 SimpleJobBuilder 내부적으로 `start()``next()` 메서드를 통해 step을 저장하고 있는 것을 알 수 있다.

    SimpleJobBuilder - start()

     

    SimpleJobBuilder - next()

     

    SimpleJobBuilder의 `build()` 메서드를 통해 드디어 SimpleJob을 만들게 된다.

    그리고 `super.enhance(job)` 메서드를 통해 SimpleJob에 필요한 속성들의 세팅을 진행하게 된다.

    또한 바로 아래 줄에 보면 SimpleJobBuilder 클래스가 가지고 있던 steps 리스트도 SimpleJob에 세팅하고 있는 것을 확인할 수 있다. 

    SimpleJobBuilder

     

    JobBuilder, SimpleJobBuilder 클래스 모두 부모로 JobBuilderHelper 클래스를 상속 받고 있는데, 내부적으로 CommonJobProperties 클래스를 정의하고 참조하고 있다. 이 클래스는 Job 설정에 필요한 공통적인 속성들을 가지고 있다. JobBuilder 클래스가 생성될 때 내부적으로 CommonJobProperties도 초기화 하는 것 같다.(아직 여기까지 확실하지 않음) 

    CommonJobProperties

     

    각설하고 다시 돌아와서 얘기해보자면 `super.enhance(job)` 메서드를 통해 Job 구동에 필요한 세팅들을 SimpleJob에 세팅하고 있는 모습을 볼 수있다.

    JobBuilderHelper - enhance()

     

    모든 세팅이 완료된 SimpleJob은 이제 스프링 부트를 통해 SimpleJobLauncher를 실행시켜서 스프링 배치를 실행하게 된다. 디버깅 모드를 통해 Job의 객체를 보면 SimpleJob인 것을 확인할 수 있고, 이외에도 여러가지 세팅들이 기본 값으로 초기화 된 것을 알 수 있다.(restartable, jobRepository, listener, jobParameterIncrementer, jobParametersValidator etc..)

    그리고 최종적으로 스프링 배치 작업이 성공적으로 이루어진다.

    SimpleJobLauncher - run()

     

    이제 나중에 다시 배우게 될 내용이지만 방금 위에서 본 기본 값으로 세팅되는 것들을 우리가 직접 세팅했을 때 설정 값이 어떻게 바뀌는지 확인해보자

     

    우선 기본 값 세팅이 아닌 우리가 직접 세팅한 것으로 SimpleJob을 설정하기 위해선 코드를 추가해야 한다.

    기존 코드에서 다음 코드를 추가하고, 실행해보자 

    [SimpleJobConfiguration] - Job 정의 코드를 수정 및 추가

        @Bean
        public Job batchJob() {
            return jobBuilderFactory.get("SimpleJobConfiguration")
                    .incrementer(new RunIdIncrementer())
                    .start(step1())
                    .listener(new JobExecutionListener() {
                        @Override
                        public void beforeJob(JobExecution jobExecution) {
    
                        }
    
                        @Override
                        public void afterJob(JobExecution jobExecution) {
    
                        }
                    })
                    .validator(new JobParametersValidator() {
                        @Override
                        public void validate(JobParameters parameters) throws JobParametersInvalidException {
    
                        }
                    })
                    .preventRestart()
                    .next(step2())
                    .next(step3())
                    .build();
        }

     

    방금 위에서 봤던 디버깅 내용과 다르게 restartable, listener, jobParametersIncrementer, jobParametersValidator의 값들이 우리가 세팅한대로 변경된 것을 알 수 있다.

    SimpleJobLauncher - run()

     

    이것으로 SimpleJob이 Job을 생성하는 과정에서 API를 설정하게 되면 모든 값들을 가지고 있고, 배치 수행 중에 그 값들을 활용한다는 것을 알 수 있었다.

     

    이번에는 SimpleJob이 배치 수행중 특정 STEP에서 실패가 의도적으로 발생했을 때, 최종적으로 SimpleJob도 실패하는 경우도 살펴보자

     

    다음과 같이 코드를 추가하고 실행해보자

    @Bean
        public Step step3() {
            return stepBuilderFactory.get("step3")
                    .tasklet(new Tasklet() {
                        @Override
                        public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
                            chunkContext.getStepContext().getStepExecution().setStatus(BatchStatus.FAILED); // 배치 상태 코드 FAILED
                            contribution.setExitStatus(ExitStatus.STOPPED);                                 // 배치 종료 코드 STOPPED
                            System.out.println("STEP3 HAS EXECUTED!");
                            return RepeatStatus.FINISHED;
                        }
                    }).build();
        }

     

     

    StepExecution 메타 데이터 테이블이다.

    STATUS 코드는 FAILED, EXIT_CODE는 STOPPED 인 것을 확인할 수 있다.  

    StepExecution

     

    이번에는 JobExecution 메타 데이터 테이블이다.

    마찬가지로 상태 값이 STEP 값과 동일하다.

    이것은 JOB의 상태 값의 반영은 최종 STEP의 결과에 따라 값이 반영되는 것을 의미한다. 즉, SimpleJob이 실행되고 최종적인 상태 값은 STEP의 상태 값을 반영되는 것이다.

    JobExecution

     

     

    SimpleJob - validator()


    기본 개념

    • Job 실행에 꼭 필요한 파라미터를 검증하는 용도
    • DefaultJobParametersValidator 구현체를 지원하며, 좀 더 복잡한 제약 조건이 있다면 JobParametersValidator 인터페이스를 직접 구현할 수도 있다.

     

    JobParametersValidator 구조

    JobParameters 값을 매개변수로 받아 검증한다.

     

    DefaultJobParametersValidator  흐름도

    필수 키와 옵션 키를 설정할 수 있다. 말 그대로 필수 키에 설정한 값이 없으면 오류를 발생시키고 Job이 실행되지 않으며, 옵션 키를 설정한 경우 설정한 값이 없더라도 필수 키만 존재한다면 Job을 정상적으로 실행하게 된다.

    1. requiredKeys: 필수 키를 설정한다.
    2. optionalKeys: 옵션 키를 설정한다.
    3. 각 Job의 파라미터에 값을 넣었을 때 동작 예시

     

     

    실습을 통해 SimpleJob - validator 동작 방식과 사용법에 대해 알아보자

     

    CASE 1. 사용자 정의 검증기를 만들어 파라미터를 검증하기

    우선 배치 Job을 실행시키는 기본 코드는 아래와 같다.

     

    [ValidatorConfiguration]

    package io.spring.batch.helloworld.ch4.validator;
    
    
    import lombok.RequiredArgsConstructor;
    import org.springframework.batch.core.Job;
    import org.springframework.batch.core.Step;
    import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
    import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
    import org.springframework.batch.repeat.RepeatStatus;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @RequiredArgsConstructor
    @Configuration
    public class ValidatorConfiguration {
    
        private final JobBuilderFactory jobBuilderFactory;
        private final StepBuilderFactory stepBuilderFactory;
    
        @Bean
        public Job batchJob() {
            return jobBuilderFactory.get("ValidatorConfiguration")
                    .start(step1())
                    .next(step2())
                    .next(step3())
                    .validator(new CustomJobParametersValidator())
                    .build();
        }
    
        @Bean
        public Step step1() {
            return stepBuilderFactory.get("step1")
                    .tasklet((contribution, chunkContext) -> {
                        System.out.println("STEP1 HAS EXECUTED!");
                        return RepeatStatus.FINISHED;
                    }).build();
        }
    
        @Bean
        public Step step2() {
            return stepBuilderFactory.get("step2")
                    .tasklet((contribution, chunkContext) -> {
                        System.out.println("STEP2 HAS EXECUTED!");
                        return RepeatStatus.FINISHED;
                    }).build();
        }
    
        @Bean
        public Step step3() {
            return stepBuilderFactory.get("step3")
                    .tasklet((contribution, chunkContext) -> {
                        System.out.println("STEP3 HAS EXECUTED!");
                        return RepeatStatus.FINISHED;
                    }).build();
        }
    }

     

     

    [CustomJobParametersValidator] - 사용자 정의 validator

    package io.spring.batch.helloworld.ch4.validator;
    
    import org.springframework.batch.core.JobParameters;
    import org.springframework.batch.core.JobParametersInvalidException;
    import org.springframework.batch.core.JobParametersValidator;
    
    public class CustomJobParametersValidator implements JobParametersValidator {
    
        /**
         * JobParameters 파라미터에 우리가 설정한 파라미터 값들이 들어오게 되는데 그 값들을 활용해서 검증기를 구현할 수 있다.
         */
        @Override
        public void validate(JobParameters jobParameters) throws JobParametersInvalidException {
            if (jobParameters.getString("name") == null) {
                throw new JobParametersInvalidException("name parameter is required");
            }
    
        }
    }

     

     

    이제 사용자 정의 validator의 기본 코드를 모두 작성하였으니, 디버깅을 실행해보자

    우선 JobBuilderHelper(JobBuilder의 부모) 클래스에서 CommonJobProperties(Job을 만드는데 필요한 공통 속성을 정의해 둔 클래스)에 Validator가 저장된다.

    디버깅 내용에서 알 수 있듯이 우리가 정의한 Validator가 지정된 것을 알 수 있다.

     

    이후 SimpleJob을 생성할 때 SimpleJob의 부모인 AbstractJob을 호출하게 되는데 JobParameterValidator를 방금 전에 우리가 생성한 객체로 설정하는 것을 알 수 있다. 디버깅 모드로 확인해보면 우리가 따로 사용자 정의 validator를 만들지 않았을 때의 기본 값은 스프링 배치에서 제공하는 DefaultParametersValidator라는 것을 알 수 있다.

     

    이제 SimpleJob 설정 구성을 마치고, SimpleJobLauncher에서 배치 Job을 실행하기 전에 스프링 배치는 Validator를 검증하게 된다.

    참고로 스프링 배치는 배치를 실행하기 전 다음과 같이 validator를 검증하게 된다.

    1. SimpleJobLauncher에서 Job을 실행하기 전(JobRepository에 메타 데이터를 저장하고 생성하기 전) 파라미터에 대한 검증을 validator로 체크한다.
    2. AbstractJob에서 SimpleJob을 다 만들고 Job을 실행하기 전 파라미터에 대한 검증을 validator로 체크한다.

     

    파라미터에서 name 키 값을 설정했기 때문에 검증은 정상적으로 이뤄지게 되고, 배치 작업도 정상적으로 수행하게 된다.

     

    이제 메타 데이터 테이블을 초기화 한 뒤, `name` 키를 삭제하고 실행해서 예외를 발생해보자

    우리가 에러 메시지에 적은대로 에러가 난 것을 확인할 수 있다.

     

    JOB_INSTANCE 메타 데이터 테이블의 모습인데, 아예 배치 Job이 실행되지 않은 것을 알 수 있다.

    JOB_INSTANCE

     

    CASE 2. 스프링 배치에서 제공하는 DefaultJobParametersValidator를 사용해서 파라미터 검증하기

     

    먼저 스프링 배치에서 제공하는 validator 구조부터 살펴보자

    필수 키와 옵션 키가 필드로 정의되어 있으며, 문자 배열로 받아 초기화를 시킨다. 

    DefaultJobParametersValidator

     

    이후 Setter를 호출해 인자로 받은 문자 배열을(필수 키, 옵션 키) Collection 타입에 맞도록 형변환을 진행하게 되며, 필드에 초기화 한 값과 `validate()` 메서드의 로직을 통해 올바르게 키 값들이 설정되어 있는지 검증하게 된다.

    로직을 간단히 설명하자면 다음과 같다.

    • 첫 번째 박스에서는 옵션 키를 검증하는 곳으로서 옵션 키와 필수 키가 설정되어 있지 않다면 키를 넣지 않았으므로 에러가 발생하게 된다. 즉, 옵션 키를 설정하지 않았더라도 필수 키만 설정되어 있다면 에러가 발생하지 않을 것이다.
    • 두 번째 박스에서는 필수 키를 검증하는 곳으로서 필수 키가 설정되어 있지 않다면 에러가 발생하게 된다.

     

    이제 코드를 DefaultJobParametersValidator를 사용하는 코드로 수정하고 디버깅을 해보자

    기본 코드는 아래와 같다.

    [ValidatorConfiguration]

    @Configuration
    public class ValidatorConfiguration {
    
        private final JobBuilderFactory jobBuilderFactory;
        private final StepBuilderFactory stepBuilderFactory;
    
        @Bean
        public Job batchJob() {
            return jobBuilderFactory.get("ValidatorConfiguration")
                    .start(step1())
                    .next(step2())
                    .next(step3())
    //                .validator(new CustomJobParametersValidator())
                    .validator(new DefaultJobParametersValidator(new String[]{"name", "date"}, new String[]{"count"}))
                    .build();
        }
        ...
    }

     

     

    파라미터는 다음과 같이 `name`, `count` 두 개만 설정했다. 현재 필수 값(date)가 빠진 상황

     

     

    현재 DefaultJobParametersValidator의 설정 값을 보면 아래와 같다.

    우리는 필수 키 `date`를 설정하지 않았기 때문에 에러가 발생할 것이다.

     

     

    실행결과는 다음과 같다. 아래와 같이 필수 키 `date`를 설정하지 않아서 오류가 났다고 알려주고 있다.

    즉, JobParameterValidator는 필수 키만 설정하면 문제 없이 배치 Job을 수행할 수 있다.

     

     

    PreventRestart()


    기본 개념

    • Job의 재시작 여부를 설정한다.
    • 기본 값은 `true`이며 `false`로 설정시 해당 배치 Job은 재시작을 지원하지 않는다.
      • Job이 실패하게 되었을 경우 원래는 Job의 재시작이 가능하지만, 이 옵션을 `false`로 설정하게 되면 재시작이 불가능하고 JobRestartException이 발생하게 된다.
    • 재시작과 관련 있는 기능으로 Job을 처음 실행하는 것과는 아무런 상관이 없다.

     

    흐름도

    처음 Job을 실행하고 나서 JobExecution을 확인해 보았을 때 같은 Job을 처음 수행하는 것이라면 정상적으로 배치 Job을 수행하게 된다. 하지만 JobExecution을 확인해 보았을 때 이미 수행이 되었지만 실패한 배치 Job이라 재실행이 가능하더라도 `preventRestart` 옵션이 `false`라면 재실행이 불가능한 흐름이다.

    즉, Job의 실행이 처음이 아닌 경우는 Job의 성공/실패와 상관없이 오직 preventRestart 설정 값에 따라서 실행 여부를 판단하게 된다.

    preventRestart 흐름도

     

     

    실습을 통해 자세히 알아보자

    원래 우리가 알고 있던 JobExecution은 실패 후 재실행이 가능했다. 하지만 `preventRestart()` 옵션을 통해 해당 배치 Job이 재실행이 불가능한 것을 확인해 볼 것이다.

     

    우선 기본 코드는 아래와 같다. 아래 코드를 실행하면 당연히 에러가 발생하면서 Job이 실패하게 된다.

    메타 데이터 테이블 확인은 앞전에 많이 확인했으니 생략한다.

    [PreventRestartConfiguration] - step2에 예외를 던지는 코드를 삽입한다.

    package io.spring.batch.helloworld.ch4.preventRestart;
    
    
    import lombok.RequiredArgsConstructor;
    import org.springframework.batch.core.Job;
    import org.springframework.batch.core.Step;
    import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
    import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
    import org.springframework.batch.core.job.DefaultJobParametersValidator;
    import org.springframework.batch.repeat.RepeatStatus;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    @RequiredArgsConstructor
    public class PreventRestartConfiguration {
    
        private final JobBuilderFactory jobBuilderFactory;
        private final StepBuilderFactory stepBuilderFactory;
    
        @Bean
        public Job batchJob() {
            return jobBuilderFactory.get("PreventRestartConfiguration")
                    .start(step1())
                    .next(step2())
                    .preventRestart()
                    .build();
        }
    
        @Bean
        public Step step1() {
            return stepBuilderFactory.get("step1")
                    .tasklet((contribution, chunkContext) -> {
                        System.out.println("STEP1 HAS EXECUTED!");
                        return RepeatStatus.FINISHED;
                    }).build();
        }
    
        @Bean
        public Step step2() {
            return stepBuilderFactory.get("step2")
                    .tasklet((contribution, chunkContext) -> {
                        System.out.println("STEP2 HAS EXECUTED!");
                        throw new RuntimeException("step2 was failed");
    //                    return RepeatStatus.FINISHED;
                    }).build();
        }
    
    }

     

    이번에는 다시 한번 디버깅으로 실행해보자

    `preventRestart()`를 설정하게 되면 JobBuilderHelper에서 첫 번째로 `false`로 설정하게 된다.

    참고로 properties 필드는 CommonJobProperties 클래스로 앞전 포스터에서도 많이 나왔는데 Job을 만들 때 공통 속성을 설정하는 클래스이다. 

     

     

    그리고 실제로 SimpleJob을 만들기 직전에 AbstractJob(SimpleJob의 부모)을 실행할 때 위에서 설정한 CommonJobProperties 속성들을 SimpleJob에 설정하게 된다. 이 설정을 통해 이 배치 Job은 실패하더라도 재실행이 불가능한 Job 상태로 변하게 된다.

     

    이제 SimpleJob이 생성되고 SimpleJobLauncher에서 배치 Job을 수행하기 전 아래와 같은 내용을 수행하게 된다.

    1. 현재 실행중인 Job이 실행된 적이 있는지 확인하게 된다. 즉, JobExecution을 조회한다.
    2. 이미 현재 케이스에선 이전에 실행한 적이 있으므로 첫 번째 if 문을 통과하게 되고, `preventRestart()` 옵션을 주었기 때문에 두 번째 if문도 통과하게 되어 에러를 던지게 된다.

    SimpleJobLauncher

     

    최종적으로 다음과 같은 콘솔화면이 나타난다.

    내용은 다음과 같다. "이미 JobInstance는 존재하며, 재실행이 불가능함"으로 Job 재실행에 실패하게 된다. 끝.

     

     

    댓글