Spring Batch 핵심 구조와 운영
Job·Step·Chunk와 실행 Metadata를 기준으로 대량 작업의 재시작, 오류 처리, 중복 실행을 통제한다.
이 문서의 목차
Overview
Spring Batch는 단순한 반복문이나 Scheduler가 아니다. 끝이 있는 대량 작업을 Job과 Step으로 모델링하고, Chunk 단위 Transaction과 실행 Metadata를 이용해 실패·재시작·Skip·Retry를 관리하는 Framework다. Quartz가 “언제 시작할까?”를 담당한다면 Spring Batch는 “어떤 단계를 어떤 복구 규칙으로 처리할까?”를 담당한다.
한 문장 설명: 대량 데이터를 일정 묶음으로 처리하고 작업 일지를 남겨, 중간에 실패해도 안전하게 이어서 실행하게 하는 Framework다.
핵심 용어
| 용어 | 역할 |
|---|---|
| Job | 하나의 완결된 Batch 업무 정의 |
| Step | Job을 구성하는 순차적 처리 단계 |
| JobLauncher | Job과 JobParameters를 받아 실행을 시작 |
| JobRepository | Instance·Execution·Step 상태와 Count를 DB에 저장 |
| JobInstance | Job 이름 + 식별 JobParameters로 구분되는 논리 실행 |
| JobExecution | JobInstance의 실제 실행 시도 한 번 |
| StepExecution | Step 실행 상태, Read/Write/Skip/Commit Count |
| ExecutionContext | 재시작에 필요한 Reader 위치 등 Key-Value Checkpoint |
| Tasklet | 한 번의 명확한 작업을 Step으로 수행 |
| Chunk | 여러 Item을 읽고 가공하고 묶어서 쓰는 Transaction 단위 |
왜 필요한가
100만 건을 한 Transaction으로 처리하면 마지막 한 건 실패로 전체가 Rollback되고 Lock·Undo·Memory 비용이 커진다. 반대로 직접 Loop만 작성하면 어디까지 성공했는지, 같은 작업인지 새 작업인지, 어떤 Item을 Skip했는지 운영자가 알기 어렵다. Spring Batch는 처리 Flow와 실행 상태를 분리해 재현 가능한 운영 단위를 만든다.
전체 실행 흐름
flowchart LR; T[Quartz CLI API Trigger] --> L[JobLauncher]; L --> I[JobInstance]; I --> E[JobExecution]; E --> S[StepExecution]; S --> R[ItemReader]; R --> P[ItemProcessor]; P --> W[ItemWriter]; W --> C[Chunk Commit]; E <--> JR[(JobRepository)]; S <--> EC[ExecutionContext]
- Trigger가 Job 이름과 JobParameters를 JobLauncher에 전달한다.
- JobRepository는 같은 Job 이름과 식별 Parameter의 JobInstance가 있는지 판단한다.
- 실제 시도인 JobExecution과 각 StepExecution을 생성한다.
- Step이 Tasklet 또는 Chunk 방식으로 실행된다.
- Chunk Commit마다 Count와 ExecutionContext가 갱신된다.
- 실패하면 상태가 FAILED로 남고, 같은 Instance의 Restart는 새 JobExecution으로 이어진다.
- 완료된 같은 JobInstance를 다시 실행하는 것과 새 Parameter로 새 Instance를 만드는 것은 다르다.
Tasklet과 Chunk
Tasklet
Tasklet은 파일 이동, 임시 Table 정리, 단일 Procedure 호출처럼 한 덩어리 작업에 적합하다. execute() 반환값이 완료인지 계속 가능한지에 따라 반복을 제어한다. 보통 한 호출은 Step Transaction 경계 안에서 실행되므로 외부 API처럼 DB Rollback으로 되돌릴 수 없는 Side Effect는 별도 멱등 처리가 필요하다.
Chunk
Chunk 지향 Step은 ItemReader → ItemProcessor → ItemWriter → Commit을 반복한다.
- ItemReader: Item을 하나씩 읽는다.
null이면 입력 종료다. - ItemProcessor: Item 하나를 변환·검증한다.
null반환은 Filter를 뜻한다. - ItemWriter: 모인 Item 묶음을 한 번에 저장한다.
- Chunk Size: 한 Transaction에서 Commit할 Item 수다.
Chunk Size가 크면 Commit 횟수는 줄지만 Memory, Lock 보유 시간, 실패 Chunk 재처리량이 커진다. 작으면 복구 단위는 세밀하지만 DB 왕복과 Metadata Update가 늘어난다. “항상 1,000이 빠르다” 같은 고정 답은 없다.
JobInstance, Execution과 Parameter
dailySettlement(date=2026-08-12)가 완료됐다면 같은 식별 Parameter는 같은 JobInstance다. 실패 후 같은 Parameter로 실행하면 Restart가 될 수 있지만, 완료된 Instance의 재실행은 보통 거절된다. 단순히 현재 시각 Parameter를 매번 넣으면 항상 새 Instance가 되어 중복 실행 검사를 우회하므로 업무 Key를 식별 Parameter로 설계해야 한다.
@StepScope는 Step 실행 시점에 Bean을 만들며 JobParameters와 ExecutionContext를 Late Binding할 때 사용한다. 모든 Bean을 Step Scope로 만들기보다 실행별 값이 필요한 Reader·Writer 등에 제한한다.
재시작과 ExecutionContext
“51번째에서 실패하면 무조건 51번째부터”는 정확하지 않다. 마지막으로 Commit된 Chunk는 유지되고 실패한 Chunk는 Rollback된다. 실제 재개 위치는 Reader가 ItemStream.open/update으로 ExecutionContext에 상태를 저장하고 복원할 수 있는지에 달려 있다.
Paging Reader는 안정적인 Unique Sort Key가 필요하다. 입력 순서가 실행 중 바뀌면 누락·중복이 생길 수 있다. Cursor, Paging, File Reader마다 Restart 특성이 다르며 순서가 불안정한 Source는 단순 Count만으로 안전하게 복원되지 않는다.
Skip, Retry와 Restart
| 기능 | 범위 | 용도 |
|---|---|---|
| Retry | 현재 실행 중 같은 작업 재시도 | 일시적 Timeout, Deadlock |
| Skip | 실패 Item을 기록하고 다음 Item 진행 | 허용 가능한 불량 데이터 |
| Restart | 실패한 JobInstance를 새 Execution으로 재개 | Process 장애, 운영 재실행 |
| Rollback | 미완료 Chunk 변경 취소 | Transaction 정합성 |
모든 Exception을 Retry하면 영구 오류로 시간을 낭비한다. Skip Limit은 데이터 손실 허용량이라는 Business 결정이다. Retry/Skip Listener에 개인정보 원문을 무심코 Logging하지 않는다.
Example
@Bean
Step customerStep(
JobRepository repository,
PlatformTransactionManager tx,
ItemReader<Customer> reader,
ItemProcessor<Customer, CustomerSummary> processor,
ItemWriter<CustomerSummary> writer
) {
return new StepBuilder("customerStep", repository)
.<Customer, CustomerSummary>chunk(100, tx)
.reader(reader)
.processor(processor)
.writer(writer)
.faultTolerant()
.retry(DeadlockLoserDataAccessException.class)
.retryLimit(3)
.skip(ValidationException.class)
.skipLimit(20)
.build();
}@Bean
@StepScope
JdbcPagingItemReader<Order> orderReader(
@Value("#{jobParameters['businessDate']}") LocalDate businessDate
) {
// businessDate와 unique sort key로 재시작 가능한 범위를 만든다.
}외부 Side Effect와 멱등성
DB Writer는 Chunk Rollback으로 복구할 수 있지만 Email, 결제, HTTP 호출은 이미 성공한 뒤 응답만 잃을 수 있다. Job Restart나 Retry가 같은 요청을 다시 보낼 수 있으므로 Business ID 기반 처리 기록, Idempotency Key, Transactional Outbox를 사용한다. “Spring Batch가 재시작을 지원한다”와 “업무가 정확히 한 번 발생한다”는 같은 말이 아니다.
확장 방식 선택
Single-thread Chunk부터 측정한다. Local Multi-thread Step은 한 JVM Resource를 공유하고 Reader Thread Safety를 확인해야 한다. Partitioning은 입력 범위를 독립 StepExecution으로 나눈다. Remote Partitioning은 Manager와 Worker를 Broker로 연결하지만 JobRepository·Broker·DB Connection 총합이 새 병목이 된다.
실무에서 발생하는 문제
- Scheduler가 같은 Job을 중복 Launch해 동일 업무가 겹친다.
- 현재 시각 Parameter로 매번 새 JobInstance를 만들어 중복 방지를 우회한다.
- Paging Sort가 Unique하지 않아 Restart 시 누락·중복이 생긴다.
- Chunk 안에서 느린 외부 API를 호출해 Transaction과 Connection을 오래 점유한다.
- Thread 수만 늘려 DB Pool, Lock, 외부 Rate Limit을 포화시킨다.
- Skip된 데이터를 성공 처리량에 포함해 품질 문제를 숨긴다.
- JobRepository 정리 정책이 없어 Metadata Table이 계속 커진다.
- Listener에서 실제 처리 Logic을 숨겨 Flow와 Transaction 경계가 불명확해진다.
Trade-off와 흔한 오해
Spring Batch는 Restart와 관측성을 제공하지만 Metadata Schema, 실행 규칙, 데이터 보존 운영이 필요하다. 소량의 단순 작업에는 일반 Service와 Platform Scheduler가 더 단순할 수 있다.
- Spring Batch는 Scheduler가 아니다.
- Chunk Size는 읽는 총량이나 Page Size와 반드시 같은 값이 아니다.
- 실패 시 정확히 실패 Item 다음부터 시작한다고 단정할 수 없다.
- Skip은 오류 해결이 아니라 명시적으로 허용한 데이터 제외다.
- JobRepository가 외부 Side Effect의 Exactly Once를 보장하지 않는다.
- Parallelism이 늘면 처리량이 선형으로 증가하지 않는다.
Production Considerations
Job/Step Duration, Read·Write·Filter·Skip Count, Commit·Rollback, Retry, items/sec, 마지막 성공 시각을 관찰한다. JobRepository Table 증가량과 Slow Query, DB Pool Waiting, Lock, Heap/GC, 외부 API 오류를 같은 Timeline으로 본다. 장시간 실행 Job은 배포 종료 시 Graceful Stop과 Restart 절차를 Runbook에 적는다. 오래된 Metadata 삭제 전에 감사·재시작 요구를 확인한다.
다른 사람에게 설명한다면
30초: “Spring Batch는 대량 작업을 Job과 Step으로 나누고 Chunk 단위로 Transaction을 Commit하며 JobRepository에 실행 상태와 ExecutionContext를 저장합니다. 그래서 실패 후 재시작할 수 있지만 재개 위치는 마지막 Commit과 Reader의 상태 저장 방식에 따라 달라집니다.”
2분: JobInstance와 JobExecution 차이, Reader→Processor→Writer→Commit 흐름, Chunk Size Trade-off, Skip·Retry·Restart 차이, 외부 Side Effect 멱등성과 Quartz 역할 분리를 순서대로 설명한다.
Interview Questions / Follow-up Questions
- JobInstance, JobExecution, StepExecution의 차이는?
- Chunk Size가 성능·Lock·복구 범위를 어떻게 바꾸는가?
- Tasklet과 Chunk는 언제 선택하는가?
- ExecutionContext는 언제 저장되고 어떻게 복원되는가?
- Retry, Skip, Restart는 어떻게 다른가?
- 완료된 Job을 같은 Parameter로 다시 실행하면?
- Paging Reader가 재시작 가능하려면 정렬에 어떤 조건이 필요한가?
- 외부 API Writer가 멱등해야 하는 이유는?
- Quartz와 Spring Batch의 책임 차이는?
- Multi-thread·Partitioning 전에 무엇을 측정해야 하는가?
Related Topics
Quartz Scheduler, Remote Partitioning, 멱등성·중복·순서를 함께 본다.
SOURCE REFERENCES
이 문서의 근거
본문은 Dev Atlas 안에서 완결되며, 검증이 필요할 때만 원문을 확인할 수 있습니다.