카테고리 없음

[Spring Batch 5.0][공식 문서 번역] Job 구성 및 실행(Configuring and Running a Job)

제이동 개발자 2024. 11. 10. 22:20
728x90

[Spring Batch][공식 문서 번역] Job 구성 및 실행(Configuring and Running a Job)

 Job 객체는 단순히 여러 Step을 담는 컨테이너처럼 보일 수 있지만, 다양한 구성 옵션을 이해해야 합니다. 또한, Job이 실행되는 방식과 실행 중 메타데이터가 저장되는 방법에 대해 고려할 사항이 많습니다. 이 장에서는 Job의 다양한 구성 옵션과 실행 시 고려해야 할 사항을 설명합니다.

 

섹션 요약

 

1. Job 구성 (Configuring a Job)

 Job 인터페이스에는 여러 구현이 존재하지만, 이러한 구현은 제공되는 빌더(자바 설정) 또는 XML 네임스페이스(XML 기반 설정) 뒤에 추상화되어 있습니다. 다음 예제에서는 Java 및 XML 설정을 보여줍니다.

@Bean
public Job footballJob(JobRepository jobRepository) {
    return new JobBuilder("footballJob", jobRepository)
                     .start(playerLoad())
                     .next(gameLoad())
                     .next(playerSummarization())
                     .build();
}

 

 Job(그리고 일반적으로 그 안의 Step)에는 JobRepository가 필요합니다. JobRepository의 설정은 Java 설정을 통해 처리됩니다.

 

 위 예제는 세 개의 Step 인스턴스로 구성된 Job을 나타냅니다. Job 관련 빌더는 병렬 처리(Split), 선언적 흐름 제어(Decision), 흐름 정의의 외부화(Flow)를 돕는 다른 요소들도 포함할 수 있습니다.

 

1.1 재시작 가능성 (Restartability)

 배치 작업을 실행할 때 중요한 문제 중 하나는 Job이 재시작될 때의 동작입니다. 특정 JobInstance에 대해 이미 JobExecution이 존재한다면, Job의 실행은 "재시작"으로 간주됩니다. 이상적으로는 모든 Job이 중단된 지점에서 다시 시작할 수 있어야 하지만, 특정 상황에서는 이를 구현하기 어려울 수 있습니다. 이 경우, 새로운 JobInstance를 생성하는 것은 전적으로 개발자의 책임입니다. 그러나 Spring Batch는 이에 대한 일부 지원을 제공합니다. Job이 재시작되지 않아야 하며 항상 새로운 JobInstance로 실행되어야 한다면, restartable 속성을 false로 설정할 수 있습니다.

@Bean
public Job footballJob(JobRepository jobRepository) {
    return new JobBuilder("footballJob", jobRepository)
                     .preventRestart()
                     ...
                     .build();
}

 

 다르게 표현하자면, restartable을 false로 설정하면 "이 Job은 다시 시작을 지원하지 않는다"는 의미가 됩니다. 재시작할 수 없는 Job을 재시작하려고 하면 JobRestartException이 발생합니다. 다음 Junit 코드는 이 예외를 발생시키는 예입니다:

Job job = new SimpleJob();
job.setRestartable(false);

JobParameters jobParameters = new JobParameters();

JobExecution firstExecution = jobRepository.createJobExecution(job, jobParameters);
jobRepository.saveOrUpdate(firstExecution);

try {
    jobRepository.createJobExecution(job, jobParameters);
    fail();
}
catch (JobRestartException e) {
    // expected
}

 

 재시작할 수 없는 Job에 대해 JobExecution을 처음 생성할 때는 문제가 발생하지 않지만, 두 번째 시도 시 JobRestartException이 발생합니다.

 

1-2. Job 실행 가로채기 (Inheriting Job Execution)

 Job이 실행되는 동안, 특정 이벤트에 대한 알림을 받아 사용자 정의 코드를 실행하는 것이 유용할 수 있습니다. SimpleJob은 적절한 시점에 JobListener를 호출함으로써 이를 지원합니다.

public interface JobExecutionListener {

    // Job 실행 전 실행
    void beforeJob(JobExecution jobExecution);

    // Job 실행 후 실행
    void afterJob(JobExecution jobExecution);
}

 

 SimpleJob에 JobListener를 추가하려면 Job에 리스너를 설정하여 추가할 수 있습니다. 다음 예제는 Java Job 정의에 리스너 메서드를 추가하는 방법을 보여줍니다:

@Bean
public Job footballJob(JobRepository jobRepository) {
    return new JobBuilder("footballJob", jobRepository)
                     .listener(sampleListener())
                     ...
                     .build();
}

 

 afterJob 메서드는 Job의 성공 여부와 상관없이 항상 호출된다는 점에 주의하세요. 성공 또는 실패 여부를 확인해야 할 경우 JobExecution에서 해당 정보를 얻을 수 있습니다.

public void afterJob(JobExecution jobExecution){
    if (jobExecution.getStatus() == BatchStatus.COMPLETED ) {
        //job success
    }
    else if (jobExecution.getStatus() == BatchStatus.FAILED) {
        //job failure
    }
}

 

이 인터페이스와 관련된 애노테이션은 다음과 같습니다:

  • @BeforeJob
  • @AfterJob

 

1-3. 부모 Job 상속 (Inheriting from a Parent Job

 여러 Job이 유사하지만 동일하지 않은 구성을 공유하는 경우, 속성을 상속하는 "부모" Job을 정의하면 유용할 수 있습니다. Java의 클래스 상속과 유사하게, "자식" Job은 부모의 요소와 속성을 결합하여 자신의 요소와 통합합니다.

 

 다음 예제에서 baseJob은 추상 Job 정의로, 리스너 목록만 정의합니다. job1은 baseJob의 리스너 목록을 상속하고 자신의 리스너 목록과 병합하여 두 개의 리스너와 하나의 Step(step1)을 포함하는 Job을 생성하는 구체적인 정의입니다.

<job id="baseJob" abstract="true">
    <listeners>
        <listener ref="listenerOne"/>
    <listeners>
</job>

<job id="job1" parent="baseJob">
    <step id="step1" parent="standaloneStep"/>

    <listeners merge="true">
        <listener ref="listenerTwo"/>
    <listeners>
</job>

 

 부모 Step 상속에 관한 자세한 정보는 Inheriting from a Parent Step 섹션을 참조하십시오.

 

1-4. JobParametersValidator

 XML 네임스페이스에서 선언된 Job 또는 AbstractJob의 하위 클래스는 실행 시 Job 파라미터를 검증하는 Validator를 선택적으로 선언할 수 있습니다. 이는 Job이 필수 매개변수를 모두 포함하여 시작되도록 확인할 때 유용합니다. DefaultJobParametersValidator를 사용하면 단순한 필수 및 선택적 매개변수 조합을 제한할 수 있으며, 더 복잡한 제약 조건이 필요한 경우 인터페이스를 직접 구현할 수 있습니다.

 

Validator 구성은 Java 빌더를 통해 지원됩니다.

@Bean
public Job job1(JobRepository jobRepository) {
    return new JobBuilder("job1", jobRepository)
                     .validator(parametersValidator())
                     ...
                     .build();
}

 

 

2. Java 설정 (Java Configuration)

 Spring 3에서는 XML 대신 Java로 애플리케이션을 구성할 수 있는 기능이 도입되었습니다. Spring Batch 2.2.0부터는 동일한 Java 구성을 사용하여 배치 작업을 구성할 수 있습니다. Java 기반 구성에는 @EnableBatchProcessing 애노테이션과 두 가지 빌더가 포함됩니다.

 

 @EnableBatchProcessing 애노테이션은 Spring 계열의 다른 @Enable* 애노테이션과 유사하게 작동합니다. 여기서 @EnableBatchProcessing은 배치 작업을 구축하기 위한 기본 구성을 제공합니다. 이 기본 구성 내에서는 StepScope 및 JobScope의 인스턴스가 생성되며, 여러 빈이 자동 연결될 수 있도록 제공되며 Spring Batch에서는 아래 항목들을 자동으로 Bean에 등록해 줍니다.

  • JobRepository
  • JobLauncher
  • JobRegistry
  • JobExplorer
  • JobOperator

 

 기본 구현에서는 위의 목록에 언급된 빈을 제공하며, DataSource와 PlatformTransactionManager가 컨텍스트 내에서 빈으로 제공될 것을 요구합니다. 데이터 소스와 트랜잭션 매니저는 JobRepository와 JobExplorer 인스턴스에서 사용됩니다. 기본적으로 dataSource라는 이름의 데이터 소스와 transactionManager라는 이름의 트랜잭션 매니저가 사용됩니다. @EnableBatchProcessing 애노테이션의 속성을 사용하여 이러한 빈을 사용자 정의할 수 있습니다. 다음 예제는 사용자 지정 데이터 소스와 트랜잭션 매니저를 제공하는 방법을 보여줍니다:

@Configuration
@EnableBatchProcessing(dataSourceRef = "batchDataSource", transactionManagerRef = "batchTransactionManager")
public class MyJobConfiguration {

	@Bean
	public DataSource batchDataSource() {
		return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.HSQL)
				.addScript("/org/springframework/batch/core/schema-hsqldb.sql")
				.generateUniqueName(true).build();
	}

	@Bean
	public JdbcTransactionManager batchTransactionManager(DataSource dataSource) {
		return new JdbcTransactionManager(dataSource);
	}

	@Bean
	public Job job(JobRepository jobRepository) {
		return new JobBuilder("myJob", jobRepository)
				//define job flow as needed
				.build();
	}

}
💡 @EnableBatchProcessing 애노테이션은 하나의 구성 클래스에만 필요합니다. 해당 애노테이션이 있는 클래스를 정의하면, 앞서 설명한 모든 구성을 사용할 수 있습니다.

 

 

 버전 5.0부터는 DefaultBatchConfiguration 클래스를 통해 기본 인프라스트럭처 빈을 구성하는 대안적인 프로그래밍 방식이 제공됩니다. 이 클래스는 @EnableBatchProcessing이 제공하는 것과 동일한 빈을 제공하며, 배치 작업을 구성하기 위한 기본 클래스로 사용할 수 있습니다. 다음은 이를 사용하는 일반적인 예입니다:

@Configuration
class MyJobConfiguration extends DefaultBatchConfiguration {

	@Bean
	public Job job(JobRepository jobRepository) {
		return new JobBuilder("job", jobRepository)
				// define job flow as needed
				.build();
	}

}

 

 데이터 소스와 트랜잭션 매니저는 애플리케이션 컨텍스트에서 해결되어 job repository와 job explorer에 설정됩니다. 필요한 세터를 재정의하여 인프라스트럭처 빈 구성을 사용자 정의할 수 있습니다. 다음 예제는 인스턴스의 문자 인코딩을 사용자 정의하는 방법을 보여줍니다:

@Configuration
class MyJobConfiguration extends DefaultBatchConfiguration {

	@Bean
	public Job job(JobRepository jobRepository) {
		return new JobBuilder("job", jobRepository)
				// define job flow as needed
				.build();
	}

	@Override
	protected Charset getCharset() {
		return StandardCharsets.ISO_8859_1;
	}
}
💡 @EnableBatchProcessing은 DefaultBatchConfiguration과 함께 사용하면 안 됩니다. Spring Batch를 구성할 때는 @EnableBatchProcessing을 통한 선언적 방식과 DefaultBatchConfiguration을 확장하는 프로그래밍 방식 중 하나만 선택하여 사용해야 하며, 두 가지 방식을 동시에 사용해서는 안 됩니다.

 

 

3. JobRepository 구성 (Configuring a JobRepository)

 앞서 설명한 것처럼 JobRepository는 Spring Batch 내의 JobExecution 및 StepExecution과 같은 다양한 도메인 객체의 기본 CRUD 작업에 사용됩니다. 이는 JobLauncher, Job, Step과 같은 주요 프레임워크 기능에서 필요로 합니다.

 

 @EnableBatchProcessing을 사용할 경우 JobRepository가 기본적으로 제공됩니다. 이 섹션에서는 JobRepository를 사용자 정의하는 방법을 설명합니다. JobRepository의 구성 옵션은 @EnableBatchProcessing 애노테이션의 속성을 통해 지정할 수 있으며, 다음 예제와 같이 구성할 수 있습니다:

@Configuration
@EnableBatchProcessing(
		dataSourceRef = "batchDataSource",
		transactionManagerRef = "batchTransactionManager",
		tablePrefix = "BATCH_",
		maxVarCharLength = 1000,
		isolationLevelForCreate = "SERIALIZABLE")
public class MyJobConfiguration {

   // job definition

}

 

 여기 나열된 구성 옵션은 필수가 아닙니다. 설정하지 않을 경우 이전에 설명된 기본값이 사용됩니다. max varchar length는 샘플 스키마 스크립트의 긴 VARCHAR 열 길이에 따라 기본값이 2500으로 설정됩니다.

 

3.1 JobRepository의 트랜잭션 구성

 네임스페이스 또는 제공된 FactoryBean을 사용하는 경우, 리포지토리 주변에 트랜잭셔널 어드바이스가 자동으로 생성됩니다. 이는 실패 후 재시작을 위해 필요한 상태를 포함한 배치 메타데이터가 올바르게 영속화되도록 보장하기 위함입니다. 리포지토리 메서드가 트랜잭션이 아닌 경우 프레임워크의 동작이 제대로 정의되지 않습니다. create* 메서드 속성의 격리 수준은 별도로 지정하여, 동일한 작업을 동시에 실행하려고 할 때 한 프로세스만 성공하도록 보장합니다. 기본 격리 수준은 SERIALIZABLE이며, 이는 상당히 높은 수준입니다. 대부분의 경우 READ_COMMITTED로도 잘 작동하며, 두 프로세스가 충돌할 가능성이 거의 없으면 READ_UNCOMMITTED를 사용해도 괜찮습니다. 하지만 create* 메서드 호출이 매우 짧기 때문에, 데이터베이스 플랫폼이 이를 지원하는 한 SERIALIZED가 문제를 일으킬 가능성은 낮습니다. 그러나 이 설정은 언제든지 재정의할 수 있습니다.

 

 다음 예제는 Java에서 격리 수준을 재정의하는 방법을 보여줍니다.

@Configuration
@EnableBatchProcessing(isolationLevelForCreate = "ISOLATION_REPEATABLE_READ")
public class MyJobConfiguration {

   // job definition

}

 

 네임스페이스를 사용하지 않는 경우, AOP를 사용하여 리포지토리의 트랜잭션 동작을 구성해야 합니다. 다음 예제는 Java에서 리포지토리의 트랜잭션 동작을 구성하는 방법을 보여줍니다.

@Bean
public TransactionProxyFactoryBean baseProxy() {
	TransactionProxyFactoryBean transactionProxyFactoryBean = new TransactionProxyFactoryBean();
	Properties transactionAttributes = new Properties();
	transactionAttributes.setProperty("*", "PROPAGATION_REQUIRED");
	transactionProxyFactoryBean.setTransactionAttributes(transactionAttributes);
	transactionProxyFactoryBean.setTarget(jobRepository());
	transactionProxyFactoryBean.setTransactionManager(transactionManager());
	return transactionProxyFactoryBean;
}

 

3-2. 테이블 접두사 변경 (Changing the Table Prefix)

 JobRepository의 또 다른 수정 가능한 속성은 메타데이터 테이블의 테이블 접두사입니다. 기본적으로 모든 테이블 이름 앞에 'BATCH_'가 붙으며, 예를 들어 BATCH_JOB_EXECUTION 및 BATCH_STEP_EXECUTION이 있습니다. 하지만 테이블 이름에 스키마 이름을 추가해야 하거나 동일한 스키마 내에서 여러 메타데이터 테이블 세트를 사용하는 경우 접두사를 변경해야 합니다.

 다음 예제는 Java에서 테이블 접두사를 변경하는 방법을 보여줍니다.

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

   // job definition

}

 

 위 예제에 따라, 메타데이터 테이블에 대한 모든 쿼리는 SYSTEM.TEST_ 접두사가 추가되어 실행됩니다. 예를 들어, BATCH_JOB_EXECUTION 테이블은 SYSTEM.TEST_JOB_EXECUTION으로 참조됩니다

💡 테이블 접두사만 구성 가능하며, 테이블 및 열 이름 자체는 변경할 수 없습니다.

 

3-3. 비표준 데이터베이스 유형의 리포지토리 사용

 지원되는 플랫폼 목록에 포함되지 않은 데이터베이스 플랫폼을 사용하는 경우 SQL 변형이 충분히 유사하다면 지원되는 유형 중 하나를 사용할 수 있습니다. 이를 위해 namespace의 단축 구문 대신 JobRepositoryFactoryBean을 사용하여 가장 가까운 데이터베이스 유형을 설정할 수 있습니다.

 

 다음 예제는 JobRepositoryFactoryBean을 사용하여 가장 가까운 데이터베이스 유형을 설정하는 방법을 보여줍니다:

@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 방식으로 수동으로 연결하는 방법이 유일한 옵션이 될 수 있습니다.

 

 

4. JobLauncher 구성 (Configuring a JobLauncher)

 @EnableBatchProcessing을 사용할 때 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의 execute 메서드에 전달되며, 결국 JobExecution이 호출자에게 반환됩니다.

Figure 1. Job Launcher Sequence

 

 이 순서는 간단하며 스케줄러에서 실행할 때 잘 작동합니다. 하지만 HTTP 요청에서 실행을 시작하려고 할 때 문제가 발생할 수 있습니다. 이 경우, 작업이 비동기적으로 실행되어 TaskExecutorJobLauncher가 즉시 호출자에게 반환되어야 합니다. 이는 배치 작업과 같은 장시간 실행되는 프로세스가 필요한 경우 HTTP 요청을 열어 두는 것이 좋은 방법이 아니기 때문입니다. 다음 이미지는 비동기 호출 예시를 보여줍니다.

Figure 2. Asynchronous Job Launcher Sequence

 

 이를 위해 TaskExecutor를 구성하여 TaskExecutorJobLauncher가 즉시 반환되도록 설정할 수 있습니다. 다음은 TaskExecutorJobLauncher가 즉시 반환되도록 구성하는 Java 예시입니다:

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

 

 스프링 TaskExecutor 인터페이스의 모든 구현을 사용하여 작업이 비동기적으로 실행되는 방식을 제어할 수 있습니다.

 

 

5. Job 실행 (Running a Job)

 최소한 배치 작업을 실행하려면 두 가지가 필요합니다: 실행할 Job과 JobLauncher입니다. 두 객체는 같은 컨텍스트에 포함될 수도 있고, 다른 컨텍스트에 포함될 수도 있습니다. 예를 들어, 명령줄에서 작업을 실행하는 경우, 각 Job에 대해 새로운 JVM이 인스턴스화되므로 각 작업은 자체적인 JobLauncher를 갖게 됩니다. 그러나 웹 컨테이너 내에서, 예를 들어 HttpRequest의 범위 내에서 실행하는 경우, 보통 하나의 JobLauncher(비동기 작업 실행을 위해 설정됨)가 여러 요청에서 호출되어 작업을 실행합니다.

 

5-1. 명령줄에서 작업 실행하기 (Running Jobs from the Command Line)

 엔터프라이즈 스케줄러에서 작업을 실행하려면 명령줄이 기본 인터페이스입니다. 이는 대부분의 스케줄러(Quartz를 제외하고, NativeJob을 사용하는 경우)는 주로 셸 스크립트로 시작된 운영 체제 프로세스와 직접 작업하기 때문입니다. 자바 프로세스를 시작하는 방법에는 셸 스크립트 외에도 Perl, Ruby 또는 Ant나 Maven과 같은 빌드 도구를 사용할 수 있습니다. 그러나 대부분의 사람들이 셸 스크립트에 익숙하므로, 이 예시는 셸 스크립트에 중점을 두고 설명합니다.

 

CommandLineJobRunner

 작업을 시작하는 스크립트는 자바 가상 머신을 실행해야 하므로, 주요 진입점으로 사용할 클래스가 필요합니다. Spring Batch는 이를 위해 CommandLineJobRunner라는 구현체를 제공합니다. 이는 애플리케이션을 부트스트랩하는 한 가지 방법일 뿐이며, 자바 프로세스를 시작하는 방법은 다양합니다. CommandLineJobRunner는 네 가지 작업을 수행합니다:

  • 적절한 ApplicationContext를 로드합니다.
  • 명령줄 인수를 JobParameters로 구문 분석합니다.
  • 인수에 따라 적절한 작업을 찾습니다.
  • 애플리케이션 컨텍스트에 제공된 JobLauncher를 사용해 작업을 실행합니다.

 이 모든 작업은 전달된 인수만으로 수행됩니다. 다음 표는 필수 인수를 설명합니다:

 

Table 1. CommandLineJobRunner arguments

jobPath ApplicationContext를 생성하는 데 사용되는 XML 파일의 위치입니다.
이 파일에는 전체 작업을 실행하는 데 필요한 모든 내용이 포함되어야 합니다.
jobName 실행할 작업의 이름입니다.

 

 이 인수는 반드시 첫 번째로 파일 경로를, 두 번째로 작업 이름을 순서대로 전달해야 합니다. 그 이후의 모든 인수는 작업 매개변수로 간주되며 JobParameters 객체로 변환됩니다. 이들은 name=value 형식이어야 합니다.

 

 다음 예시는 자바에서 작업 매개변수로 날짜를 전달하는 방법을 보여줍니다:

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

 

💡 기본적으로 CommandLineJobRunner는 DefaultJobParametersConverter를 사용하여 키/값 쌍을 자동으로 식별 작업 매개변수로 변환합니다. 그러나 작업 매개변수가 식별용인지 아닌지를 명시적으로 지정할 수 있으며, 이는 true 또는 false로 끝나는 접미사를 통해 설정할 수 있습니다.

다음 예시에서 schedule.date는 식별 작업 매개변수이며, vendor.id는 그렇지 않습니다.
<bash$ java CommandLineJobRunner endOfDayJob.xml endOfDay \
                                 schedule.date=2007-05-05,java.time.LocalDate,true \
                                 vendor.id=123,java.lang.Long,false

<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에서 주 클래스를 선언하려면 매니페스트를 사용하는 것이 좋습니다. 그러나 단순화를 위해 클래스가 직접 사용되었습니다. 이 예시는 The Domain Language of Batch의 EndOfDay 예제를 사용합니다. 첫 번째 인수는 io.spring.EndOfDayJobConfiguration으로, 이는 Job을 포함하는 구성 클래스의 완전한 클래스 이름입니다. 두 번째 인수인 endOfDay는 실행할 작업 이름을 나타냅니다. 마지막 인수인 schedule.date=2007-05-05,java.time.LocalDate는 java.time.LocalDate 타입의 JobParameter 객체로 변환됩니다.

 

 다음은 endOfDay의 자바 샘플 구성을 보여줍니다:

@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();
    }
}

 

 앞서 설명한 예시는 과도하게 단순화된 것이며, Spring Batch에서 배치 작업을 실행하려면 여러 추가 요구 사항이 있지만, CommandLineJobRunner에서 필요한 두 가지 주요 요구 사항인 Job과 JobLauncher를 보여주기 위한 예시입니다.

 

Exit Codes

 배치 작업을 명령줄에서 실행할 때, 엔터프라이즈 스케줄러가 자주 사용됩니다. 대부분의 스케줄러는 비교적 단순하며 프로세스 수준에서만 작동합니다. 즉, 스케줄러는 운영 체제 프로세스(예: 호출되는 셸 스크립트)에 대해서만 알 수 있습니다. 이 경우, 작업의 성공 여부나 실패 여부를 스케줄러에 전달하는 유일한 방법은 반환 코드(return code)를 사용하는 것입니다. 반환 코드는 프로세스가 실행된 결과를 나타내기 위해 스케줄러에게 반환되는 숫자입니다. 가장 간단한 경우에는 0이 성공, 1이 실패를 나타냅니다. 그러나 더 복잡한 시나리오도 있을 수 있습니다. 예를 들어, “작업 A가 4를 반환하면 작업 B를 시작하고, 5를 반환하면 작업 C를 시작하라”는 경우입니다. 이러한 동작은 스케줄러 수준에서 구성되지만, Spring Batch와 같은 처리 프레임워크에서 특정 배치 작업에 대해 종료 코드(exit code)를 숫자로 반환할 수 있는 방법을 제공하는 것이 중요합니다. Spring Batch에서는 이를 ExitStatus로 캡슐화하고 있으며, 이는 5장에서 더 자세히 다뤄집니다. 종료 코드에 대해 논의할 때 중요한 점은 ExitStatus에 exitCode 속성이 있으며, 이 값은 프레임워크나 개발자에 의해 설정되고 JobLauncher에서 반환된 JobExecution의 일부로 반환된다는 것입니다. CommandLineJobRunner는 이 문자열 값을 숫자로 변환하는 데 ExitCodeMapper 인터페이스를 사용합니다:

public interface ExitCodeMapper {

    public int intValue(String exitCode);

}

 

 ExitCodeMapper의 본질적인 계약은 문자열 종료 코드에 대해 숫자 형태로 반환하는 것입니다. 작업 실행기에서 사용하는 기본 구현은 SimpleJvmExitCodeMapper로, 이는 완료 시 0을, 일반적인 오류 시 1을, 작업 실행기 오류(예: 제공된 컨텍스트에서 Job을 찾을 수 없는 경우) 시 2를 반환합니다. 만약 위 세 가지 값 이상이 필요한 경우, ExitCodeMapper 인터페이스의 사용자 정의 구현체를 제공해야 합니다. CommandLineJobRunner는 ApplicationContext를 생성하는 클래스이므로, 이를 '연결'할 수 없고, 따라서 덮어써야 할 값들은 자동으로 주입되어야 합니다. 즉, ExitCodeMapper의 구현체가 BeanFactory 내에 있으면, 컨텍스트가 생성된 후 실행기에 주입됩니다. 자신의 ExitCodeMapper를 제공하려면, 해당 구현체를 루트 레벨 빈으로 선언하고, 그것이 실행기에 의해 로드된 ApplicationContext의 일부가 되도록 해야 합니다.

 

5-2. 웹 컨테이너 내에서 작업 실행 (Running Jobs from within a Web Container)

 전통적으로 오프라인 처리(예: 배치 작업)는 앞서 설명한 대로 명령줄에서 실행되었습니다. 그러나 배치 작업을 HttpRequest 내에서 실행하는 것이 더 나은 경우가 많습니다. 이러한 사용 사례에는 보고서 생성, 즉석 작업 실행, 웹 애플리케이션 지원 등이 포함됩니다. 배치 작업은 본질적으로 실행 시간이 길기 때문에 가장 중요한 고려사항은 작업을 비동기적으로 실행하는 것입니다.

Figure 1. Asynchronous Job Launcher Sequence From Web Container

 

 

 이 경우, 컨트롤러는 Spring MVC 컨트롤러입니다. 컨트롤러는 비동기적으로 작업을 실행할 수 있도록 설정된 JobLauncher를 사용하여 작업을 시작하고, 즉시 JobExecution을 반환합니다. 이때 작업은 여전히 실행 중일 수 있습니다. 그러나 비차단(non-blocking) 방식 덕분에 컨트롤러는 즉시 반환할 수 있으며, 이는 HttpRequest를 처리할 때 필수적인 동작입니다.

@Controller
public class JobLauncherController {

    @Autowired
    JobLauncher jobLauncher;

    @Autowired
    Job job;

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

 

 

6. 고급 메타데이터 사용 (Advanced Metadata Usage)

 지금까지 JobLauncher와 JobRepository 인터페이스에 대해 설명했습니다. 이 둘은 배치 작업을 간단히 실행하고 배치 도메인 객체에 대한 기본 CRUD 작업을 수행하는 역할을 합니다:

Figure 1. Job Repository

 

 

 JobLauncher는 JobRepository를 사용하여 새로운 JobExecution 객체를 생성하고 이를 실행합니다. 이후 Job과 Step 구현체는 동일한 JobRepository를 사용하여 실행 중에 실행 상태를 업데이트합니다. 기본적인 작업들은 간단한 시나리오에 충분하지만, 수백 개의 배치 작업과 복잡한 스케줄링 요구 사항을 가진 대규모 배치 환경에서는 메타데이터에 대한 보다 고급 접근이 필요합니다.

Figure 2. Advanced Job Repository Access

 

 JobExplorer와 JobOperator 인터페이스는 메타데이터를 쿼리하고 제어하는 기능을 추가로 제공합니다.

 

6-1. Querying the Repository

 고급 기능을 사용하기 전에 가장 기본적인 요구사항은 기존 실행에 대해 저장소를 쿼리할 수 있는 기능입니다. 이 기능은 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);
}

 

 JobExplorer는 그 메서드 시그니처에서 볼 수 있듯이 JobRepository의 읽기 전용 버전이며, JobRepository처럼 팩토리 빈을 사용해 쉽게 구성할 수 있습니다.

 

 다음 예제는 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는 동일한 테이블을 사용하기 때문에 접두사를 설정할 수 있는 기능도 필요합니다.

 

 다음 예제는 Java에서 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();
}
...

 

6-2. JobRegistry

 JobRegistry(및 그 부모 인터페이스인 JobLocator)는 필수는 아니지만, 컨텍스트에서 사용 가능한 작업을 추적하거나 중앙에서 배치 작업을 관리하고 싶을 때 유용합니다. 예를 들어, 다른 곳(예: 자식 컨텍스트)에서 생성된 작업들을 애플리케이션 컨텍스트 내에서 모을 때도 유용합니다. 또한, 등록된 작업의 이름과 기타 속성을 조작하기 위해 사용자 정의 JobRegistry 구현을 사용할 수 있습니다. 프레임워크에서 제공하는 구현은 직관적인 맵을 기반으로, 작업 이름과 작업 인스턴스를 연결하는 방식입니다.

 

 @EnableBatchProcessing을 사용하면 JobRegistry가 자동으로 제공됩니다. 다음 예제는 자신의 JobRegistry를 구성하는 방법을 보여줍니다:

...
// This is already provided via the @EnableBatchProcessing but can be customized via
// overriding the bean in the DefaultBatchConfiguration
@Override
@Bean
public JobRegistry jobRegistry() throws Exception {
	return new MapJobRegistry();
}
...

 

 JobRegistry를 채우는 방법은 여러 가지가 있습니다: 빈 후처리기를 사용하거나, 스마트 초기화 싱글턴을 사용하거나, 레지스트러 생애 주기 컴포넌트를 사용할 수 있습니다. 이들에 대한 설명은 후속 섹션에서 다룹니다.

 

JobRegistryBeanPostProcessor

 JobRegistryBeanPostProcessor는 작업이 생성될 때마다 모든 작업을 등록할 수 있는 빈 후처리기입니다. 다음 예제는 Java에서 JobRegistryBeanPostProcessor를 포함하는 방법을 보여줍니다.

@Bean
public JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor(JobRegistry jobRegistry) {
    JobRegistryBeanPostProcessor postProcessor = new JobRegistryBeanPostProcessor();
    postProcessor.setJobRegistry(jobRegistry);
    return postProcessor;
}

 

 비록 필수는 아니지만, 이 예제에서 후처리기는 자식 컨텍스트에 포함될 수 있도록 id가 지정되었습니다(예: 부모 빈 정의로서) 이 후처리기는 그곳에서 생성된 모든 작업이 자동으로 등록되도록 합니다.

 

 버전 5.1부터 @EnableBatchProcessing 애노테이션은 자동으로 애플리케이션 컨텍스트에 jobRegistryBeanPostProcessor 빈을 등록합니다.

 

JobRegistrySmartInitializingSingleton

 JobRegistrySmartInitializingSingleton는 JobRegistry 내에서 모든 싱글턴 작업을 등록하는 SmartInitializingSingleton입니다. 다음 예제는 Java에서 JobRegistrySmartInitializingSingleton을 정의하는 방법을 보여줍니다:

@Bean
public JobRegistrySmartInitializingSingleton jobRegistrySmartInitializingSingleton(JobRegistry jobRegistry) {
    return new JobRegistrySmartInitializingSingleton(jobRegistry);
}

 

AutomaticJobRegistrar

 AutomaticJobRegistrar는 자식 컨텍스트를 생성하고 해당 컨텍스트에서 작업이 생성될 때마다 이를 JobRegistry에 등록하는 생애 주기 컴포넌트입니다. 이를 통해 얻을 수 있는 장점은 자식 컨텍스트의 작업 이름이 여전히 전역적으로 고유해야 하지만, 의존성에는 "자연스러운" 이름을 부여할 수 있다는 점입니다. 예를 들어, 각 파일에 하나의 작업만 있지만 동일한 빈 이름을 가진 다른 ItemReader 정의들이 있을 수 있습니다. 이러한 파일들이 동일한 컨텍스트에 포함되면 reader 정의가 충돌하고 덮어쓰여질 수 있지만, 자동 레지스트러를 사용하면 이러한 충돌을 방지할 수 있습니다. 이를 통해 애플리케이션의 별도의 모듈에서 기여된 작업을 통합하는 데 용이해집니다.

 

 다음 예제는 Java에서 AutomaticJobRegistrar를 포함하는 방법을 보여줍니다.

@Bean
public AutomaticJobRegistrar registrar() {

    AutomaticJobRegistrar registrar = new AutomaticJobRegistrar();
    registrar.setJobLoader(jobLoader());
    registrar.setApplicationContextFactories(applicationContextFactories());
    registrar.afterPropertiesSet();
    return registrar;

}

 

 레지스트러는 두 가지 필수 속성을 가집니다. ApplicationContextFactory 배열(이전 예제에서 편리한 팩토리 빈을 사용하여 생성)과 JobLoader입니다. JobLoader는 자식 컨텍스트의 생애 주기를 관리하고 작업을 JobRegistry에 등록하는 책임을 집니다.

 

 ApplicationContextFactory는 자식 컨텍스트를 생성하는 책임을 집니다. 가장 일반적인 사용법은 이전 예제에서처럼 ClassPathXmlApplicationContextFactory를 사용하는 것입니다. 이 팩토리의 특징 중 하나는 기본적으로 부모 컨텍스트에서 자식 컨텍스트로 일부 구성을 복사한다는 점입니다. 예를 들어, 자식 컨텍스트에서 PropertyPlaceholderConfigurer나 AOP 구성을 재정의할 필요가 없으며, 부모와 동일해야 하는 경우 이를 자동으로 상속받습니다.

 

 AutomaticJobRegistrar는 JobRegistryBeanPostProcessor와 함께 사용할 수 있습니다(단, DefaultJobLoader를 함께 사용해야 합니다). 예를 들어, 메인 부모 컨텍스트와 자식 위치에서 정의된 작업들이 있는 경우 이를 결합하는 것이 유용할 수 있습니다.

 

6-3. JobOperator

 앞서 설명한 바와 같이, JobRepository는 메타데이터에 대한 CRUD 작업을 제공하고, JobExplorer는 메타데이터에 대해 읽기 전용 작업을 제공합니다. 그러나 이러한 작업들은 Job을 중지, 재시작 또는 요약하는 등 일반적인 모니터링 작업을 수행할 때 함께 사용하는 것이 가장 유용합니다. 이러한 작업들은 배치 작업자들이 흔히 수행하는 작업입니다. Spring Batch는 이러한 작업을 JobOperator 인터페이스를 통해 제공합니다.

public interface JobOperator {

    List<Long> getExecutions(long instanceId) throws NoSuchJobInstanceException;

    List<Long> getJobInstances(String jobName, int start, int count)
          throws NoSuchJobException;

    Set<Long> getRunningExecutions(String jobName) throws NoSuchJobException;

    String getParameters(long executionId) throws NoSuchJobExecutionException;

    Long start(String jobName, String parameters)
          throws NoSuchJobException, JobInstanceAlreadyExistsException;

    Long restart(long executionId)
          throws JobInstanceAlreadyCompleteException, NoSuchJobExecutionException,
                  NoSuchJobException, JobRestartException;

    Long startNextInstance(String jobName)
          throws NoSuchJobException, JobParametersNotFoundException, JobRestartException,
                 JobExecutionAlreadyRunningException, JobInstanceAlreadyCompleteException;

    boolean stop(long executionId)
          throws NoSuchJobExecutionException, JobExecutionNotRunningException;

    String getSummary(long executionId) throws NoSuchJobExecutionException;

    Map<Long, String> getStepExecutionSummaries(long executionId)
          throws NoSuchJobExecutionException;

    Set<String> getJobNames();

}

 

 위에서 언급한 작업들은 JobLauncher, JobRepository, JobExplorer, JobRegistry와 같은 여러 인터페이스에서 나온 메서드를 포함하고 있습니다. 이로 인해, 제공되는 JobOperator의 구현체인 SimpleJobOperator는 많은 의존성을 가집니다.

 

 다음 예제는 Java에서 SimpleJobOperator를 정의하는 전형적인 방법을 보여줍니다.

 /**
  * All injected dependencies for this bean are provided by the @EnableBatchProcessing
  * infrastructure out of the box.
  */
 @Bean
 public SimpleJobOperator jobOperator(JobExplorer jobExplorer,
                                JobRepository jobRepository,
                                JobRegistry jobRegistry,
                                JobLauncher jobLauncher) {

	SimpleJobOperator jobOperator = new SimpleJobOperator();
	jobOperator.setJobExplorer(jobExplorer);
	jobOperator.setJobRepository(jobRepository);
	jobOperator.setJobRegistry(jobRegistry);
	jobOperator.setJobLauncher(jobLauncher);

	return jobOperator;
 }

 

 버전 5.0부터 @EnableBatchProcessing 애노테이션은 자동으로 애플리케이션 컨텍스트에 job operator 빈을 등록합니다.

💡 만약 JobRepository에 테이블 접두사를 설정한 경우, JobExplorer에도 접두사를 설정하는 것을 잊지 마세요.

 

6-4. JobParametersIncrementer

 JobOperator의 대부분의 메서드는 자명하며, 해당 인터페이스의 Javadoc에서 더 자세한 설명을 찾을 수 있습니다. 하지만 startNextInstance 메서드는 주목할 만한 부분입니다. 이 메서드는 항상 새로운 Job 인스턴스를 시작합니다. 이는 JobExecution에 심각한 문제가 있을 때, Job을 처음부터 다시 시작해야 하는 경우에 매우 유용합니다. 만약 이전의 파라미터 집합과 다르다면 JobLauncher는 새로운 JobParameters 객체가 필요하지만, startNextInstance 메서드는 Job에 연결된 JobParametersIncrementer를 사용하여 새로운 인스턴스로 강제로 시작합니다.

public interface JobParametersIncrementer {

    JobParameters getNext(JobParameters parameters);

}

 

 JobParametersIncrementer의 계약은 주어진 JobParameters 객체에서 필요한 값을 증가시켜 "다음" JobParameters 객체를 반환하는 것입니다. 이 전략은 프레임워크가 어떤 JobParameters의 변경이 "다음" 인스턴스를 만드는지 알 수 없기 때문에 유용합니다. 예를 들어, JobParameters에 날짜만 있는 경우, 다음 인스턴스를 생성할 때 그 값을 하루나 일주일 증가시켜야 할지 모르기 때문입니다(예를 들어, 작업이 주간 작업이라면). Job을 식별하는 데 도움이 되는 모든 숫자 값에 대해서도 동일한 일이 적용됩니다.

public class SampleIncrementer implements JobParametersIncrementer {

    public JobParameters getNext(JobParameters parameters) {
        if (parameters==null || parameters.isEmpty()) {
            return new JobParametersBuilder().addLong("run.id", 1L).toJobParameters();
        }
        long id = parameters.getLong("run.id",1L) + 1;
        return new JobParametersBuilder().addLong("run.id", id).toJobParameters();
    }
}

 

 이 예제에서는 run.id라는 키 값을 사용하여 JobInstances를 구분합니다. 만약 JobParameters가 null이라면, 그 Job이 한 번도 실행되지 않았다고 간주하고 초기 상태를 반환합니다. 그렇지 않으면, 이전 값을 가져와 하나를 증가시켜 반환합니다.

 

 자바에서 정의된 Job의 경우, 빌더에서 제공하는 incrementer 메서드를 통해 Job에 증가기를 연결할 수 있습니다.

@Bean
public Job footballJob(JobRepository jobRepository) {
    return new JobBuilder("footballJob", jobRepository)
    				 .incrementer(sampleIncrementer())
    				 ...
                     .build();
}

 

6-5. Sopping a Job

 JobOperator의 가장 일반적인 사용 사례 중 하나는 Job을 정상적으로 중지하는 것입니다.

Set<Long> executions = jobOperator.getRunningExecutions("sampleJob");
jobOperator.stop(executions.iterator().next());

 

 중지 작업은 즉시 이루어지지 않으며, 특히 실행이 프레임워크가 제어할 수 없는 개발자 코드(예: 비즈니스 서비스)에서 실행 중일 때는 즉시 중지할 수 없습니다. 그러나 제어가 프레임워크로 돌아오면, 현재 StepExecution의 상태를 BatchStatus.STOPPED로 설정하고 이를 저장한 후, JobExecution도 동일하게 처리한 뒤 종료됩니다.

 

6-6. Aborting a Job

 실패한 Job 실행은 다시 시작할 수 있습니다(만약 Job이 재시작 가능하다면). 그러나 상태가 ABANDONED인 Job 실행은 프레임워크에서 재시작할 수 없습니다. ABANDONED 상태는 또한 StepExecution에서 사용되어, 이전 실패한 실행에서 스킵 가능한 상태로 표시됩니다. 만약 실행 중인 Job이 이전 실패한 실행에서 ABANDONED로 표시된 Step을 만나면, 정의된 작업 흐름과 StepExecution의 종료 상태에 따라 다음 Step으로 넘어갑니다.

 

 만약 프로세스가 죽었다면(예: kill -9 또는 서버 실패), Job은 물론 실행 중이지 않지만, JobRepository는 이를 알지 못합니다. 프로세스가 종료되기 전에 이를 알려주지 않기 때문입니다. 이 경우, 실행 상태가 실패하거나 중단된 것으로 간주되도록 수동으로 상태를 변경해야 합니다(상태를 FAILED 또는 ABANDONED로 변경). 이는 비즈니스 결정이며, 이를 자동화할 수는 없습니다. 상태를 FAILED로 변경하는 것은 해당 Job이 재시작 가능하고, 재시작 데이터가 유효하다는 확신이 있을 때만 해야 합니다.

728x90