Spring Batch (1/8)

다음 편: [Spring Batch] 2. Job과 Step 구조

Spring Batch 가이드 시리즈

01 배치 기초

- 배치 처리란 무엇인가 ← 현재 편

- Job과 Step 구조

02 Chunk 처리

- Chunk 기반 처리

- 다양한 Reader와 Writer

03 실행 제어

- 실행 제어와 흐름

- 재시도와 건너뛰기

04 운영과 면접

- 스케줄링과 운영

- 면접 대비

사용자가 요청할 때 즉시 응답하는 API와 달리, 새벽 2시에 아무도 모르게 수만 건의 데이터를 처리하는 작업이 있다. 정산, 통계 집계, 대량 알림 발송 — 이런 작업은 실시간으로 처리할 필요도 없고, 실시간으로 처리할 수도 없다. 이런 대량 데이터의 일괄 처리를 배치 처리라고 부른다.

배치 처리란

배치 처리는 대량의 데이터를 모아서 한꺼번에 처리하는 방식이다. 사용자의 요청에 즉시 응답하는 실시간 처리와 대비되는 개념이다.

편의점 택배를 생각해보자. 고객이 택배를 맡길 때마다 배송 트럭이 출발하면 비효율적이다. 대신 하루 동안 모인 택배를 저녁에 한 번에 싣고 출발한다. 배치 처리도 이와 같다. 데이터를 모아두었다가, 정해진 시점에 일괄적으로 처리한다.

실시간 처리와 배치 처리의 차이를 비교하면 다음과 같다.

flowchart LR subgraph RT["실시간 처리"] direction TB U1["사용자 요청"] --> S1["즉시 처리"] S1 --> R1["즉시 응답"] end RT ~~~ BT subgraph BT["배치 처리"] direction TB D1["데이터 축적"] --> T1["정해진 시점"] T1 --> P1["일괄 처리"] end style RT fill:#E8F8E8,stroke:#4CAF50 style BT fill:#E8F4F8,stroke:#2196F3
구분실시간 처리배치 처리
트리거사용자 요청스케줄 또는 수동 실행
처리 단위건 by 건수천~수백만 건
응답 시간밀리초~초분~시간
대표 예시API 호출, 검색정산, 통계, 데이터 마이그레이션

배치가 필요한 상황

모든 작업이 배치로 처리되어야 하는 건 아니다. 배치가 필요한 상황에는 공통된 패턴이 있다.

대량 데이터 처리

수만~수백만 건의 데이터를 한 번에 처리해야 할 때다. 하나씩 API로 처리하면 시간이 너무 오래 걸리거나 시스템에 과부하가 걸린다.

  • 월말 정산 — 한 달치 주문 데이터를 집계해서 정산 금액을 계산
  • 데이터 마이그레이션 — 레거시 DB에서 새 DB로 수백만 건의 레코드를 이관
  • 대량 이메일/알림 발송 — 10만 명의 사용자에게 프로모션 알림을 전송

주기적 집계

일정 주기로 데이터를 모아 통계를 내야 할 때다.

  • 일일 매출 리포트 — 하루 동안의 거래를 집계해서 대시보드에 반영
  • 인기 피드 순위 — 24시간 동안의 좋아요 수를 집계해서 순위를 갱신
  • 사용자 활동 통계 — 주간 활동 로그를 분석해서 리포트 생성

시스템 유지보수

서비스 운영에 필요하지만 사용자에게 직접 보이지 않는 관리 작업이다.

  • 만료 데이터 정리 — 90일이 지난 임시 파일이나 만료된 토큰을 삭제
  • 캐시 워밍 — 서비스 시작 전에 자주 조회되는 데이터를 미리 캐시에 적재
  • 데이터 정합성 검증 — 두 시스템 간의 데이터가 일치하는지 주기적으로 확인
배치 vs 실시간 판단 기준

- 즉시 응답이 필요한가? → 실시간

- 대량 데이터를 한꺼번에 처리해야 하는가? → 배치

- 정해진 시간에 주기적으로 실행되는가? → 배치

- 처리 시간이 수 분 이상 걸리는가? → 배치

하나라도 해당하면 배치를 고려할 만하다.

Spring Batch란

Spring Batch는 Java 진영의 표준 배치 프레임워크다. Spring 생태계의 일부로, 대량 데이터 처리에 필요한 구조와 기능을 제공한다.

배치를 직접 구현한다고 생각해보자. 데이터를 읽고, 가공하고, 저장하는 코드를 짜는 건 어렵지 않다. 하지만 실무에서는 단순한 로직 외에도 해결해야 할 문제가 많다.

  • 10만 건 중 5만 건째에서 에러가 나면 처음부터 다시?
  • 메모리에 100만 건을 한 번에 올릴 수 없으면?
  • 어제 실행한 배치가 정상 완료되었는지 어떻게 확인하지?
  • 실패한 배치를 이어서 다시 실행할 수 있을까?

Spring Batch는 이런 문제를 프레임워크 차원에서 해결한다.

mindmap root((Spring Batch)) 구조 Job / Step 계층 Chunk 기반 처리 Tasklet 모델 안정성 재시작 / 재시도 건너뛰기(Skip) 트랜잭션 관리 운영 실행 이력 관리 JobParameters 모니터링 확장 병렬 처리 파티셔닝 원격 청킹

Spring Batch의 핵심 특징

Chunk 기반 처리

데이터를 한 번에 전부 메모리에 올리지 않는다. 일정 크기(chunk)로 나눠서 읽기 → 가공 → 쓰기를 반복한다.

100만 건을 처리한다면, 1,000건씩 끊어서 1,000번 반복한다. 각 chunk 단위로 트랜잭션이 관리되므로, 중간에 실패해도 이미 처리된 chunk는 보존된다.

이 구조를 그림으로 보면 다음과 같다.

sequenceDiagram autonumber participant Reader as ItemReader participant Processor as ItemProcessor participant Writer as ItemWriter participant TX as 트랜잭션 loop chunk 단위 반복 (예: 1,000건씩) TX->>TX: 트랜잭션 시작 rect rgb(232, 248, 232) Note over Reader,Writer: Chunk 처리 Reader->>Reader: 1,000건 읽기 Reader->>Processor: 데이터 전달 Processor->>Processor: 가공/변환/필터링 Processor->>Writer: 가공된 데이터 전달 Writer->>Writer: 1,000건 일괄 저장 end TX->>TX: 커밋 end

Reader가 데이터를 읽고, Processor가 가공하고, Writer가 저장한다. 이 3단계가 chunk 단위로 반복되며, 각 chunk마다 트랜잭션이 걸린다. 5번째 chunk에서 실패하면 1~4번째 chunk의 결과는 이미 커밋되어 있으므로 안전하다.

재시작과 재시도

배치 작업이 중간에 실패하면, 실패한 지점부터 이어서 재시작할 수 있다. Spring Batch는 각 Step의 실행 상태를 DB에 기록해두기 때문에, 어디까지 처리했는지 정확히 알고 있다.

특정 레코드에서 일시적 오류가 발생하면, 해당 레코드만 재시도하거나 건너뛸 수도 있다. 네트워크 타임아웃 같은 일시적 문제에 유용하다.

실행 이력 관리

모든 배치 실행은 메타데이터 테이블에 기록된다. 언제 실행했는지, 성공했는지, 몇 건을 처리했는지, 얼마나 걸렸는지가 자동으로 저장된다.

erDiagram BATCH_JOB_INSTANCE ||--o{ BATCH_JOB_EXECUTION : "실행 이력" BATCH_JOB_EXECUTION ||--o{ BATCH_STEP_EXECUTION : "Step별 기록" BATCH_JOB_EXECUTION ||--o{ BATCH_JOB_EXECUTION_PARAMS : "실행 파라미터" BATCH_JOB_INSTANCE { bigint JOB_INSTANCE_ID PK varchar JOB_NAME varchar JOB_KEY } BATCH_JOB_EXECUTION { bigint JOB_EXECUTION_ID PK varchar STATUS timestamp START_TIME timestamp END_TIME varchar EXIT_CODE } BATCH_STEP_EXECUTION { bigint STEP_EXECUTION_ID PK varchar STEP_NAME bigint READ_COUNT bigint WRITE_COUNT bigint SKIP_COUNT varchar STATUS }

운영팀에서 "어제 정산 배치가 정상적으로 돌았나?"라고 물으면, 메타데이터 테이블을 조회해서 바로 확인할 수 있다.

Spring Batch 아키텍처 개요

Spring Batch의 전체 구조를 큰 그림으로 먼저 잡아두자. 세부 내용은 이후 편에서 하나씩 파고든다.

block-beta columns 3 block:app["애플리케이션 레이어"]:3 columns 3 Job Step Tasklet/Chunk end block:core["코어 레이어"]:3 columns 3 JobLauncher JobRepository StepExecution end block:infra["인프라 레이어"]:3 columns 3 ItemReader ItemProcessor ItemWriter end app --> core core --> infra

세 개의 레이어로 나뉜다.

  • 애플리케이션 레이어 : 개발자가 직접 작성하는 배치 Job과 Step이다. 비즈니스 로직을 담는다.
  • 코어 레이어 : Spring Batch의 핵심 런타임이다. Job을 실행하고(JobLauncher), 실행 이력을 기록하고(JobRepository), Step의 상태를 관리한다.
  • 인프라 레이어 : 데이터를 읽고(ItemReader), 가공하고(ItemProcessor), 저장하는(ItemWriter) 구현체들이다. DB, 파일, 메시지 큐 등 다양한 데이터 소스를 지원한다.

개발자가 집중하는 영역은 애플리케이션 레이어와 인프라 레이어다. 코어 레이어는 Spring Batch가 자동으로 관리한다.

Spring Batch ≠ 스케줄러

Spring Batch는 배치 "처리"를 담당하지, 배치를 "언제 실행할지"는 관여하지 않는다. 실행 시점은 @Scheduled, Quartz, Jenkins, 쿠버네티스 CronJob 같은 외부 스케줄러가 결정한다. Spring Batch는 "실행되면 어떻게 처리할 것인가"에 집중한다.

실제 사용 사례

배치 처리가 실제로 어떻게 활용되는지 몇 가지 사례를 살펴보자.

이커머스 정산

매일 자정에 전날의 주문 데이터를 집계해서 판매자별 정산 금액을 계산한다. 수십만 건의 주문을 한 건씩 API로 처리하면 몇 시간이 걸리지만, Spring Batch의 chunk 기반 처리로 수 분 내에 완료할 수 있다.

SNS 피드 랭킹

24시간 동안의 좋아요, 댓글, 조회수를 집계해서 인기 피드 순위를 갱신한다. 실시간으로 매번 집계 쿼리를 돌리면 DB에 과부하가 걸리므로, 주기적으로 배치로 집계한 결과를 캐시에 올린다.

옷장을 부탁해 프로젝트에서의 배치 활용

피드 좋아요 수 집계, 만료된 알림 정리, 날씨 데이터 주기적 갱신 등이 배치 처리 후보다. 현재는 실시간으로 처리하고 있지만, 트래픽이 늘어나면 집계성 작업을 배치로 분리하는 것이 일반적인 성장 패턴이다.

데이터 마이그레이션

레거시 시스템에서 신규 시스템으로 수백만 건의 데이터를 이관한다. 단순 INSERT가 아니라 데이터 형식 변환, 유효성 검증, 중복 제거 등의 가공이 필요하다. Spring Batch의 Reader → Processor → Writer 구조가 이 패턴에 정확히 맞는다.

Spring Batch 프로젝트 설정

Spring Boot에서 Spring Batch를 시작하려면 의존성을 추가하고 기본 설정을 한다.

의존성 추가

// build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-batch'
    runtimeOnly 'com.h2database:h2' // 메타데이터 테이블용 DB (개발용)
}

spring-boot-starter-batch가 Spring Batch의 핵심 모듈을 포함한다. 메타데이터 테이블을 저장할 DB가 필요하므로, 개발 환경에서는 H2를 함께 추가한다. 운영 환경에서는 MySQL이나 PostgreSQL을 사용한다.

application.yml 설정

spring:
  batch:
    jdbc:
      initialize-schema: always  # 메타데이터 테이블 자동 생성
    job:
      enabled: false  # 애플리케이션 시작 시 자동 실행 방지

initialize-schema: always는 Spring Batch가 필요로 하는 메타데이터 테이블을 자동으로 생성한다. job.enabled: false는 애플리케이션 시작 시 모든 Job이 자동 실행되는 것을 방지한다. 이 설정이 없으면 서버를 띄울 때마다 배치가 돌아간다.

job.enabled 설정을 빼먹지 마라

Spring Batch는 기본적으로 애플리케이션 시작 시 등록된 모든 Job을 자동 실행한다. 개발 중에 서버를 재시작할 때마다 배치가 돌면 곤란하다. 반드시 job.enabled: false로 설정하고, 필요할 때 명시적으로 실행하라.

자주 하는 실수

배치와 실시간 처리를 같은 서버에서 돌리기

배치는 CPU와 메모리를 집중적으로 사용한다. API 서버와 같은 인스턴스에서 배치를 돌리면, 배치 실행 중 API 응답 시간이 느려질 수 있다. 가능하면 배치 전용 인스턴스를 분리하거나, 트래픽이 적은 새벽 시간대에 실행하라.

[!DANGER] 메타데이터 테이블을 무시하기

Spring Batch의 메타데이터 테이블은 단순한 로그가 아니다. 재시작, 실행 이력 조회, 중복 실행 방지의 핵심이다. H2 같은 인메모리 DB를 운영에 쓰면 서버 재시작 시 이력이 사라져서 재시작 기능이 동작하지 않는다. 운영 환경에서는 반드시 영속적인 DB를 사용하라.

[!DANGER] 전체 데이터를 메모리에 올리려는 시도

findAll()로 100만 건을 한 번에 메모리에 올리면 OutOfMemoryError가 발생한다. Spring Batch의 존재 이유가 바로 이 문제 해결이다. 반드시 chunk 기반으로 나눠서 처리하라. Reader가 페이징으로 데이터를 조금씩 읽어오는 것이 핵심이다.