Part 1. 배치의 본질과 분류: 스케줄, 이벤트, 수동, 대량, Near-real-time
Spring Boot 배치를 기능 비교가 아닌 운영 아키텍처 관점으로 분해하고, 어떤 배치 모델을 선택해야 하는지 기준을 제시한다.
Series: Spring Boot 배치 전략 완전 정복
총 12편 구성. 현재 1편을 보고 있습니다.
- 01Part 1. 배치의 본질과 분류: 스케줄, 이벤트, 수동, 대량, Near-real-timeCURRENT
- 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 메모리, 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) 문제 제기
실무에서 "배치"는 하나의 기술이 아니라, 다른 실패 모델을 가진 여러 실행 모델의 묶음이다. 같은 Spring Boot 애플리케이션 안에서 @Scheduled, Quartz, Spring Batch, 수동 실행 API가 동시에 존재하는 이유도 여기에 있다. 문제는 많은 팀이 이들을 "구현 편의성"으로만 선택한다는 점이다.
- 정시 정산은 분 단위 지연이 허용되지 않는다.
- 포인트 재계산은 몇 시간 지연돼도 되지만 데이터 정합성이 우선이다.
- 운영자 재처리는 속도보다 재현 가능성과 감사 추적이 우선이다.
- 색인 동기화는 완전 실시간보다 "수 분 내 반영"이 비용 효율적이다.
즉, 배치 전략은 코드 스타일이 아니라 SLA, RPO, 장애 복구 방식, 데이터 볼륨으로 결정해야 한다.
2) 핵심 개념 정리
배치를 먼저 5가지로 분류하면 의사결정이 단순해진다.
| 분류 | 트리거 | 대표 기술 | 강점 | 주요 리스크 |
|---|---|---|---|---|
| 스케줄 기반 | 시간(cron) | @Scheduled, Quartz | 예측 가능성 | 멀티 인스턴스 중복 실행 |
| 이벤트 기반 | 메시지/변경 이벤트 | Kafka Consumer, CDC | 낮은 지연 | 폭주 시 역압력 필요 |
| 수동 실행 | 운영자 트리거 | Admin API/UI | 통제력, 재처리 유연성 | 권한/감사 누락 위험 |
| 대량 처리 | 대용량 범위 스캔 | Spring Batch Chunk | 트랜잭션 관리/재시작 | 메모리/락 경쟁 |
| Near-real-time | 짧은 주기 + 증분 조회 | 스케줄+이벤트 혼합 | 비용 대비 반영성 | 이중 경로 정합성 |
트랜잭션 격리 수준은 배치 분류와 같이 잡아야 한다. 예를 들어 집계성 배치는 READ COMMITTED로도 충분하지만, 같은 조건을 반복 조회해 처리하는 재고 계산 배치는 REPEATABLE READ 또는 스냅샷 읽기를 고려해야 한다. 격리 수준을 높일수록 락 경합과 지연이 증가하므로 무조건 높은 수준을 택하면 실패한다.
대량 배치에서는 JVM 메모리도 아키텍처 변수다. 예를 들어 1건당 처리 객체가 평균 4KB이고 청크가 2,000건이면 한 스텝에서 최소 8MB + 오브젝트 오버헤드 + 직렬화 버퍼가 필요하다. 여기에 멀티스레드 스텝(예: 8 threads)을 곱하면 힙 압박이 바로 발생한다.
분류 결정 다이어그램

출처: Pexels - Workplace with program code
3) 코드 예시
예시 A: 배치 타입별 정책 라우팅
public enum BatchType {
SCHEDULED,
EVENT_DRIVEN,
MANUAL,
BULK,
NEAR_REAL_TIME
}
public record BatchExecutionPolicy(
Duration timeout,
int maxRetry,
Isolation isolation,
int chunkSize
) {}
public class BatchPolicyResolver {
public BatchExecutionPolicy resolve(BatchType type) {
return switch (type) {
case SCHEDULED -> new BatchExecutionPolicy(Duration.ofMinutes(30), 1, Isolation.READ_COMMITTED, 500);
case EVENT_DRIVEN -> new BatchExecutionPolicy(Duration.ofMinutes(5), 5, Isolation.READ_COMMITTED, 200);
case MANUAL -> new BatchExecutionPolicy(Duration.ofHours(2), 0, Isolation.REPEATABLE_READ, 1000);
case BULK -> new BatchExecutionPolicy(Duration.ofHours(6), 2, Isolation.REPEATABLE_READ, 2000);
case NEAR_REAL_TIME -> new BatchExecutionPolicy(Duration.ofMinutes(2), 3, Isolation.READ_COMMITTED, 100);
};
}
}
예시 B: 실행 이력 테이블과 조회 SQL
CREATE TABLE batch_job_execution (
id BIGSERIAL PRIMARY KEY,
job_name VARCHAR(100) NOT NULL,
batch_type VARCHAR(30) NOT NULL,
requested_by VARCHAR(100) NULL,
status VARCHAR(20) NOT NULL,
started_at TIMESTAMP NOT NULL,
ended_at TIMESTAMP NULL,
error_code VARCHAR(50) NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_bje_job_started ON batch_job_execution (job_name, started_at DESC);
CREATE INDEX idx_bje_status_started ON batch_job_execution (status, started_at DESC);
-- 최근 24시간 실패한 배치 확인
SELECT id, job_name, batch_type, status, started_at, ended_at, error_code
FROM batch_job_execution
WHERE status = 'FAILED'
AND started_at >= NOW() - INTERVAL '24 hour'
ORDER BY started_at DESC
LIMIT 200;
예시 C: 키셋 기반 재처리 대상 조회
SELECT id, job_name, started_at
FROM batch_job_execution
WHERE status = 'FAILED'
AND id > :last_id
ORDER BY id ASC
LIMIT 500;
4) 실제 장애/운영 시나리오
상황: 월말 00:00 정산 배치(@Scheduled)와 운영자 수동 재처리 API가 동시에 같은 주문 테이블을 갱신했다. 두 경로 모두 "오늘 미정산" 조건으로 조회했기 때문에 동일 주문이 중복 정산되었다.
원인:
- 실행 모델이 다르지만 동일한 비즈니스 키에 대한 멱등성 키가 없었다.
- 트랜잭션 격리 수준은
READ COMMITTED였고, 재처리 쿼리에FOR UPDATE SKIP LOCKED가 없었다. - 배치 이력은 기록했지만 "어떤 입력 파라미터로 실행했는지" 저장하지 않았다.
복구:
- 정산 테이블에
UNIQUE(order_id, settlement_date)추가. - 재처리 쿼리를
SELECT ... FOR UPDATE SKIP LOCKED로 수정. - 실행 이력에
request_hash컬럼 추가해 동일 요청 중복 차단.
5) 설계 체크리스트
- 배치를 5개 타입(스케줄/이벤트/수동/대량/NRT)으로 먼저 분류했는가?
- 각 타입별 SLA, 허용 지연, 재시도 정책이 문서화되어 있는가?
- 트랜잭션 격리 수준을 배치 목적에 맞게 선택했는가?
- 배치별 멱등 키 설계(
업무키+일자등)가 존재하는가? - 실행 이력에 파라미터, 요청자, 재시도 횟수, 오류코드가 남는가?
- 대량 배치에서 청크 크기와 JVM 힙 사용량을 수치로 계산했는가?
6) 요약
배치 설계의 시작점은 "어떤 라이브러리를 쓸지"가 아니다. 어떤 실패를 감당해야 하는지, 어떤 정합성을 보장해야 하는지부터 결정해야 한다. 이 기준이 먼저 있어야 @Scheduled, Quartz, Spring Batch, 수동 배치의 역할이 명확해진다.
7) 다음 편 예고
다음 편에서는 @Scheduled를 운영 환경에서 안전하게 쓰는 방법을 다룬다. 특히 멀티 인스턴스 중복 실행, 분산락(Redis/DB), 장애 시 재기동 중복 처리 문제를 실제 패턴 중심으로 정리한다.
참고 링크
- Spring Batch Reference
- Quartz Scheduler Documentation
- PostgreSQL Transaction Isolation
- 블로그: Idempotency Key API 설계
시리즈 네비게이션
- 이전 글: 없음 (이 시리즈의 시작편)
- 다음 글: Part 2. @Scheduled 실전 운영: 단순함의 대가와 멀티 인스턴스 함정