매일 새벽 수백만 건의 결제 정산, 대량 이메일 발송, 로그 집계처럼 정해진 시간에 대량의 데이터를 처리해야 할 때 REST API로는 감당이 안 된다. 이런 상황을 위한 게 Spring Batch다.
Spring Batch는 대용량 데이터를 일괄 처리(Batch Processing) 하기 위한 Spring 기반의 경량 프레임워크다.
Job
└── Step
├── chunk 1 → Read → Process → Write → 커밋
├── chunk 2 → Read → Process → Write → 커밋
└── chunk 3 → Read → Process → Write → 커밋
| 구성 요소 | 역할 |
|---|---|
| Job | 배치 작업의 최상위 단위 |
| Step | Job을 구성하는 독립적인 단계 |
| ItemReader | DB, 파일, API 등에서 데이터를 읽음 |
| ItemProcessor | 읽은 데이터를 변환/필터링 |
| ItemWriter | 처리된 데이터를 저장/출력 |
| JobRepository | Job/Step 실행 정보를 DB에 저장 |
| JobLauncher | Job을 실행시키는 인터페이스 |
Spring Batch의 Step은 두 가지 방식 중 하나를 선택한다.
대용량 데이터 처리에 사용한다. chunk-size만큼 읽고, 처리하고, 한 번에 저장하는 방식이다.
내부 동작을 정확히 보면:
read() 가 chunk-size만큼 반복 호출됨process() 도 그만큼 반복 호출됨write() 가 처리된 데이터를 List로 한 번에 받는 구조read() → process()
read() → process()
read() → process()
→ write(List [1, 2, 3]) → 커밋 ✅
트랜잭션은 chunk 단위로 걸린다.
Step 시작
chunk 1 → read x3 → process x3 → write(List) → 커밋 ✅
chunk 2 → read x3 → process x3 → write(List) → 커밋 ✅
chunk 3 → read x3 → process x3 → write(List) → 💥 실패
→ chunk 3만 롤백! chunk 1, 2는 이미 커밋됐으니 안전 ✅
Step 종료
파일 삭제, 알림 발송처럼 단순하고 가벼운 작업에 사용한다.
트랜잭션이 Step 전체에 걸린다.
@Bean
public Step cleanStep() {
return stepBuilderFactory.get("cleanStep")
.tasklet((contribution, chunkContext) -> {
tempFileService.deleteAll();
return RepeatStatus.FINISHED;
})
.build();
}
Tasklet으로 대용량 데이터를 처리하면 위험하다.
30만 건 처리 중...
29만 9999건 완료
마지막 1건 💥 실패
→ 30만 건 전부 롤백 ❌
→ 처음부터 다시 30만 건
| 상황 | 방식 |
|---|---|
| 대량 데이터 읽고 → 저장 | Chunk |
| 파일 삭제, 알림 발송, 초기화 | Tasklet |
| API 한 번 호출하고 끝 | Tasklet |
| DB 100만 건 마이그레이션 | Chunk |
한 Job 안에서 Tasklet과 Chunk를 자유롭게 조합할 수도 있다.
@Bean
public Job myJob() {
return jobBuilderFactory.get("myJob")
.start(cleanStep()) // Tasklet: 임시 파일 삭제
.next(processStep()) // Chunk: 대량 데이터 처리
.next(notifyStep()) // Tasklet: 완료 알림 발송
.build();
}
JobRepository는 Job/Step 실행 정보를 DB에 저장한다. 이유는 배치 작업은 실패했을 때 어떻게 복구하느냐가 가장 중요하기 때문이다.
Spring Batch는 실패한 Job을 중단 지점부터 재시작할 수 있다.
단, Reader가 ExecutionContext에 상태를 저장해야 중간부터 재시작이 가능하다.
100만 건 처리 중...
70만 건 완료 ✅ (ExecutionContext에 진행 상태 저장)
30만 건 처리 중 → 💥 서버 다운
→ Restart 시 70만 1건부터 재시작 가능 ✅ (상태 저장된 경우)
→ Stateless Reader라면 처음부터 다시 시작 ❌
JpaPagingItemReader 같은 Spring 제공 Reader는 기본적으로 상태 저장이 되지만,
커스텀 Reader를 직접 구현하는 경우에는 ItemStreamReader를 구현해서 상태 저장 로직을 직접 작성해야 한다.
Spring Batch는 JobName + JobParameters 조합으로 JobInstance를 결정한다.
같은 파라미터로 실행하면 상태와 관계없이 실행 자체가 불가능하다.
// 매번 다른 파라미터를 넣어야 새로운 JobInstance로 인식
jobLauncher.run(settlementJob, new JobParametersBuilder()
.addLong("time", System.currentTimeMillis()) // 이게 없으면 두 번째 실행 불가
.toJobParameters());
실무에서 addLong("time", ...) 으로 타임스탬프를 넣는 이유가 바로 이것이다.
실행일시 | Job명 | 상태 | 처리건수
2026-05-17 02:00 | settlementJob | COMPLETED | 980,000건
2026-05-16 02:00 | settlementJob | FAILED | 450,000건
Spring Batch는 자동으로 아래 메타 테이블들을 생성한다.
| 테이블 | 저장 내용 |
|---|---|
BATCH_JOB_INSTANCE | JobName + JobParameters 조합 (논리적 실행 단위) |
BATCH_JOB_EXECUTION | Job의 실제 실행 기록 (시작/종료시간, 상태) |
BATCH_STEP_EXECUTION | Step별 실행 기록 (읽은 건수, 처리 건수, 실패 건수) |
BATCH_JOB_EXECUTION_PARAMS | Job 실행 시 넘긴 파라미터 |
.<ViewLog, AuthorSettlement>chunk(1000)
.reader(reader())
.processor(processor())
.writer(writer())
.faultTolerant()
.skip(DataAccessException.class) // 이 예외는 skip
.skipLimit(10) // 최대 10건까지 skip 허용
.retry(DeadlockLoserDataAccessException.class) // 이건 재시도
.retryLimit(3)
.build();
Skip/Retry/Restart는 "다시 실행할 수 있게 해주는 것" 이고,
트랜잭션 롤백은 "데이터를 어디까지 되돌리냐" 의 문제다. 레이어가 다르다.
웹툰 플랫폼의 작가 정산 배치를 예시로 보자.
시나리오: 매일 새벽 2시, 전날 열람 데이터를 집계해서 작가 정산 테이블에 저장
[서비스 운영 중 - 낮]
사용자가 웹툰 클릭 → 결제 완료 → ViewLog 테이블에 자동 저장
[새벽 2시 배치]
ViewLog에서 전날 데이터 읽기 (Reader)
↓
수수료 30% 제외하고 작가 수익 계산 (Processor)
↓
AuthorSettlement 테이블에 저장 (Writer)
@Configuration
@RequiredArgsConstructor
public class SettlementJobConfig {
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
private final EntityManagerFactory entityManagerFactory;
// Job
@Bean
public Job settlementJob() {
return jobBuilderFactory.get("settlementJob")
.start(settlementStep())
.build();
}
// Step
@Bean
public Step settlementStep() {
return stepBuilderFactory.get("settlementStep")
.<ViewLog, AuthorSettlement>chunk(1000)
.reader(viewLogReader())
.processor(settlementProcessor())
.writer(settlementWriter())
.build();
}
// Reader: 전날 열람 로그 읽기
@Bean
public JpaPagingItemReader<ViewLog> viewLogReader() {
return new JpaPagingItemReaderBuilder<ViewLog>()
.name("viewLogReader")
.entityManagerFactory(entityManagerFactory)
.pageSize(1000)
.queryString("SELECT v FROM ViewLog v WHERE v.createdAt >= :yesterday")
.parameterValues(Map.of("yesterday", LocalDate.now().minusDays(1)))
.build();
}
// Processor: 수수료 계산
@Bean
public ItemProcessor<ViewLog, AuthorSettlement> settlementProcessor() {
return viewLog -> {
int authorRevenue = (int) (viewLog.getPrice() * 0.7); // 30% 수수료
return new AuthorSettlement(viewLog.getWebtoonId(), authorRevenue);
};
}
// Writer: 정산 테이블에 저장
@Bean
public JpaItemWriter<AuthorSettlement> settlementWriter() {
return new JpaItemWriterBuilder<AuthorSettlement>()
.entityManagerFactory(entityManagerFactory)
.build();
}
}
제네릭 타입의 흐름을 정리하면:
JpaPagingItemReader<ViewLog> // Reader: 읽을 타입
ItemProcessor<ViewLog, AuthorSettlement> // Processor: 입력 → 출력 타입
JpaItemWriter<AuthorSettlement> // Writer: 쓸 타입
.<ViewLog, AuthorSettlement>chunk(1000) // <Reader 타입, Writer 타입>
타입이 체인처럼 연결되며, 중간에 타입이 맞지 않으면 컴파일 에러가 나기 때문에 타입 안정성도 보장된다.
Job 하나당 Config 파일 하나로 관리하는 게 일반적인 패턴이다.
batch
├── SettlementJobConfig.java // 작가 정산
├── StatisticsJobConfig.java // 열람 통계
└── CleanUpJobConfig.java // 오래된 로그 삭제
Spring Batch 자체는 "어떻게 처리하냐"만 담당하고, "언제 실행하냐"는 별개다.
// 새벽 2시에 자동 실행
@Scheduled(cron = "0 0 2 * * *")
public void run() {
jobLauncher.run(settlementJob, new JobParametersBuilder()
.addLong("time", System.currentTimeMillis())
.toJobParameters());
}
보통 새벽에 배치를 실행하는 이유는 트래픽이 없는 시간대여야 하기 때문이다. 배치는 DB를 대량으로 읽고 쓰기 때문에 서비스 운영 중에 돌리면 DB 부하 급증, 테이블 Lock 등 장애로 이어질 수 있다.
스케줄링 외에도 다양한 실행 방법이 있다.
| 방식 | 설명 |
|---|---|
@Scheduled | 정해진 시간 자동 실행 |
| REST API 트리거 | 관리자 페이지 버튼으로 수동 실행 |
| 앱 시작 시 실행 | spring.batch.job.enabled=true |
| 외부 스케줄러 | Jenkins, Kubernetes CronJob 등 |
현재 개발 중인 AI 영어 학습 프로젝트에서는 실시간 피드백은 API로 처리하고, Spring Batch는 아래 두 가지 작업에 활용할 예정이다.
1. 주간 리포트 생성
일주일치 Feedback 데이터 읽기 (Reader)
↓
주간 평균 점수, 약점 패턴 집계 (Processor)
↓
WeeklyReport 테이블에 저장 (Writer)
2. 학습 목표 달성 여부 판단
user_goals 테이블 읽기 (Reader)
↓
current_value >= target_value 체크 (Processor)
↓
is_achieved 업데이트 (Writer)
실시간 처리가 필요한 건 API로, 주기적인 집계가 필요한 건 Spring Batch로 분리한 구조다.
read() / process() 는 반복 호출, write() 는 List로 한 번에 처리