오늘 하루에 집중하자
  • [Spring Batch] 3. Job 관련 도메인(Job, JobInstance, JobParameter, JobExecution)
    2024년 06월 11일 01시 13분 50초에 업로드 된 글입니다.
    작성자: nickhealthy

    들어가기 전에!


    해당 게시글은 인프런 정수원님의 Spring Batch 강의를 듣고 추후 복기하고자 공부한 내용을 가볍게 정리한 것입니다.

    문제가 될 시 삭제하겠습니다.

     

    또한 현재 스프링 배치의 최신버전과 상이한 내용이 있습니다.

    해당 내용 참고해서 읽어주시면 감사하겠습니다!

     

    Job이란?


    기본 개념

    배치 계층 구조에서 가장 상위에 있는 개념으로서 하나의 배치 작업 자체를 의미한다.

    예를 들어, "API 서버의 접속 로그 데이터를 통계 서버로 옮기는 배치"가 Job이다.

    • Job 구성(Configuration)을 통해 생성되는 객체 단위로서 배치 작업을 어떻게 구성하고 실행할 것인지 전체적으로 설정하고 명세해 놓은 객체
    • 배치 Job을 구성하기 위한 최상위 인터페이스이며 스프링 배치가 기본 구현체를 제공
    • 여러 Step을 포함하고 있는 컨테이너로서 반드시 한 개 이상의 Step으로 구성해야 함

     

    기본 구현체

    스프링 배치가 기본 구현체 2개를 제공

     

    SimpleJob

    • 순차적으로 Step을 실행시키는 Job
    • 모든 Job에서 유용하게 사용할 수 있는 표준 기능을 갖고 있음

    FlowJob

    • 특정한 조건과 흐름에 따라 Step을 구성하여 실행시키는 Job
    • Flow 객체를 실행시켜서 작업을 진행함

     

    Job과 Step의 구성

    Job과 Step의 실행 흐름도와 구성이 어떻게 되어있는지 알아보자

    먼저 전반적인 그림은 아래와 같다.

     

    Job과 Step의 구성 및 실행 흐름

     

    • JobParameters: Job을 실행시킬 때마다 다른 인자를 줘서 각각의 Job 실행이 다른 배치 작업임을 명시한다.
      • 동일한 배치 작업 내용의 명세를 가진 Job을 실행시키더라도 JobParameters의 인자가 다르면 다른 내용(데이터)의 배치 작업을 수행한다고 스프링 배치는 인식한다. 생각해보면 동일한 내용의 배치 작업을 여러 번 수행시키는 것은 의미가 없기 때문에 이렇게 설계해 놓은 것 같다. 따라서 스프링 배치는 이전에 실행했던 동일한 내용의 JobParameters 인자가 들어오면 배치 작업을 수행하지 않는다.
      • JobParameters는 추후 더 자세히 설명
    • JobLauncher: `run()` 메서드를 통해 Job을 실행시키는 객체, 인자로는 Job, Parameters 객체가 들어감
    • Job: 스프링 배치에서 제공하는 Job 인터페이스를 구현한 구현체를 통해 `execute()` 메서드로 Job을 실행시킴
      • Step은 사진과 같이 Job 안에 여러 개로 구성될 수 있다. 따라서 Job은 일종의 컨테이너 역할을 하면서 정의한 Step 순서대로 배치를 실행한다.

     

    Job 도메인(클래스)의 필드 알아보기

    우선 Job 인터페이스에 `execute()` 메서드를 통해 Job을 실행시킨다.

    그리고 Job의 인터페이스를 구현한 `AbstractJob` 추상클래스가 있다. 각각의 필드는 나중에 알아볼 것

    SimpleJob은 Steps를 담는 필드가, FlowJob은 Flow 객체를 담는 필드가 있다.

    Job 도메인 구조

     

    JobInstance


    들어가기 전에!

    Job, Step과 같이 실제로 배치 작업을 수행하기 위한 도메인들이 있는 반면에, JobInstance, JobExecution, StepExecution 등은 작업이 수행되는 단계마다 메타 데이터(Job 또는 Step의 실행/상태 정보 등)를 DB에 저장하기 위한 용도로 생성되어지고 역할을 수행하는 도메인들이다.

     

    도메인 개념을 학습하면서 "Job과 JobInstance은 뭐가 다른 것이지?"라고 생각할 수 있는데, 이런 모호한 부분들을 잘 짚고 넘어가게 된다면 스프링 배치에 대해서 잘 이해할 수 있다.

     

    기본 개념

    • Job이 실행될 때 생성되는 Job의 논리적 실행 단위 객체로서 고유하게 식별 가능한 작업 실행을 나타낸다.
    • Job의 설정과 구성은 동일하지만, Job이 실행되는 시점에 처리하는 내용은 다르기 때문에 Job의 실행을 구분해야 함
      • 예를 들어 하루에 한 번씩 배치 Job이 실행된다면 매일 실행되는 각각의 Job을 JobInstance로 표현한다.
    • JobInstance의 생성 및 실행
      • 처음 실행하는 Job + JobParameter 일 경우 새로운 JobInstance를 생성한다.
      • 이전과 동일한 Job + JobParameter으로 실행할 경우 이미 존재하는 JobInstance를 리턴한다.
        • 내부적으로 jobName + jobKey(JobParameter의 해시값)를 가지고 JobInstance 객체를 얻어 동일한 객체인지 판별한다. 이때 동일한 JobInstance 객체를 리턴하면 이전에 작업했었던 똑같은 배치 작업을 처리한다고 판단하기에 배치 작업을 처리하지 않고 실패하게 된다.
    • Job과는 1:M의 관계이다.

     

    BATCH_JOB_INSTANCE  테이블과 매핑

    JOB_NAME(Job)과 JOB_KEY(JobParameter 해시값)이 동일한 데이터는 중복해서 저장할 수 없다.

     

     

    JobInstance의 흐름도

    JobInstance의 흐름도를 설명하기 위해서는 아직 배우지 않은 다른 도메인 클래스도 알 필요가 있다.

    일단 간단하게 개념만 알고가자

    1. JobLauncher는 Job을 실질적으로 수행시키는 도메인 객체이다. 배치 작업이 시작할 때 JobLauncher가 `run()` 메서드를 실행하게 된다.
    2. JobLauncher가 수행하기 위해선 2가지의 인자가 필요한데, 그것이 바로 위에서도 언급한 JobName, JobParamerter이다.
    3. JobRepository 도메인 객체는 배치 작업을 수행할 때 발생하는 메타 데이터들을 관리하고, DB에 업데이트 하는 객체다.
    4. JobLauncher가 수행하게 되면 JobRepository를 호출해 JobName, JobParameter 파라미터를 가지고 JobInstance 객체가 있는지 조회하게 된다. 
    5. JobName, JobParameter를 KEY로 데이터베이스에 조회해 JobInstance가 있다면 기존 JobInstance를 리턴하고 배치 작업이 수행되지 않으며, 조회되지 않는다면 처음 수행되는 배치 작업으로 인식해 배치 작업을 수행하게 된다.

     

    JobInstance의 흐름도1

     

     

     

    다음 그림을 통해 조금 더 자세히 알아보자

    1. 일별 정산을 처리하는 배치 Job이 존재한다.
    2. JobInstanceA는 Job은 '일별 정산', JobParameters는 '2021.01.01'이다. 처음 수행되는 Job이기에 JobInstanceA가 새로 생성된다.
    3. JobInstanceB는 Job은 '일별 정산', JobParameters는 '2021.01.02'이다. Job 자체는 '일별 정산'으로 똑같은 Job이지만, JobParameters가 다르기에 DB에서 JobInstance가 조회되지 않는다. 따라서 스프링 배치는 처음 수행되는 Job이라고 판단하고(Job은 같지만, 인자가 달라서 다른 데이터를 배치 작업할 것이라고 판단하는 것), JobInstanceB가 새로 생성된다.
    4. 메타 데이터 테이블에도 각각 JobInstance가 생긴 것을 확인할 수 있다.

    JobInstance의 흐름도2

     

    예제

    [JobInstanceConfiguration] - Job을 정의한 클래스

    package io.spring.batch.helloworld.ch3.jobInstance;
    
    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.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 JobInstanceConfiguration {
    
        private final JobBuilderFactory jobBuilderFactory;
        private final StepBuilderFactory stepBuilderFactory;
    
        @Bean
        public Job job() {
            return jobBuilderFactory.get("JobInstanceConfiguration")
                    .start(step1())
                    .next(step2())
                    .build();
        }
    
        @Bean
        public Step step1() {
            return stepBuilderFactory.get("step1")
                    .tasklet(new Tasklet() {
                        @Override
                        public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) throws Exception {
                            System.out.println("step1 executed!");
                            return RepeatStatus.FINISHED;
                        }
                    }).build();
        }
    
        @Bean
        public Step step2() {
            return stepBuilderFactory.get("step2")
                    .tasklet(new Tasklet() {
                        @Override
                        public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) throws Exception {
                            System.out.println("step2 executed!");
                            return RepeatStatus.FINISHED;
                        }
                    }).build();
        }
    
    }

     

    [JobInstanceRunnerTest]

    테스트를 위해 스프링 배치에 의해 배치 작업이 자동으로 실행되지 않고, jobLauncher를 통해 수동적으로 수행하기 위한 클래스

    package io.spring.batch.helloworld.ch3.jobInstance;
    
    import org.springframework.batch.core.Job;
    import org.springframework.batch.core.JobParameters;
    import org.springframework.batch.core.JobParametersBuilder;
    import org.springframework.batch.core.launch.JobLauncher;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.ApplicationArguments;
    import org.springframework.boot.ApplicationRunner;
    import org.springframework.stereotype.Component;
    
    /**
     * 테스트를 위해 스프링 배치에 의해 배치 작업이 자동으로 실행되지 않고, jobLauncher를 통해 수동적으로 수행하기 위한 클래스
     * - ApplicationRunner: 스프링 부트가 제공하며, 스프링 부트가 초기화가 완료되면 가장 먼저 호출되는 인터페이스이다.
     */
    @Component
    public class JobInstanceRunnerTest implements ApplicationRunner {
    
        /* @EnableBatchProcessing 어노테이션에 의해 스프링 배치가 초기화 될 때 JobLauncher를 자동으로 빈으로 등록해준다. */
        @Autowired
        private JobLauncher jobLauncher;
    
        /* JobInstanceConfiguration 클래스에 등록된 Job을 빈으로 의존성 주입을 통해 Job을 주입 받을 수 있다. */
        @Autowired
        private Job job;
    
        @Override
        public void run(ApplicationArguments args) throws Exception {
            JobParameters jobParameters = new JobParametersBuilder()
                    .addString("name", "user1")
                    .toJobParameters();
    
            jobLauncher.run(job, jobParameters);
        }
    }

     

     

    [applicaiton.yml]

    스프링 배치가 자동으로 수행되는 것을 막기 위해 `spring.batch.job.enabled: false`로 설정해야 자동으로 수행되지 않고, 방금 위에서 우리가 생성한 jobLauncher로 수행할 수 있다.

    spring:
      config:
        activate:
          on-profile: mysql
      datasource:
        hikari:
          jdbc-url: jdbc:mysql://localhost:3306/spring_batch
          username: <username>
          password: <password>
          driver-class-name: com.mysql.cj.jdbc.Driver
      batch:
        jdbc:
          initialize-schema: always
        job:
          enabled: false

     

    이제 실행을 해서 결과를 확인해보자

    정상적으로 수행되었고, 아래는 JobInstance 메타 데이터 테이블인데 하나의 레코드가 생성된 것을 확인할 수 있다.

    JobInstance 테이블

     

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

    우리가 인자로 준 `name`, `user1` 키와 값이 들어있다. 그리고 FK로 JOB_EXECUTION_ID가 설정되어 있는 것을 볼 수 있는데, JOB_EXECUTION 테이블과 1:M의 관계라는 것을 알 수 있다. 참고로 JOB_EXECUTION 테이블은 Job이 실행될 때마다 하나의 레코드가 생성된다.

    JobParameter 테이블

     

     

    이번에는 한번 더 같은 Job과 JobParameter를 가지고 우리가 앞전에 배웠던 내용을 그대로 수행해보자

     

    그대로 수행했을 때 앞에서 배운대로 에러가 발생하게 되고, 에러의 내용은 JobInstance가 이미 존재한다는 내용이다.

    스프링 배치는 이처럼 같은 배치 Job에 같은 인자를 주게되면 동일한(이미 작업했던) 작업을 수행한다고 보고 배치 처리를 수행하지 않게 된다.

    재실행 했을 때 에러

     

    그럼 이번에는 같은 Job에 인자를 수정해서 배치 작업을 돌려보자

    (`user1`를 `user2`로 변경)

     

    JobInstance 메타 데이터 테이블인데 2개가 생성된 것을 확인할 수 있다. JobParameter도 확인해보자

    JobInstance 테이블

     

    JobParameter도 마찬가지로 다른 인자로 데이터가 저장된 것을 확인할 수 있다.

    JobParameter 테이블

     

    이렇게 해서 JobInstance는 스프링 배치 작업을 고유하게 식별할 수 있는 객체라는 것을 알 수 있었고, 동일한 인자로는 반복 수행이 불가능한 것을 알 수 있었다.

     

    JobParameter


    기본 개념

    • Job을 실행할 때 함께 포함되어 사용되는 파라미터를 가진 도메인 객체
    • 하나의 Job에 존재할 수 있는 여러 개의 JobInstance를 구분하기 위한 용도
      • 바로 앞전의 실습에서 확인해 보았다.
    • JobParameters와 JobInstance는 1:1 관계

     

    생성 및 바인딩

    아래와 같이 JobParameter를 생성하는 방법은 3가지가 있다.

    • 애플리케이션 실행 시 주입
      • `Java -jar LogBatch.jar requestDate=20210101`
      • 위와 같이 인자로 변수를 설정해서 애플리케이션을 실행시키면 변수의 값이 JobParameter의 값에 저장이 되도록 스프링 배치 내부적으로 구현되어 있다.
    • 코드로 생성
      • JobParameterBuilder, DefaultJobParametersConverter
    • SpEL(Spring Expression Language)이용
      • `@Value("#{jobParameter[requestDate]}")`, `@JobScope`, @StepScope` 선언 필수

     

    BATCH_JOB_EXECUTION_PARAM 테이블과 매핑

    JOB_EXECUTION과는 1:M의 관계이다.(JobExecution이 부모 역할)

     

     

    JobParameters 도메인 객체 구조

    1. JobParameter(메타 데이터 저장을 위한 객체)의 Wrapper 객체이다. 내부적으로 LinkedHashMap으로 구현되어 있으며, KEY 값은 String, VALUE 값은 JobParameter 객체로 설정되어 있다.
    2. 실제 메타 데이터를 저장하는 JobParameter 객체이다. ParameterType에는 ENUM 클래스로 4가지로 정해져있다.
    3. 파라미터 타입은 String, Date, Long, Doublue로 구분된다.
    4. 각각 Parameter 객체에 파라미터를 저장했을 때 다음과 같이 저장된다.
    5. 메타 데이터 테이블에는 각 파라미터 타입마다 컬럼들이 정의되어 있다.

    JobParameters 도메인 객체 구조

     

     

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

    이제 실습을 통해 애플리케이션 실행으로 파라미터를 주입하는 방식, 코드로 생성하는 방식을 알아보고, 실제로 JobParameter가 어떻게 동작하는지 자세히 알아보자

    먼저 코드로 JobParameter를 생성하는 방식부터 알아보자. 기본 코드는 아래와 같다.

     

    [JobParameterConfiguration] - Job, Step 구성 기본 코드

    package io.spring.batch.helloworld.ch3.jobParameter;
    
    import java.util.Date;
    
    import org.springframework.batch.core.Job;
    import org.springframework.batch.core.JobParameters;
    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.scope.context.ChunkContext;
    import org.springframework.batch.core.step.tasklet.Tasklet;
    import org.springframework.batch.repeat.RepeatStatus;
    import org.springframework.context.annotation.Bean;
    
    import lombok.RequiredArgsConstructor;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    @RequiredArgsConstructor
    public class JobParameterConfiguration {
    
        private final JobBuilderFactory jobBuilderFactory;
        private final StepBuilderFactory stepBuilderFactory;
    
        @Bean
        public Job job() {
            return jobBuilderFactory.get("JobParameterConfiguration")
                    .start(step1())
                    .next(step2())
                    .build();
        }
    
        public Step step1() {
            return stepBuilderFactory.get("step1")
                    .tasklet(new Tasklet() {
                        @Override
                        public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext)
                                throws Exception {
                            System.out.println("JobParameterConfiguration step1 executed!!");
                            return RepeatStatus.FINISHED;
                        }
                    }).build();
    
        }
    
        public Step step2() {
            return stepBuilderFactory.get("step2")
                    .tasklet(new Tasklet() {
                        @Override
                        public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext)
                                throws Exception {
                            System.out.println("JobParameterConfiguration step2 executed!!");
                            return RepeatStatus.FINISHED;
                        }
                    }).build();
    
        }
    
    }

     

     

    [JobParameterRunnerTest] - 테스트를 위한 클래스

    애플리케이션 실행 시 파라미터 주입, 코드로 직접 파라미터 주입 테스트를 위해 수동 모드로 실행하게 된다.

    package io.spring.batch.helloworld.ch3.jobParameter;
    
    import org.springframework.batch.core.Job;
    import org.springframework.batch.core.JobParameters;
    import org.springframework.batch.core.JobParametersBuilder;
    import org.springframework.batch.core.launch.JobLauncher;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.ApplicationArguments;
    import org.springframework.boot.ApplicationRunner;
    import org.springframework.stereotype.Component;
    
    import java.util.Date;
    
    /**
     * 테스트를 위한 클래스, 자동으로 실행하면 아래와 같은 테스트를 실행하는데 어려움이 있어 수동으로 실행하게 된다.
     * [테스트 내용]
     * - 애플리케이션 실행 시 파라미터를 주입하는 방법
     * - 코드로 직접 JobParameter를 주입하는 방법
     */
    @Component
    public class JobParameterRunnerTest implements ApplicationRunner {
    
        @Autowired
        private JobLauncher jobLauncher;
    
        @Autowired
        private Job job;
    
        @Override
        public void run(ApplicationArguments args) throws Exception {
            JobParameters jobParameters = new JobParametersBuilder()
                    .addString("name", "user1")
                    .addLong("seq", 1L)
                    .addDate("date", new Date())
                    .addDouble("age", 16.5)
                    .toJobParameters();
    
            jobLauncher.run(job, jobParameters);
    
        }
    }

     

     

    [application.yml] - 자동 실행을 막기 위해 다음과 같은 설정이 필요하다.

    `spring.batch.job.enable=false` 설정하기

    spring:
      config:
        activate:
          on-profile: mysql
      datasource:
        hikari:
          jdbc-url: jdbc:mysql://localhost:3306/springbatch?useUnicode=true&characterEncoding=utf8
          username: [username]
          password: [password]
          driver-class-name: com.mysql.cj.jdbc.Driver
      batch:
        job:
          enable: false
        jdbc:
          initialize-schema: always

     

    이제 스프링 배치를 실행하여 메타 데이터 결과를 확인해보자

    먼저 JOB_EXECUTION_PARAMS 테이블이다.

    앞전에 설명한대로 JOB_EXECUTION 테이블과 1:M의 관계를 보여준다. 즉, Job이 한번 실행되면 JOB_EXECUTION 테이블은 한번 생성되며, JOB_EXECUTION_PARAMS는 여러 개의 데이터가 생성될 수 있다.

     

    다음으로 이렇게 저장한 파라미터의 값을 어디서 어떻게 참조할 수 있는지 확인해보자

    그러기 위해선 앞서 작성한 코드를 조금 더 추가해야 확인할 수 있다. 인자가 같아서 재실행이 불가능하므로 테이블을 비운 후 재실행하는 것을 권장한다.

     

    [JobParameterConfiguration] - 이전 코드 `step1` 메서드에 다음과 같은 코드를 추가했다.

    @Bean
        public Step step1() {
            return stepBuilderFactory.get("step1")
                    .tasklet(new Tasklet() {
                        @Override
                        public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext)
                                throws Exception {
                            System.out.println("JobParameterConfiguration step1 executed!!");
                            JobParameters jobParameters1 = stepContribution.getStepExecution().getJobExecution()
                                    .getJobParameters();
                            System.out.println("jobParameters1 = " + jobParameters1);
    
                            jobParameters1.getString("name");
                            jobParameters1.getLong("seq");
                            jobParameters1.getDate("date");
                            jobParameters1.getDouble("age");
    
                            Map<String, Object> jobParameters2 = chunkContext.getStepContext().getJobParameters();
                            System.out.println("jobParameters2 = " + jobParameters2);
    
                            return RepeatStatus.FINISHED;
                        }
                    }).build();
    
        }

     

    이제 다시 실행해보면, 다음과 같이 우리가 정의한 JobParameterRunnerTest에서 스프링 배치를 시작하는 것을 확인할 수 있다.

     

    파라미터를 참조하는 방법은 다음과 같이 `StepContribution`, `ChunkContext` 두 가지 방법이 있다.

    • StepContribution
      • 이 방법은 우리가 jobLauncher를 실행하기 위해 JobParameter를 직접 만들어 파라미터를 넣었던 타입을 직접 반환하는 방식이다.(이 방법이 좋음)
    • ChunkContext
      • 이 방식은 코드를 보면 알 수 있지만 `StepContext`에서 바로 `JobParameter`를 반환하는 방식이다.
      • 반환되는 방식 타입 또한 Map으로 반환이 된다.
      • 디버깅으로 확인해봤을 때 변경이 불가능한 Map 타입으로 반환되는 것을 확인할 수 있다.

    결론적으로 두 가지 방법 모두 '읽기' 작업에서 활용 가능하다.

     

    이제 애플리케이션 실행으로 파라미터를 주입하는 방식을 알아보자

    우선 동적으로 우리가 넣은 파라미터가 정상적으로 나오는지 확인하기 위해선 다시 스프링 배치가 자동으로 실행하는 방식으로 바꿔야 한다. 이전 코드 그대로 실행하면 우리가 파라미터를 직접 만들었던 내용만 출력되기 때문이다.

    따라서 JobParameterRunnerTest 클래스의 `@Component` 어노테이션을 주석처리 해주고, application.yml에서 `spring.batch.job.enable`를 주석처리 해준다.

     

    이제 애플리케이션 실행을 위해서 Jar 파일로 프로젝트를 추출해보자

    인텔리제이를 기준으로 방법은 간단한데, MAVEN - CLEAN, PACKAGE 순으로 실행시키면 target 폴더 안에 `<projectName>.jar` 파일이 생성된다.

     

    아래 사진처럼 target 폴더에 jar 파일이 생겼고, 커맨드라인에 아래와 같은 명령어로 애플리케이션을 실행시킬 수 있다.

    `java -jar hello-world-0.0.1-SNAPSHOT.jar "name=user1" "seq(long)=2L" "date(date)=2024/01/01" "age(double)=16.5"`

     

    애플리케이션 실행 결과 다음과 같이 콘솔이 찍힌 것도 확인할 수 있다.

    물론 데이터베이스도 application.yml에 설정된 환경 설정이 적절하다면 데이터가 업데이트 된다.

     

    jar로 실행하는 것 말고도, IDE에서 다음과 같이 실행할 수도 있다. 위와 같은 맥락이라고 생각하면 된다. 끝!

     

     

    JobExecution


    기본 개념

    JobInstance에 대한 한 번의 시도를 의미하는 객체로서 Job 실행 중에 발생한 정보들을 저장하고 있는 객체이다.

    즉, JobExecution은 Job을 실행할 때마다 생성되는 객체로 봐도 무방하다.(JobInstance도 실행하면 한번 생기기 때문에)

    JobExecution의 속성 값으로는 시작 시간, 종료 시간, 상태(시작, 완료, 실패), 종료 상태의 속성을 가진다.

     

    위에서 Job을 실행할 때마다 생성되는 객체라고 설명했는데 Job이 실행 가능한 기준은 과연 무엇일까?

    바로 JobInstance와 FK로 매핑된 JobExecution의 상태로 한번 또는 여러 번 실행 가능하다.

    아래와 같이 정리할 수 있다.

    • JobExecution은 'FAILED' 또는 'COMPLETED' 등의 Job의 실행 결과 상태를 가지고 있다.
    • JobExecution의 실행 결과가 'COMPLETED'면 JobInstance 실행이 완료된 것으로 간주해서 재실행이 불가능하다.
    • JobExecution의 실행 결과가 'FAILED'면 JobInstance 실행이 완료되지 않은 것으로 간주해서 재실행이 가능하다.
      • 이땐 JobParameter가 동일한 값으로 Job을 실행할지라도 JobInstance를 계속 실행할 수 있다.
    • JobExecution의 실행 결과가 'COMPLETED' 될 때까지 하나의 JobInstance 내에서 여러 번의 시도가 생길 수 있다.

     

    이전에 JobInstance를 설명하면서 썼던 그림인데 JobExecution까지 확장해서 더 자세히 알아보자.

    1. 새로운 JobInstance가 생성되면 새로운 JobExecution도 함께 생성된다.
    2. 만약 기존에 생성된 JobInstance가 있다면 다음과 같은 두 가지 케이스로 나뉘게 된다.
    3. JobExecution의 BatchStatus 상태가 COMPLETE 인 경우, 재시도를 하면 이미 완료된 JobInstance에 대해 스프링 배치는 예외를 던진다.
    4. JobExecution의 BatchStatus 상태가 FAILED 인 경우, 새로운 JobExecution을 생성하고 재실행이 가능하다.

    즉, 정리하면 같은 JobInstance에 대해 성공/실패 유무에 따라 재실행이 가능하고, 재실행을 했다면 JobExecution은 항상 새롭게 생성된다.

     

    BATCH_JOB_EXECUTION 테이블과 매핑

    JobInstance와 JobExecution은 1:M의 관계로서 JobInstance에 대한 성공/실패의 내역을 가지고 있다.

    JobInstance가 부모격 역할을 한다.

     

     

    JobExecution 도메인 객체 속성 정보는 다음과 같다.

    JobExecution 도메인 객체 속성

     

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

    테스트를 수행하기 위한 기본 코드는 아래와 같다.

    [JobExecutionConfiguration] - Job과 Step의 기본 코드 

    package io.spring.batch.helloworld.ch3.jobExecution;
    
    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.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 JobExecutionConfiguration {
    
        private final JobBuilderFactory jobBuilderFactory;
        private final StepBuilderFactory stepBuilderFactory;
    
        @Bean
        public Job job() {
            return jobBuilderFactory.get("JobParameterConfiguration")
                    .start(step1())
                    .next(step2())
                    .build();
        }
    
        @Bean
        public Step step1() {
            return stepBuilderFactory.get("step1")
                    .tasklet((stepContribution, chunkContext) -> {
                        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 stepContribution, ChunkContext chunkContext)
                                throws Exception {
                            System.out.println("STEP2 HAS EXECUTED!!");
                            return RepeatStatus.FINISHED;
                        }
                    }).build();
    
        }
    
    }

     

    또한 JobParameter를 아래와 같이 설정한다.

     

    이 상태에서 그대로 돌려보고 메타 데이터를 확인해보자

    성공적으로 실행되었고, 다음은 JobInstance의 상태이다.

     

    JobExecution의 메타 테이블 상태이다.

    우리는 이미 앞전에 배운 내용을 통해 JobParameter가 같은 상태에서 재실행하면 어떤 일이 발생하는지 알고 있다.

    같은 데이터에 같은 배치 작업임을 스프링 배치가 인식하고 실패하게 된다. 하지만 실습을 위해 한번 더 실행해본다. 

     

     

    한번 더 실행하고 난 뒤 모습이다. 같은 jobParameter를 수행하여 실패하게 되었다.

     

    이번에는 강제로 에러를 발생하게 하여 같은 배치 작업에 같은 JobParameter를 재실행이 가능하도록 테스트 해보자

    그러기 위해선 우선 기존에 실행되었던 DB 데이터를 지우고, 다음과 같은 코드도 추가해야 한다.

     

    이제 실행하고 나서 메타 데이터를 확인해보자

    당연히 실패하게 되었고, JobExecution의 메타 테이블은 다음과 같은 상태가 된다.

    JobExecution 테이블

     

    이 상태에서 동일한 조건으로 한번 더 실행해본다.

    원래는 동일한 Job과 JobParameter로 실행이 안되어야 하는데 FAILED 상태이므로 재실행이 가능하게 됐다.

    JobExecution 테이블

     

    이번에는 성공으로 조건을 바꿔서 테스트를 마무리 해보자.

    (예외처리 부분 주석처리)

     

    데이터 레코드가 하나 더 생기고 COMPLETED 상태가 되었다. 그리고 유심히 봐야 할 부분은 같은 JobInstance ID에 대해 JobExecution이 여러 개 생성된 점이다.

    JobExecution

     

    JobInstance 테이블은 여전히 한 개의 데이터만 유지하고 있다. 실습 끝!

    JobInstance

     

     

    이제 마지막으로 방금 테스트 한 내용을 정리하는 시간을 가져보자

    JobInstanceA는 JobExecution이 COMPLETED 상태로 한번만 실행된 것을 확인할 수 있다.

    JobInstanceB는 JobParameter를 다르게 하여 새로 생성된 JobInstance이며, JobExecution의 상태가 COMPLETED로 한번에 수행되지 않았기 때문에 JobExecution이 두 개가 생겼고 배치 작업도 두번 이루어진 것을 알 수 있다.

    이제 이러한 개념을 가지고 메타 테이블을 확인하여 어떻게 배치 작업이 수행되었는지를 확인할 수 있다.

    댓글