3 min read
Part 5. Spring Batch 확장: Partition과 Multi-threaded Step의 트레이드오프
Spring Batch 병렬 처리 전략을 처리량, 순서 보장, 운영 복잡도 관점에서 비교하고 선택 기준을 제시한다.
Series: Spring Boot 배치 전략 완전 정복
총 12편 구성. 현재 5편을 보고 있습니다.
- 01Part 1. 배치의 본질과 분류: 스케줄, 이벤트, 수동, 대량, Near-real-time
- 02Part 2. @Scheduled 실전 운영: 단순함의 대가와 멀티 인스턴스 함정
- 03Part 3. Quartz 클러스터 아키텍처: JobStore, Misfire, 대규모 스케줄 관리
- 04Part 4. Spring Batch 핵심: Chunk, 트랜잭션 경계, Restart 가능한 Job 설계
- 05Part 5. Spring Batch 확장: Partition과 Multi-threaded Step의 트레이드오프CURRENT
- 06Part 6. 수동 배치 전략: REST 트리거, Admin UI, 파라미터 재처리, 롤백
- 07Part 7. DB 대량 조회 전략: OFFSET/LIMIT 한계와 Keyset, ID Range, Covering Index
- 08Part 8. OpenSearch/Elasticsearch 배치 전략: Scroll, Search After, PIT, Bulk, Rollover
- 09Part 9. 분산 환경 배치: Leader Election, Kubernetes CronJob, 락 전략 비교
- 10Part 10. 성능 최적화: 배치 사이즈, 커밋 간격, JVM 메모리, Backpressure
- 11Part 11. 장애 대응 아키텍처: Partial Failure, Poison Data, DLQ, 재시도, 멱등성
- 12Part 12. 통합 레퍼런스 아키텍처와 최종 선택 가이드

출처: Pexels - Server racks on data center
버전 기준
- Java 21
- Spring Boot 3.3.x
- Spring Batch 5.2.x
- Quartz 2.3.x
- PostgreSQL 15
- OpenSearch 2.x
1) 문제 제기
처리량 병목이 생기면 팀은 거의 본능적으로 스레드를 늘린다. 하지만 배치에서 병렬화는 단순 가속 버튼이 아니다. 병렬도를 올리면 다음 문제가 동시에 커진다.
- 순서 보장 붕괴
- 락 경쟁 증가
- 실패 시 재처리 범위 확대
- 메모리 사용량 급증
그래서 병렬화는 "최대 TPS"가 아니라 "실패했을 때 복구 가능한 구조"를 기준으로 선택해야 한다.
2) 핵심 개념 정리
병렬화 방식 비교
| 방식 | 장점 | 단점 | 적합한 데이터 |
|---|---|---|---|
| Multi-threaded Step | 구현 단순, 빠른 적용 | Reader/Writer thread-safe 요구 | 순서 비중 낮은 독립 레코드 |
| Partition Step | 구간 분리로 충돌 감소 | 파티션 설계 복잡 | ID 범위 분할 가능한 대량 데이터 |
| Remote Chunking | 수평 확장 유리 | 메시지 인프라/운영 복잡 | 초대용량, 장시간 처리 |
파티션 기준
ID Range: 가장 단순하고 재시작이 쉽다.Hash Partition: 키 분산은 좋지만 디버깅 난이도 상승.Time Window: 로그성 데이터에 유리.
병렬 처리 다이어그램
Mermaid diagram rendering...

출처: Pexels - Drone shot of cargo containers
3) 코드 예시
예시 A: ID Range Partitioner
public class IdRangePartitioner implements Partitioner {
private final JdbcTemplate jdbcTemplate;
public IdRangePartitioner(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public Map<String, ExecutionContext> partition(int gridSize) {
Long minId = jdbcTemplate.queryForObject("SELECT COALESCE(MIN(id),0) FROM orders WHERE status='PENDING'", Long.class);
Long maxId = jdbcTemplate.queryForObject("SELECT COALESCE(MAX(id),0) FROM orders WHERE status='PENDING'", Long.class);
long targetSize = ((maxId - minId) / gridSize) + 1;
Map<String, ExecutionContext> result = new HashMap<>();
long start = minId;
long end = start + targetSize - 1;
int partition = 0;
while (start <= maxId) {
ExecutionContext context = new ExecutionContext();
context.putLong("startId", start);
context.putLong("endId", Math.min(end, maxId));
result.put("partition" + partition, context);
start += targetSize;
end += targetSize;
partition++;
}
return result;
}
}
예시 B: Partition Step 설정
@Bean
public Step masterStep(JobRepository jobRepository,
Step workerStep,
Partitioner partitioner,
TaskExecutor taskExecutor) {
return new StepBuilder("masterStep", jobRepository)
.partitioner("workerStep", partitioner)
.step(workerStep)
.gridSize(8)
.taskExecutor(taskExecutor)
.build();
}
예시 C: 파티션 대상 조회 SQL
SELECT id, customer_id, total_amount
FROM orders
WHERE status = 'PENDING'
AND id BETWEEN :start_id AND :end_id
ORDER BY id ASC
LIMIT :chunk_size;
예시 D: 인덱스와 락 경합 완화
CREATE INDEX idx_orders_status_id ON orders (status, id);
-- 워커별로 잠긴 레코드를 건너뛰어 경합 최소화
SELECT id
FROM orders
WHERE status = 'PENDING'
AND id BETWEEN :start_id AND :end_id
FOR UPDATE SKIP LOCKED
LIMIT :chunk_size;
4) 실제 장애/운영 시나리오
상황: 16개 파티션으로 주문 집계를 병렬화했지만, 결과 테이블에 동일 (order_id, stat_date)가 중복 저장됐다.
원인:
- Reader는 파티션 범위를 나눴지만 Writer는 공통 집계 키를 사용했다.
- 결과 테이블에 유니크 인덱스가 없어 경쟁 조건이 그대로 반영됐다.
REPEATABLE READ를 사용하면서 장시간 트랜잭션이 유지되어 락 대기가 급증했다.
조치:
- 결과 테이블에
UNIQUE(order_id, stat_date)추가. - Writer를
INSERT ... ON CONFLICT DO UPDATE로 변경. - 파티션 크기를 균등 분할이 아닌 히스토리 기반 가중 분할로 조정.
5) 설계 체크리스트
- 병렬화 목적이 처리량인지 지연시간인지 명확한가?
- 파티션 키가 충돌 없는 쓰기 경계를 보장하는가?
- Reader/Writer가 thread-safe한가?
- 파티션 실패 시 재시작 범위를 좁게 복구할 수 있는가?
- JVM 힙 사용량(청크 x 스레드 x 객체 크기)을 계산했는가?
- 유니크 인덱스와 멱등 Writer로 중복 쓰기를 차단했는가?
6) 요약
병렬 처리는 속도를 주지만, 설계가 약하면 장애를 증폭한다. 파티션 경계, 쓰기 멱등성, 재시작 범위를 먼저 설계한 뒤 스레드를 늘려야 운영 가능한 성능이 나온다.
7) 다음 편 예고
다음 편에서는 수동 배치 전략을 다룬다. REST 트리거, Admin UI 실행, 파라미터 재처리, 롤백/감사 추적까지 포함해 "운영자가 안전하게 개입하는 시스템"을 설계한다.
참고 링크
- Spring Batch Reference
- Quartz Scheduler Documentation
- PostgreSQL Transaction Isolation
- 블로그: Idempotency Key API 설계