오늘 하루에 집중하자
  • [Spring Batch] 5. 배치 초기화 설정
    2024년 06월 20일 22시 19분 52초에 업로드 된 글입니다.
    작성자: nickhealthy

    스프링 배치 초기화 설정


    스프링 배치를 시작할 때 스프링이 자동으로 초기화 하는 영역이 있다.

    어떤 작업을 하는지 간단히 알아보고, 디버깅을 통해 직접 어떻게 동작하는지 알아보자

    우선 아래의 개념들을 알고 있어야 한다.

     

    1. JobLauncherApplicationRunner

    • Spring Batch 작업을 시작하는 ApplicationRunner로서 BatchAutoConfiguration에서 생성됨
    • 스프링 부트에서 제공하는 ApplicationRunner의 구현체로 어플리케이션이 정상적으로 구동되자 마자 실행됨
      • ApplicationRunner는 스프링 부트가 환경 구성을 모두 마친 뒤에 곧바로 실행하는 클래스이다.
    • 기본적으로 빈으로 등록된 모든 job을 실행시킨다.

    JobLauncherApplication - jobLauncher 실행

     

    2. Batch Properties

    • Spring Batch의 환경 설정 클래스
      • Job 이름, 스키마 초기화 설정, 테이블 Prefix 등의 값을 설정할 수 있다.
    • application.properties or application.yml 파일에 설정함
    • 예시
    batch:
      job:
        names: ${job.name:NONE}
      initialize-schema: NEVER
      tablePrefix: SYSTEM

     

    다음은 BatchProperties 클래스이다.

    `@ConfigurationProperties(prefix = "spring.batch")` 어노테이션은 application.properties, application.yml에 설정하는 `spring.batch...`를 의미한다.

    위 예시에서 보이듯 `spring.batch.job.names:` 부분에서 `spring.batch`는 아래 사진처럼 prefix를 의미하고, 그 이후에 나오는 설정은 어떤 클래스에 속성을 어떻게 설정할 것인지에 대한 내용이다.

     

     

    3. Job 실행 옵션

    • 지정한 Batch Job만 실행하도록 할 수 있음
      • `spring.batch.job.names: ${job.name:NONE}`
      • 직접 실행할 Job을 명시해줄 수도 있지만, 확장성이 있도록 외부 파라미터를 받아서 실행하도록 설정할 수 있다.
    • 어플리케이션 실행시 Program arguments로 job 이름을 입력한다.
      • `--job.name=helloJob`
      • `--job.name=helloJob, SimpleJob` (하나 이상의 job을 실행할 경우 쉼표로 구분해서 입력함)

     

    디버깅을 통해 자세히 알아보기


    CASE 1.

    우선 시작하기 전에 앞서 간단한 설정을 하고 시작하자

    이 설정은 나중에 배치가 어떻게 수행되는지 테스트 하기 위한 용도로 사용된다.

    Edit Configurations - Program Arguments 탭에 `name=user1`을 입력하자

    Edit Configurations - Program Arguments

     

    1. 스프링 배치를 실행하기 위한 기본 코드 작성

    package io.spring.batch.helloworld.ch4.jobConfiguration;
    
    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 JobConfiguration {
    
        private final JobBuilderFactory jobBuilderFactory;
        private final StepBuilderFactory stepBuilderFactory;
    
        @Bean
        public Job batchJob1() {
            return jobBuilderFactory.get("JobConfiguration")
                    .incrementer(new RunIdIncrementer())
                    .start(step1())
                    .next(step2())
                    .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!123");
                            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();
        }
    }

     

     

    2. BatchAutoConfiguration 클래스에서 JobLauncherApplicationRunner 클래스를 생성한다.

    • 또한 바로 다음 사진을 확인해보면 jobNames 값은 공백인 것을 확인할 수 있는데 이것은 우리가 application.yml 설정 파일에서 어떤 job을 실행할 것인지 아무것도 지정을 하지 않았기 때문이다.

    1. BatchAutoConfiguration

     

    2. jobNames 값이 공백임

     

    현재 application.yml 파일의 설정 내용을 살펴보자.

    현재 보이는 화면에서는 스프링 배치가 어떤 job을(배치 작업) 진행 해야할 지 모르는 상태이다.

    단, 스프링은 컴포넌트 스캔을 통해 `@Configuration, @Bean`으로 등록한 모든 빈들을 등록하기 때문에 등록된 빈(Job)을 스프링이 모두 찾아낸다.

    그리고 앞서 언급한대로 스프링 배치가 등록된 모든 Job을 기본적으로 실행시키게 된다.

    3. application.yml

     

    3. JobLauncherApplicationRunner를 생성한 뒤, job(배치 작업)을 수행하기 전에 값을 세팅한다.

    • job을 정의할 때 만든 job 이름을 세팅하고, step도 2개가 설정되어 있는 것을 확인할 수 있다.
    • jobName을 지정하지 않았지만, `@Configuration, @Bean`과 같은 어노테이션을 통해 등록된 job을 인식하는 것 같다.(확실하지 않음)

     

    3.1. 이전에 설정했던 Program Arguments 탭의 `name=user1`가 args의 파라미터로 들어온 것을 확인할 수 있다.

     

    3.2. converter 작업과 동시에 JobParameters에 파라미터 값을 세팅하게 된다.

    3.3. job의 값은 현재 우리가 설정한 job의 정의서대로 내용이 들어가 있는 것을 확인할 수 있다.

     

    3.5. 첫 번째 박스는 등록된 jobNames가 있다면 스프링 배치가 job 이름이 있는지 검증 후 수행할 job들을 수집하게 된다. 또한 split 함수에서 `,`로 사용한다는 것을 잘 기억하자

    3.6. 지금 이 예제에서는 첫 번째로 표시한 박스의 조건식에 걸리지 않고, 바로 두 번째로 넘어오게 되는데 그 이유는 jobNames는 앞전에 말한 것처럼 application.yml 파일에 정의되어 있지 않기 때문에 조건식이 성립하지 않기 때문이다.

    그리고 `execute()`를 통해 job에 정의된대로 배치 작업을 수행하게 된다.

     

    이렇게 수행할 job을 지정하지 않고(jobName 미지정) job의 수행과정을 살펴보았다.

    정리해보면 다음과 같다.

    1. BatchAutoConfiguration에서 JobLauncherApplicationRunner을 생성 및 jobName 세팅(수행할 job의 이름을 세팅)
    2. JobLauncherApplicationRunner 생성된 후 정의된 job을 `Collection<Job>`에 저장(수행할 job을 세팅)
    3. 파라미터들을 JobParameters에 저장
    4. `Collection<Job>`에 저장된 값들을 루프를 돌면서 `execute()` 호출 후 배치 작업을 진행

     

    CASE 2.

    이번에는 jobName을 설정하고 우리가 원하는 배치를 돌리기 위해서 job을 동적으로 받을 수 있도록 설정해보자

    그럴려면 우선 application.yml 파일을 세팅해야 한다.

    application.yml

     

    세팅이 되었다면 한번 배치 작업을 돌려보자

    `--job.name=JobConfiguration`을 입력해서 배치를 돌려보면 정상적으로 수행된다.

    이제 배치 작업을 동적으로 처리할 수 있다!

    혹시 등록되지 않은 job 이름을 넣게되면 위에서 설정한 NONE이 실행하게 된다. NONE이라는 이름도 job으로 등록한 적이 없기 때문에 배치 작업은 아무것도 수행되지 않는다.

     

    이제 디버깅 모드로 실행해보자

    다시 BatchAutoConfiguration 클래스에서 JobLauncherApplicationRunner 생성하게 되고, JobName을 세팅하게 된다.

    이번에는 jobNames에 값이 있는데, 이건 우리가 설정한 application.yml 파일의 내용이 들어가게 된 것이다.

    즉, 우리가 설정한대로 동적으로 실행할 job 이름을 설정한 것이다.

     

    중간의 내용은 위의 내용과 모두 동일하여 스킵

    이번에는 첫 번째 예제와 다르게 조건문에 들어오게 되었다.

    그리고 지금 멈춰있는 라인에서의 또 다른 조건문이 있는데, 우리가 설정한 jobName과 job 이름이 동일한지 체크하는 구문이다. 동일하지 않다면 수행할 job이 매칭되지 않고, 없다는 뜻이기도 하므로 `continue` 키워드를 통해 `execute()` 메서드까지 가지 못하고 배치 작업을 수행하지 않게 된다.

     

    이번에는 jobName을 설정하고 수행하는 예제를 해보았다.

    이번 예제의 프로세스는 이전과 거의 비슷한데 아래와 같이 조금 추가된 내용이 있다.

    1. BatchAutoConfiguration에서 JobLauncherApplicationRunner을 생성 및 jobName 세팅(수행할 job의 이름을 세팅)
    2. JobLauncherApplicationRunner 생성된 후 정의된 job을 `Collection<Job>`에 저장(수행할 job을 세팅)
    3. 파라미터들을 JobParameters에 저장
    4. `Collection<Job>`에 저장된 값들을 루프를 돌면서,
    5. 지정한 jobName과 job이 동일한지 체크한다. 
    6. 만약 동일하다면 `execute()` 호출 후 배치 작업을 진행하고, 그렇지 않다면 배치 작업을 수행하지 않고 종료하게 된다.

    그외 다른 옵션들


    방금 전까지 job을 동적으로 수행할 수 있도록 application.yml를 세팅하고 어떤 흐름으로 시작되는지 살펴보았다.

    이제 그 외 다른 옵션들을 살펴보자

    • Job 클래스 - `spring.batch.job.enable`(default 값은 true)
      • 이 옵션은 자동으로 배치를 실행해주는 옵션이다. `@EnableBatchProcessing` 어노테이션을 달면 스프링 배치와 관련된 빈들을 자동으로 세팅해주고, 실행하게 된다. 이 부분에서 자동으로 실행을 막는 것이 이 옵션이다. 만약 이 옵션을 `false`로 적용했을 땐 jobLauncher를 구현해서 직접 배치 작업을 실행해야 한다.
    • Jdbc 클래스
      • `spring.batch.jdbc.initialize-schema`
        • 이 옵션은 앞의 포스팅에서도 언급했던 내용인데 메타 데이터를 위한 테이블을 자동으로 생성할 것인지, 수동으로 생성할 것인지에 대한 옵션이다. 옵션 값으로는 `ALWAYS`, `EMBEDDED`, `NEVER`이 있다.
      • `spring.batch.jdbc.table-prefix`
        • 이 옵션은 이름 그대로 메타 데이터 테이블의 prefix를 정해주는 옵션이다.
        • 단, 이 옵션을 사용해서 테이블의 prefix를 변경하고 싶다면 직접 테이블의 이름을 BATCH_에서 설정한 이름으로 수정해줘야 한다. 

     

    정말 마지막으로.. Job 두 개 실행하기


    이번에는 우리가 직접 지정한 Job 두 개가 한번에 실행 가능한지 확인해보는 시험이다.

    이전에 작성한 JobConfiguration을 복사해서 빈 이름과 내용을 바꿨다.

    코드는 아래와 같다.

    [JobConfiguration1]

    package io.spring.batch.helloworld.ch4.jobConfiguration;
    
    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 JobConfiguration1 {
    
        private final JobBuilderFactory jobBuilderFactory;
        private final StepBuilderFactory stepBuilderFactory;
    
        @Bean
        public Job batchJob1() {
            return jobBuilderFactory.get("JobConfiguration1")
                    .incrementer(new RunIdIncrementer())
                    .start(step1())
                    .next(step2())
                    .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!123");
                            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();
        }
    }

     

     

    [JobConfiguration2]

    package io.spring.batch.helloworld.ch4.jobConfiguration;
    
    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 JobConfiguration2 {
    
        private final JobBuilderFactory jobBuilderFactory;
        private final StepBuilderFactory stepBuilderFactory;
    
        @Bean
        public Job batchJob2() {
            return jobBuilderFactory.get("JobConfiguration2")
                    .incrementer(new RunIdIncrementer())
                    .start(step3())
                    .next(step4())
                    .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!123");
                            return RepeatStatus.FINISHED;
                        }
                    }).build();
        }
    
        @Bean
        public Step step4() {
            return stepBuilderFactory.get("step4")
                    .tasklet(new Tasklet() {
                        @Override
                        public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
                            System.out.println("STEP4 HAS EXECUTED!");
                            return RepeatStatus.FINISHED;
                        }
                    }).build();
        }
    }

     

    그리고 지정한 Job 두 개를 실행하기 위해서 Program Arguments 설정도 잊지 말자

     

    이제 디버깅 모드로 돌려보자

    이번에는 파라미터로 넘어온 job의 size가 2개가 되었고, JobLauncherApplicationRunner - jobs 필드에 저장하고 있다.

     

    중간 단계는 이전과 동일하기 때문에 넘어감

    첫 번째 박스에서 job과 jobNames가 동일하기 때문에 조건식에 걸리지 않고, `execute()`를 통해 첫 번째 job이 실행되었다.

    또한 job이 두 개이기 때문에 루프를 한번 더 돌아서 두 번째 job인 JobConfiguration2를 `execute()`를 통해 실행시키게 된다.

    두 번째 박스에서도 이전에 jobNames를 설정한 값이 잘 들어가 있는 것을 확인할 수 있다.

     

    최종적으로!!

    모든 Job이 성공적으로 수행된 것을 확인할 수 있다. 끝!

    댓글