Skip to content

Spring Batch 간단 정리

Spring Batch 간단 정리




서론

예전에 이 글을 썼을 때는 “Spring Batch가 뭔지 간단히 정리해두자” 정도의 느낌으로만 적었습니다.
지금은 배치를 실제 프로덕션에서 몇 년 동안 운영해보면서, 단순 개념 정리만으로는 부족하다는 생각이 많이 듭니다.

이번 글에서는 Spring Batch를 처음 접하는 분을 기준으로,

  • Spring Batch가 무엇인지
  • 언제 쓰는 게 맞는지
  • TaskletChunk 방식의 차이
  • 실무에서 반드시 신경 써야 하는 포인트

를 한 번에 정리해보려고 합니다.





Spring Batch란 무엇인가?

Spring Batch는 대량의 데이터를 안정적으로 일괄 처리하기 위한 프레임워크입니다.

  • 대량 데이터 처리에 필요한 공통 기능을 제공
    • 로그/모니터링
    • 트랜잭션 관리
    • 재시작, 재시도, 스킵 처리
    • 실행 이력/통계 저장
  • 개발자는 비즈니스 로직(Reader/Processor/Writer)에만 집중할 수 있도록 도와주는 레이어라고 보면 됩니다.

스케줄러와는 다른 역할

초기에 가장 많이 하는 오해가 하나 있습니다.

“Spring Batch를 쓰면 스케줄러 없이도 매일 1시에 자동 실행되겠지?”

실제로는 그렇지 않습니다.

  • Spring Batch
    • Job, Step, Reader/Processor/Writer를 정의하고 실행/재시작/통계를 관리
  • 스케줄러 (Quartz, Cron, Jenkins 등)
    • “언제” 실행할지를 결정하고 JobLauncher를 호출

즉, Spring Batch는 배치 작업을 어떻게 처리할지를 담당하고,
실제 언제 돌릴지는 스케줄러가 담당합니다.





언제 Spring Batch를 쓰는 게 맞는가?

스프링 배치의 전형적인 패턴은 읽기 → 가공 → 쓰기입니다.
실무에서 대표적으로 이런 케이스에 잘 어울립니다.

  1. 통계/집계 작업

    • 하루치 접속 로그를 모아서 일별 접속 통계 테이블로 적재
    • 매일 가입/탈퇴한 유저 수 집계
    • 일별 정산 금액 집계
  2. 정기적으로 반복되는 비즈니스 작업

    • 월말에 미납 고객에게 이메일/알림 발송
    • 매일 새벽에 정산 마감 처리
    • 주기적으로 캐시 테이블 리빌드
  3. 대량 데이터 마이그레이션

    • 구 시스템에서 신 시스템으로 데이터 이전
    • 특정 테이블의 스키마 변경 후 재적재
    • 커밋 단위를 제어하면서 안정적으로 롤백 가능한 마이그레이션
💡Spring Batch를 굳이 안 써도 되는 경우
  • 한두 번만 돌릴 아주 작은 스크립트
  • 수십 건 수준의 소량 데이터만 처리하는 작업
  • 실시간 API에서 바로 처리해도 되는 단순 로직
    이런 경우에는 CommandLineRunner나 간단한 스크립트로 충분합니다.




핵심 구성 요소 한 번에 보기

Spring Batch를 이해할 때 최소한 이 정도 용어만 정리해두면 훨씬 편합니다.

  • Job: 하나의 배치 작업 단위 (예: “일별 접속 로그 집계 Job”)
  • Step: Job을 이루는 작은 단계 (예: “원본 로그 읽기 Step”, “집계 테이블 적재 Step”)
  • JobExecution / StepExecution: 각 실행에 대한 상태/통계 정보
  • ItemReader / ItemProcessor / ItemWriter: 읽기 → 가공 → 쓰기를 담당하는 세 가지 축
  • Chunk: 트랜잭션 단위로 처리할 “묶음” 크기 (예: 1000건 단위로 커밋)

배치 실행 흐름을 아주 단순화하면 아래와 비슷합니다.

flowchart TB
  Scheduler["스케줄러 (Cron/Jenkins 등)"] --> JobLauncher[JobLauncher]
  JobLauncher --> Job[배치 Job]
  Job --> Step1["Step 1 - Reader/Processor/Writer"]
  Step1 --> Step2["Step 2 - 후처리 Step"]
  Step2 --> EndNode[종료]

  style Scheduler fill:#e3f2fd
  style JobLauncher fill:#e8eaf6
  style Job fill:#e8f5e9
  style Step1 fill:#fff3e0
  style Step2 fill:#fff3e0
  style EndNode fill:#ffebee




두 가지 처리 방식: Tasklet vs Chunk

Spring Batch로 Job을 설계할 때 흔히 나오는 선택지가 두 가지입니다.

  • Tasklet 기반 처리
  • Chunk 기반 처리

개인적으로는 단일 작업/간단한 유틸성 Job은 Tasklet,
대량 데이터 처리/여러 Step이 섞인 복잡한 Job은 Chunk 쪽에 더 잘 맞는다고 생각합니다.


Tasklet 방식

Tasklet은 말 그대로 “한 번에 처리하는 작업”에 가깝습니다.

@Configuration
public class SimpleTaskletJobConfig {

    @Bean
    public Job simpleTaskletJob(JobRepository jobRepository, Step simpleTaskletStep) {
        return new JobBuilder("simpleTaskletJob", jobRepository)
                .start(simpleTaskletStep)
                .build();
    }

    @Bean
    public Step simpleTaskletStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
        return new StepBuilder("simpleTaskletStep", jobRepository)
                .tasklet((contribution, chunkContext) -> {
                    // 여기에서 read / process / write 를 한 번에 처리
                    // 예: 설정 값 몇 개 읽어서 정리하는 간단한 작업
                    return RepeatStatus.FINISHED;
                }, transactionManager)
                .build();
    }
}
  • 장점
    • 코드가 단순하고 이해하기 쉽습니다.
    • “파일 하나만 변환해서 결과를 내는” 식의 유틸성 Job에 적합합니다.
  • 단점
    • 데이터가 커질수록 한 번에 처리하기 부담스럽습니다.
    • 실패 지점을 세밀하게 제어하거나, 부분 재시도/스킵을 관리하기 어렵습니다.

Chunk 기반 방식

Chunk 기반 처리는 “N개씩 잘라서 반복 처리”하는 패턴입니다.

@Configuration
public class UserStatisticsJobConfig {

    private static final int CHUNK_SIZE = 1000;

    @Bean
    public Job userStatisticsJob(JobRepository jobRepository, Step userStatisticsStep) {
        return new JobBuilder("userStatisticsJob", jobRepository)
                .start(userStatisticsStep)
                .build();
    }

    @Bean
    public Step userStatisticsStep(
            JobRepository jobRepository,
            PlatformTransactionManager transactionManager,
            ItemReader<UserLoginLog> loginLogReader,
            ItemProcessor<UserLoginLog, DailyUserStat> loginLogProcessor,
            ItemWriter<DailyUserStat> dailyUserStatWriter
    ) {
        return new StepBuilder("userStatisticsStep", jobRepository)
                .<UserLoginLog, DailyUserStat>chunk(CHUNK_SIZE, transactionManager)
                .reader(loginLogReader)
                .processor(loginLogProcessor)
                .writer(dailyUserStatWriter)
                .build();
    }
}

핵심은 chunk(CHUNK_SIZE, transactionManager)입니다.

  • CHUNK_SIZE 건 단위로
    • Reader로 데이터를 읽고
    • Processor로 가공한 뒤
    • Writer로 묶어서 한번에 저장
  • 각 청크 단위로 트랜잭션이 걸리기 때문에
    • 한 청크에서 실패해도 나머지 청크에는 영향이 없습니다.
    • 트랜잭션 시간을 짧게 가져갈 수 있어서 데드락·타임아웃을 줄일 수 있습니다.
💡실무에서의 선택 기준
  • 처리 건수가 적고, “한 번만” 실행해도 되는 유틸성 작업 → Tasklet
  • 수십만 건 이상을 주기적으로 처리해야 하는 Job → Chunk 기반 Step + Reader/Processor/Writer
  • 개인적으로는 “데이터량이 애매하게 많다” 싶으면 처음부터 Chunk로 가는 편이 더 안전했습니다.




실무에서 Spring Batch 설계할 때 주의할 점

1. I/O와 커넥션 풀 고갈

대량 데이터를 다루다 보면 배치가 데이터베이스·외부 시스템 I/O를 한꺼번에 잡아먹는 경우가 자주 있습니다.

  • 너무 큰 청크 사이즈로 한 번에 많은 데이터를 가져오면
    • DB 커넥션 풀 고갈
    • 긴 트랜잭션으로 인한 락 경쟁
    • 다른 API 서비스 지연
  • 반대로 너무 작은 청크 사이즈는
    • 커밋이 너무 자주 일어나서 오히려 오버헤드 증가

결국 DB와 인프라 상황에 맞는 적당한 청크 사이즈를 찾는 튜닝 작업이 필요합니다.


2. 다른 서비스에 미치는 영향

배치는 보통 새벽 시간대에 돌리긴 하지만,
요즘은 24시간 트래픽이 들어오는 서비스가 많기 때문에 “남의 시간”은 거의 없습니다.

  • 배치가 돌 때
    • 동일 DB를 사용하는 API 응답 시간이 튀지 않는지
    • 캐시/메시지 큐 등에 부하를 주지 않는지
    • 외부 API rate limit를 넘기지 않는지
  • 가능하다면
    • 읽기 전용 Replica DB에서 읽고 Master에는 최소한만 쓰도록 설계
    • 배치 전용 커넥션 풀/리소스 풀을 따로 두는 것도 방법입니다.

3. 실패 감지와 재처리 전략

실제로 한 번 겪어보면 가장 크게 느끼는 부분이 “배치가 조용히 실패하는 상황”입니다.

예전에 반드시 동작해야 하는 배치 Job이 몇 주 동안 조용히 실패하고 있었는데,
알림 설정이 제대로 안 되어 있어서 꽤 늦게 알아차린 적이 있습니다.
그 사이에 쌓인 데이터는 결국 수작업과 추가 배치로 다시 정리해야 했습니다.

실무에서는 최소한 이 정도는 꼭 준비하는 편이 좋았습니다.

  • Job/Step 실행 결과를 모니터링 시스템에 연동
  • 실패 시 슬랙/이메일/문자 등으로 알림
  • skip / retry 정책 명시
  • 처리 실패 데이터를 별도 테이블/파일에 저장해서 재처리 Job으로 분리
⚠️모니터링 없는 배치는 언젠가 사고가 납니다
  • 배치 Job이 “돌아가는 것처럼 보이는 상태”가 가장 위험합니다.
  • 실행 이력과 실패 이력을 모니터링하지 않으면, 이미 데이터가 뒤틀린 뒤에야 문제를 인지하게 됩니다.

4. 트랜잭션 범위와 Chunk 설계

Spring Batch는 기본적으로 청크 단위 트랜잭션을 사용하지만,

  • Reader에서 너무 많은 비즈니스 로직을 넣거나
  • Writer에서 외부 API 호출까지 같이 묶어버리면

트랜잭션이 불필요하게 길어질 수 있습니다.

가능하면

  • DB에 쓰는 부분만 트랜잭션 안에서 처리하고
  • 메일 발송·외부 API 호출·푸시 알림 등은
    • 별도 Step
    • 혹은 이벤트/메시지 큐로 분리

하는 쪽이 운영 측면에서 훨씬 안전했습니다.





결론

Spring Batch는 “배치 프레임워크”라기보다,
대량 데이터를 안전하게 처리하기 위한 운영 플랫폼에 가깝다고 느낍니다.

  • 단순 개념 정리를 넘어
    • 언제 쓰는 게 맞는지
    • 어떤 방식(Tasklet/Chunk)으로 설계할지
    • 트랜잭션·I/O·모니터링을 어떻게 가져갈지 를 처음 단계에서 함께 고민해야 합니다.

개인적으로는

  • 통계/정산/마이그레이션처럼 반드시 처리돼야 하는 작업
  • 수십만 건 이상의 데이터를 주기적으로 다루는 작업

에는 Spring Batch가 여전히 가장 현실적인 선택이라고 생각합니다.
다만, 운영 경험이 쌓일수록 “설계와 모니터링”이 배치 코드 자체만큼이나 중요하다는 걸 계속 느끼게 됩니다.

다음에 기회가 되면, 실제로 운영했던 배치 Job 하나를 예로 들어서
Chunk 사이즈 튜닝·재시도/스킵 전략·알림 설계를 더 구체적으로 정리해보려고 합니다.





참고 :

https://docs.spring.io/spring-batch/docs/current/reference/html/index.html
https://spring.io/guides/gs/batch-processing/
https://khj93.tistory.com/entry/Spring-Batch%EB%9E%80-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B3%A0-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0





읽어주셔서 감사합니다.🖐


Ramsbaby
Written byRamsbaby
이 블로그는 직접 개발/운영하는 블로그이므로 당신을 불쾌하게 만드는 불필요한 광고가 없습니다.

#My Github#소개 페이지#Blog OpenSource Github#Blog OpenSource Demo Site