백엔드 개발을 하다 보면 아래와 같은 요구사항을 만나게 된다.
- 매일 새벽 정산 작업 실행
- 예약 푸시 메시지 발송
- CSV → DB 적재
- 회원 데이터 일괄 변경 (휴면계정 전환)
- 로그 데이터 집계
- 외부 API 데이터 동기화
이런 기능들을 구현하는 방법을 찾아보다 보면 자연스럽게 스프링 배치(Spring Batch) 를 사용하는 방법들이 많이 나온다.
하지만, 굳이 스프링 배치를 사용해야 할까? for 문이나 while 문 같은 반복문에 @Scheduled 조합하면 되는 거 아닌가? 라는 생각이 들었다.
생각해 보면 단순히 여러 건의 데이터를 한 번에 처리하는 것 자체는 스프링 배치 없이도 충분히 구현할 수 있다. 반복문으로 데이터를 순회할 수도 있고, 스케줄러를 붙여 특정 시간마다 실행할 수도 있다. 하지만 조금 더 찾아보고 내부 구조까지 살펴보니 현실의 배치 시스템은 단순히 반복 실행만 하면 끝나는 문제가 아니었다.
대량 데이터를 처리해야 할 수도 있고, 실행 중 장애가 발생할 수도 있으며, 실패한 작업을 다시 이어서 실행해야 하는 상황도 생긴다.
스프링 배치는 단순히 반복문을 대신하기 위한 기술이 아니라 대용량 작업을 안정적으로 실행하고, 중단·복구 가능하게 만들며, 운영까지 고려한 배치 시스템을 만들기 위한 프레임워크에 더 가까웠다.
이번 글에서는 왜 스프링 배치가 필요한지 알아보자.
반복문으로도 배치는 만들 수 있다.
1
2
3
4
5
6
7
8
@Scheduled(cron = "0 0 * * * *")
public void sendPush() {
List<User> users = userRepository.findAll();
for(User user : users){
pushService.send(user);
}
}
위와 같이 배치의 핵심인 반복 처리 자체는 자바 기본 기능만으로도 구현할 수 있다.
그러면 질문이 하나 생긴다.
Q. 그럼 굳이 왜 Spring Batch를 사용할까?
답은 단순하다.
A. 반복 자체는 쉽다. 하지만, 안정적으로 운영하는 것이 어렵다.
반복문 방식의 한계
100만 건의 푸시 메시지를 보내야 한다고 가정하자.
1
2
3
for(User user : users){
pushService.send(user);
}
위의 코드는 처음에는 잘 동작한다. 하지만 운영 환경에서는 예상하지 못한 문제가 발생할 수 있다. 만약, 100만 건 발송 시작 후 70만 건 처리 완료 후 서버 장애가 발생하여 프로세스가 종료되면 어떻게 될까?
1. 어디까지 처리했는지 모른다.
실제 70만 건까지 처리했지만, 애플리케이션은 어디까지 실행했는지 모른다. 이 상태에서 재실행하게 되면 처음부터 다시 시작한다.
2. 중복 발송 가능
누구에게까지 보냈는지 알 수 없어서, 이미 보낸 사용자에게 또 보낼 수 있다.
3. 누락 가능
만약 중간 지점을 찾았다고 하더라도 수동으로 계산하다 보면 누락이 발생할 수 있다.
4. 장애 복구가 어렵다.
아래와 같은 요구사항이 생길 수 있다.
- 실패 시 재시도
- 실패한 부분부터 다시 실행
- 실행 이력 저장
- 중복 실행 방지
- 운영 모니터링
- 트랜잭션 관리
- 메모리 관리
- 대량 데이터 처리
- 실행 중단 후 재개
이걸 반복문만으로 직접 구현하려면 코드가 매우 복잡해진다.
Spring Batch의 핵심 목적
스프링 배치는 단순 반복문 프레임워크가 아니다. 핵심은 대용량 작업을 안정적으로 운영하기 위한 프레임워크다.
스프링 배치는 반복문 위에 아래와 같은 운영 기능을 추가한다.
- Restart: 중단된 위치부터 재실행
- Retry: 일시적 실패 재시도
- ex)
- DB Deadlock
- 일시적 네트워크 장애
- API Timeout 등
- Skip: 문제 데이터만 건너뛰기
- 100만 건 중 1건 오류 → 전체 실패 X → 오류 데이터 Skip
- CheckPoint: 현재 Reader 위치, Chunk Commit 시점, 재시작 정보 등을 저장하여 어디까지 처리했는지 저장한다.
- Metadata 관리: 스프링 배치는 실행 상태를 DB에 저장한다.
- Job 상태
- Step 상태
- Commit 횟수
- Skip 횟수
- Read Count
- Write Count
- Retry 횟수
- ExecutionContext
즉, 배치 작업 진행 상태를 영속화하는 것이 핵심이다.
Spring Batch 내부 구조
핵심 구조
1
2
3
4
5
Job
└ Step
├ Reader
├ Processor
└ Writer
Job
배치 작업 전체를 의미한다. 예를 들어 회원 휴면계정 전환 작업이 있다면 하나의 Job이 된다. Job은 여러 개의 Step으로 구성되며, 실행 흐름을 관리한다.
1
2
3
4
5
휴면계정 전환 Job
Step1 → 대상 회원 조회
Step2 → 휴면 상태 변경
Step3 → 완료 로그 저장
즉, Job은 배치 작업 전체 흐름을 관리하는 단위라 볼 수 있다.
Step
Job 내부 실행 단계다. 실제 작업은 대부분 Step 단위에서 수행된다. 예를 들어 CSV 파일을 DB에 적재하는 배치라면 아래처럼 구성할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
CSV 적재 Job
Step1
→ 파일 다운로드(Tasklet)
Step2
→ CSV 읽기
→ 데이터 변환
→ DB 저장(Chunk)
Step3
→ 완료 메일 발송(Tasklet)
Step은 독립적으로 실행될 수 있으며, 실패 시 Step 단위로 재시작할 수도 있다.
Reader
Reader는 데이터를 읽어오는 역할을 담당한다. 중요한 점은 Reader 자체는 Chunk를 알지 못한다는 것이다. 대표적으로 아래와 같은 곳에서 데이터를 읽을 수 있다.
- DB 조회
- CSV 파일 읽기
- API 데이터 조회
- Kafka 메시지 소비
Reader는 단순히 Item 하나를 반환한다. Chunk Size = 1,000이라면 내부적으로는 다음과 비슷하게 동작한다.
1
2
3
4
5
6
7
read()
→ User1
→ User2
→ User3
...
→ User1000
Spring Batch 내부의 Chunk 처리 엔진이 Reader를 반복 호출하여 Chunk Size만큼 데이터를 모은다.
Processor
읽어온 데이터를 가공하거나 비즈니스 로직을 수행하는 역할을 담당한다. (필수는 아니다.)
1
2
3
4
5
User
↓
휴면 대상 여부 검사
↓
휴면 회원 객체 변환
대표적인 사용 예시:
- 데이터 검증
- DTO 변환
- 필터링
- 값 계산
- 비즈니스 규칙 적용
중요한 점은 Processor는 Chunk 단위가 아니라 Item 단위로 호출된다는 것이다. Chunk Size = 1,000이라면
1
2
3
4
5
6
7
8
9
10
11
Reader → 1,000개 읽기
Processor
1건 처리
1건 처리
1건 처리
...
1,000건 처리
Writer → 저장
즉, Processor는 내부적으로 1,000번 호출된다.
Writer
Writer는 최종 결과를 외부 시스템에 반영하는 역할을 담당한다.
대표적인 예시:
- DB 저장
- Kafka 발행
- 파일 쓰기
- 메일 발송
Processor와 가장 큰 차이점은 호출 단위다. Processor는 Item 단위로 호출되지만 Writer는 Chunk 단위로 호출된다. Chunk Size = 1,000이라면
1
2
3
4
5
6
7
8
9
10
11
12
Processor
User1
User2
...
User1000
↓
Writer(List<User> users)
DB Batch Insert
즉, Writer는 한 번에 Chunk 전체를 받아 처리한다. 이 방식 덕분에 DB Batch Insert 등을 활용할 수 있어 성능 측면에서도 유리하다.
Chunk?
스프링 배치에서 Chunk는 데이터를 일정 단위로 묶어서 처리하는 방식이다. 만약, 100만 건을 처리할 때 Chunk를 1,000으로 설정하면 100만 건을 한 번에 처리하는 대신, 1,000건씩 읽고 처리하고 저장하는 작업을 반복한다.
동작
1
1,000 읽기 → 1,000 처리 → 1,000 저장 → Commit → 상태 저장 → 다음 Chunk
실제 흐름
1
1 ~ 1,000 → Commit → 1,001 ~ 2,000 → Commit → 2,001 ~ 3,000 → Commit
만약, 4,500번째 처리 중 오류가 발생했다면
- 1 ~ 4,000 → 이미 Commit 완료
- 4,001 ~ 5,000 → 현재 Chunk Rollback
- 재시작 시 Checkpoint 정보를 기반으로 4,001 부터 이어서 실행할 수 있다.
Chunk를 사용하는 이유?
만약, 100만건의 데이터를 한 번에 처리하려면 메모리에 100만 건의 데이터가 모두 적재되어야 하고, 이를 처리하려면 메모리 문제가 발생할 수 있다. 대신 Chunk를 사용하면 Chunk 단위로 처리하므로 메모리 사용량이 일정하게 유지된다.
Tasklet?
스프링 배치에서는 Chunk 기반 처리 외에도 Tasklet 방식이 존재한다. Tasklet은 Step 내부에서 단일 작업 단위를 수행하는 모델로 개발자가 직접 작업 로직과 반복 여부를 제어하는 방식이다.
주로 아래와 같은 작업에 사용된다.
- 파일 이동/삭제
- 배치 시작 전 데이터 초기화
- API 호출
- 로그 출력
- 단일 SQL 실행 등
동작
1
Tasklet 실행 → 작업 수행 → 종료
실제 흐름
1
Step 시작 → Tasklet 실행 → Step 종료
Tasklet을 사용하는 이유
Tasklet이 단순 실행이라면 굳이 Tasklet을 사용하지 않고 직접 구현해도 될 거 같지만, Tasklet은 단순히 반복문 대신 사용하는 기능이 아니다. 핵심은 Chunk 모델에 맞지 않는 작업도 스프링 배치의 관리 체계 안에서 실행하기 위해서 사용한다는 점이다.
예를 들어 아래 작업들은 Reader → Processor → Writer 구조로 표현하기가 애매하다.
- S3/FTP 파일 다운로드
- 압축 해제
- 외부 API 호출
- 임시 파일 삭제
- 완료 메일 발송
이런 작업을 일반 코드로 직접 구현할 수도 있지만, 그러면 단순한 자바 코드일 뿐 배치 실행 이력이나 실패 관리 등을 직접 처리해야 한다.
반면, Tasklet으로 실행하면 스프링 배치가 아래 기능들을 함께 제공한다.
- Job/Step 실행 관리
- 상태 저장
- 트랜잭션 관리
- JobRepository 기록
- 실패/재시도 처리
- 실행 흐름 제어
즉, Tasklet은 Chunk 모델에 맞지 않는 작업을 배치 엔진 위에서 실행하기 위한 방식 이다.
Tasklet vs Chunk
그럼, Tasklet을 여러 번 호출하면 Chunk와 똑같은 것 아니냐고 생각할 수 있다. Tasklet을 여러 번 반복 호출하면 Chunk와 비슷하게 보일 수 있지만, 핵심 차이는 반복 책임을 누가 가지는가에 있다.
구조
1
2
3
4
TaskletStep
└ ChunkOrientedTasklet
├ SimpleChunkProvider
└ SimpleChunkProcessor
Chunk 모델은 내부적으로 ChunkOrientedTasklet 위에서 동작한다.
Tasklet
Tasklet으로 반복을 구현하려면 개발자가 반복을 직접 구현해야 한다.
특징
- 반복 책임 → 개발자
- 상태 관리 → Spring Batch
- 트랜잭션 → Spring Batch
사용 예시
- FTP 다운로드
- S3 파일 다운로드
- 압축 해제
- API 호출
- 메일 발송
- 로그 정리
Chunk
반복 자체를 프레임워크가 담당한다. 개발자는 Reader 구현, Processor 구현, Writer 구현만 하면 반복은 스프링 배치가 수행한다.
사용 예시
- ETL
- 통계 집계
- 정산
- 대량 데이터 처리
결론
- Chunk → 대량 데이터 처리 특화 모델
- Tasklet → 범용 작업 실행 모델
정리
스프링 배치를 한 문장으로 정의하면 상태 머신(State Machine) + 실행 엔진(Execution Engine) 이다.
결국, Spring Batch는 반복문 + 상태 머신 + 실행 엔진 + 메타데이터 관리 + 운영 기능을 제공하는 프레임워크로 신뢰성 있는 장시간 작업 운영을 위해 사용한다.
Spring Batch는 반복문과는 다른가?
스프링 배치의 내부 구현도 결국 반복문이다. 다만 반복문 위에 상태 관리, 재시작, Retry, Skip, 메타데이터, 트랜잭션, 운영 기능 등을 올린 것이다.
