Spring Batch 마스터 노트 시리즈 1편. 야간·주말에 수백만 건을 처리하는 배치의 토대 — 온라인 처리와 결정적으로 다른 자동화 모델, Job·Step·Chunk·Tasklet 4단어, JobInstance vs JobExecution의 헷갈리는 차이, JobRepository 6개 메타데이터 테이블, Batch Status vs Exit Status까지 한 흐름으로.
이 글은 Spring Batch 마스터 노트 시리즈의 첫 번째 편입니다. 야간 2시에 수백만 건의 거래를 정산하고, 매월 1일에 청구서를 한꺼번에 발행하고, 매일 새벽 데이터 웨어하우스에 ETL을 돌리는 — 이런 작업은 사용자가 화면에서 버튼 누르는 방식으로는 절대 못 합니다. 배치 처리 의 영역이에요.
이 시리즈는 8편으로 Spring Batch의 거의 모든 영역을 다룹니다. 1편의 목표는 단순해요. Job·Step·Chunk·JobRepository 네 단어를 손에 잡히게 만드는 것. 이 네 개념이 잡혀야 2편 Job 설정, 3편 청크 처리가 자연스럽게 따라옵니다.
이 시리즈는 Spring Batch 공식 문서, Spring Boot 가이드, 여러 Java 백엔드 학습 자료를 참고해 한국어 학습 노트로 풀어쓴 자료입니다.
읽으면서 IDE에 spring-boot-starter-batch 의존성을 추가하고 직접 Job을 띄워 보면 본문이 머리에 훨씬 잘 박혀요. 30분이면 첫 Tasklet Step이 콘솔에 로그를 찍는 걸 볼 수 있습니다.
처음 배치 처리가 어렵게 느껴지는 이유
이유는 두 가지예요.
첫째, 온라인 처리(웹·API) 와 사고방식이 완전히 다릅니다. 웹 요청은 사용자 클릭 → 즉시 응답이 핵심이지만, 배치는 스케줄 → 자동 실행 → 결과만 기록이에요. 사용자가 없는 환경에서 오류를 어떻게 처리할지, 실패한 Job을 어떻게 재시작할지가 첫 단계에서 막힙니다.
둘째, Spring Batch의 도메인 모델이 복잡합니다. Job·Step·Chunk·Tasklet·JobInstance·JobExecution·JobRepository — 비슷한 이름들이 한꺼번에 들어와요. 특히 JobInstance와 JobExecution의 차이가 헷갈리면 재시작 동작 전체가 안 보입니다.
해결법은 한 가지예요. 모든 배치 = "정해진 시간에 자동으로 + 데이터를 변환해서 + 어딘가에 저장하는 작업" 한 줄로 줄이세요. Spring Batch는 이 작업의 공통 뼈대(재시작·트랜잭션·로깅·모니터링) 를 다 만들어 둔 도구입니다. 비즈니스 로직(무엇을 읽고·어떻게 변환하고·어디에 쓸지)에만 집중하면 돼요.
배치 처리란 — 온라인과 결정적으로 다른 모델
| 구분 | 온라인 처리 | 배치 처리 |
|---|---|---|
| 트리거 | 사용자 요청 | 스케줄·이벤트·수동 |
| 응답 시간 | 즉시 (ms~s) | 수분~수시간 |
| 데이터 규모 | 소량 (개별 레코드) | 대량 (수백만 레코드) |
| 실행 방식 | 지속 대기 | 일회성·주기적 |
| 사용자 개입 | 있음 | 없음 (자동화) |
| 오류 처리 | 즉시 사용자 알림 | 로그·스킵·재시도 |
배치가 필요한 자리:
- 매일 새벽 수백만 건 정산
- 외부 시스템 CSV → DB 일괄 적재
- 월말 보고서·청구서 일괄 생성
- 데이터 마이그레이션 (일회성 대량 변환)
- 시스템 간 데이터 동기화
여기서 시험 함정이 하나 있어요. "배치 = 느린 처리"가 아닙니다. 배치는 응답이 즉시 필요 없을 뿐이지 처리 속도 자체는 매우 빨라요. 청크 단위 트랜잭션·병렬 처리·Bulk INSERT 같은 최적화로 단건 처리보다 수십 배 빠릅니다.
배치 처리 사용 사례
ETL — 가장 전형적인 패턴
[CSV 파일] --읽기--> [Spring Batch] --변환/검증--> [데이터베이스]
[레거시 DB] --읽기--> [Spring Batch] --마이그레이션--> [신규 DB]
[외부 API] --읽기--> [Spring Batch] --정제--> [데이터 웨어하우스]
Extract(추출) → Transform(변환) → Load(적재) 세 단계. 1편의 출발점이자 5편 이커머스에서 본격적으로 다룬 패턴이에요.
금융 처리 — 정확성이 생명
은행·카드사·보험사의 핵심 인프라. 이자 계산·청구서·정산이 매일 야간 배치로 돌아갑니다. 이 영역에서는 Spring Batch의 재시작 기능과 감사 추적이 결정적이에요. 잘못된 정산은 회사 평판을 깎고 법적 문제로도 이어질 수 있으니까요.
보고서 생성
[데이터베이스] → [배치 Job] → [집계 처리] → [보고서 파일] → [이메일 발송]
매일·매주·매월 시점에 경영진에게 자동 발송되는 보고서. 복잡한 집계 쿼리를 업무 시간 외에 돌려 시스템 부하 분산.
재고 관리
이커머스·물류·제조에서 창고·POS·ERP 시스템 간 재고를 주기적으로 동기화. 재고 부족 알림·자동 발주 트리거도 배치로.
Spring Batch — 공통 뼈대를 만들어 둔 프레임워크
Spring Batch가 제공하는 5가지 핵심 가치:
- 재시작 가능성 — 실패한 Job을 중단 지점부터 재실행
- 스킵 기능 — 오류 레코드를 건너뛰고 나머지 계속 처리
- 청크 처리 — 대량 데이터를 묶음 단위로 메모리 효율 처리
- 병렬 처리 — 여러 Step 동시 실행으로 시간 단축
- 모니터링 — 모든 실행 내역이 DB에 자동 기록
이 5가지를 직접 짜려면 수개월. Spring Batch는 이 뼈대를 다 만들어 두고 비즈니스 로직만 채우면 됩니다.
Job — 배치 작업 전체
Job 은 배치 처리의 최상위 개념. 하나 이상의 Step으로 구성되며, Step들의 실행 순서·흐름을 정의합니다.
@Bean
public Job myJob() {
return jobBuilderFactory.get("myJob")
.start(step1())
.next(step2())
.build();
}
여기서 시험 함정이 하나 있어요. Job은 재사용 가능한 설정 단위입니다. 같은 Job을 다른 JobParameters로 여러 번 실행할 수 있어요. "오늘 날짜로 일일 보고서", "어제 날짜로 일일 보고서"처럼 같은 Job을 다른 파라미터로 매일 실행합니다.
Step — Job의 독립 처리 단계
Step은 Job의 한 처리 단계예요. 자신의 트랜잭션 경계를 가지는 완결된 작업 단위.
두 가지 유형으로 나뉩니다.
Tasklet 기반 Step — 단순 작업
파일 삭제, 이메일 발송, 외부 API 호출 같은 단순 작업.
@Bean
public Step taskletStep() {
return stepBuilderFactory.get("taskletStep")
.tasklet((contribution, chunkContext) -> {
System.out.println("단순 작업 수행");
return RepeatStatus.FINISHED;
})
.build();
}
Chunk 기반 Step — 대량 데이터 처리
ItemReader → ItemProcessor → ItemWriter 패턴으로 대량 데이터를 처리. 이 시리즈의 가장 핵심.
@Bean
public Step chunkStep() {
return stepBuilderFactory.get("chunkStep")
.<Product, Product>chunk(10) // 청크 크기 10
.reader(itemReader())
.processor(itemProcessor())
.writer(itemWriter())
.build();
}
3편에서 자세히 다룹니다.
Chunk — 트랜잭션 단위의 묶음
Chunk는 한 트랜잭션으로 처리되는 데이터의 묶음입니다.
[ItemReader] → item → item → item → ... (chunk 크기만큼)
↓
[ItemProcessor] (각 아이템 처리)
↓
[ItemWriter] (묶음으로 한 번에 쓰기)
↓
[Commit Transaction]
청크 크기 선택은 trade-off예요.
| 청크 크기 | 메모리 | 트랜잭션 | 실패 시 재처리 |
|---|---|---|---|
| 1 | 최소 | 매우 많음 | 1개 |
| 10 | 낮음 | 많음 | 최대 10개 |
| 100 | 중간 | 보통 | 최대 100개 |
| 1000 | 높음 | 적음 | 최대 1000개 |
여기서 정말 중요한 시험 함정 — 청크 크기를 너무 키우면 메모리 폭발 + 실패 시 재처리 범위 확대예요. 너무 작으면 트랜잭션 오버헤드로 성능 저하. 일반적으로 10~1000 사이, 시작은 100 정도가 무난합니다.
Tasklet — 단순 작업의 인터페이스
public interface Tasklet {
@Nullable
RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception;
}
RepeatStatus.FINISHED 반환 = 작업 종료. CONTINUABLE 반환 = 같은 작업 반복 실행.
여기서 시험 함정이 하나 있어요. RepeatStatus.CONTINUABLE을 잘못 반환하면 무한 루프예요. 종료 조건이 명확하지 않으면 항상 FINISHED를 반환하는 게 안전합니다.
JobRepository — 메타데이터 저장소
JobRepository는 배치 처리의 모든 메타데이터를 저장하는 컴포넌트예요. Job 실행 이력·Step 실행 이력·실행 상태·시간 — 모든 정보를 DB에 영속화.
자동 관리되는 6개 테이블:
BATCH_JOB_INSTANCE — Job 인스턴스
BATCH_JOB_EXECUTION — Job 실행 정보
BATCH_JOB_EXECUTION_PARAMS — 실행 파라미터
BATCH_JOB_EXECUTION_CONTEXT — 실행 컨텍스트
BATCH_STEP_EXECUTION — Step 실행 정보
BATCH_STEP_EXECUTION_CONTEXT — Step 실행 컨텍스트
이 메타데이터 덕분에 재시작·이중 실행 방지·모니터링 이 가능해집니다.
JobLauncher — Job 실행자
public interface JobLauncher {
JobExecution run(Job job, JobParameters parameters);
}
기본 동기 실행. 비동기 실행도 설정 가능.
여기서 시험 함정이 하나 있어요. Spring Boot는 기본적으로 앱 시작 시 모든 Job을 자동 실행합니다. REST API로 Job을 제어하려면 spring.batch.job.enabled=false로 자동 실행을 끄세요. 안 끄면 앱 시작할 때마다 의도치 않게 Job이 돌아요.
JobInstance vs JobExecution — 가장 헷갈리는 두 개념
이 둘의 차이를 이해하는 게 Spring Batch의 핵심.
JobInstance — 논리적 실행 단위
Job 이름 + JobParameters 의 조합으로 식별. "오늘 날짜로 실행한 일일 보고서 Job"이 하나의 JobInstance.
같은 JobParameters로 실행하면 같은 JobInstance를 참조해요. 이미 성공 완료된 JobInstance는 재실행 불가 — 이중 처리 방지.
JobExecution — 실제 실행 시도
JobInstance의 실행 시도. 하나의 JobInstance가 여러 JobExecution을 가질 수 있어요 (실패 후 재시도).
JobInstance (날짜=2026-01-01, 보고서=일일)
├── JobExecution #1: 00:00 실행 → FAILED (네트워크 오류)
├── JobExecution #2: 01:00 재실행 → FAILED (DB 오류)
└── JobExecution #3: 02:00 재실행 → COMPLETED ✓
JobInstance (날짜=2026-01-02, 보고서=일일)
└── JobExecution #4: 00:00 실행 → COMPLETED
여기서 정말 중요한 시험 함정 — 이미 COMPLETED 된 JobInstance에 같은 파라미터로 재실행 시도하면 JobInstanceAlreadyCompleteException 예외가 발생합니다. 개발 중에는 RunIdIncrementer로 매번 새 JobInstance를 만들거나, time 파라미터에 System.currentTimeMillis()를 추가해서 회피.
@Bean
public Job myJob() {
return jobBuilderFactory.get("myJob")
.incrementer(new RunIdIncrementer()) // 자동 run.id 증가
.start(myStep())
.build();
}
JobParameters — 실행 식별자
JobParameters params = new JobParametersBuilder()
.addString("reportDate", "2026-01-01")
.addLong("time", System.currentTimeMillis())
.toJobParameters();
jobLauncher.run(myJob, params);
타입 — String, Long, Double, Date.
JobParameters가 JobInstance를 식별하니, 운영에서는 날짜·배치 ID 같은 의미 있는 파라미터를 사용합니다.
StepExecution — Step 실행 정보
각 Step 실행마다 생성되는 객체. readCount·writeCount·skipCount·filterCount 등을 추적.
public ExitStatus afterStep(StepExecution stepExecution) {
long readCount = stepExecution.getReadCount();
long writeCount = stepExecution.getWriteCount();
long skipCount = stepExecution.getSkipCount();
System.out.println("읽음: " + readCount + ", 씀: " + writeCount + ", 스킵: " + skipCount);
return ExitStatus.COMPLETED;
}
Batch Status vs Exit Status — 두 가지 상태
자주 헷갈리는 두 상태 시스템.
Batch Status — 프레임워크 내부
| 상태 | 의미 |
|---|---|
| COMPLETED | 성공 완료 |
| STARTING | 시작 중 |
| STARTED | 실행 중 |
| STOPPING | 중지 요청 |
| STOPPED | 중지 (재시작 가능) |
| FAILED | 실패 (재시작 가능) |
| ABANDONED | 포기 (재시작 불가) |
| UNKNOWN | 알 수 없음 |
Spring Batch가 자동 관리. 개발자가 직접 변경 어려움.
Exit Status — 흐름 제어용
기본 — COMPLETED, FAILED, STOPPED, NOOP, UNKNOWN.
개발자가 커스텀 ExitStatus 반환 가능. Job Flow의 .on("EXIT_CODE") 조건에 사용.
public ExitStatus afterStep(StepExecution stepExecution) {
if (stepExecution.getWriteCount() == 0) {
return new ExitStatus("NO_DATA"); // 커스텀 상태
}
return ExitStatus.COMPLETED;
}
여기서 정말 중요한 시험 함정 — .on("COMPLETED")는 Batch Status가 아닌 Exit Status를 참조합니다. 두 시스템이 같은 이름을 쓰지만 의미가 다르니, 조건부 흐름 설정 시 항상 Exit Status 기준으로 생각하세요.
H2 vs MySQL — 메타데이터 DB 선택
H2 (개발용)
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
spring.batch.jdbc.initialize-schema=always
여기서 시험 함정이 하나 있어요. H2는 인메모리 DB라 앱 재시작하면 모든 배치 이력이 사라져요. 개발/테스트 전용. 운영에는 절대 X.
MySQL (운영용)
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
spring.datasource.url=jdbc:mysql://localhost:3306/batch_schema
spring.datasource.username=batch_user
spring.datasource.password=batch_user
spring.batch.jdbc.initialize-schema=always
운영에서는 initialize-schema=never로 두고 별도 마이그레이션 스크립트 사용 — 기존 배치 이력 삭제 위험 회피.
시험 직전 한 번 더 — 자주 헷갈리는 함정 모음
여기까지가 1편의 핵심입니다. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.
- 배치 처리 = 사용자 개입 없이 대량 데이터 자동 처리
- 온라인(즉시) vs 배치(스케줄·자동·대량) — 사고방식 자체가 다름
- 배치 사용 사례 — ETL / 금융 정산 / 보고서 / 재고 관리
- Spring Batch 5가지 가치 — 재시작·스킵·청크·병렬·모니터링
- Job = 최상위 단위, 하나 이상의 Step으로 구성
- Step = 독립 처리 단계, Tasklet 또는 Chunk 유형
- Tasklet = 단순 작업 (
RepeatStatus.FINISHED반환) CONTINUABLE반환은 무한 루프 위험- Chunk = 트랜잭션 단위 묶음, 일반적으로 10~1000
- 청크 크기 trade-off — 메모리 vs 트랜잭션 vs 재처리 범위
- JobRepository = 메타데이터 저장소, 6개 테이블 자동 관리
- BATCH_JOB_INSTANCE / EXECUTION / PARAMS / STEP_EXECUTION 등
- JobLauncher = Job 실행자, 기본 동기
- Spring Boot 앱 시작 시 자동 실행 =
spring.batch.job.enabled=false로 끄기 - JobInstance = Job + Parameters 논리적 단위
- JobExecution = JobInstance의 실제 실행 시도
- 같은 파라미터 재실행 =
JobInstanceAlreadyCompleteException RunIdIncrementer= 매번 새 JobInstance 생성- StepExecution = readCount/writeCount/skipCount 추적
- Batch Status = 프레임워크 내부 상태
- Exit Status = 흐름 제어용 상태
.on("COMPLETED")는 Exit Status (Batch Status X)- 커스텀 ExitStatus =
new ExitStatus("CUSTOM_CODE") - H2 = 인메모리, 개발용 / MySQL = 운영용
- 운영
initialize-schema=never로 이력 보호
시리즈 다른 편
같은 시리즈의 다른 글들도 같은 톤으로 묶어 정리되어 있어요. 1편 모델이 잡히면 2편 Job 설정부터 본격 코드입니다.
- 1편 — Spring Batch 입문 (현재 글)
- 2편 — Job/Step 설정 (Tasklet과 Chunk Step)
- 3편 — 청크 처리 (Reader·Processor·Writer)
- 4편 — ItemReader 마스터 (CSV·JdbcCursor·Paging)
- 5편 — ItemWriter 마스터 (FlatFile·JdbcBatch·Composite)
- 6편 — Job Flow와 리스너
- 7편 — 오류 처리 (Skip·Retry·SkipPolicy)
- 8편 — Spring Batch 5 마이그레이션
공식 문서: Spring Batch Reference와 Spring Boot Batch Starter에서 더 깊이 갈 수 있어요.
다음 글(2편)에서는 @EnableBatchProcessing·JobBuilderFactory·StepBuilderFactory·REST API로 Job 실행·조건부 흐름·JobExecutionDecider까지 — Spring Batch 4.x 기준 Job/Step 설정의 본격을 풀어 갑니다.