본문 바로가기

Reading Record/스프링 배치 완벽 가이드

4장 잡과 스텝 이해하기 : ExecutionContext 까지

4장. 잡과 스텝 이해하기

잡은 처음부터 끝까지 독립적으로 실행할 수 있는 고유하며 순서가 지정된 여러 스텝의 목록이다.

 

잡의 생명주기

JobRunner: Job 실행의 시작

CommandLineJobRunner 스크립트, CLI 를 통한 잡의 실행

 

JobRegistryBackgroundJobRunner 스케줄러를 통해 잡 실행 시 스프링이 부트스트랩 될 때 실행 가능한 JobRegistry를 생성하는데, JobRegistry를 생성하는 러너

JobLauncherCommandLineRunner 기본적으로 모든 Job타입의 빈을 기동시에 실행(@deprecated since 2.3.0 in favor of {@link JobLauncherApplicationRunner})

 

위 JobRunner는 표준이아니며 실제로 프레임워크 실행 시 구동되는 것은 JobLanucher

SimpleJobLauncher: 스프링에서 제공하는 기본 Launcher

  • 내부적으로 TaskExecutor를 사용하고있음, SyncTaskExecutor 를 사용하면 JobLauncher와 동일한 스레드에서 잡이 실행

출처: https://mio-java.tistory.com/35

하나의 Job 구성이 파라미터가 달라지며 실행되면 JobInstance가 생성되고 JobExecution 이 성공적으로 완료된 상태이면 JobInstance는 완료되었다고 간주한다.

출처: https://jojoldu.tistory.com/326

 

잡 구성하기

@EnableBatchProcessing애플리케이션 내에서 한 번만 적용하면 되며 배치. 수행에 필요한 인프라스트럭처를 제공함

Bom-batch 배치 프로젝트에서 사용하는 공용 configuration 하나에만 해당 애너테이션을 작성

@Configuration
@EnableBatchProcessing // 애플리케이션 내에서 한 번만 적용됨
@ComponentScan(value = {
        "com.javabom.definitiveguide.chap2",
        "com.javabom.definitiveguide.chap13",
        "com.javabom.definitiveguide.chap4"
})
public class DefiniteBatchConfig {
}

 

com.javabom.definitiveguide.chap4.job.HelloWorldConfiguration.java

@Configuration
@RequiredArgsConstructor
public class HelloWorldConfiguration {
    private static final String JOB_NAME = "chap4_job";
    /**
     * 실제로 스프링 배치의 잡과 스텝을 생성하는데 사용되는 JobBuilder, StepBuilder 인스턴스 생성
     */
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;

    @Bean(name = JOB_NAME)
    public Job job() {
        return this.jobBuilderFactory.get(JOB_NAME) // Job 의 이름 지정
            .start(step1())
            .build(); // 최종적으로 SimpleJob 생성
    }

    @Bean(name = JOB_NAME + "-step1")
    public Step step1() {
        return stepBuilderFactory.get(JOB_NAME + "-step1")
            .tasklet(((contribution, chunkContext) -> { // tasklet 을 사용하는 스텝
                System.out.println("Hello, World!");
                return RepeatStatus.FINISHED;
            })).build();
    }
}

 

 

잡 파라미터

동일한 식별 파라미터로 잡을 두 번 이상 실행할 수 없음

 

잡에 파라미터를 전달하는 방법

스프링배치는 잡에 파라미터를 전달하거나 파라미터를 자동 증가시키거나 검증할 수 있도록 함

JobRunner는 JobParameters 객체를 생성해 JobInstace에 전달

java -jar definitive-guide/build/libs/definitive-guide.jar --spring.batch.job.names=chap4_job foo=bar

 

스프링배치에서는 기본적으로 파라미터 타입을 변환하는 기능을 제공한다.

CommandLineJobRunner#start

JobParameters jobParameters = jobParametersConverter.getJobParameters(StringUtils
                    .splitArrayElementsIntoProperties(parameters, "="));

 

DefaultJobParametersConverter

    public static final String DATE_TYPE = "(date)";

    public static final String STRING_TYPE = "(string)";

    public static final String LONG_TYPE = "(long)";

    private static final String DOUBLE_TYPE = "(double)";

 

사용예시

java -jar definitive-guide/build/libs/definitive-guide.jar --spring.batch.job.names=chap4_job foo(string)=bar

 

 

 

파라미터가 식별에 사용되지 않도록 하는 예시

name=BOM 파라미터는 JobInstance 식별에 사용되지않음

java -jar definitive-guide/build/libs/definitive-guide.jar --spring.batch.job.names=chap4_job date=2021-10-16 -name=BOM

첫번째 잡이 실패했을 때 name=BOM 의 값이 변경되어도 date 값이 동일하면 기존 JobInstance 를 기반으로 새 JobExecution 을 생성함

 

잡 파라미터에 접근하기

ChunkContext 사용

private Tasklet helloWorldTasklet() {
        /**
         * contribution: StepContribution
         * 아직 커밋되지 않은 현재 트랜잭션에 대한 정보(읽기수, 읽기수)
         *
         * chunkContext: ChunkContext
         * 실행 시점의 잡 상태
         * 태스크릿 내의 처리중인 청크와 관련된 정보, 청크 정보 중에는 스텝, 잡 정보도 있음
         */
        return (contribution, chunkContext) -> { // tasklet 을 사용하는 스텝
            String name = (String)chunkContext.getStepContext() // chunkContext에서 파라미터 값 얻기
                .getJobParameters()
                .get("name");

            System.out.println("Hello, World! " + name);
            return RepeatStatus.FINISHED;
        };
    }

 

Late binding

JobScope 이나 StepScop 빈에선은 late binding 을 통해 jobParameters 를 직접 참조하지 않고도 파라미터 값을 얻어올 수 있음

  @Bean(name = JOB_NAME + "-step2")
    @JobScope
    public Step step2(@Value("#{jobParameters['name']}") String name) {
        return stepBuilderFactory.get(JOB_NAME + "-step2")
            .tasklet(((contribution, chunkContext) -> {
                System.out.println("Step2 : " + name);
                return RepeatStatus.FINISHED;
            }))
            .build();
    }

빈 생성 시점을 Job 실행시점 또는 Step 실행 시점으로 미루어서 JobParameter를 빈 생성 시에 바인딩 할 수 있도록 한다.

 

 

잡 파라미터 유효성 검증: JobParametersValidator

스프링 배치가 제공하는 유효성 검증 인터페이스

throws JobParametersInvalidException 발생 유무로 파라미터의 유효성 판단

@Component
public class NameFormatValidator implements JobParametersValidator {
    @Override
    public void validate(JobParameters parameters) throws JobParametersInvalidException {
        if (Objects.isNull(parameters)) {
      // void 타입이며 해당 Exception 유무로 유효성 판단
            throw new JobParametersInvalidException("parameter is empty");
        }
        JobParameter name = parameters.getParameters().get("name");
        if (Objects.isNull(name) || name.getType() != JobParameter.ParameterType.STRING) {
            throw new JobParametersInvalidException("type is not null and not string");
        }
    }
}
@Bean(name = JOB_NAME)
    public Job job() {
        return this.jobBuilderFactory.get(JOB_NAME) // Job 의 이름 지정
            .validator(nameFormatValidator)
            .start(step1())
            .next(step2(null))
            .build(); // 최종적으로 SimpleJob 생성
    }
Caused by: org.springframework.batch.core.JobParametersInvalidException: type is not null and not string

 

 

 

스프링 배치가 제공하는 기본 유효성 검증기

DefaultJobParametersValidator

.validator(new DefaultJobParametersValidator(new String[]{"name"}, new String[]{"date"}))
Caused by: org.springframework.batch.core.JobParametersInvalidException: The JobParameters do not contain required keys: [name]

 

required와 optional 둘다 없는 파라미터가 들어가면 Exception 발생

Caused by: org.springframework.batch.core.JobParametersInvalidException: The JobParameters contains keys that are not explicitly optional or required: [datedd]

 

 

CompositeJobParametersValidator

두 개 이상의 유효성 검증기 사용

@Bean
public CompositeJobParametersValidator compositeJobParametersValidator() {
   CompositeJobParametersValidator compositeJobParametersValidator = new CompositeJobParametersValidator();

   compositeJobParametersValidator.setValidators(Arrays.asList(defaultJobParametersValidator(), nameFormatValidator));

   compositeJobParametersValidator.afterPropertiesSet();

   return compositeJobParametersValidator;
}

 

 

 

잡 파라미터 증가시키기: JobParameterIncrementer

 

기본적으로 제공하는 RunIdIncrementer

return new JobParametersBuilder(params).addLong(this.key, id).toJobParameters();

 

 

 

잡 리스너 적용하기

 

잡 실행과 관련있는 리스너: JobExecutionListener

 

어노테이션을 통해 해당 리스너 인터페이스를 직접 구현하지 않을 수도 잆음

public class JobLoggerListener {

    /**
     * Expected signature: void beforeJob({@link JobExecution} jobExecution)
     * @param jobExecution
     */
    @BeforeJob
    public void beforeJob(JobExecution jobExecution) {
        System.out.println(jobExecution.getJobInstance().getJobName() + "is started");
    }

    /**
     * Expected signature: void afterJob({@link JobExecution} jobExecution)
     */
    @AfterJob
    public void afterJob(JobExecution jobExecution) {
        System.out.println(jobExecution.getJobInstance().getJobName() + " has completed with the status "
            + jobExecution.getStatus());
    }
}

 

 

 

ExecutionContext

JobExecution ----------------> ExeutionContext (잡 전체용 글로벌 데이터)

|

|

|1..*

StepExeciton ------------------> ExecutionContext (개별 스텝용 데이터)

 

 

job context 조작하기 HelloWorldJobContext.java

        /**
         * job 글로벌 Context
         */
        ExecutionContext jobContext = chunkContext.getStepContext()
            .getStepExecution()
            .getJobExecution()
            .getExecutionContext();

        if (Objects.isNull(jobContext.get("user.name"))) {
            log.info("jobContext user.name is empty, start put user.name: " + name);
            jobContext.put("user.name", name); // job 글로벌 Context에 저장
        } else {
            log.info("jobContext user.name is not empty");
        }

 

 

step 개별 Context 의 값을 job context로 승격하기

    /**
     * step 이 성공적으로 마치면 job context에 step context 내용을 저장함
     * @return
     */
    public StepExecutionListener promotionListner() {
        ExecutionContextPromotionListener listener = new ExecutionContextPromotionListener();

        listener.setKeys(new String[] {"step.name"});

        return listener;
    }

 

 

database 테이블에 저장된 내용

BATCH_STEP_EXECUTION_CONTEXT

{"@class":"java.util.HashMap","batch.taskletType":"com.javabom.definitiveguide.chap4.job.HelloWorldStepContext","step.name":"bom","batch.stepType":"org.springframework.batch.core.step.tasklet.TaskletStep"}

BATCH_JOB_EXECUTION_CONTEXT

{"@class":"java.util.HashMap","user.name":"bom"}