3 min read
Part 10. 성능 최적화: 배치 사이즈, 커밋 간격, JVM 메모리, Backpressure
배치 처리량을 무작정 높이지 않고 시스템 한도 내에서 안정적으로 최적화하는 실전 튜닝 기준을 제시한다.
Series: Spring Boot 배치 전략 완전 정복
총 12편 구성. 현재 10편을 보고 있습니다.
- 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의 트레이드오프
- 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 메모리, BackpressureCURRENT
- 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) 문제 제기
배치 성능 튜닝에서 가장 위험한 행동은 "느리니까 chunk를 키운다"는 단일 처방이다. 실제로는 CPU, DB IOPS, 네트워크, 외부 API 한도가 서로 다르기 때문에 한 지점을 밀어도 다른 지점이 먼저 붕괴한다.
성능 최적화의 목표는 최고 TPS가 아니라 안정 구간 확보다.
- SLA 내 완료 시간 충족
- 장애 시 복구 시간 예측 가능
- OLTP 트래픽과 공존 가능
2) 핵심 개념 정리
배치 사이즈 결정 기준
- 단건 처리시간
t측정(Reader+Processor+Writer). - 목표 처리량
R계산(총 건수/허용 시간). - DB/외부 시스템 허용 동시성
C반영. chunk = (R / C) * 안전계수로 시작 후 P95 지표로 조정.
커밋 간격 트레이드오프
- 커밋 간격 증가: 트랜잭션 오버헤드 감소, 롤백 비용 증가.
- 커밋 간격 감소: 안정성 증가, TPS 감소.
JVM 메모리 계산 예시
- 레코드 평균 객체 크기: 6KB
- chunk: 1,000
- 동시 스레드: 6
- 예상 객체 메모리:
6KB x 1,000 x 6 = 약 36MB - 실제는 오버헤드(배열, 문자열, 직렬화 버퍼, 캐시)로 2~3배 필요
즉 최소 100MB 이상을 배치 워킹셋으로 확보하고, 힙의 30~40% 이내로 제어하는 것이 안전하다.
백프레셔 제어 루프
Mermaid diagram rendering...

출처: Pexels - Multi monitors workspace
3) 코드 예시
예시 A: 처리량 기반 동적 청크 계산
public class ChunkSizer {
public int calculate(long totalCount, Duration deadline, int maxConcurrency) {
long seconds = Math.max(deadline.getSeconds(), 1);
long targetPerSecond = Math.max(totalCount / seconds, 1);
long perWorker = Math.max(targetPerSecond / Math.max(maxConcurrency, 1), 1);
int chunk = (int) Math.min(Math.max(perWorker * 2, 100), 5000);
return chunk;
}
}
예시 B: 백프레셔 적용 TaskExecutor
@Bean
public ThreadPoolTaskExecutor batchTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(100);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
예시 C: 성능 지표 조회 SQL
SELECT job_name,
date_trunc('minute', started_at) AS minute_bucket,
AVG(duration_ms) AS avg_duration_ms,
PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY duration_ms) AS p95_duration_ms,
SUM(processed_count) AS total_processed
FROM batch_execution_metrics
WHERE started_at >= NOW() - INTERVAL '1 hour'
GROUP BY job_name, date_trunc('minute', started_at)
ORDER BY minute_bucket DESC;
예시 D: Keyset 기반 처리 창(window) 조회
SELECT id, payload
FROM event_queue
WHERE id > :last_id
ORDER BY id ASC
LIMIT :window_size;
4) 실제 장애/운영 시나리오
상황: 배치 완료 시간을 줄이기 위해 chunk를 1,000에서 10,000으로 늘리고 워커를 4에서 12로 확대했다. 초기 10분은 TPS가 상승했지만 이후 Full GC가 반복되고 DB 쓰기 지연이 증가해 전체 완료 시간이 오히려 늘었다.
원인:
- 힙 워킹셋이 급증해 GC 빈도와 pause time 증가.
- 커밋 단위가 과도해 락 홀딩 시간이 길어짐.
- downstream(OpenSearch Bulk) 429 증가를 무시하고 동일 속도로 밀어넣음.
개선:
- chunk 2,000, 워커 6으로 축소.
- 429 비율이 2% 초과 시 동시성 1단계 하향.
- P95 처리시간과 GC pause를 동일 대시보드에서 모니터링.
5) 설계 체크리스트
- 목표 완료 시간과 총 건수 기준으로 필요한 처리량을 계산했는가?
- chunk/동시성 증가 전후의 P95 지표를 비교했는가?
- 힙 워킹셋 추정치를 계산하고 여유 버퍼를 확보했는가?
- 커밋 간격이 락 홀딩 시간을 과도하게 늘리지 않는가?
- downstream 오류율(429, timeout)에 따라 백프레셔가 동작하는가?
- 성능 최적화가 API 트래픽 SLA를 침해하지 않는가?
6) 요약
성능 최적화는 "더 크게, 더 많이"가 아니라 "한도 내에서 안정적으로"다. 처리량 모델, 메모리 모델, 백프레셔 모델을 함께 설계해야 운영에서 지속 가능한 성능을 얻는다.
7) 다음 편 예고
다음 편에서는 장애 대응 아키텍처를 다룬다. Partial Failure, Poison Data, DLQ, 재시도/멱등성 설계를 통해 "실패해도 복구 가능한 배치"를 완성한다.
참고 링크
- Spring Batch Reference
- Quartz Scheduler Documentation
- PostgreSQL Transaction Isolation
- 블로그: Idempotency Key API 설계