[SYSTEM BUILD] 2. 청크 지향 처리 (Chunk-Oriented Processing)

y001·2026년 2월 2일

Spring Batch Guide

목록 보기
6/19
post-thumbnail

1. 코드 레벨 구현 예시

1-1. 청크 지향 처리 조립

청크 지향 처리 Step의 시작점은 StepBuilder.chunk()이다. tasklet()이 태스크릿 지향 처리 Step을 만드는 진입점이라면, chunk()는 청크 지향 처리 Step을 만드는 진입점이다. 이 메서드를 호출하면 Step은 “읽기–처리–쓰기” 파이프라인을 청크 단위로 반복 실행하는 구조로 구성된다.

@Bean
public Step processStep(JobRepository jobRepository,
                        PlatformTransactionManager transactionManager) {
    return new StepBuilder("processStep", jobRepository)
            .<CustomerDetail, CustomerSummary>chunk(10, transactionManager)
            .reader(itemReader())
            .processor(itemProcessor())
            .writer(itemWriter())
            .build();
}

1-2. 청크 사이즈 지정

chunk(10, transactionManager)에서 10은 “한 번에 몇 개의 아이템을 묶어서 처리할 것인가”를 의미한다. 이 값은 단순한 반복 횟수 설정이 아니라, 청크 지향 처리의 성능과 안정성에 직접적인 영향을 주는 운영 파라미터다.

청크 사이즈가 큰 경우 한 번에 메모리에 적재되는 데이터의 양이 증가하며, 트랜잭션 경계 또한 커진다. 이로 인해 처리 중 실패가 발생하면 롤백 범위가 넓어질 수 있다. 반면 커밋 횟수와 ItemWriter 호출 횟수가 줄어들어, 전체 처리량은 상대적으로 향상될 수 있다.

청크 사이즈가 작은 경우 트랜잭션 경계가 작아져 실패 시 롤백되는 데이터의 범위가 제한된다. 메모리 사용량 또한 낮게 유지된다. 다만 read와 write 호출, 그리고 커밋 횟수가 증가하면서 트랜잭션 및 I/O 오버헤드가 커질 수 있다.

1-3. 제네릭 타입으로 데이터 흐름 정의

청크 지향 처리의 또 다른 핵심은 chunk() 앞에 붙는 제네릭 타입이다.

.<CustomerDetail, CustomerSummary>chunk(10, transactionManager)

이 두 타입은 Step 내부 데이터 흐름을 규정하는 선언이다. 첫 번째 타입은 ItemReader가 반환하는 입력 데이터의 타입을, 두 번째 타입은 ItemProcessor를 거쳐 ItemWriter로 전달되는 출력 데이터의 타입을 의미한다.

이 선언을 통해 Reader는 입력 타입을 생산하고, Processor는 이를 출력 타입으로 변환하며, Writer는 출력 타입의 묶음을 처리하도록 역할이 고정된다. 그 결과 읽기와 쓰기 단계의 책임이 명확히 분리되고, 데이터 변환의 위치 또한 코드 레벨에서 분명해진다.

2. 구성

2-1. ItemReader

ItemReader는 데이터 소스에서 아이템을 하나씩 읽어오는 컴포넌트다.

public interface ItemReader<T> {
    T read() throws Exception;
}

ItemReader는 청크 단위로 데이터를 반환하지 않는다. read()는 항상 아이템 단위로 데이터를 반환하며, 청크는 Spring Batch가 내부에서 아이템을 수집해 구성한다. read()null을 반환하는 시점이 Step의 종료 조건이며, 이는 더 이상 읽을 데이터가 없음을 의미한다. Spring Batch는 이 신호를 기준으로 청크 반복을 종료하고 Step을 완료 처리한다.

2-2. ItemProcessor

ItemProcessor는 ItemReader가 읽어온 아이템을 가공하는 컴포넌트다.

public interface ItemProcessor<I, O> {
    O process(I item) throws Exception;
}

ItemProcessor는 입력 데이터를 출력 데이터로 변환하거나, 조건에 따라 필터링하거나, 유효성 검증을 수행한다. process()null을 반환하면 해당 아이템은 처리 대상에서 제외되어 ItemWriter로 전달되지 않는다. 검증 실패 시 예외를 발생시키면 Step은 실패로 처리되며, 필요에 따라 skip이나 retry 정책과 결합할 수 있다.

ItemProcessor는 필수 요소는 아니다. 데이터 변환이나 필터링이 필요 없는 경우에는 생략할 수 있으며, 이 경우 ItemReader가 반환한 아이템은 그대로 ItemWriter로 전달된다.

2-3. ItemWriter

ItemWriter는 처리된 결과를 최종적으로 저장하거나 외부로 출력하는 컴포넌트다.

public interface ItemWriter<T> {
    void write(Chunk<? extends T> chunk) throws Exception;
}

ItemWriter는 아이템을 개별적으로 받지 않고, 항상 청크 단위로 묶인 데이터를 전달받는다. 이 구조는 쓰기 성능과 직접적으로 연결된다. 특히 JDBC batch insert나 batch update처럼 묶음 처리가 가능한 환경에서는 청크 단위 쓰기가 I/O 효율과 처리량을 크게 향상시킨다.

알겠다. 그럼 설명 없이 구조만 보이게, 시니어 블로그 톤으로 딱 필요한 만큼만 정리해서 다시 준다.

3. 청크 단위 반복

청크 지향 처리는 동일한 흐름을 청크 단위로 반복 실행한다. 청크 사이즈가 10이고 전체 데이터가 25건인 경우, 실제 실행 흐름은 다음과 같다.

[1번째 청크]
read()     → 10회 호출 (아이템 10개 수집)
process()  → 10회 호출
write()    → 1회 호출 (10개)
commit

[2번째 청크]
read()     → 10회 호출 (아이템 10개 수집)
process()  → 10회 호출
write()    → 1회 호출 (10개)
commit

[3번째 청크]
read()     → 5회 호출
read()     → null 반환
process()  → 5회 호출
write()    → 1회 호출 (5개)
commit

ItemReader는 항상 아이템 단위로 호출되며, 청크 크기만큼 아이템이 수집된 이후에 처리 단계가 시작된다. ItemProcessor는 아이템마다 호출되고, ItemWriter는 청크 단위로 한 번만 호출된다.

read()null을 반환하는 시점이 반복의 종료 조건이다. 이 경우에도 이미 수집된 아이템은 마지막 청크로 처리되며, 청크 사이즈를 채우지 못하더라도 정상적으로 커밋된다.

0개의 댓글