3 min read

Part 10. 성능 최적화: 배치 사이즈, 커밋 간격, JVM 메모리, Backpressure

배치 처리량을 무작정 높이지 않고 시스템 한도 내에서 안정적으로 최적화하는 실전 튜닝 기준을 제시한다.

Series: Spring Boot 배치 전략 완전 정복

12편 구성. 현재 10편을 보고 있습니다.

썸네일 - 대규모 서버 인프라
썸네일 - 대규모 서버 인프라

출처: 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) 핵심 개념 정리

배치 사이즈 결정 기준

  1. 단건 처리시간 t 측정(Reader+Processor+Writer).
  2. 목표 처리량 R 계산(총 건수/허용 시간).
  3. DB/외부 시스템 허용 동시성 C 반영.
  4. 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 증가를 무시하고 동일 속도로 밀어넣음.

개선:

  1. chunk 2,000, 워커 6으로 축소.
  2. 429 비율이 2% 초과 시 동시성 1단계 하향.
  3. P95 처리시간과 GC pause를 동일 대시보드에서 모니터링.

5) 설계 체크리스트

  • 목표 완료 시간과 총 건수 기준으로 필요한 처리량을 계산했는가?
  • chunk/동시성 증가 전후의 P95 지표를 비교했는가?
  • 힙 워킹셋 추정치를 계산하고 여유 버퍼를 확보했는가?
  • 커밋 간격이 락 홀딩 시간을 과도하게 늘리지 않는가?
  • downstream 오류율(429, timeout)에 따라 백프레셔가 동작하는가?
  • 성능 최적화가 API 트래픽 SLA를 침해하지 않는가?

6) 요약

성능 최적화는 "더 크게, 더 많이"가 아니라 "한도 내에서 안정적으로"다. 처리량 모델, 메모리 모델, 백프레셔 모델을 함께 설계해야 운영에서 지속 가능한 성능을 얻는다.

7) 다음 편 예고

다음 편에서는 장애 대응 아키텍처를 다룬다. Partial Failure, Poison Data, DLQ, 재시도/멱등성 설계를 통해 "실패해도 복구 가능한 배치"를 완성한다.

참고 링크

시리즈 네비게이션

댓글