Spring/Spring Batch

[Spring Batch] 4장 : 잡 설정 및 실행(Configuring and Running a Job) 2

공대키메라 2023. 11. 28. 11:36

지난 시간에는 잡 설정 및 실행을 읽다가 멈추었다.

(지난 내용이 궁금하면 여기 클릭!)

 

이번에는 지난 내용에 이어서 다시 글을 읽어보려고 한다.

이번 섹션도 아주 길지만 한 번 열심히 읽어보려고 한다.

 


 

 

1. Changing the Table prefix

JpaRepository의 또다른 수정 가능한 속성은 메타데이터 테이블의 접두사다. 기본적으로 "BATCH_"로 시작한다. BATCH_JOB_EXECUTION과 BATCH_STEP_EXECUTION은 두개의 예이다. 하지만 이 접두사를 수정할 잠재적인 이유가 있다. 만약 스카미의 이름들이 테이블 이름들에 추가를 하거나 메타데이터 테이블의 하나 이상이 같은 스키마에 있을 필요가 있다면 테이블 접두사는 변경되어야 한다. 

 

@Configuration
@EnableBatchProcessing(tablePrefix = "SYSTEM.TEST_")
public class MyJobConfiguration {

   // job definition

}

 

이전 변경 사항을 고려하면 메타데이터 테이블에 대한 모든 쿼리에는 SYSTEM.TEST_라는 접두사가 붙는다. BATCH_JOB_EXECUTION을 SYSTEM.TEST_JOB_EXECUTION으로 말한다.

 

* 유의

오직 테이블 접두사만 설정 할 수 있다. 테이블과 컬럼명은 아니다.

 

2. Non-standard Database Types in a Repository

지원되는 플랫폼 목록에 없는 데이터베이스 플랫폼을 사용하는 경우 SQL 변형이 충분히 유사하다면 지원되는 유형 중 하나를 사용할 수 있다. 이를 수행하려면 namespace shortcut 대신 원시(raw) JobRepositoryFactoryBean을 사용하고 이를 사용하여 데이터베이스 유형을 가장 가까운 일치(closest match)로 설정하면 된다.

다음 예에서는 JobRepositoryFactoryBean을 사용하여 데이터베이스 유형을 Java에서 가장 가까운 일치로 설정하는 방법을 보여준다. 

 

@Bean
public JobRepository jobRepository() throws Exception {
    JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
    factory.setDataSource(dataSource);
    factory.setDatabaseType("db2");
    factory.setTransactionManager(transactionManager);
    return factory.getObject();
}

 

 

데이터베이스 유형이 지정되지 않은 경우 JobRepositoryFactoryBean은 DataSource에서 데이터베이스 유형을 자동 감지하려고 시도한다. 플랫폼 간의 주요 차이점은 주로 기본 키를 증가시키는 전략에 의해 설명되므로 incrementerFactory도 재정의해야 하는 경우가 많습니다(Spring Framework의 표준 구현 중 하나를 사용하여).

그래도 작동하지 않거나 RDBMS를 사용하지 않는 경우 유일한 옵션은 SimpleJobRepository가 의존하는 다양한 Dao 인터페이스를 구현하고 일반적인 Spring 방식으로 수동으로 연결하는 것이다

 

3. Configuring a JobLauncher

@EnableBatchProcessing을 사용할 때 JobRegistry가 제공된다. 이 섹션은 어떻게 JobRegistry를 설정하는 방법에 대해 묘사한다. 

 

JobLauncher의 가장 기본 구현체는 TaskExecutorJobLauncher다. 이것의 유일하게 필요한 의존성은 JobRepository다. 

 

다음 예시는 Java에서 TaskExecutorJobLauncher를 보여준다.

 

...
@Bean
public JobLauncher jobLauncher() throws Exception {
	TaskExecutorJobLauncher jobLauncher = new TaskExecutorJobLauncher();
	jobLauncher.setJobRepository(jobRepository);
	jobLauncher.afterPropertiesSet();
	return jobLauncher;
}
...

 

일단 JobExecution이 얻어지면, Job에 실행 메소드를 넘겨주는데 궁극적으로는 아래 그림이 보여주듯이 호출자에게 JobExeution을 반환한다.

 

Figure 2. Job Launcher Sequence

 

순서는 간단하고 스케줄러로부터 실행될 때 잘 작동한다. 하지만 HTTP request로부터 실행을 시도할 때 문제가 발생한다.

이 시나리오에서, 실행은 비동기적으로 되야하는데 TaskExeutorJobLauncher가 즉시 호출자에게 반환되도록 하기 위함이다. 이는 장기 실행 프로세스(예: 일괄 작업)에 필요한 시간 동안 HTTP 요청을 열어 두는 것이 좋지 않기 때문입니다. 다음 이미지가 예시의 순서를 보여준다.

 

Figure 3. Asynchronous Job Launcher Sequence

 

TaskExecutor를 설정해서 이 시나리오가 잘 되도록 TaskExecutorJobLauncher를 설정할 수 있다.

 

다음 자바 예시가 즉시 반환하는 TaskExecutorJobLauncher을 설정한다.

 

@Bean
public JobLauncher jobLauncher() {
	TaskExecutorJobLauncher jobLauncher = new TaskExecutorJobLauncher();
	jobLauncher.setJobRepository(jobRepository());
	jobLauncher.setTaskExecutor(new SimpleAsyncTaskExecutor());
	jobLauncher.afterPropertiesSet();
	return jobLauncher;
}

 

잡이 비동기적으로 실행되도록 제어하기 위해 spring TaskExecutor 인터페이스의 구현체를 사용할 수 있다.

 

4. Running a Job

최소한으로 배치 잡을 실행하는건 두 가지를 요구한다. 실행될 Job과 JobLauncher다.

두 개는 같은 혹은 다른 컨텍스트 내에서 포함된다. 예를 들어 커맨드 라인으로 잡을 실행한다면 새로운 JVM이 각각 Job에 대해 인스턴스화된다.(instantiated). 따라서, 모든 Job은 자신의 JobLaunche를 가진다. 하지만, 만약 HttpRequest의 스코프 이내에 있는 웹 컨테이너 안에서 실행을 한다면  많은 요청들이 그들의 잡을 실행하는데 호출되는 하나의 JobLauncher가 있다.

 

(즉, Job과 JobLauncher를 어떻게 구성하고 사용할 수 있는지에 대한 설명이며, 이는 실행 환경에 따라 달라질 수 있음을 나타내는걸로 보인다.)

 

5. Running Jobs from the Command Line

만약 엔터프라이즈 스케줄러에서 job을 실행하고 싶다면, 커맨드 라인이 주요한 인터페이스다. 이는 대부분의 스케줄러(Quartz 제외, NativeJob을 사용하지 않는 경우)가 주로 쉘 스크립트로 시작되는 운영 체제 프로세스와 직접 작동하기 때문이다.  shell script뿐만 아니라 펄, 루비 또는 심지어 Ant나 Maven같은 다른 빌드 툴에서도 Java 처리를 실행할 많은 방법이 있다. 하지만, 대부분의 사람들이 shell script에 익숙하기 때문에 이 예시는 그에 집중할 것이다.

 

6. The CommandLineJobRunner

잡을 실행하는 script가 JVM(Java Virtual Machine)를 작동시키기 때문에 주된 진입점(primary entry point)오서 작동하는 main method를 가진 클래스가 필요하다. Spring Batch는 이러한 목적을 돕는 구현체를 제공한다 : CommandLineJobRunner. 이것은 어플리케이션을 bootstrap하기 위한 한 가지 방법임을 유의하라. Java 처리를 실행할 많은 방법이 있고 이 클래스는 최종 클래스로 간주해서는 안된다. CommandLineJobRunner는 4가지 업무를 수행한다.

 

  • 적절한 ApplicationContext를 로드한다.
  • JobParameters에 커맨드 라인 변수를 파싱한다.
  • 인수(arguments) 에 따라서 적절한 잡을 배치한다.
  • job을 실행하는 application context에서 제공된 JobLauncher를 사용한다.

이 모든 업무들은 변수들이 넘어오면 해결된다. 다음 테이블이 필요한 변수들을 묘사한다. 

 

Table 1. CommandLineJobRunner arguments

jobPath The location of the XML file that is used to create an ApplicationContext. This file should contain everything needed to run the complete Job.
jobName The name of the job to be run.

 

이러한 인수(arguments)들은 넘겨져야만 하는데 path가 첫 번째로, name이 두 번째로 말이다. 잡 파라미터가 되도록 고려한 후에 모든 변수들이 JobParameter 객체로 변하고 name=value의 형태로 되야 한다. 

 

다음 예시가 Java에서 정의된 잡의 잡 파라미터로 넘어온 date를 보여준다.

 

<bash$ java CommandLineJobRunner io.spring.EndOfDayJobConfiguration endOfDay schedule.date=2007-05-05,java.time.LocalDate

 

 

* Note

기본적으로 CommandLineRunner는 DefaultJobParametersConverter를 사용하는데 이는 job parameters를 식별하는 key/value 쌍으로 암시적으로 변환한다. 그러나 식별할 작업 매개 변수와 그렇지 않은 작업 매개 변수를 각각 true 또는 false로 접미사를 붙여 명시적으로 지정할 수 있다.

다음 예제에서 schedule.date는 식별 작업 매개 변수이며 vendor.id 은 그렇지 않다.

 

<bash$ java CommandLineJobRunner io.spring.EndOfDayJobConfiguration endOfDay \
                                 schedule.date=2007-05-05,java.time.LocalDate,true \
                                 vendor.id=123,java.lang.Long,false

 

커스텀 JobParametersConverter를 사용해서 이 행동을 오버라이드 할 수 있다.

 

대부분의 경우 매니페스트를 사용하여 주요 클래스를 jar에 표시하려고 한다. 하지만 심플함을 위해 클래스가 직접적으로 사용된다. 이 예시는 Domain Language of Batch로부터 EndOfDay 예시를 사용한다. 첫 번째 인수는 io.spring.EndOfDayJobConfiguration이며, Job이 포함된 구성 클래스의 정규화된 클래스 이름이다.

두 번째 인수 endOfDay는 작업 이름을 나타낸다. 마지막 인수 schedule.date=2007-05-05,java.time.LocalDate는 java.time.LocalDate 유형의 JobParameter 개체로 변환된다.

 

@Configuration
@EnableBatchProcessing
public class EndOfDayJobConfiguration {

    @Bean
    public Job endOfDay(JobRepository jobRepository, Step step1) {
        return new JobBuilder("endOfDay", jobRepository)
    				.start(step1)
    				.build();
    }

    @Bean
    public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
        return new StepBuilder("step1", jobRepository)
    				.tasklet((contribution, chunkContext) -> null, transactionManager)
    				.build();
    }
}

 

앞의 예는 일반적으로 스프링 배치에서 배치 작업을 실행하는 데 더 많은 요구 사항이 있기 때문에 지나치게 단순하지만 CommandLineJobRunner의 두 가지 주요 요구 사항인 Job과 JobLauncher를 보여주는 역할을 한다.

 

해당 섹션은 command line 에서 실행하는 경우에 대해 설명한 것 같다.
사실 크게 고려하지 않았던 부분이고 spring batch 예시를 따라가기 바빠서 그냥 intelliJ에서 argument를 설정 해주면 끝이겠네 했었다.

생각해보니 linux 환경에서 jar형태 배포된 spring batch를 실행할 수 도 있을 것 같다.
그 경우에는 사람이 매번 접속해서 할 수 는 없으니 다른 별도의 shell script를 짜서 실행하겠지?

 

7. Exit Codes

command-line으로 배치 잡을 실행할 때 엔터프라이즈 스케줄러가 자주 사용된다. 대부분의 스케줄러는 상당히 멍청하고(?!?!) 오직 처리 레벨에서만 작동한다. 이것은 몇 개의 OS처리에 대해서만 안다는 것을 의미한다(그들이 호출하는 shell script 같은 것). 이 시나리오에서 job의 성공과 실패에 대해 스케줄러와 다시 통신하는 유일한 방법은 반환 코드를 통해서이다. 반환 코드는 실행의 결과를 나타내기 위해 처리에 의해 스케줄러에게 반환되는 수다. 가장 간단한 경우에 0 은 성공이고 1은 실패다. 하지만, 더 복잡한 시나리고아 있을 수 있는데 "job A가 4를 반환하면 job B를 실행하고, 만약 5를 반환하면 job C를 실행하라" 처럼 말이다. 이러한 행위의 형태는 스케줄러 레벨에서 설정되지만 Spring Batch같은 처리 프레임워크가 특정 batch job의 exit code의 수적인 표현을 반환하는 방식을 제공하는건 중요하다. Spring Batch에서, 이것은 ExitStatus로 캡슐화되어있는데 Chapter5에서 더 자세하게 다룬다. exit code 를 논의하는 목적에서 알아야 할 유일한 중요한 사항은 ExitStatus가 프레임워크에 의해 설정되는 exit code 속성을 가진다는 점이고 JobLauncher로부터 반환된 JobExecution의 부분으로 반환된다는 점이다.  CommandLineJobRunner는 ExitCodeMapper 인터페이스를 사용해 수를 string value로 변환한다.

 

public interface ExitCodeMapper {

    public int intValue(String exitCode);

}

 

ExitCodeMapper의 필수 약정(essential contract)은 string exit code 를 받으면, 숫자가 반환될 것이라는 점이다. job runner에 의해 사용된 기본 구현체는 SimpleJbmExitCodeMapper인데 완료시에 0을, 일반적인 에러에는 1을, 제공된 context에서 Job을 찾을 수 없는 어떤 job runner 에러에서는 2을 반환한다. 위의 세 가지 값보다 더 복잡한 것이 필요한 경우 ExitCodeMapper 인터페이스의 사용자 정의 구현을 제공해야 한다. CommandLineJobRunner는 ApplicationContext를 생성하는 클래스이므로 '함께 연결할' 수 없으므로 덮어써야 하는 모든 값은 자동으로 연결되어야 한다. 이것은 만약 ExitCodeMapper의 구현체가 BeanFactory내에서 발견된다면 context가 생성된 후에 runner안으로 주입된다는 말이다. 자신의 ExitCodeMapper를 제공하기 위해 할 필요한 모든 것은 루트 레벨 빈으로 구현체를 선언하는 것이고 그것이 runner에 의해 로드된 ApplicationContext의 부분이 되도록 하는 것이다.

 

8. Running Jobs from within a Web Container

역사적으로, 앞서 말했듯이 오프라인 처리 (batch job 같은) command-line으로 실행되어왔다. 하지만 HttpRequest로부터 실행하는것이 더 나은 많은 경우가 있다. 많은 그러한 use case는 reporting, 특별한 job running 그리고 web application 지원을 포함한다. batch job이 오래 실행해서, 대부분의 중요한 관심사는 비동기적으로 job을 실행하는 것이다. 

 

Figure 4. Asynchronous Job Launcher Sequence From Web Container

 

 

이러한 경우에 controller는 Spring MVC controller다. Spring MVC에 대해 더 Spring Framework Reference Guide에서 보아라. controller가 비동기적으로 실행되도록 설정한 JobLauncher를 사용하는 Job을 실행하는데 즉시 JobExecution을 반환한다. Job은 여전히 작동중이다. 하지만 nonblocking 행위는 HttpRequest를 다룰 필요가 있을때 controller에게 즉시 반환한다. 다음 리스트가 예시를 보여준다.

@Controller
public class JobLauncherController {

    @Autowired
    JobLauncher jobLauncher;

    @Autowired
    Job job;

    @RequestMapping("/jobLauncher.html")
    public void handle() throws Exception{
        jobLauncher.run(job, new JobParameters());
    }
}

 

9. Advanced Metadata Usage

지금까지 JobLauncher와 JobRepository 인터페이스 두 개를 논의해왔다. 함께, 이 둘은 job의 간단한 실행과 batch domain object의 기본 CRUD 작업을 나타낸다.

 

Figure 5. Job Repository

 

JobLauncher는 새로운 JobExecution 객체를 생성하고 실행하기 위해서 JobRepository를 사용한다. Job과 Step은 나중에 Job을 실행중 동일한 실행의 기본 업데이트를 위해 JobRepository를 사용한다. 기본 작업들은 간단한 시나리오를 만족한다. 하지만, 수 백 개의 배치 잡들과 복잡한 스케줄링 요청을 가진 큰 배치 환경에서 메타데이터에 대한 더 많은 진보된 접근이 필요하다.

 

Figure 6. Advanced Job Repository Access

 

JobExplorer와  JobOperator 인퍼테이스는 다음 오는 섹션에 논의되는데, 쿼링과 메타데이터를 다루기 위한 추가적인 기능을 더한다.

 

이 부분은 그냥 더 복잡한 환경에서 metadata를 사용할 경웨 JobOperator와 JobExplorer를 사용할 수 있다는 것을 알려주는 부분인 것 같다.
이것들이 정확히 뭔지는 뒤에 나온다니 우선은 그러려니 하자.

 

10. Querying the Repository

어떤 진보된 특정들에 앞서 가장 기본적인 요구사항은 기존 exceution에게 repository를 query하는 능력이다. 이 능력은 JobExplorer 인터페이스가 제공한다.

 

public interface JobExplorer {

    List<JobInstance> getJobInstances(String jobName, int start, int count);

    JobExecution getJobExecution(Long executionId);

    StepExecution getStepExecution(Long jobExecutionId, Long stepExecutionId);

    JobInstance getJobInstance(Long instanceId);

    List<JobExecution> getJobExecutions(JobInstance jobInstance);

    Set<JobExecution> findRunningJobExecutions(String jobName);
}

 

method signature에서 명확한 것은 JobExplorer가 JobRepository의 read-only 버전이라는 것이고 JobRepository 처럼 쉽게 factory bean을 사용해서 설정될 수 있다.

 

다음 예시가 어떻게 Java에서 JobExplorer를 설정할 수 있는지 보여준다.

 

...
// This would reside in your DefaultBatchConfiguration extension
@Bean
public JobExplorer jobExplorer() throws Exception {
	JobExplorerFactoryBean factoryBean = new JobExplorerFactoryBean();
	factoryBean.setDataSource(this.dataSource);
	return factoryBean.getObject();
}
...

 

 

이 챕터 전에, 우리는 다른 버전 혹은 스키마를 허락하는 JobRepository의 테이블 접두사를 수정할 수 있다는 점을 알았다.

JobExplorer가 같은 테이블에서 작동하기 때문에, 접두사를 세팅할 능력이 또 필요하다.

 

...
// This would reside in your DefaultBatchConfiguration extension
@Bean
public JobExplorer jobExplorer() throws Exception {
	JobExplorerFactoryBean factoryBean = new JobExplorerFactoryBean();
	factoryBean.setDataSource(this.dataSource);
	factoryBean.setTablePrefix("SYSTEM.");
	return factoryBean.getObject();
}
...

 

JobExplorer에 대한 글을 읽는 중에 궁금증이 생겼다.
JobExplorer가 JobRepository의 read-only 버전이라면 JobRepositroy를 그냥 주입받아서 사용하지 굳이 이것을 사용해야 하나 하는 생각이 들었다.

그래서 GPT에게 질문을 했다.

나 : JobRepository와 JobExplorer가 필요하니?

해당 답에 대한 답변은 아래에 있다.

 

더보기

JobRepository

  • 역할: JobRepository는 배치 작업(Job)과 스텝(Step)의 실행에 대한 메타데이터를 저장, 관리합니다. 이 메타데이터에는 배치 작업의 상태, 성공/실패 여부, 실행 시간 등이 포함됩니다.
  • 기능: 배치 프로세스의 실행 도중에 작업의 상태를 업데이트하고, 재시작 가능한 작업에 대한 정보를 유지합니다. 또한, 트랜잭션 관리와 오류 처리를 담당합니다.
  • 사용 사례: 작업의 실행, 중지, 재시작 등의 과정에서 활발히 사용됩니다.

JobExplorer

  • 역할: JobExplorer는 JobRepository에 저장된 메타데이터를 읽기 전용으로 조회하는 역할을 합니다. 즉, 배치 작업의 실행 이력이나 상태 정보를 조회하는 데 사용됩니다.
  • 기능: JobExplorer는 JobRepository의 데이터를 변경하지 않고, 단순히 조회하는 기능을 제공합니다. 이를 통해 현재 및 이전 배치 작업의 상태, 실행 내역 등을 안전하게 접근할 수 있습니다.
  • 사용 사례: 배치 작업의 상태를 모니터링하거나 리포트를 생성할 때 주로 사용됩니다.

왜 JobExplorer가 필요한가?

  1. 데이터 무결성: 배치 시스템에서는 작업 실행 중 데이터 무결성을 보장하는 것이 중요합니다. JobExplorer는 데이터를 변경하지 않고 조회만 하므로, 데이터 무결성을 해치지 않으면서 필요한 정보를 안전하게 조회할 수 있습니다.
  2. 성능 최적화: JobExplorer를 사용하면 읽기 전용 작업에 최적화된 방식으로 데이터를 조회할 수 있어, 성능상의 이점이 있습니다. 쓰기 작업에 비해 읽기 작업은 일반적으로 더 가볍고 빠릅니다.
  3. 분리된 관심사: 배치 작업의 실행과 모니터링 또는 리포팅은 서로 다른 관심사입니다. JobExplorer와 JobRepository를 분리함으로써, 각각의 기능을 더 명확하고 효율적으로 관리할 수 있습니다.

 

여기에 더해서 어떤 부분이 얼마나 성능 최적화가 되는지 궁금했다.

 

나 : JobExplorer로 조회를 하면 성능 최적화가 가능하다는데 얼마나 성능이 향상되는거야?

GPT의 답변

JobExplorer를 사용하여 성능 최적화가 가능하다는 주장은 일반적으로 JobExplorer가 읽기 전용 접근을 제공하기 때문에 나오는 것입니다. 이러한 접근 방식은 성능 향상에 기여할 수 있지만, 실제 성능 향상의 정도는 여러 요인에 따라 달라집니다. 이를 구체적으로 수치로 표현하기는 어렵습니다. 그 이유는 다음과 같습니다:

  1. 사용 환경에 따라 다름: 데이터베이스의 크기, 쿼리의 복잡성, 시스템의 하드웨어 사양, 네트워크 지연 등과 같은 여러 환경적 요인에 따라 성능 향상의 정도가 달라집니다.
  2. 읽기와 쓰기 작업의 차이: 일반적으로 데이터베이스에서 읽기 작업은 쓰기 작업보다 덜 자원 집약적입니다. JobExplorer는 읽기 전용이므로, 쓰기 작업을 필요로 하는 JobRepository에 비해 상대적으로 가볍습니다. 하지만 이 차이가 어떤 상황에서 얼마나 큰 영향을 미치는지는 상황에 따라 다릅니다.
  3. 최적화의 종류: 성능 최적화는 응답 시간의 단축, 처리량 증가, 자원 사용의 효율성 개선 등 여러 형태로 나타날 수 있습니다. JobExplorer의 사용은 이러한 영역들 중 일부에 영향을 줄 수 있습니다.
  4. 읽기 전용 트랜잭션: JobExplorer의 읽기 전용 트랜잭션은 일반적인 읽기/쓰기 트랜잭션에 비해 더 낮은 격리 수준을 사용할 수 있어, 데이터베이스의 잠금과 관련된 오버헤드를 줄일 수 있습니다. 이는 특히 동시성이 높은 환경에서 유리할 수 있습니다.

성능 향상의 정도를 정확하게 예측하거나 일반화하는 것은 어렵습니다. 실제 성능 향상을 평가하려면, 해당 시스템의 특정 환경과 요구 사항에 맞추어 테스트 및 벤치마킹을 수행하는 것이 필요합니다.

 


 

해당 글이 너무 길어지는 관계로 이번에도 아쉽지만... 이 내용에 이어서 다음 글에서 JobRegistry를 읽어보도록 하겠다.

 

spring docs를 읽으면 좋은 점이 출처가 단순하다는 점이다. ㅎㅎ...

 

출처:

https://docs.spring.io/spring-batch/docs/5.0.4/reference/html/job.html#configureJob