프레임워크(Framework)/Spring

[Spring Batch] 스프링 배치 JobInstance 이해하기 - Spring boot 2.x

잇트루 2023. 6. 25. 02:59
반응형
본 내용은 온라인 강의 사이트 인프런의 정수원 님의 강의 내용이 포함되어 있습니다.
스프링 배치 - Spring Boot 기반으로 개발하는 Spring Batch
 

스프링 배치 - Spring Boot 기반으로 개발하는 Spring Batch - 인프런 | 강의

초급에서 중~고급에 이르기까지 스프링 배치의 기본 개념부터 API 사용법과 내부 아키텍처 구조를 심도있게 다룹니다. 그리고 스프링 배치 각 기능의 흐름과 원리를 학습하게 되고 이를 바탕으

www.inflearn.com

 

 

Intro

스프링 배치의 도메인은 크게 두 가지로 나눌 수 있다.

  1. 배치를 특정 단계 또는 흐름에 따라 처리하고 구성하는 역할을 하는 도메인
    • Job, Step, Flow, Tasklet, …
  2. 배치의 단계마다 실행 정보나 상태 정보를 데이터베이스에 저장하기 위한 메타데이터 도메인
    • JobInstance, JobExecution, StepExecution, …

 

 

JobInstance

JobInstance는 Job이 실행될 때 생성되는 Job의 논리적 실행 단위 객체로서 고유하게 식별 가능한 작업 실행을 나타낸다. Job의 설정과 구성은 동일하지만 Job이 실행되는 시점에 처리하는 내용은 다르기 때문에 Job의 실행을 구분한다.

  • 하루에 한 번씩 Job이 실행된다면 매일 실행되는 각각의 Job을 JobInstance로 표현한다.

Job과 JobInstance는 일대다(1:M) 관계다.

 

JobInstance의 생성 및 실행

  • 처음 시작하는 Job과 JobParameter일 경우 새로운 JobInstance를 생성한다.
  • 이전과 동일한 Job과 JobParameter일 경우 이미 존재하는 JobInstance를 리턴한다.
    • 내부적으로 Name과 Key를 통해 JobInstance 객체를 얻는다.

 

batch_job_instance 테이블과 매핑

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

  • name과 key가 모두 동일한 인스턴스는 존재하지 않음
  • 매일 한 번씩 실행하는 Job이 있다고 가정하면, name은 같으나 key가 다른 인스턴스가 생성됨

 

JobInstance 생성 흐름도

하루에 한 번씩 시스템을 정산 배치 프로그램이 있다고 가정하면, 다음과 같은 흐름으로 JobInstance가 생성된다.

  1. JobLauncher는 사용자가 구성한 배치 Job을 실행시키는 역할을 하는 클래스로 Job과 JobParameters를 인자로 받아 Job을 실행시킨다.
  2. JobRepository 클래스는 배치의 실행 중에 발생하는 메타데이터를 저장하고 업데이트하는 역할을 한다. JobRepositry는 JobLauncher로부터 받은 Job과 JobParameters를 DB에 저장된 정보와 비교하여 실행 여부를 판단한다.
  3. 확인된 정보(Job, JobParameters)가 이미 존재하는 경우 기존 JobInstance를 리턴하고, 존재하지 않으면 JobInstance를 생성한다.

 

따라서 일별 정산 시스템의 경우 다음과 같이 JobInstance를 생성하여 데이터베이스에 정보를 저장한다.

  • 2021년 1월 1일에 실행된 Job은 일별 정산에 대한 Job과 2021년 1월 1일에 해당하는 JobParameters를 통해 JobInstance를 생성하여 테이블에 저장된다.
  • 2021년 1월 2일에 실행된 Job은 일별 정산에 대한 Job과 2021년 1월 2일에 해당하는 JobParameters를 통해 JobInstance를 생성하여 테이블에 저장된다.
  • 이는 날짜에 따라 JobParameters가 달라지고 같은 Job에 대하여 새로운 JobInstance를 생성하여 별도로 구분하여 데이터베이스에 저장하는 것을 알 수 있다.
  • 만약, Job과 JobParameters가 모두 같은 경우 JobInstance는 생성되지 않으며 예외를 발생하게 된다.

 

 

동일한 JobInstance 실행하기

먼저 간단한 Batch Job을 작성한다.

@Configuration
@RequiredArgsConstructor
public class JobInstanceConfiguration {

    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;

    @Bean
    public Job job() {
        return jobBuilderFactory.get("job")
                .start(step1())
                .next(step2())
                .build();
    }

    public Step step1() {
        return stepBuilderFactory.get("step1")
                .tasklet(new Tasklet() {
                    @Override
                    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
                        System.out.println("Step1");
                        return RepeatStatus.FINISHED;
                    }
                })
                .build();
    }

    public Step step2() {
        return stepBuilderFactory.get("step2")
                .tasklet(new Tasklet() {
                    @Override
                    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
                        System.out.println("Step2");
                        return RepeatStatus.FINISHED;
                    }
                })
                .build();
    }
}

 

JobInstance의 Job과 Prameters를 동일하게 생성하기 위해서는 JobInstance를 생성하는 코드를 직접 정의해 주어야 한다.

@Component
public class JobRunner 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")
                .toJobParameters();

        jobLauncher.run(job, jobParameters);
    }
}

ApplicationRunner를 직접 구현하기 위해 JobRunner 구현 클래스를 작성한다. ApplicationRunner는 스프링 배치에서 제공하는 인터페이스로 스프링 부트가 초기화되면 자동으로 호출하는 run 메서드를 제공한다. 이후 JobLauncher와 위에서 정의한 Job을 의존성 주입받는다.

run 메서드를 오버라이딩하여 JobParameters를 직접 생성한다. add 메서드를 통해 파라미터를 직접 지정할 수 있으며 Date, String, Double, Long 네 가지 타입으로 파라미터를 구성할 수 있다. toJobParameters() 메서드를 통해 직접 구성한 JobPrameters 객체를 생성한 후 주입받은 jobLauncher의 run 메서드에 직접 정의한 job과 jobParameters를 인자로 받아 Job을 실행할 수 있다.

 

마지막으로 스프링 배치의 자동 실행을 차단하고 직접 작성한 JobRunner를 실행하기 위한 설정을 한다.

spring:
  datasource:
    ...
  batch:
    job:
      enabled: false # 배치를 자동실행하지 않는다.
  ...

 

결과

첫 번째 실행시 정상적으로 배치가 실행되며 Job의 실행 정보가 데이터베이스에 저장된다.

2023-06-23 01:44:47.352  INFO 31460 --- [           main] c.e.springbatch.SpringBatchApplication   : Started SpringBatchApplication in 2.677 seconds (JVM running for 3.806)
2023-06-23 01:44:47.513  INFO 31460 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=job]] launched with the following parameters: [{name=user1}]
2023-06-23 01:44:47.587  INFO 31460 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step1]
Step1
2023-06-23 01:44:47.639  INFO 31460 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [step1] executed in 52ms
2023-06-23 01:44:47.694  INFO 31460 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step2]
Step2
2023-06-23 01:44:47.743  INFO 31460 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [step2] executed in 49ms
2023-06-23 01:44:47.783  INFO 31460 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=job]] completed with the following parameters: [{name=user1}] and the following status: [COMPLETED] in 238ms

batch_job_instance 테이블

 

batch_job_execution_params 테이블

 

이후 동일한 job 구성과 설정 상태에서 다시 실행하면 jobInstance가 생성되지 않고 예외가 발생한다.

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2023-06-23 02:12:11.101 ERROR 31008 --- [           main] o.s.boot.SpringApplication               : Application run failed

java.lang.IllegalStateException: Failed to execute ApplicationRunner
	at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:762) ~[spring-boot-2.7.12.jar:2.7.12]
	at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:749) ~[spring-boot-2.7.12.jar:2.7.12]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:314) ~[spring-boot-2.7.12.jar:2.7.12]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303) ~[spring-boot-2.7.12.jar:2.7.12]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292) ~[spring-boot-2.7.12.jar:2.7.12]
	at com.example.springbatch.SpringBatchApplication.main(SpringBatchApplication.java:12) ~[classes/:na]
Caused by: org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException: A job instance already exists and is complete for parameters={name=user1}.  If you want to run this job again, change the parameters.
	at org.springframework.batch.core.repository.support.SimpleJobRepository.createJobExecution(SimpleJobRepository.java:139) ~[spring-batch-core-4.3.8.jar:4.3.8]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
...

예외 내용을 살펴보면 다음과 같이 동일한 jobInstance가 존재하여 예외가 발생음을 알 수 있다.

Caused by: org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException: A job instance already exists and is complete for parameters={name=user1}. If you want to run this job again, change the parameters.

 

JobParameters의 설정을 변경하고 실행하면 정상적으로 Job을 실행시킬 수 있다.

@Component
public class JobRunner implements ApplicationRunner {

    @Autowired
    private JobLauncher jobLauncher;

    @Autowired
    private Job job;

    @Override
    public void run(ApplicationArguments args) throws Exception {

        JobParameters jobParameters = new JobParametersBuilder()
                .addString("name", "user2") // user1에서 user2로 변경
                .toJobParameters();

        jobLauncher.run(job, jobParameters);
    }
}
2023-06-23 02:17:16.075  INFO 28952 --- [           main] c.e.springbatch.SpringBatchApplication   : Started SpringBatchApplication in 2.096 seconds (JVM running for 3.111)
2023-06-23 02:17:16.221  INFO 28952 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=job]] launched with the following parameters: [{name=user2}]
2023-06-23 02:17:16.284  INFO 28952 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step1]
Step1
2023-06-23 02:17:16.328  INFO 28952 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [step1] executed in 44ms
2023-06-23 02:17:16.379  INFO 28952 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step2]
Step2
2023-06-23 02:17:16.418  INFO 28952 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [step2] executed in 39ms
2023-06-23 02:17:16.447  INFO 28952 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=job]] completed with the following parameters: [{name=user2}] and the following status: [COMPLETED] in 199ms

batch_job_instance 테이블

 

batch_job_execution_params 테이블

반응형