Spring Batch 시리즈를 포스팅하고 있습니다. 기준이 되는 버전은 5.x 버전입니다.
1. Spring Batch 소개 - 현재 포스팅
2. Job과 Step의 구조 및 생성
3. Job과 Step의 실행 과정
4. Job의 흐름 제어 - Flow
5. FlowJob의 실행 과정
6. 청크 기반 프로세싱의 구조 및 생성
7. 청크 기반 프로세싱의 진행 과정
8. Flat File, JSON 형식 파일 읽기 및 쓰기
9. DB 데이터 읽기 및 쓰기
10. Step의 예외 제어하기(스킵, 재시도)
11. 배치 작업 확장하기
배치 애플리케이션
Batch 라는 단어를 영영사전에서 찾아보면 "a set of jobs that are processed together on a computer" 라고 설명한다. 컴퓨터에서 함께 처리되는 일련의 작업이라고 해석할 수 있는데, 말 그대로이다. 회원 멤버십 갱신, 구독 기능 처럼 대량의 데이터를 사용하는 복잡한 비즈니스 로직을 수행하거나, 월간 사용량 모니터링 처럼 통계성 데이터를 집계하는 등 대규모로 데이터를 일괄 처리할 때는 배치 애플리케이션을 구성하여 처리하곤 한다. Spring에서는 이러한 배치 애플리케이션을 설계할 수 있도록 Spring Batch를 제공한다.
Spring Batch는 로깅 및 추적, 트랜잭션 관리, 작업 처리 통계, 작업의 재시작 및 건너뛰기 등 대량의 데이터를 처리하는 데 필수적인 기능들을 제공하며, 이는 일회성이 아니라 재사용이 가능한 형태로 제공한다. 성능적으로는 최적화와 파티셔닝 기술을 통해 대용량 및 고성능 배치 작업에 이점이 있다. 소규모의 간단한 배치 작업부터 대용량 작업까지 모두 Spring Batch를 사용하여 처리할 수 있다.
Spring Batch 기본 도메인 이해
Spring Batch는 수십 년간 일반적으로 사용되어 왔던 배치 아키텍처를 따라 설계되어 있다. 한번 Spring Batch의 도메인을 구성하는 주요 개념들을 그린 다이어그램을 살펴보자.
하나의 배치 프로세스는 하나의 `Job`과 해당 `Job`을 이루는 여러 개의 `Step`으로 표현된다. 이 `Job`은 `JobLauncher`를 통해 실행되며, 실행중인 `Job`의 메타데이터를 `JobRepository`를 통해 관리한다.
결국 Spring Batch를 구성하는 가장 기초는 `Job`, `Step`이다. 이번 시간에는 두 도메인과 관련 도메인들을 간단하게 살펴보며 Spring Batch의 기본적인 흐름을 이해해보자.
Job
`Job`은 배치 프로세스를 캡슐화한, 하나의 배치 작업 그 자체를 의미하는 도메인이다. 예를 들어, 월별로 회원의 멤버십을 갱신하는 기능이 필요한 경우 회원의 멤버십을 갱신하는 작업 자체를 `Job`으로 표현한다.
`Job` 적어도 한 개의 `Step`으로 구성되며, `Step`을 저장하는 컨테이너 역할을 수행한다고 봐도 된다. `Step`을 통해 전체 배치 프로세스를 구성하는 역할을 한다.
JobInstance
`Job`을 실체화한 도메인 객체이다.
클래스와 인스턴스를 생각해보자. 클래스는 객체의 설계도, 객체가 어떻게 이루어져 있는지 명시한다면, 인스턴스는 이 설계도를 바탕으로 구현된 실체이다.
`Job`과 `JobInstance`도 마찬가지이다. `Job`은 하나의 배치 프로세스에 대한 설계도이고, 이 설계도를 바탕으로 만들어진 인스턴스가 `JobInstance`이다.
예를 들어, 회원의 활동에 따라 월마다 멤버십을 갱신하는 배치 작업을 하나 만들었다고 해보자. 여기서 배치 작업 자체는 여러개의 `Step`을 만들어 `Job`으로 설계한다. 그리고 이 `Job`을 각각 1월, 2월, 3월, ... 실행되도록 실체화 한 것은 `JobInstance`를 통해 표현된다.
JobParameters
우리가 하나의 클래스로부터 인스턴스를 만들기 위해서는 생성자를 호출한다. 생성자에는 클래스를 구성하는 멤버를 초기화 하기 위해 필요한 파라미터를 선언하며, 해당 파라미터에 선언된 값을 인자로 주어 생성자를 호출하면 인스턴스가 생성된다.
`JobInstance`도 마찬가지이다. `Job`이 전체 배치 프로세스의 설계도라면, 해당 배치 프로세스를 수행하기 위해 어떤 등급의 회원을 대상으로 진행하는지, 어떤 날을 기준으로 진행하는지 등등 요구사항에 따라 다양한 파라미터가 필요할 것이다. 이러한 파라미터는 `JobParameters`를 통해 표현한다.
일반적으로 두 개의 인스턴스가 같은 멤버를 가지고 있다면, `equals`를 구현해 두 인스턴스를 같은 인스턴스로 취급하곤 한다. `JobInstance`도 마찬가지이다. 하나의 `Job`에서 `JobParameters`를 통해 `JobInstance`를 생성할 수 있는데, 이 `JobParameters`가 같은 경우에는 같은 `JobInstance`로 판단한다.
배치 작업은 대규모 데이터를 다루는 경우에 주로 설계하기 때문에, 배치 작업의 실행에 주의해야 한다. 만약 이미 처리된 배치 작업을 개발자의 실수로 한번 더 실행되서 데이터가 중복으로 바뀌는 경우에는 엄청난 피해가 일어날 수 있다. 이를 방지하기 위해 Spring Batch 는 `JobInstance`의 유일성을 보장하도록 설계되어 있다. 같은 `JobParmeters`로 Job을 실행시키면, 동일한 `JobInstance`로 판단해서 이전에 해당 `JobInstance`가 성공한 경우에는 `Job`을 실행시키지 않는다.
반대로 생각하면 `Job`이 실패하는 경우에도 유일성이 보장되어 있어야 재시작이 가능하다. 중간에 `Step`에서 예외가 발생하면, 해당 `Job`은 실패하는데, 이 경우에는 실패한 지점부터 다시 시작할 수 있어야 할 것이다. 이는 `JobInstance`가 유일해야만 어떤 `Job`을 다시 시작할지 구분할 수가 있다.
결론적으로 위와 같은 이유 때문에 `JobParameters`는 `JobInstance`의 중복을 구분하고 유일한 `JobInstance`를 구별하는 역할을 한다.
JobExecution
`JobExecution`은 `Job`의 실행 한 번을 나타낸다.
하나의 배치 작업은 실행 결과에 따라 여러번 실행될 수 있다. 예를 들어, 월별로 회원 멤버십을 갱신하는 배치 작업을 1월에 실행했는데, 첫 번째 실행 때 데이터의 오류가 있어 정상적으로 완료되지 않았다고 해보자. 그렇다면 데이터를 수정해서 두 번째로 실행을 할 것이다. 여기서 첫 번째 실행, 두 번째 실행이 `JobExecution`을 통해 표현된다고 생각하면 된다.
즉, 위 예시에서 회원 멤버십을 갱신하는 배치 작업은 `Job`을 통해 설계하고, 1월에 실행하려는 배치 작업은 `JobInstance`를 통해 표현하며, 해당 `JobInstance`를 실제 실행하는 것은 `JobExecution`을 통해 나타낸다.
따라서 하나의 `Job`에 대해 동일한 `JobParameters`로 실행하는 경우 `JobInstance`는 단 하나만 생성되는 반면 `JobExecution`은 동일한 `JobParameters`로 실행할 때마다 생성된다.
`JobExecution`은 `Job`의 실행 중에 발생한 일들을 기록하기 위해 여러 속성을 저장한다. 다음은 JobExecution에 저장되는 주요 속성이다.
속성 | 타입 | 설명 |
`status` | `BatchStatus` | Job의 실행 상태를 나타낸다. 실행 중일 때는 `STARTED`, 실패 시 `FAILED`, 완료 시 `COMPLETED`로 설정된다. |
`startTime` | `LocalDateTime` | 실행이 시작된 시간을 나타낸다. 만약 `Job`이 시작되지 않는 경우, `null`이다. |
`endTime` | `LocalDateTime` | 실행이 완료된 시간을 나타낸다. 만약 `Job`이 완료되지 않은 경우, `null`이다. |
`exitStatus` | `ExitStatus` | 실행 결과를 나타낸다. 만약 `Job`이 완료되지 않은 경우, `null`이다. |
`executionContext` | `ExecutionStatus` | 실행 간 영속되어야 하는 사용자의 데이터를 저장해놓는 공간이다. |
`failureExceptions` | `List<Throwable>` | 실행 중 발생한 예외 리스트이다. |
Step
`Step`은 배치 작업의 독립적이고 순차적인 단계를 캡슐화한 도메인 객체이다. `Job`이 전체 배치 작업의 설계도라면, `Step`은 배치 작업을 수행하기 위한 최소 단위 작업의 설계도라고 생각하면 된다. 따라서 `Job`은 적어도 한 개의 `Step`으로 구성되게 된다.
`Job`에 여러 개의 `Step`이 정의된 경우, 하나의 `Step`이라도 실패한다면 해당 `Job`은 실패로 처리된다.
StepExecution
`JobExecution`이 `Job`의 실행 한 번을 나타내는 것 처럼 `StepExecution` 역시 `Step`의 실행 한 번을 나타낸다. `JobExecution`과 마찬가지로 `Step`이 실행되는 동안의 커밋 수, 시작 시간, 상태 등 배치 실행 전반에 걸처 유지해야 하는 속성들을 저장한다.
ExecutionContext
`Step`과 `Job`에서 상태를 저장할 수 있도록 `Map<Key, Value>`처럼 키 - 값 쌍의 컬렉션을 가지고 있는 도메인 객체이다. `StepExecution`, `JobExecution`에서 각각의 `ExecutionContext`를 얻을 수 있다.
엑셀 파일에서 데이터를 읽는 단순한 `Job`이 있다고 생각해보자. 두 개의 `Step`으로 구성되어 있고, 각 `Step`에서는 10분동안 최대한 읽을 수 있을 만큼 읽은 후 다음 `Step`으로 넘긴다고 해보자.
이런 경우, 이전 `Step`에서 10분동안 몇 개의 행을 읽었는지 그 상태를 다음 `Step`에서 알아야 할 것이다. 그래야 이전 `Step`에서 종료된 행 부터 다음 `Step`에서 실행할 수 있기 때문이다.
이렇게 종료된 행 처럼, `Job` 전반에서 공유되어야 하는 상태는 `JobExecution`에서 얻을 수 있는 `ExecutionContext`에 저장하면 된다. 그러면 `Job` 전체에서 해당 상태 값을 얻을 수 있게 된다.
JobRepository
앞서 설명한 도메인 객체의 영속 메커니즘을 제공하는 도메인 객체이다. `JobLauncher`, `Job`, `Step` 구현체에 CRUD 작업을 제공하며, `JobExecution`, `StepExecution`을 저장하고 가져오는 역할도 수행한다.
추후에 자세히 알아보겠지만, Spring Batch에서는 위에서 알아본 핵심 도메인 객체들은 각각의 속성들을 영속하기 위해 메타데이터 스키마라는것을 제공하고 있고, 이러한 메타데이터들을 RDBMS에 영속한다. 그리고 도메인 객체들을 초기화할 때는 메타데이터로부터 초기화를 하게 되고, 저장, 갱신하는 경우에도 해당 스키마에서 이루어진다. 이러한 영속 메커니즘을 제공하는 것이 `JobRepository`이다.
JobLauncher
`Job`과 `JobParameters`를 통해 주어진 `Job`을 실행시키는 도메인 객체이다.
도메인 정리
도메인 객체 간 관계를 간단하게 다이어그램으로 나타내보았다.
다시 한 번 정리하자면, 하나의 `Job`은 배치 작업의 설계도이고 작업의 최소 단위인 `Step`으로 구성된다. `Job`은 `JobParameters`와 함께 `JobInstance`로 실체화 하며, 각 실행은 `JobExecution`으로 나타낸다. 실행 간 공유되어야 하는 정보들은 `ExecutionContext`를 통해 영속한다.
`Step`역시 한 번의 실행을 `StepExecution`으로 나타내며, 각 `Step`에 한정하여 실행 간 공유되어야 하는 정보들은 `ExecutionContext`를 통해 영속한다.
간단한 배치 구성해보기
지금까지 설명했던 걸 바탕으로, 간단하게 배치 작업을 만들어서 실행까지 해보면서 Spring Batch의 흐름을 간략하게만 살펴보자. 회원의 멤버십을 갱신하고, 이에 대한 알림을 보낸다고 가정하여 배치 작업을 하나 설계해보자.
Spring Batch에 대한 의존성은 추가되었다고 가정하고, 간단한 설정을 우선 해보자. 이전에 말했지만 배치 작업과 관련된 메타데이터를 모두 RDBMS에 저장한다. 따라서 Datasource 설정을 먼저 진행한 후에 `Job`을 만들어보자.
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost/batch_basic
username: root
password: 11111
batch:
jdbc:
initialize-schema: always
DB는 편한대로 사용하면 된다. 배치 애플리케이션 초기화 시에 메타데이터와 관련된 스키마도 같이 초기화되도록 `spring.batch.jdbc.initialize-schema`를 always로 설정하자.
이제 간단한 `Job`을 만든 다음 실행시켜보자.
@Configuration
class BatchConfiguration(
private val jobRepository: JobRepository,
private val transactionManager: PlatformTransactionManager,
) {
@Bean
fun membershipJob(): Job {
return JobBuilder("membership-job", jobRepository)
.start(membershipUpdateStep())
.next(notifyStep())
.build()
}
@Bean
fun membershipUpdateStep(): Step {
return StepBuilder("membership-update-step", jobRepository)
.tasklet(transactionManager) { contribution, chunkContext ->
println("membership update...")
RepeatStatus.FINISHED
}.build()
}
@Bean
fun notifyStep(): Step {
return StepBuilder("notify-step", jobRepository)
.tasklet(transactionManager) { contribution, chunkContext ->
println("notify...")
RepeatStatus.FINISHED
}.build()
}
}
fun StepBuilder.tasklet(
transactionManager: PlatformTransactionManager,
tasklet: Tasklet
): TaskletStepBuilder {
return this.tasklet(tasklet, transactionManager)
}
`Job`은 `JobBuilder`를 통해서 만들 수 있다. `start`를 통해 시작하는 `Step`을 지정할 수 있으며, `next`를 통해 그 다음 진행해야 할 `Step`을 지정할 수 있다.
각각의 `Step`은 `StepBuilder`를 통해 만들 수 있다. Spring Batch에서 제공하는 `Step` 구현체는 여러 가지가 있는데, 그 중 가장 기본적인 `Step`으로는 `Tasklet`을 호출하여 진행하는 `TaskletStep`이 있다.
`TaskletStep`을 만들기 위해서는, `StepBuilder`의 `tasklet` 메서드를 호출하면 된다. (람다로 사용하기 편하게 확장함수로 바꿔놓았다.) `tasklet` 함수의 파라미터로 람다를 넣어서 `Tasklet`을 구현했는데, 해당 블록이 `Step`실행 시 실행된다고 간단하게 이해하면 된다.
`TaskletStep`의 경우에는 `Tasklet`의 반환값인 `RepeatStatus` 상수 값에 따라서 `Tasklet`을 반복한다. `FINISHED`, `CONTINUABLE` 두 상수가 존재하는데, 아무런 조건 없이 `CONTINUABLE`을 리턴한다면 계속 `Tasklet`블록이 실행되니 주의하자.
이제 Spring 애플리케이션을 실행해보자. 따로 설정하지 않으면 Spring 애플리케이션을 실행하는 것 만으로도 배치 작업이 실행된다.
c.c.s.SpringBatchBasicApplicationKt : Started SpringBatchBasicApplicationKt in 2.194 seconds (process running for 2.621)
o.s.b.a.b.JobLauncherApplicationRunner : Running default command line with: []
o.s.b.c.l.support.SimpleJobLauncher : Job: [SimpleJob: [name=membership-job]] launched with the following parameters: [{}]
o.s.batch.core.job.SimpleStepHandler : Executing step: [membership-update-step]
c.s.b.BatchConfiguration$$SpringCGLIB$$0 : membership update...
o.s.batch.core.step.AbstractStep : Step: [membership-update-step] executed in 42ms
o.s.batch.core.job.SimpleStepHandler : Executing step: [notify-step]
c.s.b.BatchConfiguration$$SpringCGLIB$$0 : notify...
o.s.batch.core.step.AbstractStep : Step: [notify-step] executed in 22ms
o.s.b.c.l.support.SimpleJobLauncher : Job: [SimpleJob: [name=membership-job]] completed with the following parameters: [{}] and the following status: [COMPLETED] in 163ms
로그를 확인해보면 위에서 설정한 대로 `membershipUpdateStep`, `notifyStep`이 순차적으로 실행된 것을 볼 수 있다.
위에서 설정했던 DB도 확인해보자.
위와 같이 메타데이터 관련된 테이블이 자동으로 생성된 것을 볼 수 있다. 한번 batch_job_instance을 조회해보자.
위와 같이 하나의 데이터가 삽입되어 있는 것을 볼 수 있다.
이처럼 Spring Batch는 배치 작업과 관련된 메타데이터를 해당 스키마를 통해 관리한다. 각 스키마에 대한 상세한 설명은 추후에 관련 내용들을 상세하게 다루면서 같이 살펴볼 예정이다.
정리
이번 포스팅에서는 배치 작업이란 무엇인지, Spring Batch가 어떤 도메인을 가지고 작동하는지, 간략하게 어떻게 흘러가는지만 살펴보았다. 다음 포스팅에서는 Spring Batch가 제공하는 `Job`과 `Step`의 기본 구현들의 구조를 살펴보고, 생성자 클래스들을 상세하게 다룰 예정이다.
사실 배치 애플리케이션을 처음 구성한다면, 모두 처음 들어보는 용어들이라 설명이 와닿지 않을텐데, 포스팅들을 순서대로 읽으면서 점차 익숙해져가면 이해하기 더 쉬울 것이다.