티스토리 뷰

카테고리 없음

Spring Batch

oneny 2025. 11. 6. 20:48

Spring Batch

Spring Batch는 대용량 데이터 처리를 위한 경량 프레임워크로, Job과 Step으로 구성된 배치 작업을 정의하고 실행한다. 로깅/추적, 트랜잭션 관리, 작업 처리 통계, 작업 재시작, 스킵, 리소스 관리 등 대용량 레코드 처리에 필수적인 기능을 제공한다. Spring Batch의 주요 사용 사례는 다음과 같다.

  • 대용량 데이터 ETL(Extract, Transform, Load)
  • 정기적인 대량 데이터 처리(매출 집계, 통계 생성)
  • 외부 시스템과의 데이터 동기화
  • 파일 import/export wkrdjq
  • 대량 메일 발송 및 알림 처리

 

Spring Batch 특징 및 장점

  • 풍부한 필수 기능 제공
    • 트랜잭션 관리, 로깅/추적, 체크포인트/재시작, 통계 집계, 예외 처리(Skip), 재시도(Retry) 등의 기능을 간편히 활용 가능
    • 실패한 배치 Job을 중단 지점부터 재시작하거나 동일 파라미터로 중복 실행 방지 지원
  • Spring 생태계와 쉬운 통합
    • Spring Framework 기반으로 DI, AOP 활용 가능
    • Spring Boot 환경에서 Starter 의존성 추가만으로 자동 설정(DataSource, JobReposiroty 등) 제공
  • 검증된 성능과 안정성
    • 금융권, 이커머스 등 다양한 산업에서 검증된 신뢰성과 성능 제공
    • 청크 기반 처리, 멀티스레딩, 파티셔닝을 통한 효율적인 대용량 데이터 처리 지원
    • 청크 크기, 스레드 개수 등 세부적인 성능 튜닝 가능
  • 다양한 Reader/Writer 지원
    • 파일(CSV, XML, JSON), DB(JDBC, JPA), JMS, MongoDB 등의 다양한 데이터 소스를 기본적으로 지원
    • 커스텀 ItemReader/Writer로 새로운 데이터 소스에 쉽게 대응 가능

 

메타데이터 스키마

출처: https://docs.spring.io/spring-batch/reference/schema-appendix.html#page-title

Spring Batch는 배치 실행 정보를 관리하기 위해 위 스키마에서 확인할 수 있듯 6개의 메타데이터 테이블을 사용한다. 각 테이블은 Java 도메인 객체(JobInstance, JobExecution 등)와 1:1로 매핑되어 실행 이력과 상태를 저장한다. 이들은 JobRepository가 내부적으로 관리하며, 배치 실행 중 발생한 모든 정보를 기록하고 추적한다. 테이블 구조 및 역할은 다음과 같다.

 

  • BATCH_JOB_INSTANCE
    • Job의 논리적 인스턴스를 의미하며, 동일한 Job 이름과 파라미터 조합이 하나의 인스턴스로 식별된다.
    • Java 객체: JobInstance
  • BATCH_JOB_EXECUTION
    • 특정 JobInstance의 실제 실행 이력(성공, 실패, 시작 및 종료 시간 등)을 저장한다.
    • Java 객체: JobExecution
  • BATCH_JOB_EXECUTION_PARAMS
    • Job 실행 시 전달된 파라미터를 관리한다.
    • Java 객체: JobParameters
  • BATCH_STEP_EXECUTION
    • JobExecution 내 각 Step의 실행 상태, 처리 건수, 실패 사유 등을 저장한다.
    • Java 객체: StepExecution
  • BATCH_JOB_EXECUTION_CONTEXT
    • JobExecution 수준의 컨텍스트 정보를 직렬화하여 저장하여 실행 도중 필요한 상태 값이나 중간 데이터 유지에 사용한다.
    • Java 객체: ExecutionContext
  • BATCH_STEP_EXECUTION_CONTEXT
    • StepExecution별로 독립적인 컨텍스트를 저장하여 Step 단위의 상태 복원 및 재시작 시 활용한다.
    • Java 객체: ExecutionContext

 

Job

Spring Batch의 Job은 배치 처리의 최상위 단위로, 특정 비즈니스 목적을 달성하기 위한 배치 프로세스의 시작점이다. Job은 구성(Configuration) 실행(Execution) 두 역할을 동시에 수행한다. 즉, 하나의 Job은 여러 Step을 순차적 또는 조건부로 조합하여 복잡한 배치 워크플로우를 구성하여 전체 배치 프로세스의 청사진 역할을 한다.

 

Job  구현체 종류

Spring Batch는 아래와 같이 Job 인터페이스의 대표적인 두 가지 구현체를 제공하고, 각각의 Job 구현체는 Step을 어떤 방식으로 실행할지, 그리고 흐름을 어떻게 제어할지에 따라 구분된다.

  • SimpleJob: 가장 기본적인 Job 구현체
    • 내부에 정의된 Step들을 순차적으로 한 줄 흐름으로 실행한다.
    • 추가적인 조건 분기 없이 선형적인 작업 흐름을 처리할 때 적합한다.
  • FlowJob: 복잡한 흐름 제어(조건 분기, 병렬 흐름 등)가 필요한 경우 사용되는 구현체
    • Flow 객체를 기반으로 실행 흐름을 구성하며, StepA 성공 시 StepB, 실패 시 StepC 실행 등으로 가능
    • 조건에 따라 분기하거나, 특정 Step을 재시도, 병렬 실행, skip하는 등 보다 정교한 시나리오 설계에 적합한다.

 

JobInstance

create table BATCH_JOB_INSTANCE
(
    JOB_INSTANCE_ID bigint       not null primary key, -- pk
    VERSION         bigint       null, -- 버전
    JOB_NAME        varchar(100) not null, -- Job을 구성할 떄 부여하는 Job의 이름
    JOB_KEY         varchar(32)  not null, -- jobName과 jobParameters를 합쳐 해시값
    constraint JOB_INST_UN unique (JOB_NAME, JOB_KEY)
)

JobInstance는 특정 Job이 특정 파라미터로 실행된 논리적 실행 단위 객체를 말한다. 즉, 같은 Job이라도 실행 파라미터(예: date)가 다르다면 Job명 + JobParameters 조합(jobKey 해시값)이 다르기 때문에 새로운 JobInstance로 간주하여 중복 실행을 방지한다. JobInstance 기준으로 실행 이력을 관리하기 때문에 실패/재시작을 위한 여러 번의 실행(JobExecution)과 연결될 수 있어 "2025-10-11자 집계 작업을 실패 후 재시작할 때"와 같은 논리적 묶음 관리가 가능하다.

 

JobExecution

CREATE TABLE BATCH_JOB_EXECUTION
(
    JOB_EXECUTION_ID BIGINT      NOT NULL PRIMARY KEY, -- 식별키
    VERSION          BIGINT, -- 버전
    JOB_INSTANCE_ID  BIGINT      NOT NULL, -- JOB_INSTANCE 식별키
    CREATE_TIME      DATETIME(6) NOT NULL, -- JobExecution이 생성된 시점
    START_TIME       DATETIME(6) DEFAULT NULL, -- JobExecution이 시작된 시점 
    END_TIME         DATETIME(6) DEFAULT NULL, -- JobExecution이 끝난 시점, 실패나 중단 시 저장되지 않을 수 있음
    STATUS           VARCHAR(10), -- 실행 상태(COMPLETED, FAILED, STOPPED, ...)
    EXIT_CODE        VARCHAR(2500), -- 실행 종료코드(COMPLETED, FAILED, NOOP, ...)
    EXIT_MESSAGE     VARCHAR(2500), -- Job 실행 중 발생한 오류 메시지나 사용자 정의 종료 메시지 저장
    LAST_UPDATED     DATETIME(6), -- JobExecution 업데이트 시점, STATUS 바뀔 때마다 갱신
    constraint JOB_INST_EXEC_FK foreign key (JOB_INSTANCE_ID)
        references BATCH_JOB_INSTANCE (JOB_INSTANCE_ID)
) ENGINE = InnoDB;

JobExecution은 JobInstance 한 번의 실제 이력을 말한다. 하나의 JobInstance(특정 JobName + JobParameters)에 대해 여러 번 실행(재시작, 실패 후 재시도 등)이 있을 수 있는데 실행마다 별도의 JobExecution이 생성되고, 각각 status(성공/실패), 시작/종료 시각, 에러메시지, 종료 코드 등을 저장한다. 즉, JobExecution이 쌓이면 "언제, 누가, 어떤 조건으로 실행했고, 성공/실패했는지"를 전부 기록하고, 배치 실패 원인 추적, SLA(처리시간 보장), 장애 대응, 이중실행 방지 등에서 매우 중요한 역할을 한다.

 

JobParameters

CREATE TABLE BATCH_JOB_EXECUTION_PARAMS
(
    JOB_EXECUTION_ID BIGINT       NOT NULL, -- JobExecution 식별키, JOB_EXECUTION과는 일대다 관계
    PARAMETER_NAME   VARCHAR(100) NOT NULL, -- 파라미터명
    PARAMETER_TYPE   VARCHAR(100) NOT NULL, -- 파라미터 타입
    PARAMETER_VALUE  VARCHAR(2500), -- 파라미터 값
    IDENTIFYING      CHAR(1)      NOT NULL, -- 식별여부(Y, N)
    constraint JOB_EXEC_PARAMS_FK foreign key (JOB_EXECUTION_ID) references BATCH_JOB_EXECUTION (JOB_EXECUTION_ID)
) ENGINE = InnoDB

JobParameters는 Job 실행 시 입력되는 파라미터 집합으로 JobInstance의 유일성을 결정짓는 핵심 요소이다. 실행 시점, 대상 일자, 파일명 등 작업을 구분하는데 보통 쓰인다. 위에서 설명했듯이 JobName + JobParameters의 조합이 기존에 존재하면, 동일 JobInstance로 인식하고, 파라미터 하나라도 다르다면 새로운 JobInstance를 생성한다.

 

JobParameters params1 = new JobParametersBuilder()
    .addString("requestDate", "2025-10-15", true)   // 식별 파라미터
    .addString("filePath", "/tmp/data.csv", false)  // 단순 실행 파라미터
    .toJobParameters();

위 IDENTIFYING 컬럼은 JobParamter가 JobInstance를 식별하는 기준에 포함되는지 여부를 나타내어 위 코드처럼 JobParameters 객체를 생성 시 지정할 수 있다.

 

Job 실행 흐름

위 다이어그램은 Spring Batch에서 JobLauncher가 Job을 실행할 때 JobRepository가 내부적으로 JobInstance를 어떻게 판별하고 관리하는지를 나타낸다.

JobLauncher는 배치 실행을 요청하는 진입점으로 run을 호출하여 Job과 JobParameters를 받아 JobRepository로 전달하는 역할을 한다. JobRepository는 모든 실행 이력을 관리하는 컴포넌트로 중복 실행을 방지하기 위해 BATCH_JOB_INSTNACE 테이블의 JobName과 JobKey(JobParameters의 해시)로 기존 JobInstance가 있는지 검사하고, 존재하지 않으면 새 JobInstance를 생성 후 DB에 저장한다. 이후 JobExecution(BATCH_JOB_EXECUTION)을 생성하고 실행을 기록한다. 실행 시 전달된 파라미터는 BATCH_JOB_EXECUTION_PARAMS에 함께 저장한다.

 

@Slf4j
@Configuration
public class HelloBatchConfiguration {

    @Bean
    public Job helloJob(JobRepository jobRepository, Step helloStep1, Step helloStep2) {
        return new JobBuilder("helloJob", jobRepository)
                .start(helloStep1)
                .next(helloStep2)
                .build();
    }

    @Bean
    public Step helloStep1(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
        return new StepBuilder("helloStep", jobRepository)
                .tasklet((StepContribution contribution, ChunkContext chunkContext) -> {
                    System.out.println("hello spring batch111");

                    Thread.sleep(5_000L);

                    return RepeatStatus.FINISHED;
                }, transactionManager)
                .build();
    }

    @Bean
    public Step helloStep2(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
        return new StepBuilder("helloStep", jobRepository)
                .tasklet((StepContribution contribution, ChunkContext chunkContext) -> {
                    System.out.println("hello spring batch222");
                    throw new IllegalArgumentException("에러!!");

//                    return RepeatStatus.FINISHED;
                }, transactionManager)
                .build();
    }
}

위 코드는 Spring Batch에서 두 개의 Step으로 구성된 helloJob 배치를 정의한다. 첫 번째 Step은 단순히 메시지를 출력하고 5초 대기한 후 종료되고, 두 번째 Step은 예외를 발생시켜서 실패 상태를 유도한다.

배치 작업이 실행되면 helloJob 배치는 BATCH_JOB_INSTANCE에 생성된다.

 

Job 실행 시마다 BATCH_JOB_EXECUTION이 추가되며, 그 하위에 여러 Step에 여러 Step이 실행되면 BATCH_STEP_EXECUTION이 연계되어 저장된다. 그리고 각 실행마다 관련된 파라미터(BATCH_JOB_EXECUTION_PARAMS)와 컨텍스트(BATCH_JOB_EXECUTION_CONTEXT, BATCH_STEP_EXECUTION_CONTEXT)가 기록된다.

테이블에 저장된 데이터와 함께 Job 실행 흐름을 살펴보면 처음에 helloStep2에서 실패로 Job과 Step에 대한 상태를 FAILED로 저장된 것을 확인할 수 있다. 그리고 helloStep2를 FINISHED 상태로 끝낼 수 있도록 주석을 해제하면 JobInstance를 새로 생성하지 않고, 기존 JobInstance를 사용하여 잡 실행을 처리한 것을 확인할 수 있다. 마지막으로 같은 잡을 실행할 수는 있지만 NOOP 상태 즉, 아무런 데이터를 처리하지 않은 것을 알 수 있다.

 

Step

Step은 Spring Batch에서 '실제 데이터 처리'가 일어나는 최소 단위를 말한다. 즉, Job을 구성하는 독립적인 하나의 단계로서 실제 배체 처리를 정의하고 컨트롤하는데 필요한 정보를 가지고 있는 도메인 객체이다. Step 내부에서는 Reader -> Processor -> Writer의 구조로 데이터를 읽고(Reader), 가공하고(Processor), 저장(Writer)한다. Step이 실행될 때마다 처리 건수, 성공/실패, 상태 등이 메타테이블에 기록되어 모니터링, 장애 대응, 재시작에 활용된다.

 

Step 구현체 종류

Step은 아래와 같이 Tasklet 기반(단일 작업) 또는 Chunk 기반(데이터 묶음 단위 반복 처리)으로 구현할 수 있다.

  • TaskletStep
    • 하나의 Step에서 '특정 로직'을 단일 작업으로 실행할 때 사용한다.
      • ex) 파일 삭제, 외부 시스템 알림 전송, 단건 처리 등
    • Tasklet 인터페이스를 구현해서 원하는 로직만 작성하면 된다.
  • ChunkOrientedTasklet
    • 대용량 데이터를 'Chunk'(묶음) 단위로 반복 처리할 수 있다.
    • 내부적으로 ItemReader -> ItemProcessor -> ItemWriter 구조를 따른다.
    • 데이터 집계, 이관, 대량 파일 처리 등 대부분의 배치 처리에 사용하며, 실무에서 가장 많이 사용한다.

 

StepExecution

StepExecution은 Step 한 번의 실행 이력 단위를 말하며, 언제, 어떤 상태로 실행됐고, 성공/실패/중단 등 결과가 어떻게 됐는지 모두 기록한다. StepExecution의 주요 속성은 실행 시작/종료 시간, 상태(성공, 실패 등), 처리/읽은/쓰여진 건수, 에러 메시지 등이 있으며 각 StepExecution 별 처리 건수 모니터링하며, 장애 발생 시, 어느 Step에서 문제가 발생했는지 추적할 수 있다.

StepExecution과 관련한 이력은 BATCH_STEP_EXECUTION 테이블에 저장한다.

 

StepContribution

StepContribution은 StepExecution 내부에서 한 Chunk(묶음)의 처리 결과를 누적 및 집계하는 역할을 담당한다. 청크 기반 처리에서 각 Chunk별로 Reader/Processor/Writer의 처리 결과를 기록하고, StepExecution에 반영한다.

StepContribution의 주요 속성은 다음과 같다.

  • 읽은/처리한/쓰여진/스킨된 아이템 수
  • 예외 발생, 필터링, 실패 건수 등 상세 통계
  • 성능 튜닝, 장애 분석, 청크 사이즈 조정 시 StepContribution 로그가 매우 유용함

실제 청크 사이즈마다 ItemReader -> ItemProcessor -> ItemWriter가 실행되고, 그 결과가 StepContribution에 저장된다. 모든 Chunk가 끝나면 StepContribution의 통계 정보가 StepExecution에 합산된다.

 


Chunk 기반 처리

출처: 실무를 위한 Spring Batch의 모든 것 : 성능 개선부터 프로젝트까지

Chunk는 대용량 데이터를 '덩어리(Chunk, 묶음)' 단위로 잘라서 일정 개수씩 반복 처리하는 방식을 말한다. Step 내부에서 ItemReader -> ItemProcessor -> ItemWriter 구조로 Reader에서 데이터를 Chunk 사이즈만큼 읽고, Processor에서 가공/필터링한 뒤, Writer에서 일괄 저장한다.

Chunk 기반으로 처리하면 다음과 같은 장점이 있다.

  • 메모리 효율성: 한 번에 모든 데이터를 올리지 않고, Chunk 단위로 나눠서 처리하므로 메모리 부담이 적다.
  • 성능 향상: Chunk 단위로 데이터를 일괄로 처리하면, 매 건마다 DB에 접근하거나 트랜잭션을 커밋하는 경우보다 빠르다.
  • 트랜잭션 안정성: 각 Chunk를 하나의 트랜잭션으로 간주하여 Chunk 단위로 커밋 및 롤백이 가능하고, 실패한 Chunk만 재시도하거나 스킵할 수 있다.
  • 장애 복구 용이: JobRepository와 연계되어 각 Chunk 단위의 처리 상태(읽은 건수, 커밋된 건수 등)이 저장되어 장애가 발생했을 때 중단된 지점부터 재시작하는데 사용된다.
  • 확장성: Chunk 단위 구조는 Partitioning와 같은 병렬 실행과 결합할 때 각각의 스레드나 워커 스텝에서 독립적으로 실행되어 대용량 데이터를 빠르게 병렬 처리할 수 있다.

 

Chunk 사이즈 조정, 주의해야 할 점

  • 데이터베이스 트랜잭션 커밋 문제
    • Chunk가 커질수록 한 번에 커밋되는 데이터가 많아짐
    • 대량 커밋 시 DB Lock, Deadlock, Undo/Redo 로그 폭증 등으로 오히려 전체 성능 저하
    • 장애 발생 시 롤백 비용 커짐
  • 외부 서비스 연동 시 네트워크 한계
    • 한 번에 대용량 데이터를 외부 시스템에 전송하면 상대 서버가 수신 거부, 타임아웃 발생 가능
    • 네트워크 대역폭을 초과해 실패하거나, 처리 지연
  • 메모리 사용량 폭증
    • Chunk 사이즈가 클수록 한 번에 처리할 데이터를 메모리에 모두 올려야 함
    • JVM OutOfMemory 등, 서버 메모리 한계 초과로 배치 실패 위험
  • 각 시스템 환경에 맞는 튜닝 필요
    • DB, 네트워크, 메모리 등 시스템 환경마다 최적값 다름
    • 실무에서는 보통 2,000 이상의 Chunk는 피하는 편
    • 직접 실험 및 모니터링을 통해 적정 Chunk Size 산정 필요

 

ItermReader

ItemReader는 Step에서 데이터를 읽어오는 역할을 하며, 아래와 같은 다양한 데이터 소스(JPA, JDBC, 파일, API 등)를 사용한 구현체를 사용하여 데이터를 추출할 수 있다. 대량 데이터 처리 시 한 번에 너무 많은 데이터를 읽으면 OutOfMemory 등 메모리 이슈가 발생할 수 있기 때문에 주의해야 한다. ItemReader 주요 구현체는 다음과 같다.

  • JpaPagingItemReader
    • JPA 기반으로 페이징 처리로 대용량 데이터를 안전하게 읽을 수 있다.
    • JPQL 쿼리 사용으로 한 번에 메모리 과부화를 방지할 수 있다.
    • 주의: flush/clear 타이밍, 영속성 컨텍스트 관리 필요
  • JdbcCursorItemReader
    • DB Cursor 기반으로 한 줄씩 직접 읽어올 수 있다.
    • 주의: 트랜잭션 길어질 경우 DB 부하 발생 및 커넥션 장시간 점유
  • FlatFileItemReader
    • CSV/텍스트 파일 등 외부 파일 데이터를 읽을 수 있다.
    • 주의: 파일 용량, 포맷, 인코딩 이슈 및 구분자, 줄바꿈 문자 등으로 오류 발생 가능

 

ItemProcessor

ItemProcessor는 Reader에서 읽은 데이터를 가공, 변환, 필터링(제외)하는 역할을 한다. 즉, 입력값을 받아 가공 결과를 반환하거나, 필요 없는 데이터는 null로 반환해 Writer 단계로 전달하지 않는다.

ItemProcessor에서 날짜 포맷이나 문자열 변환 등 데이터 포맷을 변경하거나 조건에 맞지 않는 데이터를 제외 또는 복잡한 비즈니스 로직을 거쳐 도메인 변환(엔티티 <-> DTO)하는 과정을 사용한다.

ItemProcessor를 사용할 떄는 외부 서비스 호출, 복잡한 연산이 많으면 전체 배치 성능 저하가 있을 수 있기 때문에 무거운 연산에 주의해야 한다. 그리고 데이터 정합성을 위해 필터링/가공 후 Writer에 넘기는 데이터와 실제 저장될 데이터의 정합성을 항상 확인하는 것이 좋다.

 

ItemWriter

ItemWriter는 Processor에서 넘어온 데이터를 실제로 DB, 파일, 외부 시스템 등 다양한 대상에 저장(쓰기)하는 역할을 한다. ItemWriter를 사용할 때 한 번에 너무 많은 Chunk만큼의 데이터를 저장하면 트랜잭션 부담, 너무 적으면 I/O 오버헤드가 발생할 수 있다. 그리고 저장 실패 시 배치 전체가 중단될 수 있어 예외에 대한 롤백, 재시도 등 전략이 필요하다. ItemWriter 주요 구현체는 다음과 같다.

  • JpaItemWriter, JdbcBatchItemWriter
    • DB에 일괄 저장, 대량 데이터 처리에 적합하다.
    • 주의: 트랜잭션, 커밋 타이밍, 영속성 컨텍스트 고나리
  • FlatFileItemWriter
    • 파일(CSV, 텍스트 등)로 저장, 대량 로그/리포트 파일 생성에 유용
    • 주의: 파일 포맷, 인코딩, 줄바꿈 문자 등 체크
  • Custom Writer
    • 외부 API, 메일, 메시지 큐 등 다양한 방식 구현 가능
    • 주의: 외부 시스템 장애, 네트워크 타임아웃 등 예외처리 필수

 

Fault-Tolerant 설정

return new StepBuilder("faultTolerantStep", jobRepository)
	.<PaymentSource, Payment>chunk(100, transactionManager)
	.reader(paymentReader())
	.processor(paymentProcessor())
	.writer(paymentWriter())
	// 내결함성 활성화
	// Skip/Retry/BackOff 등의 내결함성 옵션을 사용할 수 있도록 StepBuilder를 FaultTolerantStepBuilder로 전환한다.
	.faultTolerant()

	// Skip 설정
	.skip(IllegalArgumentException.class) // 지정한 예외 타입이 발생했을 때 해당 아이템을 건너뛰도록 등록한다.
	.skip(IllegalStateException.class)
	.skipLimit(10) // Skip 허용 최대 건수를 설정한다. 이 횟수를 초과하면 Step이 실패한다.
        // noSkip을 사용해서 예외 상속 계층 구조에서 특정 하위 예외를 명시적으로 제외할 수 있다.
	.noSkip(NullPointerException.class) // Skip 대상에서 제외할 예외 타입을 지정하여 건너뛰지 않고 Step을 실패시킨다.
	.skipPolicy(customSkipPolicy()) // 사용자 정의 또는 기본 SkipPolicy를 등록해, 복합 기준(예: 예외 타입 + 횟수)을 적용할 수 있다.

	//  Retry 설정
	.retry(PessimisticLockException.class) // 지정한 예외 타입이 발생했을 때 재시도를 수행하도록 동작한다.
	.retry(OptimisticLockException.class)
	.retry(QueryTimeoutException.class)
	.retryLimit(3) // Retry 허용 최대 횟수를 설정한다. 이 횟수를 초과하면 재시도를 중단한다.
	.retryPolicy(retryPolicy()) // 사용자 정의 또는 기본 RetryPolicy를 등록해, 복합 기준(예: 예외 타입 + 횟수)을 적용할 수 있다.
	.backOffPolicy(backOffPolicy()) // 재시도 간격(고정/지수 등)을 조정하는 BackOff 정책을 등록한다.
	.noRetry(IllegalArgumentException.class) // Retry 대상에서 제외할 예외 타입을 지정한다.

	// 롤백 제어
	.noRollback(IllegalStateException.class) // 지정한 예외 타입이 발생해도 트랜잭션을 롤백하지 않고 커밋된 데이터를 유지한다.

	.build();

대용량 데이터 처리 시 일부 데이터 오류나 외부 시스템 장애는 항상 발생할 수 있다. 전체 Job이 단 한 건의 오류로 멈춘다면 생산성 및 가용성이 크게 저하하기 때문에 faultTolerant()를 사용하면 배치 처리 중 작은 데이터 오류나 일시적 장애가 발생해도 Step 전체를 곧바로 실패시키지 않고, 건너뛰기(Skip) 또는 재시도(Retry) 로직을 적용해 계속 진행할 수 있다.

 

Skip

public class FaultTolerantStepBuilder {

	// ...
    
	protected SkipPolicy createSkipPolicy() {
		SkipPolicy skipPolicy = this.skipPolicy;
		Map<Class<? extends Throwable>, Boolean> map = new HashMap<>(skippableExceptionClasses);
		map.put(ForceRollbackForWriteSkipException.class, true);
		LimitCheckingItemSkipPolicy limitCheckingItemSkipPolicy = new LimitCheckingItemSkipPolicy(skipLimit, map);
		if (skipPolicy == null) {
			if (skippableExceptionClasses.isEmpty() && skipLimit > 0) {
				logger.debug(String.format(
						"A skip limit of %s is set but no skippable exceptions are defined. Consider defining skippable exceptions.",
						skipLimit));
			}
			skipPolicy = limitCheckingItemSkipPolicy;
		}
		else if (limitCheckingItemSkipPolicy != null) {
			skipPolicy = new CompositeSkipPolicy(new SkipPolicy[] { skipPolicy, limitCheckingItemSkipPolicy });
		}
		return skipPolicy;
	}
}

public class CompositeSkipPolicy implements SkipPolicy {

	private SkipPolicy[] skipPolicies;

	// ...

	@Override
	public boolean shouldSkip(Throwable t, long skipCount) throws SkipLimitExceededException {
		for (SkipPolicy policy : skipPolicies) {
			if (policy.shouldSkip(t, skipCount)) {
				return true;
			}
		}
		return false;
	}

}

CSV 파일의 주문 데이터 한 줄씩 읽어서 처리하는 Job이 있다고 가정하자. 파일에 한 줄이 잘못된 포맷(예: 2025-13-01) 혹은 음수 금액으로 잘못 들어가 있는 경우 해당 데이터는 무조건 수동으로 수정해야 할 '영구적 오류'이므로 재시도해도 절대 올바르게 변환되지 않는다. 이러한 상황에 .skip(InvalidFormatException).skipLimit(100)을 사용하여 최대 100건까지 이런 오류 라인을 건너뛰며 나머지 정상 데이터를 계속 처리하도록 할 수 있다.

faultTolerant()를 적용하는 경우 FaultTolerantStepBuilder를 통해 StepBuilder 내부에서 스킵 정책(SkipPolicy)을 결정하고 생성하는 createSkipPolicy가 실행된다. 해당 메서드는 LimitCheckingItemSkipPolicy를 기본으로 사용하면서, 필요할 경우 사용자 정의 정책(this.skipPolicy)과 결합하여 CompositeSkipPolicy로 묶어 shouldSkip 메서드에서 각 스킵정책을 실행한다.

스프링 배치에서 제공하는 SkipPolicy 클래스에 대해 알아보자.

 

LimitCheckingItemSkipPolicy

public Step step() {
    return new StepBuilder("faultTolerantStep", jobRepository)
          .<I, O>chunk(10)
          .reader(reader()).processor(processor()).writer(writer())
          .faultTolerant()
          .skip(MyBusinessException.class) // 스킵 허용 예외 등록
          .skipLimit(5)
          .build();
}

LimitCheckingItemSkipPolicy는 가장 기본적으로 제공되는 스킵 정책이다. 이 정책은 지정된 예외 타입일 경우에만 스킵을 허용하며, 동시에 스킵 횟수가 설저오딘 한도(skipLimit)를 초과하지 않는 경우에만 스킵을 가능하도록 동작한다. 예외가 발생할 때마다 현재까지의 스킵 횟수(skipCount)를 확인하여, 만약 skipCount가 skipLimit보다 작다면 해당 아이템을 스킵하고 다음 아이템으로 넘어가지만, 한도를 초과하면 더 이상 스킵을 허용하지 않고 해당 Skip이 실패로 처리된다.

위 예제는 MyBusinessException이 발생할 경우 최대 5번까지는 스킵이 허용되며, 6번째 예외부터는 Step이 실패로 종료된다. 해당 스킵 정책은 비즈니스 예외에 유용하고, 횟수 기반으로 안전하게 스킵을 제어할 수 있다. 대부분의 간단한 skip 시나리오에 적합하다.

 

 

AlwaysSkipItemSkipPolicy

public Step step() {
    return new StepBuilder("faultTolerantStep", jobRepository)
          .<I, O>chunk(10)
          .reader(reader()).processor(processor()).writer(writer())
          .faultTolerant()
          .skipPolicy(new AlwaysSkipItemSkipPolicy())
          .build();
}

public class AlwaysSkipItemSkipPolicy implements SkipPolicy {

	@Override
	public boolean shouldSkip(Throwable t, long skipCount) {
		return true;
	}
}

AlwaysSkipItemSkipPolicy는 모든 예외를 무조건 스킵하도록 허용하는 정책이다. 이 정책은 예외의 종류나 발생 횟수와 상관없이 항상 true를 반환하므로 배치 수행 중 어떤 오류가 발생하더라도 작업이 중단되지 않고 끝가지 실행된다.

즉, 내부적으로 shouldSkip(Throwable t, int skipCount) 메서드가 호출될 때마다 예외 타입이나 현재 스킵 횟수와 관계없이 항상 true를 반환하기 때문에, 배치 수행 도중 예외가 발생하더라도 해당 아이템은 단순히 건너뛰고 다음 아이템으로 진행된다. 이로 인해 전체 Step이 실패하지 않고 모든 아이템 처리를 끝까지 시도할 수 있다.

 

NeverSkipItemSkipPolicy

public Step step() {
    return new StepBuilder("faultTolerantStep", jobRepository)
          .<I, O>chunk(10)
          .reader(reader()).processor(processor()).writer(writer())
          .faultTolerant()
          .skipPolicy(new NeverSkipItemSkipPolicy())
          .build();
}

public class NeverSkipItemSkipPolicy implements SkipPolicy {

	@Override
	public boolean shouldSkip(Throwable t, long skipCount) {
		return false;
	}
}

NeverSkipItemSkipPolicy는 어떠한 예외도 스킵하지 않도록 하는 정책이다. 즉, 배치 수행 중 하나의 아이템이라도 예외가 발생하면 해당 Step은 즉시 실패로 간주되어 롤백이 수행된다. 이 정책은 내부적으로 shouldSkip(Throwable t, int skipCount) 메서드가 호출될 때 항상 false를 반환하기 때문에, 어떤 예외가 발생하더라도 스킵 없이 바로 실패 처리를 진행한다.

 

Retry

Retry 기능은 일시적인 장애(Transient Error)에 대해 자동으로 재시도하도록 지원하는 내장 복구 메커니즘이다. 이는 일회성 네트워크 오류나 DB 데드락 등, 일정 시간 후 재요청하면 성공할 가능성이 높은 상황에서 알시 장애 때문에 전체 Job을 중단하지 않고, 정상적으로 다음 Step으로 이어질 수 있도록 할 수 있어 매우 유용하다. 주의해야할 점으로 Retry 전략은 ItemProcessor와 ItemWriter 둘 다에 적용되지만, ItemReader에는 적용되지 않는다.

Spring Batch에서 재시도 설정 주요 메서드는 다음과 같다.

  • retry(Class<? extends Throwable> type): 지정한 예외가 발생하면 재시도를 수행하도록 등록한다.
  • retryLimit(int retryLimit): 최대 재시도 횟수를 설정한다. 이 횟수를 초과하면 더 이상 재시도하지 않는다.
  • noRetry(Class<? extends Throwable> type): 재시도 대상에서 제외할 예외 타입을 지정한다.
  • retryPolicy(RetryPolicy): 사용자 정의 또는 기본 제공 RetryPolicy를 등록해, 예외 타입/횟수/기타 조건으로 재시도를 세밀하게 제어할 수 있다.
  • backOffPolicy(BackOffPolicy): 재시도 사이의 간격(고정/지수 등)을 조정하는 BackOffPolicy를 등록한다.

 

public class FaultTolerantStepBuilder<I, O> extends SimpleStepBuilder<I, O> {

    protected BatchRetryTemplate createRetryOperations() {

        RetryPolicy retryPolicy = this.retryPolicy;
        SimpleRetryPolicy simpleRetryPolicy = null;

        Map<Class<? extends Throwable>, Boolean> map = new HashMap<>(retryableExceptionClasses);
        map.put(ForceRollbackForWriteSkipException.class, true);
        simpleRetryPolicy = new SimpleRetryPolicy(retryLimit, map);

        if (retryPolicy == null) {
            Assert.state(!(retryableExceptionClasses.isEmpty() && retryLimit > 0),
                "If a retry limit is provided then retryable exceptions must also be specified");
            retryPolicy = simpleRetryPolicy;
        }
        else if ((!retryableExceptionClasses.isEmpty() && retryLimit > 0)) {
            CompositeRetryPolicy compositeRetryPolicy = new CompositeRetryPolicy();
            compositeRetryPolicy.setPolicies(new RetryPolicy[] { retryPolicy, simpleRetryPolicy });
            retryPolicy = compositeRetryPolicy;
        }
        
        // ...
    }
}

public class SimpleRetryPolicy implements RetryPolicy {

	@Override
	public boolean canRetry(RetryContext context) {
		Throwable t = context.getLastThrowable();
		boolean can = (t == null || retryForException(t)) && context.getRetryCount() < getMaxAttempts();
		if (!can && t != null && !this.recoverableClassifier.classify(t)) {
			context.setAttribute(RetryContext.NO_RECOVERY, true);
		}
		else {
			context.removeAttribute(RetryContext.NO_RECOVERY);
		}
		return can;
	}
}

faultTolerant() 활성화 시 사용되는 FaultTolerantStepBuilder에서 createRetryOperations 메서드는 재시도 정책(RetryPolicy)을 구성하여 BatchRetryTemplate에 주입하는 진입점이다. 기본 정책으로 SimpleRetryPolicy(retryLimit, retryableExceptionMap)을 만들고, 필요 시 기존 사용자 정책과 Composite으로 묶어 예외 발생 시 정책을 순차적으로 적용이 가능하다.

재시도 가능 여부는 canRetry(context) 호출 시 순서대로 각 정책의 canRetry(...)를 검사 하나라도 true를 반환하면 즉시 true를 반환한다. 아래 SimpleRetryPolicy.canRetry()는 "재시도 대상 예외 + 시도 횟수 < 최대 시도"를 만족할 때만 재시도하며, 복구 불가 예외는 컨텍스트에 NO_RECOVERY 플래그를 남긴다.

이를 통해 다양한 재시도 규칙을 한 번에 적용 가능하고, 복잡한 운영 환경에서 정교한 장애 대응 시나리오로 확장할 수 있다. 그리고 사용자가 .retryPolicy(...)로 커스텀 정책을 주입하더라도, Spring Batch가 기본 생성하는 SimpleRetryPolicy를 그대로 유지해 누락 없이 예외를 처리할 수 있다.

 

 

SimpleRetryPolicy

private Step paymentRetryStep() {
    Map<Class<? extends Throwable>, Boolean> retryableExceptions = Map.of(
          PessimisticLockException.class, true,
          IllegalStateException.class, false
    );
    SimpleRetryPolicy policy = new SimpleRetryPolicy(3, retryableExceptions, true);

    return new StepBuilder("retryStep", jobRepository)
          .<I, O>chunk(10)
          .reader(reader()).processor(processor()).writer(writer())
          .faultTolerant()
          .retryPolicy(policy)
          .build();
}

지정된 예외 타입에 대해 최대 횟수 기반으로 재시도를 허용하는 기본 정책이다. 내부에 Map<Class<? extends Throwable>, Boolean>을 설정해, 어떤 예외를 재시도할지 등록한다. 재시도 횟수가 retryLimit을 초과하지 않았으면 canRetry(...)가 true를 반환하다.

위 코드는 세 번까지 PessimistickLockException에 대한 재시도를 허용하고, IllegalArgumentException은 즉시 실패하도록 설정한 예시이다. 해당 정책은 횟수 기반 가장 간단한 재시도 정책으로 "잠시 기다렸다가 다시 시도하면 성공할 가능성이 높은 일시적 장애에 대한 예외만, 최대 N번" 같은 기본 시나리오에 적합한다.

 

TimeoutRetryPolicy

private Step retryStep() {
    return new StepBuilder("retryStep", jobRepository)
          .<I, O>chunk(10)
          .reader(reader()).processor(processor()).writer(writer())
          .faultTolerant()
          .retryPolicy(new TimeoutRetryPolicy(5_000))
          .build();
}

지정된 시간(timeout) 동안만 재시도를 허용하는 정책이다. 첫 시도가 시작된 시점을 기준으로 누적 경과시간을 체크하고, 경과시간이 timeout 미만이면 open(...) 이후 canRetry(..)가 true이다.

위 코드는 5초 이내라면 동일 예외가 발생해도 재시도를 하도록 설정한 예시이다. "몇 초 안에 해결될 일시적 오류"에 적합하며, 네트워크 지연, 외부 API 응답 대기 같은 시나리오에 유용하다.

 

AlwaysRetryPolicy

private Step retryStep() {
    return new StepBuilder("retryStep", jobRepository)
          .<I, O>chunk(10)
          .reader(reader()).processor(processor()).writer(writer())
          .faultTolerant()
          .retryPolicy(new AlwaysRetryPolicy())
          .build();
}

모든 예외를 무조건 재시도하도록 허용하는 정책이다. canRetry(...) 호출 시 발생 횟수나 예외 타입에 상관없이 항상 true를 반환하여 무조건 재시도한다.

위 코드는 어떠한 예외가 발생하더라도 무제한으로 재시도하도록 설정한 예시로 PoC 단계에서 "일단 다 재시도해보자"할 때 유용하고, 실무에서는 잘 쓰지 않는다.

 

NeverRetryPolicy

private Step retryStep() {
    return new StepBuilder("retryStep", jobRepository)
          .<I, O>chunk(10)
          .reader(reader()).processor(processor()).writer(writer())
          .faultTolerant()
          .retryPolicy(new NeverRetryPolicy())
          .build();
}

전혀 재시도하지 않음을 선언하는 정책이다. canRetry(...)가 항상 false를 반환하여 예외 발생 즉시 실패하여 롤백 처리된다.

위 코드는 어떤한 예외도 재시도 없이 바로 Step을 실패 처리한다. 재시도를 완전히 비활성화하고 싶을 때나 재시도 자체가 비즈니스에 부정적일 때 사용할 수 있다.

 

Listener

Spring Batch의 Listener는 배치 작업의 생명주기에서 특정 시점에 실행되는 콜백 인터페이스이다. Job, Step, Chunk 각 계층마다 Before/After Listener가 존재하며, 로깅, 모니터링, 예외 처리 등 횡단 관심사를 구현할 수 있다.

 

JobExecutionListener

public interface JobExecutionListener {

    // 잡 시작 직전 호출
    default void beforeJob(JobExecution jobExecution) {
    }

    // 모든 스텝 종료 후 호츌
    default void afterJob(JobExecution jobExecution) {
    }

}

JobExecutionListener는 배치 Job의 실행 전후 시점에 개입할 수 있는 콜백 인터페이스이다. beforeJob()으로 Job 시작 전 초기화 작업을, afterJob()으로 완료 후 리소스 정리나 알림 처리를 수행한다. JobExecution 객체를 통해 실행 시간, 상태, 메타데이터 등을 조회하여 모니터링 및 알림 시스템을 구축할 수 있다.

 

StepExecutionListener

public interface StepExecutionListener extends StepListener {

    // 각 스텝 시작 직전 호출
    default void beforeStep(StepExecution stepExecution) {
    }

    // 각 스텝 종료 후 호출
    @Nullable
    default ExitStatus afterStep(StepExecution stepExecution) {
       return null;
    }

}

StepExecutionListener는 개발 Step의 실행 전후 시점에 개입하는 리스너로, beforeStep()과 afterStep() 두 개의 콜백을 제공한다. 특히 afterStep()은 ExitStatus를 반환하여 다음 Step으로의 흐름을 동적으로 제어할 수 있으며, StepExecution 객체를 통해 읽기/쓰기 통계(getReadCount(), getWriteCount(), getSkipCount() 등)에 접근하여 상세한 모니터링이 가능하다. 또한, ExecutionContext를 활용하면 Step 간 데이터 공유도 구현할 수 있다.

 

ChunkListener

public interface ChunkListener extends StepListener {

    /**
     * The key for retrieving the rollback exception.
     */
    String ROLLBACK_EXCEPTION_KEY = "sb_rollback_exception";

    // 트랜잭션이 시작된 직후, 청크 단위 처리 직전에
    default void beforeChunk(ChunkContext context) {
    }

    // 청크 처리가 성공적으로 커밋된 직후
    default void afterChunk(ChunkContext context) {
    }

    // 청크 처리 중 예외가 발생해 롤백된 직후
    default void afterChunkError(ChunkContext context) {
    }

}

ChunkListener는 Chunk 단위 처리의 시작과 완료, 에러 시점에 개입하는 리스너로, 트랜잭션 경계와 정확히 일치하다. Chunk 레벨의 세밀한 타이밍 측정, 메모리 관리, afterChunkError에서 롤백 원인 로깅, 알림 또는 별도 리커버리 로직 수행하거나 성공한 청크 수, 실패된 청크 수 집계하여 모니터링 시스템에 전송하는 등을 구현할 수 있어 대용량 배치 작업의 안정성과 성능 모니터링에 필수적이다.

 

ItemReadListener

public interface ItemReadListener<T> extends StepListener {

    // ItemReader.read() 호출 직전
    default void beforeRead() {
    }

    // read()가 정상적으로 객체를 반환한 직후
    default void afterRead(T item) {
    }

    // read() 도중 예외 발생 시 바로
    default void onReadError(Exception ex) {
    }

}

ItemReaderListener는 Spring Batch의 리스너 계층 구조에서 가장 세밀한 단위로 동작한다. beforeRead()는 read() 호출 직전, afterRead()는 실제 아이템이 반환된 직후, onReadError()는 읽기 실패 시 호출된다. 중요한 점은 afterRead()가 null 반환 시에는 호출되지 않으며, 실제 객체가 반환될 때만 실행된다.

ItemReadListener의 주요 활용 포인트는 다음과 같다.

  • 읽기 성능 모니터링: 각 레코드별 읽기 시간 측정 -> 병목 구간 탐지
  • 데이터 품질 검사: afterRead에서 값 검증, 이상치 필터링 로직 삽입
  • 에러 대비 로깅/알림: onReadError에서 예외 정보 로깅 또는 관리자 알림

 

 

ItemProcessListener

public interface ItemProcessListener<T, S> extends StepListener {

    // ItemProcessor.process(item) 호출 직전
    default void beforeProcess(T item) {
    }

    // process()가 정상적으로 결과를 반환한 직후
    default void afterProcess(T item, @Nullable S result) {
    }

    // process() 도중 예외 발생 시
    default void onProcessError(T item, Exception e) {
    }
}

ItemProcessListener는 개별 아이템의 변환 과정 전후와 에러 시점에 개입하는 리스너로, Processor의 비즈니스 로직 성능을 측정하고, 필터링된 아이템을 추적하는데 최적화되어 있다. 중요한 특징은 afterProcess()가 result 파라미터로 처리 결과를 받으며, Processor가 null을 반환해도 호출하기 때문에 필터링된 아이템도 모니터링할 수 있다.

ItemProcessListener의 주요 활용 포인트는 다음과 같다.

  • 프로세싱 타이밍 측정: 각 아이템별 처리 시간 기록 -> CPU 집약 로직 최적화
  • 필터링 알림: afterProcess에서 result == null인 필터링된 아이템 로깅
  • 예외 세부 처리: onProcessError에서 특정 항목 재처리 또는 스킵 전략 결정

 

 

ItemWriteListener

public interface ItemWriteListener<S> extends StepListener {

    // ItemWriter.write(item) 호출 직전
    default void beforeWrite(Chunk<? extends S> items) {
    }

    // write()가 정상적으로 커밋(트랜잭션 커밋)된 이후
    default void afterWrite(Chunk<? extends S> items) {
    }

    // write() 도중 예외 발생 시
    default void onWriteError(Exception exception, Chunk<? extends S> items) {
    }
}

ItemWriteListener는 Chunk 단위로 아이템들을 쓰는 과정의 전후와 에러 시점에 개입하는 리스너이다. beforWrite()는 write() 호출 직전, afterWrite()는 트랜잭션이 커밋된 후, onWriteError()는 에러 발생 시 롤백 전에 호출된다. 특히, afterWrite()는 ChunkListener.afterChunk()보다 먼저 실행되며, DB I/O 성능 측정, 배치 사이즈 동적 조정, 실패한 아이템 추적 등에 활용할 수 있다. onWriteError()에서 별도 트랜잭션 작업이 필요하면 PROPAGATION_REQUIRES_NEW를 반드시 사용해야 한다.

ItemWriteListener의 주요 활용 포인트는 다음과 같다.

  • 쓰기 타이밍 측정: 쓰기 호출 전후 시간 기록 -> DB I/O 최적화 방안 도출
  • 배치 커밋 조정: afterWrite에서 커밋 간격 조정 또는 배치 사이즈 재계산
  • 오류 아이템 로깅: onWriteError에서 실패한 레코드 상세 로깅 -> 재처리 스크립트 활용

 

Chunk Reader-Processor-Writer 예제

@Slf4j
@Configuration
@AllArgsConstructor
public class PaymentReportJobConfig {

    private static final int CHUNK_SIZE = 200;

    private final EntityManagerFactory entityManagerFactory;
    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;

    @Bean
    public Job paymentReportJob(Step paymentReportStep) {
       return new JobBuilder("paymentReportJob", jobRepository)
             .incrementer(new RunIdIncrementer())
             .start(paymentReportStep)
             .build();
    }

    @Bean
    public Step paymentReportStep(
          JpaPagingItemReader<PaymentSource> paymentReportReader
    ) {
       return new StepBuilder("paymentReportStep", jobRepository)
             .<PaymentSource, Payment>chunk(CHUNK_SIZE, transactionManager)
             // Step 소요 시간 측정
             .listener(new StepDurationTrackerListener())
             .reader(paymentReportReader)
             .processor(paymentReportProcessor())
             .writer(paymentReportWriter())
             // Chunk 소요 시간 측정
             .listener(new ChunkDurationTrackerListener())
             .build();
    }

    @Bean
    @StepScope
    public JpaPagingItemReader<PaymentSource> paymentReportReader(
          @Value("#{jobParameters['paymentDate']}") LocalDate paymentDate
    ) {
       return new JpaPagingItemReaderBuilder<PaymentSource>()
             .name("paymentSourceItemReader")
             .entityManagerFactory(entityManagerFactory)
             .queryString("SELECT ps FROM PaymentSource ps WHERE ps.paymentDate = :paymentDate")
             .parameterValues(Collections.singletonMap("paymentDate", paymentDate))
             .pageSize(10)
             .build();
    }

    private ItemProcessor<PaymentSource, Payment> paymentReportProcessor() {
       return paymentSource -> {
          if (paymentSource.getFinalAmount().equals(BigDecimal.ZERO)) {
             return null;
          }

          return Payment.builder()
                .amount(paymentSource.getFinalAmount())
                .paymentDate(paymentSource.getPaymentDate())
                .partnerCorpName(paymentSource.getPartnerCorpName())
                .status("PAYMENT")
                .build();
       };
    }

    @Bean
    public JpaItemWriter<Payment> paymentReportWriter() {
       return new JpaItemWriterBuilder<Payment>()
             .entityManagerFactory(entityManagerFactory)
             .build();
    }
}

위 코드는 특정 날짜의 결제 원천 데이터(PaymentSource)를 읽어서, 금액이 0이 아닌 건만 필터링하여 결제 데이터(Payment)로 변환한 후 저장하는 Job이다. JpaPagingItemReader로 데이터를 페이징 조회하고, chunk size 200으로 트랜잭션을 관리하며, Listener를 통해 Step과 Chunk의 실행 시간을 측정한다.

 

실행 결과

위 로그는 PaymentReportJob의 실행 결과이다. 총 5만 건의 데이터를 처리했으면, 251번의 청크로 나누어 실행되었다. 마지막 청크(#251)에서 ItemReader에서 read()가 null을 반환하여 빈 청크로 인해 종료된 것을 알 수 있다. 또한, JpaPagingItemReader을 사용하여 데이터를 조회할 때 LIMIT OFFSET 방식으로 조회한 것을 알 수 있다.

위 사진은 마지막 로그에 대해서만 캡쳐했지만, 청크 수가 증가할수록 LIMIT OFFSET 방식은 읽어야하는 데이터가 많아지므로 조회가 점점 느리게 된다. 따라서 #250 청크는 751ms가 걸리고, 총 1분 31초가 소요된 것을 확인할 수 있다.

 

 

출처

Meta-Data Schema - 공식 문서

실무를 위한 Spring Batch의 모든 것 : 성능 개선부터 프로젝트까지

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/01   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함