[스프링] 스프링 배치 기초

스프링 배치 기본 동작 구조

인프라 구조

스프링 배치는 잡(job)을 시작하고 잡 실행 메타데이터를 데이터베이스 저장하는 관련 컴포넌트를 제공한다. 이러한 컴포넌트들은 배치 애플리케이션의 실행을 도와주기 때문에 개발 시 반드시 직접 사용할 필요는 없지만, 애플리케이션 구동 시 관련 컴포넌들이 적어도 한 번, 그리고 정상적으로 구성되어야 한다.

잡 실행 관련 컴포넌트는 Job, JobParameters, JobInstance, JobExecution이며, 잡 실행 메타데이터를 데이터베이스에 저장하는 관련 컴포넌트는 JobRepository이다.


컴포넌트

JobJobParameters는 런타임 로직 파라미터화를 위한 것이다. 파라미터에 따른 JobInstance 객체를 인스턴스화를 함으로써 서로 다른 잡 실행 구별이 가능하다. 즉, JobInstanceJob JobParameter의 결합으로 서로 구별되며 동일한 Job에 대해 서로 다른 JobParameters가 결합되어 서로 다른 JobInstance가 생성된다.

서로 다른 파라미터로 실행한 잡은 서로 다른 잡이다. 동일한 파라미터로 실행한 잡은 동일한 잡이다. 서로 다른 파라미터로 잡을 실행할 때마다 서로 다른 JobInstance가 생성되며 동일한 파라미터로 잡을 실행할 때마다 서로 다른 JobExecution이 생성된다. 즉, 다음과 같이 구별할 수 있다.

  • 서로 다른 잡 실행 구별을 위한 객체 = JobInstance
  • 동일한 잡의 실행 구별을 위한 객체 = JobExecution


JobExecution은 잡의 런타임 컨텍스트다. JobInstance가 실행될 때마다 JobExecution이 생성된다. 잡 실행이 성공적이라면 JobInstance 하나당 JobExecution 하나가 생성되지만 잡 실행 도중 에러 발생 시 잡을 재시작(restart)하는 경우 JobExecution은 추가적으로 생성될 수 있다.

서로 다른 잡을 구별하기 위해 JobInstance를 사용하고, 동일한 잡의 라이프사이클 및 버전 관리를 위해 JobExecution을 사용한다.

JobRepository는 메타데이터 저장을 위한 인터페이스이다. 스프링 배치는 잡의 실행과 관련된 메타데이터를 저장하기 위해 데이터베이스 연결을 필요로 한다.


스프링 배치 구성 및 동작 흐름

  1. ItemReaderItemWriter 정의
  2. ItemProcessor 정의
  3. Step 정의
    1. Tasklet 기반 Step 구성 또는 청크 기반 Step 구성
    2. 정의한 ItemReader, ItemProcessor, ItemWriter 설정
    3. Step 빌드
  4. Job 정의
    1. 정의한 Step 설정
    2. Job 빌드
  5. Job 실행


잡과 스텝

잡은 하나 또는 여러 스텝(step)으로 구성된다. 스텝은 잡을 수행하는 가장 작은 단위이다. 스텝은 데이터를 입력(read)하는 과정, 처리된 데이터를 출력(write)하는 과정을 포함한다. 데이터 입력 과정과 출력 과정 중간에 처리(process) 과정을 포함시킬 수 있다.

잡이 여러 스텝으로 구성되는 경우 다음과 같은 방법으로 스텝 실행을 제어할 수 있다.

  • 순차 스텝: 스텝들을 순차적으로 수행한다.
  • 조건부 스텝: 조건(스텝의 상태)에 따라 서로 다른 스텝을 수행한다.
  • 동시성 스텝: 의존관계가 없는 스텝을 동시에 수행한다.


ItemReader와 ItemWriter

데이터의 입력과 출력은 각각 ItemReaderItemWriter가 담당한다. ItemReader가 입력값에 대한 처리를 수행한 후 ItemWriter에 넘겨준다.

ItemWriterItemReader로부터 값을 받은 후 입력값들(아이템 컬렉션)을 한번에 모아서 처리한다. 청크 지향 처리(chunk-oriented processing)를 사용하면 ItemReader가 입력값에 대한 처리를 수행한 후 애그리게이션한 결과를 처리 단위(청크 단위)에 따라 ItemWriter에 보낸다.

스프링 배치는 기본적으로 입력값을 캐시한 후 ItemReader로 전달하며 청크 엘리먼트에 값을 캐시하지 않도록 설정 가능하다.


ItemProcessor

ItemProcessorItemReader가 읽어들인 데이터에 원하는 로직을 적용함으로써 출력 전 입력 데이터를 처리 및 가공하는 기능 수행한다. ItemProcessor의 로직에는 데이터 유효성 검증(validation) 과정이 포함된다.

여러 종류의 ItemProcessor를 정의하고 ItemProcessor들을 서로 연결하여 데이터 검증 및 처리 작업을 나누어 진행할 수 있다.


스프링 배치의 자동 구성과 잡 실행

스프링 배치는 애플리케이션을 실행하기 위한 기본 구성을 자동으로 설정하고 여러 기능을 제공하기 위해 @EnableBatchProcessing을 제공한다. Job, Step 등의 빈을 정의하는 구성 클래스에 해당 어노테이션을 지정하면 된다.

스프링 배치를 활성화하고 사용하기 위한 공통 구조를 제공하는 베이스 구성 클래스는 AbstractBatchConfiguration이며 BatchConfigurationSelector@EnableBatchProcessing 지정 구성 클래스를 확인한다. 적절한 배치 구성 클래스에 의해 다음 빈들이 자동 구성된다.

  • JobRepository
  • JobLauncher
  • JobRegistry
  • JobExplorer
  • JobBuilderFactory
  • StepBuilderFactory
  • PlatformTransactionManager


구성 클래스에서 위 빈들을 직접 정의하고 컨텍스트에 등록할 수도 있다.

@EnableBatchProcessing은 JDBC 기반으로 배치 애플리케이션의 인프라를 구성한다. 따라서 구성 클래스에는 JobStep 빈 뿐만 아니라 JobRepository 설정을 위해 적절한 DataSource 빈 정의가 필요하다. DataSource 빈을 직접 정의하는 대신 구성 클래스가 DefaultBatchConfigurer 클래스를 상속하도록 하고 createJobRepository() 메서드를 재정의하여 JobRepository 빈을 직접 정의할 수도 있다. 애플리케이션 컨텍스트에 DataSource 빈이 등록되어 있지 않으면 로컬 메모리를 사용하는 맵 기반의 JobRepository가 구성된다.

구성 클래스가 여러 개 정의되어 있는 경우 하나의 구성 클래스에만 @EnableBatchProcessing이 지정되어야 한다.

스프링 배치는 빈의 스코프를 잡 또는 스텝으로 지정하는 @JobScope@StepScope를 제공한다. 이 어노테이션은 런타임에 잡 또는 스텝 실행 컨텍스트로부터 값을 가져와 빈에 주입하는 지연 바인딩(late binding)을 가능하게 한다.

정의된 스프링 배치 애플리케이션을 실행하는 ApplicationRunnerJobLauncherApplicationRunner이다. JobLauncherCommandLineRunner는 스프링 부트 2.3.0 버전부터 제거되었다. JobLauncherApplicationRunner는 기본적으로 컨텍스트에 존재하는 모든 잡을 실행한다. setJobNames() 메서드를 통해 잡 이름을 지정하여 특정 잡을 실행할 수도 있다. JobLauncherApplicationRunnerJobLauncher, JobExplorer, JobRepository 구현체를 의존성 주입 받으며 이 의존 객체들은 기본적으로 @EnableBatchProcessing에 의해 자동 구성 및 빈 등록된다. 스프링 배치 애플리케이션은 JobLauncher 인터페이스의 run() 메서드를 호출함으로써 실행할 수도 있다. 메인 메서드에서 SpringApplicationrun() 메서드를 호출하여 ApplicationRunner 인터페이스 구현체인 JobLauncherApplicationRunner를 사용하는 경우에도 내부적으로는 JobLauncher 인터페이스의 run() 메서드를 호출함으로써 배치 애플리케이션을 실행한다. 별도 구성을 하지 않을 경우 JobLauncher 인터페이스 구현체로 SimpleJobLauncher가 사용된다.

스프링 배치 애플리케이션의 자동 구성은 BatchAutoConfiguration에 의해 수행된다. BatchAutoConfiguration은 애플리케이션 프로퍼티 속성을 읽고 잡 실행을 처리한다. 스프링 배치와 관련된 스프링 부트의 프로퍼티는 BatchProperties링크에서 처리된다. spring.batch.job.enabled 속성을 true로 설정하면 애플리케이션 실행 시 정의된 모든 Job(애플리케이션 컨텍스트에 등록된 모든 Job 컴포넌트)이 실행된다. spring.batch.job.enabled 속성을 true로 직접 설정하는 대신 스프링 배치가 제공하는 @EnableBatchProcessing 설정 시 자동으로 처리된다. spring.batch.job.enabled 속성을 false로 설정하면 이 동작을 비활성화할 수 있다. 여러 잡이 정의되어 있다면 애플리케이션을 시작할 때 실행할 잡 이름을 spring.batch.job.name 속성에 지정하여 특정 잡만 실행할 수 있다. 정의된 여러 잡을 실행하려면 spring.batch.job.names 속성에 여러 잡 이름을 설정한다. JobLauncherApplicationRunner는 먼저 빈으로 등록된 잡을 실행한 후 JobRegistry에서 잡을 조회하여 실행한다.

기본적인 잡 실행에 필요한 객체는 Job, JobLauncher이다. 잡 실행 결과로 반환되는 JobExecution은 잡의 상태 정보를 가지고 있다. 잡의 상태 정보란 잡 생성/시작/종료 시간, 실행/종료 상태, 최종 수정 날짜 등을 말한다.

서로 다른 Job 인스턴스를 생성하기 위해서는 파라미터가 필요하다. 파라미터에 따라 서로 다른 Job 인스턴스의 식별자 키를 생성함으로써 서로 다른 잡 실행을 구별할 수 있다. 잡 실행 시점에 JobJobParameters를 합쳐 런타임 로직을 파라미터화할 수 있다.


JobParameters

잡 실행을 파라미터화할 수 있다. 잡 실행을 파라미터화한다는 것은 각각의 잡 실행을 서로 구별짓는 것을 말한다. 중복된 데이터 처리를 방지하기 위해 잡 실행은 적절히 파라미터화 되어야 한다. 스프링 배치는 잡 실행 시 전달할 파라미터를 구성하기 위해 JobParameters를 사용한다. JobParameters를 통해 잡을 파라미터화하고 스텝에서 파라미터를 사용한다. JobLauncherrun() 메서드는 실행될 잡인 Job과 잡 실행 시 사용되는 파라미터인 JobParameters를 인자로 받아 잡 인스턴스인 JobInstance를 생성한다. 동일한 Job의 서로 다른 실행을 구별짓는 JobParameter에 의해 서로 다른 JobInstance가 생성된다.

컴파일 시점이 아닌 런타임에 JobParameters를 구성하는 방법에는 여러가지가 있다.

  1. 애플리케이션 실행 시 인자로 전달받은 값으로 JobParameters 구성
  2. 애플리케이션 프로퍼티(또는 환경 변수)에 설정된 값으로 JobParameters 구성
  3. 외부 요청 파라미터로 JobParameters 구성


런타임에 JobParameters를 구성하였다면 파라미터 값을 사용하는 곳에서의 바인딩 시점이 중요하다. 스프링 배치는 JobStep의 다양한 속성들에 대해 잡 실행 시점에 지연 바인딩을 가능하게 한다.


잡과 스텝 속성의 지연 바인딩

스프링 배치는 잡 또는 스텝 실행 시 특정 빈을 인스턴스화할 수 있도록 빈 스코프 설정 어노테이션인 @StepScope@JobScope를 제공한다. @StepScope는 해당 빈을 스텝 스코프로 지정한다. 스텝이 시작될 때까지 빈은 실제로 인스턴스화될 수 없다. 스텝 실행 시 스텝 컨텍스트에서 접근 가능한 참조의 지연 바인딩을 사용하기 위해 @StepScope를 사용할 수 있다.@StepScope는 스텝 컨텍스트에 대한 스코프를 설정하므로 실행 중인 스텝 별로 해당 빈 인스턴스는 하나만 존재한다. 스프링 배치 3 버전 부터 도입된 @JobScope@StepScope와 적용 및 빈 구성 방식이 유사하다. @JobScope는 잡 컨텍스트에 대한 스코프이므로 실행 중인 잡 별로 해당 빈 인스턴스는 하나만 존재한다. 잡 실행 시 잡 컨텍스트에서 접근 가능한 참조의 지연 바인딩을 위해 @JobScope를 사용할 수 있다.

빈 정의 시 @StepScope, @JobScope를 설정하는 것은 각각 @Scope(value="step", proxyMode=대상클래스명), @Scope(value="job", proxyMode=대상클래스명)를 지정하는 것과 동일하다. 두 어노테이션은 모든 빈 정의에 명시적으로 지정할 필요가 없도록 프록시 모드를 기본값으로 설정한다. @Values을 사용하여 스텝/잡 컨텍스트로부터 값을 주입해야 하는 빈에 대해, 그리고 스텝/잡 실행과 라이프사이클을 공유해야 하는 모든 빈에 대해 이 어노테이션을 사용할 수 있다.

@StepScope, @JobScope와 SpEL을 사용하면 잡 실행 시 스텝/잡 실행 컨텍스트인 ExecutionContext에 접근하여 잡 실행과 관련된 다양한 속성값들을 참조하여 데이터의 지연 바인딩이 가능하다. 이를 통해 스텝/잡, 스텝/잡 실행 컨텍스트, 그리고 잡 실행 파라미터로부터 속성값을 가져올 수 있다. 잡이 실행되면 실행 컨텍스트인 ExecutionContext는 잡과 연관되며, 잡을 구성하는 스텝이 실행되면 ExecutionContext는 잡 뿐만 아니라 스텝과도 연관된다.

스텝 실행 시 사용되는 Reader, Processor 등의 빈 정의 시 @StepScope와 SpEL을 사용하면 스텝 실행 시점에 해당 빈에 속성값의 지연 바인딩이 가능하다.

@StepScope
@Bean
public FlatFileItemReader flatFileItemReader(@Value("#{jobParameters[input.file.name]}") String name) {
  return new FlatFileItemReaderBuilder<Foo>()
    .name("flatFileItemReader")
    .resource(new FileSystemResource(name))
    ...
}


@JobScope를 사용하는 예는 다음과 같다.

@JobScope
@Bean
public FlatFileItemReader flatFileItemReader(@Value("#{jobExecutionContext['input.name']}") String name) {
  return new FlatFileItemReaderBuilder<Foo>()
    .name("flatFileItemReader")
    .resource(new FileSystemResource(name))
    ...
}


멀티스레드 사용 스텝 또는 파티션된 스텝에서 @JobScope가 지정된 빈을 사용하는 데에는 제한이 있다. 스프링 배치는 이러한 경우에 스레드를 제어하지 않으므로 @JobScope가 지정된 빈을 사용하면 의도치 않은 상황이 발생할 수 있으므로 주의해야 한다.


잡의 비동기 실행

TaskExecutor를 이용하여 요청 스레드와 상관없이 잡을 비동기로 실행 가능하다. 이 경우 해당 빈을 스레드 논블로킹으로 관리한다.


트랜잭션

스프링 배치는 스텝 수준에서 트랜잭션을 처리한다. 스텝이 Tasklet으로 구성되는 경우 execute() 메서드에 트랜잭션이 적용된다. exexute() 메서드는 반복 실행 가능하며 각각의 실행은 독립적인 트랜잭션으로 처리된다. exexute() 메서드 호출 시마다 새로운 트랜잭션이 생성된다.

스텝이 청크 지향 스텝인 경우 설정된 사이즈의 청크 별로 트랜잭션이 적용된다. 아이템(레코드) 별 트랜잭션이 아닌 청크 단위로 트랜잭션을 적용하여 처리 효율을 높이고 스텝 실행 시 발생하는 에러가 모든 아이템이 아닌 현재 처리 중인 청크에만 영향을 미치는 견고성을 제공할 수 있다. 스텝 실행 중 오류 발생 시 청크 단위의 롤백을 수행한다. ItemWriterItemProcessor에서 예외 발생 시 청크 단위의 트랜잭션 롤백을 할 수 있다. 기본적으로 ItemReader에서 예외가 발생하는 경우에는 스텝 실행의 트랜잭션 롤백은 일어나지 않는다. 예외 발생으로 인한 트랜잭션 롤백 동작은 재시도 및 건너뛰기 구성에 관계없이 적용된다.

트랜잭션은 스텝 내 프로세스 뿐만 아니라 스텝 실행 전후 프로세스에 대해서 적용 가능하며 스텝 실행 전후 동작하는 리스너에 대해서도 적용이 가능하다. 리스너 동작 또한 스텝이 처리하는 데이터와 연관되어 있을 수 있으므로 이 경우 스텝 실행 도중 예외 발생 시 적절한 트랜잭션 처리가 필요하다.

스프링 배치는 배치 잡 내 이벤트에 반응하는 다양한 형태의 리스너를 제공한다. 처리 도중 아이템을 건너뛰는 경우 로그를 남기려고 한다면 ItemSkipListener 리스너를 스텝에 적용하면 된다. 스프링 배치는 이러한 리스너에 대해 트랜잭션을 적용할 수 있지만 상황에 따라 달라진다. 리스너의 메서드가 트랜잭션이 적용되는지 그렇지 않은지에 대한 정해진 규칙은 없으며 해당 리스너에 대한 명세를 살펴봐야 한다.

스프링 배치는 데이터의 입력, 처리, 출력 단계에 반응하는 리스너도 제공한다. 각 단계를 처리하기 전 후에 리스너를 실행하거나 처리 도중 에러가 발생하였을 때 리스너를 실행할 수 있다. 에러 발생 시 실행되는 콜백은 스프링 배치가 롤백하려는 트랜잭션에 한해서 트랜잭션이 적용된다. 만약 에러에 대한 로그를 데이터베이스에 기록하려면 REQUIRES_NEW 전파 수준을 사용하여 트랜잭션을 직접 처리해야 한다. 이렇게 함으로써 로깅 트랜잭션을 청크 트랜잭션과 독립적으로 만들고 롤백 가능하도록 만들 수 있다.

트랜잭션 관련 속성에는 전파 수준, 격리 수준, 타임아웃이 있다. 기본적으로 속성값이 설정되어 있으며 이를 재정의할 수 있다. 전파 수준의 기본값은 REQUIRED이며 격리 수준의 기본값은 READ_COMMITED이다. 배치 작업이 해당 데이터를 처리하는 유일한 프로세스인 경우에는 데이터의 동시 접근 문제에 대해 신경 쓸 필요가 없으므로 격리 수준을 낮추면 기본 격리 수준 보다 더 빠른 처리가 가능하다. 배치 작업이 다른 애플리케이션과 동시에 동작하는 경우 데이터를 적절하게 조회하고 수정하기 위해서 격리 수준을 높일 필요가 있지만 격리 수준을 높일수록 데이터 처리 성능이 저하될 수 있다.

스프링 배치 애플리케이션에서 @Transactional 어노테이션을 사용하여 스프링의 선언적 트랜잭션 관리 기능을 사용할 수 있다. @Transactional 어노테이션이 지정된 애플리케이션 코드를 호출하면 해당 코드의 트랜잭션은 스프링이 아닌 스프링 배치가 관리하는 트랜잭션을 사용한다. 이때 기본 전파 수준은 REQUIRED이므로 @Transactional이 사용하는 트랜잭션은 스프링 배치의 트랜잭션과 동일하다. 그러나 스프링이 관리하는 트랜잭션과 스프링 배치가 관리하는 트랜잭션에 충돌이 발생할 수 있으며 이를 피해야 한다. 예를 들어, @Transactional 어노테이션이 적용된 애플리케이션 코드가 스프링 배치 작업의 청크 트랜잭션을 방해할 수 있다. 트랜잭션 전파 수준 속성에 따라 스프링이 관리하는 트랜잭션이 스프링 배치가 관리하는 트랜잭션에 관여할 수 있다. 트랜잭션 관리 충돌을 막기 위해서는 @Transactional 어노테이션을 사용한 스프링의 선언적 트랜잭션 관리 기능 자체를 비활성화하거나 사용한다면 전파 수준 사용에 주의 해야한다. 트랜잭션을 매번 새로 생성하는 REQUIRES_NEW 전파 수준을 사용하면 애플리케이션 코드가 스프링 배치의 트랜잭션과 독립적으로 자체 트랜잭션에서 실행되기 때문에 문제를 일으킬 수 있다.

애플리케이션이 둘 이상의 데이터 소스(데이터베이스, 메시지 큐 등)에 대한 트랜잭션 관리를 수행해야 할 수도 있다. 이러한 트랜잭션을 글로벌(global) 트랜잭션 또는 분산(distributed) 트랜잭션이라고 한다. 데이터 소스가 하나일 경우 트랜잭션은 로컬(local) 트랜잭션이라고 한다. 단일 데이터 소스와 마찬가지로 멀티 데이터 소스에 대한 트랜잭션 연산들도 모두 ACID 특성을 만족해야 한다. 예를 들어, 하나의 스텝 실행에서 먼저 첫 번째 데이터베이스에서 데이터를 처리한 후 두 번째 데이터베이스에 데이터를 처리하는 경우 두 연산은 원자성을 가져야 하며 첫 번째 작업은 성공하였지만 두 번째 작업 중 에러가 발생한다면 첫 번째 연산은 커밋되지 않고 롤백되어야 한다.

스프링 배치는 스텝 수준에서 트랜잭션을 처리하지만 스텝 실행 중 ItemReader에서 예외가 발생하는 경우 스텝 실행의 트랜잭션 롤백은 일어나지 않는다. 스프링 배치는 최적화를 위해 아이템 출력 도중 발생하는 재시도 가능한 에러를 대비하여 청크 단위로 아이템을 읽고 버퍼에 저장한다. 출력 도중 에러 발생 시 트랜잭션을 롤백하고 실행을 재시도할 때 ItemReader로부터 아이템을 다시 읽는 대신 캐시로부터 읽은 아이템을 가져와 ItemWriter에게 전달할 수 있다. 스프링 배치가 ItemReader에 캐싱을 적용하는 기능은 데이터베이스로부터 아이템을 읽는 경우에는 문제가 되지 않는다. 아이템을 읽는 도중 에러가 발생하여 트랜잭션이 롤백되더라도 데이터베이스의 데이터는 영향을 받지 않기 때문이다. 하지만 JMS의 경우 메시지를 읽고(queue) 제거(dequeue)하는 과정이 하나의 트랜잭션으로 처리되어야 하는 경우가 있다. 이 경우 트랜잭션이 롤백된다면

이처럼 캐싱 기능을 비활성화하여 트랜잭션이 롤백되더라도 배치 처리에 문제가 없도록 할 수 있다. 또는 다음과 같이 트랜잭션의 롤백 자체가 일어나지 않도록 할 수도 있다.


스프링 배치에서 멀티 데이터소스 구성

스프링 배치는 기본적으로 잡 실행과 관련된 메타데이터를 테이블에 저장하여 관리하기 위해 애플리케이션 컨텍스트에서 하나의 DataSource 빈을 탐색한다. DataSource 빈이 정의되지 않은 경우에는 맵 기반의 JobRepository가 사용된다. DataSource 빈이 두 개 이상 존재할 경우 스프링 배치는 어떤 DataSource 빈을 배치 처리에 사용할지 모르기 때문에 빈 정의 중복으로 인해 UnsatisfiedDependencyException 예외가 발생하게 된다. 따라서 DataSource 빈 중 하나에 @Primary를 사용하여 스프링 배치가 사용할 빈을 지정해야 한다. @Primary를 지정한 빈이 정의되지 않은 경우 이름이 지정된 DataSource 빈이 사용된다.

배치 처리가 아닌 다른 용도의 데이터베이스 접근을 위한 @Primary가 지정된 DataSource 빈이 이미 정의되어 있는 경우 배치 처리용 DataSource 빈을 정의하는 @Bean 메서드에 @BatchDataSource 어노테이션을 사용하면 멀티 데이터소스 구성을 할 수 있다. @BatchDataSource 어노테이션은 배치 자동 구성 시 주입되는 DataSource 빈에 대한 빈 의존성 주입 어노테이션이다.


참고

Comments