3 min read

Part 1. 배치의 본질과 분류: 스케줄, 이벤트, 수동, 대량, Near-real-time

Spring Boot 배치를 기능 비교가 아닌 운영 아키텍처 관점으로 분해하고, 어떤 배치 모델을 선택해야 하는지 기준을 제시한다.

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

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

썸네일 - 데이터센터 운영
썸네일 - 데이터센터 운영

출처: 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)을 곱하면 힙 압박이 바로 발생한다.

분류 결정 다이어그램

Mermaid diagram rendering...

본문 이미지 - 코드 기반 운영
본문 이미지 - 코드 기반 운영

출처: 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가 없었다.
  • 배치 이력은 기록했지만 "어떤 입력 파라미터로 실행했는지" 저장하지 않았다.

복구:

  1. 정산 테이블에 UNIQUE(order_id, settlement_date) 추가.
  2. 재처리 쿼리를 SELECT ... FOR UPDATE SKIP LOCKED로 수정.
  3. 실행 이력에 request_hash 컬럼 추가해 동일 요청 중복 차단.

5) 설계 체크리스트

  • 배치를 5개 타입(스케줄/이벤트/수동/대량/NRT)으로 먼저 분류했는가?
  • 각 타입별 SLA, 허용 지연, 재시도 정책이 문서화되어 있는가?
  • 트랜잭션 격리 수준을 배치 목적에 맞게 선택했는가?
  • 배치별 멱등 키 설계(업무키+일자 등)가 존재하는가?
  • 실행 이력에 파라미터, 요청자, 재시도 횟수, 오류코드가 남는가?
  • 대량 배치에서 청크 크기와 JVM 힙 사용량을 수치로 계산했는가?

6) 요약

배치 설계의 시작점은 "어떤 라이브러리를 쓸지"가 아니다. 어떤 실패를 감당해야 하는지, 어떤 정합성을 보장해야 하는지부터 결정해야 한다. 이 기준이 먼저 있어야 @Scheduled, Quartz, Spring Batch, 수동 배치의 역할이 명확해진다.

7) 다음 편 예고

다음 편에서는 @Scheduled를 운영 환경에서 안전하게 쓰는 방법을 다룬다. 특히 멀티 인스턴스 중복 실행, 분산락(Redis/DB), 장애 시 재기동 중복 처리 문제를 실제 패턴 중심으로 정리한다.

참고 링크

시리즈 네비게이션

댓글