Spring Batch에서의 Throttling 전략 정리
Spring Batch 환경에서 대용량 데이터를 안전하게 처리하기 위한 전략을 정리한 글입니다.
대용량 CSV 처리와 안전한 속도 제어를 중심으로
대용량 배치 작업에서는 얼마나 빠르게 처리하느냐보다 시스템이 감당할 수 있는 속도로 안정적으로 처리할 수 있음을 보장하는 것이 더 중요하게 여겨진다.
특히 다음과 같은 상황에서는 의도적으로 속도를 제어(Throttling)하는 장치가 필수적이다
- 대용량 CSV 기반 일괄 처리
- 외부 API 호출이 포함된 배치
- DB delete/update와 같이 부하가 큰 작업
- 운영 중 중단 및 재처리가 필요한 배치
배치 구현체인 Spring Batch 환경에서 주로 사용되며 적용해볼 수 있는 Throttling 전략을 처리 지점별로 정리하고, CSV 기반의 대량 처리 배치에 적용하기 좋은 구조를 정의해보자.
Throttling이란 무엇일까?
Throttling은 시스템이 감당할 수 있는 한계를 넘지 않도록, 의도적으로 처리 속도나 요청량을 제한하여 천천히 처리하게 만드는 제어 기법이다.
Rate Limit이 규칙이라면, Throttling은 그 규칙을 지키기 위한 실제 행동/액션이라고 볼 수 있다.
주요 목적은 다음과 같다.
- 갑작스러운 트래픽 증가로부터 시스템 보호
- 특정 사용자/서비스가 리소스를 독점하지 않도록 제어
- 외부 시스템(API, DB 등)에 대한 부하 관리
- 예측 가능한 처리량 유지 -> 과금 폭탄 방지
Spring Batch에서 속도를 제어할 수 있는 레벨
Spring Batch에서는 병목이 발생할 수 있는 지점에 따라 여러 레벨에서 속도 제어를 할 수 있다.
- ItemReader / ItemProcessor / ItemWriter 레벨
- Chunk 레벨
- Step 레벨
- Job/Partition 레벨
아래는 실무에서 자주 사용하는 방식들을 중심으로 정리한 내용이다.
[1] Item 단위에서 Sleep 을 통해 제어하기
가장 단순한 방식은 ItemReader / ItemProcessor / ItemWriter 에서 의도적으로 sleep을 주는 것이다.
class ThrottledItemProcessor : ItemProcessor<Input, Output> {
override fun process(item: Input): Output {
Thread.sleep(100) // 100ms = 초당 최대 10건
return processItem(item)
}
}
주로 다음과 같은 경우에 사용한다.
- 외부 API 호출 rate limit 대응
- DB 부하를 줄이기 위한 임시 제어
- 야간 배치에서 처리 속도를 의도적으로 낮추고 싶은 경우
| 장점 | 단점 |
|---|---|
| 구현이 매우 간단 | 정확한 TPS 제어는 어려움 |
| 위치별 제어 가능 | 멀티스레드 시 의미가 약해질 수 있음 |
[2] Chunk Size 및 Page Size 조절하기
commit-interval(chunk size)과paging-size를 튜닝하여 I/O 횟수를 줄이는 방법
Spring Batch에서 가장 기본적인 성능 레버는 chunk size이다. chunk란, 하나의 큰 데이터를 하나의 커밋에서 처리되는 row 수만큼 쪼갠 덩어리를 말한다. 이 단위는 트랜잭션의 단위가 되고, 덩어리 내 각 row는 별도 공간에 보관되었다가 한번에 처리되는 방식이다.
Spring Batch 공식 문서 - Chunk-oriented Processing
stepBuilderFactory.get("step")
.chunk<Input, Output>(10)
- 작은 chunk
- → 커밋 자주 → 안정적이지만 느림
- I/O가 빈번하게 발생하기 때문
- TPS를 낮추고 싶을 때 유리함
- 큰 chunk
- → 빠르지만 메모리 사용량 및 롤백 부담 ↑
- OOM 발생 위험 존재
- 실패 시 영향 범위가 커짐
대량 삭제와 같은 작업에서는 chunk size를 과도하게 크게 잡기보다는 병렬과 함께 조절하여 사용하는 편이 안전하다
[3] Multi-thread Step + throttleLimit
Spring Batch에서 공식적으로 제공하는 속도 제어 수단
stepBuilderFactory.get("step")
.chunk<Input, Output>(100)
.taskExecutor(taskExecutor()) // Step 설정 시 TaskExecutor 설정 *단일 프로세스 내 병렬 처리 지원*
.throttleLimit(5) // 동시에 실행되는 스레드 수 (Spring Batch 4.x)
Spring Batch 5.x 변경사항
throttleLimit은 Spring Batch 5.0에서 deprecated, 5.2에서 제거되었다. 대신 TaskExecutor의 concurrencyLimit 설정만으로 병렬도를 제어해야 한다.
@Bean
fun taskExecutor(): TaskExecutor =
SimpleAsyncTaskExecutor().apply {
concurrencyLimit = 5
}
- chunk 단위로 스레드를 할당해 병렬로 실행하는 방식
throttleLimit은 동시에 실행되는 chunk 수의 상한
-> 이떄의 TPS는 (chunk 처리 속도) × throttleLimit 로 대략적인 예측이 가능하다
이 방식은 다음 상황에 적합하다.
- 병렬 처리는 유지하되 전체 처리량은 제한하고 싶은 경우
- CPU 코어 수 내비 처리량을 통제하고 싶은 경우
- Processor 작업량이 상대적으로 무거운 경우
- 이때는 Processor만 비동기로 실행하여 성능을 높이는 방법도 있다 (AsyncItemProcessor / AsyncItemWriter)
[4] RateLimiter를 이용한 정확한 TPS 제어
외부 API 호출이나 DB 부하 보호가 중요한 배치에서 가장 안정적인 방식
Guava의 RateLimiter 예시 (Resilience4j도 유사한 API 제공):
val rateLimiter = RateLimiter.create(10.0) // 초당 10건
class ApiItemProcessor : ItemProcessor<Input, Output> {
override fun process(item: Input): Output {
rateLimiter.acquire() // permit 획득까지 blocking
return callExternalApi(item)
}
}
이 방식의 장점은 명확하다.
- 정확한 QPS 보장
- 멀티 스레드 환경에서도 안정적
- 운영 중 예측 가능한 부하 유지
[5] Listener를 이용한 Step 속도 제어
Listener에서 sleep을 주는 방식도 가능하다.
class SlowChunkListener : ChunkListener {
override fun afterChunk(context: ChunkContext) {
Thread.sleep(50)
}
}
디버깅이나 임시 제어 용도로는 유용하지만, 주요 Throttling 수단으로 사용하기에는 관리가 어렵다.
CSV 기반 대용량 처리에서의 권장 구조
앞선 Throttling 기법의 연장선으로, < 사전 검증 → 점진 처리 → 즉시 중단 → 실패 격리/재처리 > 와 같은 구조적 설계를 통해 운영 중 발생가능한 이슈 대응까지 고려하는 것이 중요하다.
권장 구조는 다음과 같다.
Job
├─ Step0: 사전 검증 (Validation / Dry-run)
├─ Step1: 본 처리 (점진 처리 + Throttling)
└─ Step2: 결과 리포트 (성공 / 실패 / 스킵)
대용량 CSV를 처리하는 배치에서는 입력값(CSV)을 Job으로 실행시키기 위해 다음과 같은 옵션을 고려해볼 수 있다.
*구현 옵션 2가지
- **CSV 기반 파티셔닝 CSV를 여러 파일로 쪼개서 하루에 1파일(혹은 N파일)씩 처리하는 방식이 롤백 제한적인 “물리 삭제”에 가장 맞고, 실패 격리/재처리도 쉽고, 즉시 중단/재개도 단순해짐
- 라인 단위 파티셔닝 설명 추가
CSV 처리에서 가장 안전한 파티셔닝 단위는 파일 단위이다.
왜 라인 단위 파티셔닝보다 파일 단위가 더 나은 선택일까?
- FlatFileItemReader는 thread-safe 하지 않음
- 라인 단위 파티셔닝은 경계/중복/누락 리스크가 큼
- 실패 격리 및 재처리가 복잡해짐
따라서 하나의 CSV 파일 내에서 파티셔닝 하는 것보다 Job이 처리할 단위로 물리적인 분할을 사전에 진행하는 것을 권장한다.
파일 1개 = 파티션 1개 +) 동시 처리할 파일(=파티션) 수는
gridSize로 제어
점진 처리(Throttling)를 위한 3가지 핵심
- 병렬도 제한 [Soft Throttling (속도 늦추기) : 요청을 큐에 넣고 천천히 처리 → 배치, 스트리밍에 자주 사용하는 방식]
- partition gridSize
- taskExecutor pool size
- step throttleLimit
- Chunk Size
- 너무 크게 잡지 않음 (ex. 100~1000)
- 병렬도로 throughput 조절
- RateLimiter [Hard Throttling (강제 차단) : 초과 요청 → 즉시 실패 (429 Too Many Requests)]
- 외부 API 또는 DB 보호용
- 초당 처리량을 명확히 제한
+) Token Bucket / Leaky Bucket : 정해진 속도로 토큰 생성 후, 토큰이 없으면 대기 or 거절
운영 중 즉시 중단 가능한 구조
Spring Batch의 stop은 강제 종료(즉시 kill)가 아닌, 안전 종료(Chunk 경계 기준)를 전제로 한다.
*Chunk 경계에서 멈추는 게 정합성에 유리하기 때문!
운영자 → stop_flag 설정
배치 → chunk 시작 시 stop_flag 확인
→ stepExecution.setTerminateOnly() 호출
// ChunkListener 또는 ItemProcessor 내에서
if (stopFlagRepository.isStopRequested(jobExecutionId)) {
stepContribution.stepExecution.setTerminateOnly()
}
위와 같은 패턴은 정합성을 해치지 않으면서 안전하게 즉시 중단할 수 있는 방법 중 하나이다.
실패 격리와 재처리 전략
실패 레코드는 분리하여 다시 Input으로 넣을 수 있도록!
대량 처리 배치는 실패를 구분하는 것이 중요하다. 특히 데이터 처리 이후의 롤백이 어려운 형태(e.g. Hard Delete)라면, 발생 가능한 실패 유형을 정의하는 것이 좋다.
- 재시도하면 해결될 가능성 높은 실패
- 일시적 네트워크, DB 타임아웃 -> Retry + backoff
- 데이터 문제(재시도해도 똑같이 실패)
- 이미 처리된 경우, 제약 조건 위반 이슈 -> Skip 처리 또는 별도 로그/출력물(failed.csv)로 분리
- 치명적(바로 중단해야 하는 실패)
- 예상치 못한 대량 오류 -> 즉시 중단 필수
실패 레코드는 별도의 Output(CSV)으로 분리하면, 다음 실행에서 failed 파일을 다시 input으로 실행하는 등 재처리 배치를 단순하게 구성할 수 있다.
정리
대용량 CSV 기반 배치에서 안정적인 Throttling을 위해서는 단일 기법보다는 구조적인 조합이 중요하다.
파일 단위 파티셔닝
병렬도 기반 처리량 조절
chunk는 보수적으로
필요 시 RateLimiter 추가
중단/실패/재처리를 전제로 한 설계
주어진 환경과 작업 맥락에 따라 적합한 전략이 다르겠지만, 이러한 구조적인 조합은 처리량을 조절하면서도 운영 리스크를 최소화하는 데 효과적이라는 것을 배웠다!