티스토리 뷰

이 글에서는 Spring Batch의 멀티스레딩 환경과 파티셔닝에서 스레드 안정성을 확보하는 방법을 설명합니다. Spring Batch 공식 문서를 기준으로 병렬 처리 모델과 스레드 안전성 요구사항을 기술합니다.

소개 · 배경

Spring Batch는 대용량 데이터를 효율적으로 처리하기 위해 병렬 처리 기능을 제공합니다. Spring Batch Reference Manual에 따르면, 병렬 처리는 처리 시간을 단축하고 리소스 활용을 최적화하기 위한 설계입니다. 특히 파티셔닝(Partitioning)은 대용량 데이터를 여러 파티션으로 나누어 병렬로 처리하는 기법입니다.

 

파티셔닝 환경에서는 각 파티션이 독립적인 스레드에서 실행됩니다. Spring Batch는 이러한 병렬 실행을 위해 Master/Worker Step 구조를 제공합니다. Master Step은 전체 작업을 파티션으로 분할하고, Worker Step은 각 파티션을 독립적으로 처리합니다. 이러한 구조는 확장성과 성능 향상을 목적으로 설계되었습니다.

 

Spring Batch 공식 모델에서 병렬 처리는 TaskExecutor를 통해 구현됩니다. TaskExecutor는 각 파티션을 독립적인 스레드에서 실행하는 역할을 담당합니다. 그러나 여러 스레드가 동시에 실행되면 공유 자원에 대한 동시 접근이 발생할 수 있습니다. Spring Batch는 이러한 문제를 해결하기 위해 @StepScope와 ExecutionContext를 활용한 스레드 안전성 메커니즘을 제공합니다.

병렬 처리 및 스레드 안정성 개념

JVM 멀티스레딩은 여러 스레드가 동시에 실행되는 환경을 의미합니다. Java Language Specification에 따르면, 각 스레드는 독립적인 실행 경로를 가지며, 공유 메모리를 통해 데이터를 교환합니다. 여러 스레드가 동일한 자원에 접근할 때는 동시성 제어가 필요합니다.

 

동시성 이슈는 여러 스레드가 동일한 자원에 접근할 때 발생하는 문제입니다. 실행 순서에 따라 결과가 달라지는 레이스 컨디션(Race Condition)이 대표적입니다. 예를 들어, 두 스레드가 동일한 변수를 동시에 증가시키는 경우, 예상치 못한 결과가 발생할 수 있습니다.

 

public class Counter {
    private int count = 0;

    public void increment() {
        count++; // 동시 접근 시 레이스 컨디션 발생 가능
    }

    public int getCount() {
        return count;
    }
}

 

위 코드에서 여러 스레드가 increment() 메서드를 동시에 호출하면, count 변수의 값이 정확하게 증가하지 않을 수 있습니다. 이는 count++ 연산이 원자적(atomic)이지 않기 때문입니다. 이러한 문제를 해결하기 위해서는 동기화(synchronization) 메커니즘이 필요합니다.

Spring Batch의 병렬 처리 모델

Spring Batch Reference Manual에 따르면, Spring Batch는 여러 병렬 처리 모델을 제공합니다. 멀티스레드 Step은 단일 Step 내에서 여러 스레드를 사용하여 청크를 병렬로 처리하는 방식입니다. 파티셔닝은 Step을 여러 파티션으로 분할하여 각 파티션을 독립적인 스레드에서 실행하는 방식입니다.

 

파티셔닝은 대용량 데이터 처리에 적합한 병렬 처리 모델입니다. 각 파티션은 독립적인 ExecutionContext를 가지며, 이를 통해 데이터 분할과 병렬 처리가 가능합니다. Spring Batch는 이러한 파티셔닝을 위해 Partitioner 인터페이스와 TaskExecutorPartitionHandler를 제공합니다.

 

Spring Batch가 병렬 처리 모델을 제공하는 이유는 처리 시간 단축과 리소스 활용 최적화를 위해서입니다. 단일 스레드로 처리하면 1시간이 걸리는 작업을 여러 스레드로 나누면 이론적으로 처리 시간을 단축할 수 있습니다. 또한 멀티코어 CPU 환경에서 여러 스레드를 활용하면 CPU 자원을 효율적으로 사용할 수 있습니다.

 

Spring Batch 파티셔닝 아키텍처

Master/Worker Step 구조

Spring Batch Reference Manual에 따르면, 파티셔닝은 Master Step과 Worker Step으로 구성됩니다. Master Step은 PartitionStep으로 구현되며, 전체 작업을 파티션으로 분할하는 역할을 담당합니다. Worker Step은 각 파티션에서 실행되는 실제 처리 로직을 담당합니다.

 

Master Step은 Partitioner를 사용하여 전체 데이터를 여러 파티션으로 분할합니다. 각 파티션에 대한 정보는 ExecutionContext에 저장되며, 이 정보는 Worker Step에 전달됩니다. Worker Step은 전달받은 ExecutionContext를 기반으로 해당 파티션의 데이터만 처리합니다.

Partitioner의 역할

Partitioner 인터페이스는 Spring Batch API Javadoc에 정의된 인터페이스로, 전체 작업을 파티션으로 분할하는 전략을 정의합니다. partition() 메서드는 gridSize를 받아 각 파티션에 대한 ExecutionContext를 담은 Map을 반환합니다.

 

Spring Batch가 Partitioner를 제공하는 이유는 데이터 분할 전략을 추상화하기 위해서입니다. 다양한 데이터 소스와 분할 전략을 일관된 방식으로 처리할 수 있도록 설계되었습니다. 예를 들어, ID 범위 기반 분할, 날짜 범위 기반 분할, 외부 API 페이지네이션 기반 분할 등 다양한 전략을 구현할 수 있습니다.

TaskExecutor 역할

TaskExecutor는 Spring Framework가 제공하는 인터페이스로, 작업을 비동기로 실행하는 역할을 담당합니다. Spring Batch는 TaskExecutor를 사용하여 각 파티션을 독립적인 스레드에서 실행합니다.

 

Spring Batch Reference Manual에 따르면, TaskExecutor는 스레드 풀을 관리하여 여러 파티션을 병렬로 실행합니다. 적절한 스레드 풀 크기를 설정하면 스레드 간의 경합을 최소화하고 안정적인 병렬 처리를 보장할 수 있습니다. 스레드 풀 크기가 너무 크면 리소스 경합이 발생할 수 있고, 너무 작으면 병렬 처리의 이점을 충분히 활용할 수 없습니다.

TaskExecutorPartitionHandler 역할

TaskExecutorPartitionHandler는 Spring Batch API Javadoc에 정의된 클래스로, PartitionHandler 인터페이스의 구현체입니다. 이 클래스는 TaskExecutor를 사용하여 각 파티션을 병렬로 실행하는 역할을 담당합니다.

 

Spring Batch가 TaskExecutorPartitionHandler를 제공하는 이유는 파티션 실행 로직을 캡슐화하기 위해서입니다. Master Step은 PartitionHandler를 통해 파티션 실행을 위임하며, 실제 실행은 TaskExecutorPartitionHandler가 담당합니다. 이러한 설계는 파티션 실행 전략을 변경할 수 있도록 확장성을 제공합니다.

 

ItemReader·ItemWriter의 스레드 안정성 확인

ItemReader/ItemWriter가 기본적으로 스레드 안전하지 않은 이유

Spring Batch Reference Manual에 따르면, ItemReader와 ItemWriter는 기본적으로 스레드 안전하지 않습니다. 이는 이러한 인터페이스들이 상태를 유지하기 때문입니다. ItemReader는 현재 읽기 위치를 추적하고, ItemWriter는 쓰기 버퍼를 관리합니다.

 

여러 스레드가 동일한 ItemReader 또는 ItemWriter 인스턴스에 접근하면, 상태 공유로 인해 동시성 이슈가 발생할 수 있습니다. 예를 들어, 두 스레드가 동일한 ItemReader 인스턴스에서 데이터를 읽으면, 읽기 위치가 예상과 다르게 변경될 수 있습니다.

Spring Batch 공식 문서에서 권장하는 설정

Spring Batch Reference Manual에 따르면, 파티셔닝 환경에서는 각 파티션마다 독립적인 ItemReader와 ItemWriter 인스턴스를 생성해야 합니다. 이를 위해 @StepScope 어노테이션을 사용합니다.

 

@StepScope는 Spring Batch가 제공하는 어노테이션으로, 각 Step 실행마다 새로운 빈 인스턴스를 생성합니다. 파티셔닝 환경에서는 각 파티션마다 독립적인 빈 인스턴스가 생성되므로, 스레드 간의 상태 공유를 방지할 수 있습니다.

@Bean
@StepScope
public ItemReader<MyData> itemReader(@Value("#{stepExecutionContext['partitionKey']}") String partitionKey) {
    // 파티션별 독립적인 ItemReader 생성
    return new MyItemReader(partitionKey);
}

 

위 코드에서 @StepScope를 사용하여 각 파티션마다 독립적인 ItemReader 인스턴스를 생성합니다. @Value("#{stepExecutionContext['partitionKey']}")를 통해 각 파티션의 ExecutionContext에서 파티션별 정보를 주입받습니다.

파티셔닝 환경에서 saveState/ExecutionContext 처리의 의미

Spring Batch Reference Manual에 따르면, ItemReader는 saveState 속성을 통해 상태 저장 여부를 제어할 수 있습니다. 파티셔닝 환경에서는 각 파티션이 독립적인 ExecutionContext를 가지므로, saveState를 false로 설정하여 상태 저장을 비활성화할 수 있습니다.

 

saveState를 false로 설정하면 ItemReader의 상태가 ExecutionContext에 저장되지 않습니다. 이는 파티셔닝 환경에서 각 파티션이 독립적으로 실행되므로, 상태 저장이 불필요한 경우에 적합합니다. 또한 상태 저장 오버헤드를 제거하여 성능을 향상시킬 수 있습니다.

 

코드 예제 설명

파티셔닝 환경에서 스레드 안전성을 확보하기 위한 코드 예시입니다:

@Bean
public Step partitionedStep() {
    return stepBuilderFactory.get("partitionedStep")
        .partitioner("slaveStep", partitioner())
        .step(slaveStep())
        .gridSize(4)
        .taskExecutor(taskExecutor())
        .build();
}

@Bean
@StepScope
public ItemReader<MyData> itemReader(@Value("#{stepExecutionContext['partitionKey']}") String partitionKey) {
    // 파티션별 독립적인 ItemReader 생성
    return new MyItemReader(partitionKey);
}

@Bean
@StepScope
public ItemProcessor<MyData, ProcessedData> itemProcessor() {
    // 상태를 유지하지 않는 스레드 안전한 ItemProcessor 생성
    return new MyItemProcessor();
}

@Bean
@StepScope
public ItemWriter<ProcessedData> itemWriter() {
    // 상태를 유지하지 않는 스레드 안전한 ItemWriter 생성
    return new MyItemWriter();
}

 

코드 설명:

partitionedStep()은 Master Step을 구성합니다. partitioner()는 데이터 분할 전략을 정의하며, slaveStep()은 각 파티션에서 실행될 Worker Step을 지정합니다. gridSize(4)는 파티션 개수를 4로 설정하며, taskExecutor()는 병렬 실행을 위한 TaskExecutor를 설정합니다.

 

@StepScope를 사용하여 각 파티션마다 독립적인 ItemReader, ItemProcessor, ItemWriter 인스턴스를 생성합니다. 이를 통해 스레드 간의 상태 공유를 방지하고, 스레드 안전성을 확보할 수 있습니다. @Value("#{stepExecutionContext['partitionKey']}")를 통해 각 파티션의 ExecutionContext에서 파티션별 정보를 주입받아, 각 파티션이 자신에게 할당된 데이터만 처리하도록 합니다.

 

실무적 고려사항 및 팁

스레드 안전성 위반 시 발생할 수 있는 문제

스레드 안전성을 확보하지 않으면 다음과 같은 문제가 발생할 수 있습니다. 데이터 불일치가 발생할 수 있습니다. 여러 스레드가 동일한 자원에 접근하면 레이스 컨디션으로 인해 예상치 못한 결과가 발생할 수 있습니다. 또한 읽기 위치가 잘못 추적되어 데이터가 중복 처리되거나 누락될 수 있습니다.

@StepScope, saveState 설정 등의 사용 관점

@StepScope는 파티셔닝 환경에서 필수적으로 사용해야 합니다. 각 파티션이 독립적인 스레드에서 실행되므로, 각 파티션마다 독립적인 빈 인스턴스가 필요합니다. @StepScope를 사용하지 않으면 모든 파티션이 동일한 빈 인스턴스를 공유하게 되어 스레드 안전성 문제가 발생합니다.

 

saveState 설정은 파티셔닝 환경에서 선택적으로 사용할 수 있습니다. 각 파티션이 독립적으로 실행되므로 상태 저장이 불필요한 경우 saveState를 false로 설정하여 성능을 향상시킬 수 있습니다. 그러나 재시작 가능한 작업을 구현해야 하는 경우에는 상태 저장이 필요할 수 있습니다.

병렬 처리 시 각 스레드 독립 실행과 상태 관리

병렬 처리 환경에서는 각 스레드가 독립적으로 실행되어야 합니다. Spring Batch는 이를 위해 각 파티션에 독립적인 ExecutionContext를 제공합니다. 각 파티션의 ExecutionContext는 파티션별 데이터 범위, 파티션 번호 등의 정보를 저장하며, 이를 통해 각 파티션이 자신에게 할당된 데이터만 처리하도록 보장합니다.

 

상태 관리는 각 파티션의 ExecutionContext를 통해 수행됩니다. ItemReader의 상태는 ExecutionContext에 저장되며, 재시작 시 이 상태를 기반으로 마지막 처리 지점부터 재개할 수 있습니다. 파티셔닝 환경에서는 각 파티션이 독립적인 ExecutionContext를 가지므로, 각 파티션의 상태가 서로 간섭하지 않습니다.

 

맺음말 및 요약

이 글에서는 Spring Batch의 멀티스레딩 환경과 파티셔닝에서 스레드 안정성을 확보하는 방법을 설명했습니다. Spring Batch 공식 모델에 따르면, 파티셔닝은 Master/Worker Step 구조를 통해 구현되며, 각 파티션은 독립적인 스레드에서 실행됩니다.

 

스레드 안정성 점검 시 핵심 체크 포인트는 다음과 같습니다. @StepScope를 사용하여 각 파티션마다 독립적인 빈 인스턴스를 생성해야 합니다. ItemReader, ItemProcessor, ItemWriter가 상태를 공유하지 않도록 설계해야 합니다. 각 파티션의 ExecutionContext를 활용하여 파티션별 데이터를 관리해야 합니다. TaskExecutor의 스레드 풀 크기를 적절히 설정하여 리소스 경합을 최소화해야 합니다.

 

이러한 고려사항을 반영하여 안정적이고 효율적인 병렬 처리 배치 작업을 구성할 수 있습니다. Spring Batch의 병렬 처리 기능을 최대한 활용하면서도 스레드 안전성을 보장하여 신뢰성 있는 배치 시스템을 구축할 수 있습니다.