웹 서비스 백엔드 개발자들은 주로 사용자가 스마트폰에서 버튼을 누르면 0.1초 만에 응답(JSON)을 주는 '실시간(Real-time)' 처리에 익숙합니다. 그런데 실무에서는 이런 요구사항이 비일비재하게 떨어집니다. "매월 1일 자정마다, 전체 회원 1,000만 명의 지난 한 달 치 결제 내역을 싹 다 조회해서 합산한 뒤, 50만 원 이상 쓴 사람들의 등급을 VIP로 업데이트하고 쿠폰을 발급해 주세요." 이 작업을 일반적인 스프링 웹 컨트롤러 안에서 for문 1,000만 번을 돌려서 처리하면 어떻게 될까요? 99.9% 확률로 메모리 초과(OOM)로 서버가 죽거나, 중간에 DB 커넥션이 끊겨서 '500만 명까지만 처리되고 멈추는' 끔찍한 대참사가 벌어집니다. 500만 명을 다시 어떻게 롤백할까요? 상상만 해도 끔찍합니다. 이 거대하고 위험한 대용량 일괄 처리(Batch Processing)를 아주 안전하고 우아하게, 끊겨도 다시 시작할 수 있게 만들어주는 프레임워크가 바로 Spring Batch(스프링 배치)입니다.
이번 [Spring Boot 실무 완벽 가이드] 시리즈에서는 "백그라운드 데이터 처리의 지배자!" Spring Batch의 웅장한 아키텍처와 도메인 언어에 대하여 다뤄보겠습니다.

⚙️ 1. Spring Batch의 핵심 개념: Job과 Step
스프링 배치는 거대한 작업을 아주 세밀하게 쪼개서 관리합니다. 이 단어들을 모르면 코드를 단 한 줄도 짤 수 없습니다.

- Job (작업): "월간 VIP 등급 업데이트 배치"라는 하나의 거대한 전체 프로젝트 단위입니다.
- Step (단계): Job을 이루는 작은 블록들입니다. 예를 들어
Step 1: 휴면 회원 정리 -> Step 2: 결제액 합산 -> Step 3: VIP 등급 업데이트처럼 여러 개의 Step을 조립해서 하나의 Job을 완성합니다. - JobInstance: "2023년 10월 1일 자정에 실행된 Job"처럼 특정 날짜/시간에 실행된 고유한 Job의 실체입니다. 만약 10월 1일 Job이 중간에 실패하면, 다음날 다시 실행할 때 새로운 Instance가 생기는 게 아니라 어제 실패했던 그 Instance의 실패한 지점부터 기가 막히게 이어서(Resume) 실행합니다! (이것이 배치를 쓰는 가장 큰 이유입니다)
🧱 2. Reader, Processor, Writer: 완벽한 분업화
각 Step 내부는 다시 3가지 역할로 완벽하게 분업(Chunk 지향 처리)되어 있습니다.
- ItemReader: DB나 파일(CSV)에서 데이터를 '조금씩' 퍼 올립니다. (1,000만 건을 한 번에 메모리에 올리면 서버가 터지니까, 1,000건씩 쪼개서 가져옵니다.)
- ItemProcessor: Reader가 퍼 올린 데이터를 '가공'합니다. (예: 결제액을 확인하고 VIP 여부를
true로 변경하는 비즈니스 로직) 필수는 아닙니다. - ItemWriter: Processor가 예쁘게 가공해 준 데이터를 모아두었다가 한 방에 DB에
UPDATE쿼리를 쏩니다.
💻 3. 아주 심플한 Hello World Batch 코드
Spring Boot 3.x(Batch 5.x) 기준으로, 과거의 지저분했던 JobBuilderFactory가 사라지고 코드가 아주 깔끔해졌습니다.
@Configuration
@RequiredArgsConstructor
public class HelloWorldBatchConfig {
private final JobRepository jobRepository; // 배치 작업의 성공/실패 기록을 DB에 저장하는 녀석
private final PlatformTransactionManager transactionManager;
// 1. 거대한 Job 만들기
@Bean
public Job helloJob() {
return new JobBuilder("helloJob", jobRepository)
.start(helloStep()) // helloStep부터 시작해!
.build();
}
// 2. Job 안에서 실행될 Step 만들기 (여기서는 Reader/Writer 안 쓰고 단순 Tasklet(작은 일꾼) 사용)
@Bean
public Step helloStep() {
return new StepBuilder("helloStep", jobRepository)
.tasklet((contribution, chunkContext) -> {
System.out.println("===========================");
System.out.println("Hello Spring Batch!"); // 실제로는 여기에 무거운 로직이 들어감
System.out.println("===========================");
return RepeatStatus.FINISHED; // "나 일 다 끝났어!" 라고 보고
}, transactionManager)
.build();
}
}
🎯 4. 마무리 및 다음 단계
지금까지 실시간 API 처리가 감당할 수 없는 엄청난 무게의 데이터를 분업화된 구조(Step, Reader, Writer)로 쪼개어 안전하게 처리하고, 중간에 실패하더라도 완벽하게 이어서(Resume) 실행할 수 있게 해주는 거대한 인프라, Spring Batch의 핵심 도메인 개념에 대해 상세히 다루어 보았습니다. 이제 대용량 데이터를 다루는 것에 대한 두려움이 사라졌습니다.
그런데 방금 본 예제는 너무 간단한 Tasklet(한 번에 다 처리해 버리는 방식)이었습니다. 실제 1,000만 건의 데이터를 처리할 때 Tasklet을 쓰면 여전히 OOM(Out of Memory) 에러를 피할 수 없습니다. 데이터를 메모리가 터지지 않게 '조금씩 쪼개서(Chunk)' 읽고, 가공하고, DB에 안전하게 밀어 넣는 진짜 실무 배치의 핵심 기술이 필요합니다. 이어지는 34단계 포스팅에서는 "수천만 건을 단 한 번의 메모리 터짐 없이 씹어 넘긴다!" Spring Batch의 꽃, Chunk(청크) 지향 처리 메커니즘과 PagingItemReader를 활용한 성능 최적화 비법에 대해 아주 뼈 때리게 파헤쳐 보겠습니다!
'Framework > Spring Boot' 카테고리의 다른 글
| [Spring Boot] Elasticsearch 연동: RDB의 LIKE 검색 한계를 부수다 (0) | 2026.08.05 |
|---|---|
| [Spring Boot] Spring Batch Chunk 지향 처리: 1,000만 건 메모리 최적화의 꽃 (0) | 2026.08.05 |
| [Spring Boot] STOMP와 RabbitMQ: 엔터프라이즈급 실시간 채팅의 완성 (0) | 2026.08.05 |
| [Spring Boot] WebSocket 실시간 채팅: 양방향 통신의 서막 (0) | 2026.08.05 |
| [Spring Boot] GraalVM Native Image: 자바의 한계를 찢고 0.05초 만에 부팅하라 (0) | 2026.07.27 |