Spring/Spring Batch

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

공대키메라 2023. 11. 23. 16:43

지난 시간에는 Spring Batch의 도메인들에 대해 읽어보았다.

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

 

지난 내용은 spring batch에서 사용되는 domain들에 대해 설명했고 실제로 어떻게 사용하는지에 대한 설명은 없었기에 좀 재미없었을 수 있다. (맞아 쌉노잼이야 에잉 퉤텟!)

 

이번 시간에는 잡을 설정하고 실행하는 방법에 대해 읽어볼 것이다.

 


 

domain section에서 전반적인 아키텍처 디자인을 다음 그림을 사용해서 논의했다.

 

Figure 1. Batch Stereotypes

 

Job 객체가 Step을 위한 간단한 컨테이너처럼 보이지만 많은 설정 옵션을 알아야 한다. 더욱이, 어떻게 Job이 작동할 수 있고 어떻게 그 메타데이터가 실행동안 저장될 수 있는지에 대한 많은 옵션을 고려해야한다. 이 챕터는 다양한 설정 옵션과 Job의 런타임 관심(concern)에 대해 설명한다.

 

1. Configuring a Job

Job 인터페이스의 다양한 구현체들이 있다. 하지만 builder는 구성의 차이를 추상화한다. 다음의 예시는 footballJob을 생성한다.

 

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

 

Job(그리고 전형적으로 그것 안에 Step) 은 JobRepository가 필요하다. JobRepository의 설정은 Java Configuration을 통해 다룬다.

 

앞의 예시는 세 개의 Step 인스턴스로 구성된 Job을 보여준다. builder와 연관있는 Job은 또한 병렬화(parallization), 선언적 흐름 제어(declarative flow control) 그리고 흐름 정의의 외부화(externalization of flow control)를 돕는 다른 요소들을 포함할 수 있다.

 

2. Restartability

배치 잡을 실행할 때 중요한 사항은 그것이 재시작 될 떄다. 만약 JobExecution이 이미 특별한 JobInstance로 존재하면 Job의 실행은 재시작으로 여겨진다. 이상적으로 모든 잡들이 그들이 남겨진 곳에서 시작할 수 있어야만 하지만 가능하지 않은 경우에는 시나리오가 있다. 시나리오에서 새로운 JobInstance가 생성되도록 하는건 전적으로 개발자에게 달렸다. 하지만 Spring Batch는 몇 개의 도움을 제공한다. 만약 Job이 절대로 다시 시작하면 안된고 항상 새로운 JobInstance의 부분으로 실행되어야만 한다면 재시작 설정을 false로 세팅할 수 있다.

 

다음 예시가 Java에서 restartable 필드를 false로 세팅하는 방법을 보여준다.

 

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

 

이것을 다른 방식으로 표현하면 restartable을 false로 설정하는 것은 "이 Job은 다시 시작을 지원하지 않습니다"를 의미한다. Job을 재시작하는것은 JobRestartException이 던져지기 때문에 안된다. 다음의 JUnit code가 오류를 야기한다. 

 

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
}

 

재시작할 수없는 잡을 위해 JobExecution을 생성하는 첫 시도는 오류를 발생시키지 않는다. 하지만 두 번째 시도는 JobRestartException을 던진다.

 

3. Intercepting Job Execution

Job의 실행 과정 동안에 그 lifecycle에 대한 다양한 이벤트를 알려서 커스텀 코드(custom code)가 실행될 수 있도록 하는것은 유용할 수 있다. SimpleJob은 적절한 때에 JobListener를 호출해서 이것을 할 수 있도록 한다.

 

public interface JobExecutionListener {

    void beforeJob(JobExecution jobExecution);

    void afterJob(JobExecution jobExecution);
}

 

job에 listener를 설정해서 SimpleJob에 JobListener을 추가할 수 있다.

 

다음 예시는 어떻게 자바 job 정의로 listener method를 추가하는지 보여준다.

 

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

 

afterJob method 후에는 Job의 성공 실패 여부에 상관없이 호출된다는 점을 유의하라. 만약 성공 혹은 실패를 결정할 필요가 있다면 JobExection으로부터 그 정보를 얻을 수 있다.

 

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

 

이 인터페이스과 상응하는 어노테이션은 다음이 있다

  • @BeforeJob
  • @AfterJob
찾아보니 @BeforeJob과  @AfterJob은 JobExecutionListener 인터페이스의 메서드를 어노테이션으로 간편하게 사용할 수 있게 해준다고 한다.
하지만 이 방식은 상대적으로 간단한 로직에 적합하다고 한다. 제한적일 수도 있기 때문이다.

 

4.  JobParameterValidator

XML namespace에 정의되거나 AbstractJob의 서브클래스를 사용하는 job은 선택적으로 런타임에 job parameter를 위해 validator를 선언할 수 있다. 예를 들어 보두 의무적인 파라미터를 가지고 Job이 실행되는지 확인해야 할 때 유용하다. 

간단한 필수적이고 선택적인 파라미터들의 결합을 제한할 수 있는 DefaultJobParametersValidator이 있다. 더 복잡한  제약사항에 대해 자신만의 인터페이스를 구현할 수 있다.

 

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

 

5. Job Configuration

Spring 3은 XML 대신에 Java로 어플리케이션을 설정할 능력을 가져왔다.  Spring Batch 2.0.0에서 같은 Java 설정을 사용해서 batch job을 설정할 수 있었다. Java 기반의 설정에 세 개의 컴포넌트들이 있다. (@EnableBatchProcessing 어노테이션과 두 개의 빌더)

 

@EnableBatchProcessing 어노테이션은 spring family에서 다른 @Enable로 시작하는 어노테이션들과 비슷하지 작동한다. 이 경우에 @EnableBatchProcessing은 배치 잡을 생성하는 기초 설정을 제공한다. 기본 설정 내에서 StepScope와  JobScope의 인스턴스가 생성는데 autowired가 가능하게 만들어진 많은 bean들도 생성된다.

 

  • JobRepository : jobRepository로 이름붙여진 bean
  • JobLauncher : jobLaunncher로 이름붙여진 bean
  • JobRegistry : jobRegistry로 이름붙여진 bean
  • JobExplorer : jobExplorer로 이름붙여진 bean
  • JobOperator : jobOperator로 이름붙여진 bean

기본 구현체들은 앞의 list에 언급된 빈들을 제공하고 context안에서 빈으로서 제공되는 DataSource와 PlatformTransactionManger를 제공한다. data source와 transaction manager는 JobRepository와 JobExplorer 인스턴스가 사용한다. 기본적으로 data source는  dataSource라 이름이 지어지고 transaction manager는 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);
	}

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

}

 

 

* Note!

오직 하나의 설정 클래스만 @EnableBatchProcessing 어노테이션을 가져야 한다. 일단 
주석이 달린 클래스가 있으면 앞에서 설명한 모든 구성을 갖게 됩니다.

 

대체재로 버전 5.0의 spring은 기본 인프라 빈을 설정하는 프로그래밍적인 방법이 DefaultBatchConfiguration 클래스를 통해 제공된다. 이 클래스는 @EnableBatchProcessing 에 의해 제공되는 동일한 빈을 제공하고 batch job을 설정하기 위해 기본 class로 사용될 수 있다. 다음 코드가 흔한 사용 방법 예시다.

 

@Configuration
class MyJobConfiguration extends DefaultBatchConfiguration {

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

}

 

 

데이터 소스 및 트랜잭션 관리자는 애플리케이션 컨텍스트로부터 해결되고 작업 저장소 및 작업 탐색기에 설정될 것이다.

필요한 setter를 오버라이드해서 인프라 빈의 설정을 커스터마이징 할 수 있다. 다음 예시는 어떻게 인스턴스의 character encoding를 커스터마이징 하는지 보여준다.

 

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

 

* Note

@EnableBatchProcessing은 DefaultBatchConfiguration와 함께 사용되면 안된다. @EnableBatchProcessing을 통해 Spring Batch설정을 하는 선언적인 방법(declarative way)로 사용하던지 DefaultBatchConfiguration을 확장하는 프로그래밍밍적 방식으로 사용하던지 해야 한다. 동시에 사용하지 않고서 말이다.

 

6. Configuring a JobRepository

@EnableBatchProcessing을 사용할 떄 JobRepository가 제공된다. 이 섹션은 어떻게 자신의 프로젝트를 설정하는지에 대해 알려준다. 이전에 봤듯이, JobRepository는 JobExecution 및 StepExecution 같은 Spring Batch안에서 도메인 객체를 저장하는 다양한 작업들을 기본 CRUD를 위해 사용한다. 이건 JobLauncher, Job, Step같은 주요 프레임워크 특징들의 대다수를 필요로 한다. 

 

dataSource 나 transactionManager 말고는 앞서 나열된 다른 어떠한 설정 옵션들도 필요 없다. 만약 이 두개가 setting되있지 않다면 이전에 보았던 default가 사용된다. varchar의 기본 최대 길이는 2500인데 이는 sample schema script에 있는 긴 varchar 컬럼의 길이다.

 

7. Transaction Configuration for the JobRepository

namespace나 제공된 FactoryBean이 사용된다면 transactional advice가 자동적으로 repository 주변에 생성된다. 이것은 batch 메타데이터가, 실패 후에 재시작이 필요한 상태를 포함해서, 확실히 저장되도록 한다. 프레임워크의 행동은 만약 repository 메소드가 transactional하지 않으면 잘 정의되지 않는다. create* method 속성의 고립 레벨(isonlation level)은 부분적으로 정의되어 있는데 잡이 실행될 때 만약 두 개의 처리가 같은 시간에 같은 잡을 처리하려 하면 오직 하나만 성공한다. 그러한 메소드의 기본 고립 레벨은 SERIALIZABLE인데 강제적으로 멈춘다. READ_COMMIT은 보통 동등하게 잘 작동한다. READ_UNCOMMITED는 만약 두 개의 프로세스가 이 방식으로 충돌하지 않는다면 괜찮다.

하지만 create* method에 대한 호출은 매우 짧기 때문에 데이터베이스 플랫폼이 지원하는 한 SERIALIZED 가 문제를 일으킬 거 같지는 않다. 하지만 이 설정을 오버라이드 할 수 있다. 

 

다음 예시는 어떻게 Java에서 고립 레벨을 오버라이드하는지 보인다.

 

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

   // job definition

}

 

만약 namespace를 사용하지 않는다면 AOP를 사용해서 repository의 트랜잭션 행동을 설정해야만 한다.

 

다음 예시는 Java에서 repository의 트랜잭현 행동을 어떻게 설정하는지 보여준다.

 

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

 

 

spring batch docs가 혼자 무슨말을 하는지 이해가 안돼서 찾아보았다.

왜JobRepository에서 Transaction이 언급되고 있는가?
그건 JobRepository가 배치 작업의 메타데이터를 저장하는데 사용되기 때문이다.
이 정보가 정확하게 유지가 되려면 결국 트랜잭션 안에서 실행되어야 한다. 그래서 제목이 Transaction Configuration for the JobRepository 인 것 같다!

격리 수준도 다시 정리하면 다음과 같다.

SERIALIZABLE: 가장 높은 수준의 격리. 두 개 이상의 프로세스가 동시에 같은 작업을 시작하려 할 때 오직 하나만 성공하도록 보장한다. 하지만 너무 엄격할 수 있다.

READ_COMMITTED: 대부분의 경우에 적합하며 SERIALIZABLE보다 덜 엄격합니다.READ_UNCOMMITTED: 두 프로세스가 충돌할 가능성이 낮을 때 사용할 수 있습니다.

create* 메서드의 기본 격리 수준은 SERIALIZABLE이지만, 필요에 따라 변경할 수 있습니다.

좀 알아먹기 쉽게 정리하지 참... ㅠㅠ (GPT형 고마워요!)

 

 

그런데 왜 트랜잭션 격리가 필요한지도 좀 궁금했다. (궁금하면 500원)


다음 글에 궁금한 점을 정리했다.

더보기

트랜잭션 격리 수준의 필요성

  1. 데이터 무결성 보장: 트랜잭션은 작업이 데이터베이스와 상호 작용할 때 발생하는 모든 변경사항을 안전하게 처리합니다. 격리 수준을 적절히 설정함으로써 다른 트랜잭션이 동시에 같은 데이터를 수정하지 못하게 하여 데이터의 무결성을 보장합니다.
  2. 동시성 제어: Spring Batch 작업은 종종 여러 인스턴스가 동시에 실행될 수 있습니다. 예를 들어, 같은 작업을 실행하는 여러 프로세스가 있을 때, 이들 사이의 충돌을 방지해야 합니다. 트랜잭션 격리 수준은 이러한 동시성 문제를 관리하는 데 도움이 됩니다.
  3. 작업의 재시작과 복구: Spring Batch는 실패 후 재시작 기능을 지원합니다. 트랜잭션이 제대로 관리되지 않으면, 재시작 시 데이터 일관성 문제가 발생할 수 있습니다. 격리 수준을 통해 이러한 문제를 예방할 수 있습니다.

격리 수준의 영향

  • SERIALIZABLE: 가장 높은 격리 수준으로, 트랜잭션이 완전히 격리되어 실행됩니다. 하지만 동시성이 매우 낮아질 수 있으며, 성능에 영향을 줄 수 있습니다.
  • READ_COMMITTED: 대부분의 데이터베이스 시스템에서 기본값으로 사용되며, 적당한 수준의 격리와 동시성을 제공합니다.
  • READ_UNCOMMITTED: 가장 낮은 격리 수준으로, 동시성은 높지만, 다른 트랜잭션이 아직 커밋하지 않은 데이터를 읽을 수 있어 데이터 무결성 문제가 발생할 수 있습니다.

따라서, Spring Batch에서 트랜잭션 격리 수준을 적절히 설정하는 것은 데이터 무결성과 시스템의 안정성을 보장하는 데 매우 중요합니다.

 

라고 GPT형님이 잘 정리해서 알려주었다.

출처를 물어보니 특정 문서나 출판물에 직접적으로 근거한 것은 아니라고 한다.

그러면 나도 동일한 것을 보고 있는 것인데 왜 수준이 다르지... ㅠㅠ

 


 

이번 시간에는 Configuring and Running a Job에서 잡을 설정하고, 자바에서 어떻게 설정을 하고 JobRepository에 트랜잭션 level의 설정법을 알아보았다.

 

너무 글이 길어지는 관계로 다음에 이어서 글을 더 읽어보겠다.

그리고 충격적이게고 오늘 전후로 무언가 spring docs가 변경이 되었다. 필자는 5.0.4 기준으로 글을 다 읽어갈 계획이다.

 

출처:

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