티스토리 뷰

안녕하세요. 이번 글에서는 Spring Batch에서 여러 Step으로 구성된 Job에서 Step 간에 데이터를 공유하는 방법에 대해 정리해보겠습니다. Spring Batch가 공식적으로 제공하는 ExecutionContext 개념과, StepExecutionListener를 활용한 방식을 설명합니다. 

 

배치 Job이 복잡해질수록 Step 간에 상태나 계산 결과를 전달해야 하는 요구는 자연스럽게 발생합니다. 이러한 요구를 해결하기 위해 Spring Batch는 별도의 커스텀 저장소 없이도 사용할 수 있는 상태 관리 메커니즘을 제공합니다.

 


 

소개 및 배경

Spring Batch Job은 여러 Step으로 구성되며, 각 Step은 기본적으로 독립적인 실행 단위입니다. 하지만 실무에서는 한 Step의 실행 결과를 다음 Step에서 활용해야 하는 경우가 많습니다.

 

예를 들어, 첫 번째 Step에서 외부 API를 호출해 데이터를 조회하고, 그 결과를 두 번째 Step에서 가공하거나 저장해야 할 수 있습니다. 또는 특정 Step의 실행 결과에 따라 이후 Step의 실행 여부를 결정해야 하는 경우도 있습니다.

 

이처럼 Step 간에 데이터를 전달해야 할 때 Spring Batch는 ExecutionContext를 제공합니다. 공식 문서에 따르면, ExecutionContextJob 또는 Step 실행 중 필요한 상태 정보를 저장하고 재시작 시 복원하기 위한 컨텍스트 객체입니다. 내부적으로는 Map<String, Object> 구조를 사용하며, 배치 실행 상태를 안전하게 관리하는 역할을 합니다.

 


 

JobExecutionContext와 StepExecutionContext의 차이

Spring Batch는 두 가지 범위의 ExecutionContext를 제공합니다.

StepExecutionContext

  • 각 Step의 StepExecution에 속한 ExecutionContext입니다.
  • 해당 Step 실행 범위 내에서만 사용됩니다.
  • 다른 Step에서는 접근할 수 없습니다.
  • Step 내부에서 임시 상태를 저장하거나, Chunk 처리 중 상태를 유지하는 용도로 사용됩니다.

JobExecutionContext

  • 전체 Job의 JobExecution에 속한 ExecutionContext입니다.
  • Job 실행 기간 동안 유지되며, 모든 Step에서 접근할 수 있습니다.
  • Step 간 데이터를 공유하려면 반드시 JobExecutionContext를 사용해야 합니다.

즉, Step 간 변수 공유가 목적이라면 StepExecutionContext가 아닌 JobExecutionContext를 기준으로 설계해야 합니다.

 


 

ExecutionContext의 개념과 특징

ExecutionContext의 공식 정의 

Spring Batch 공식 문서에 따르면 ExecutionContext는 다음과 같은 특징을 가집니다.

  • 데이터 저장 구조 : 내부적으로 Map<String, Object> 형태로 데이터를 저장합니다.
  • 직렬화 요구사항 : ExecutionContext에 저장되는 값은 직렬화 가능해야 합니다. Job 재시작 시 메타데이터 저장소에 저장되고 복원되므로, 직렬화되지 않은 객체를 저장하면 예외가 발생할 수 있습니다.
  • 생명주기 : JobExecutionContext는 Job 실행 동안 유지되며, StepExecutionContext는 해당 Step 실행 범위로 한정됩니다.
  • 동시성 주의 : ExecutionContext 자체는 동기화를 제공하지 않습니다. 파티셔닝이나 병렬 Step 환경에서 동시에 수정될 경우 동시성 문제가 발생할 수 있으므로 설계 시 주의가 필요합니다.

주요 메서드

ExecutionContext는 다음과 같은 메서드를 제공합니다.

  • put(String key, Object value)
  • get(String key)
  • getString(String key)
  • getLong(String key)
  • getInt(String key)
  • containsKey(String key)
  • remove(String key)

타입별 조회 메서드를 활용하면 불필요한 캐스팅을 피할 수 있습니다. 


Step 간 변수 공유 구현 방식

Step 간 변수 공유는 다음과 같은 흐름으로 구현합니다.

  1. Step 실행 중 공유할 데이터를 수집합니다.
  2. Step 종료 시점에 JobExecutionContext에 데이터를 저장합니다.
  3. 이후 Step에서 해당 데이터를 조회하여 사용합니다.

이때 Step 종료 시점의 책임을 명확히 하기 위해 StepExecutionListener를 사용하는 방식이 가장 구조적으로 명확합니다.

 


StepExecutionListener를 활용한 공유 방식

StepExecutionListener의 역할

StepExecutionListener는 Step 실행 전후에 호출되는 콜백 인터페이스입니다.

  • beforeStep(StepExecution stepExecution)
  • afterStep(StepExecution stepExecution)

Step 간 데이터 전달은 주로 afterStep 메서드에서 처리합니다. Step의 실행이 정상적으로 종료된 시점에 결과를 정리해 JobExecutionContext에 저장하기에 적합한 위치입니다.

 


afterStep에서 데이터 저장

@Override
public ExitStatus afterStep(StepExecution stepExecution) {
    ExecutionContext jobContext =
        stepExecution.getJobExecution().getExecutionContext();

    jobContext.put("sharedKey", sharedValue);

    return stepExecution.getExitStatus();
}

 

이 방식은 Step의 비즈니스 로직과 데이터 전달 책임을 분리할 수 있어 유지보수성이 높습니다.

 

다음 Step에서 데이터 조회

@Override
public void beforeStep(StepExecution stepExecution) {
    ExecutionContext jobContext =
        stepExecution.getJobExecution().getExecutionContext();

    String sharedValue = jobContext.getString("sharedKey");
    processSharedData(sharedValue);
}

 

이처럼 JobExecutionContext를 통해 이전 Step의 결과를 안전하게 재사용할 수 있습니다.

 

Tasklet에서 직접 공유하는 방식

Tasklet 내부에서도 JobExecutionContext에 직접 접근할 수 있습니다.

ExecutionContext jobContext = chunkContext.getStepContext()
    .getStepExecution()
    .getJobExecution()
    .getExecutionContext();

jobContext.put("apiResult", apiResult);

 

Listener 없이도 Step 간 데이터 공유가 가능하지만, Step 종료 시점의 책임을 명확히 하기 위해 Listener 기반 접근이 구조적으로 더 명확한 경우가 많습니다. 

 

 

공식 권장 방식: ExecutionContextPromotionListener

Spring Batch는 StepExecutionListener를 직접 구현하지 않아도 되도록 ExecutionContextPromotionListener라는 공식 Listener 구현체를 제공합니다. 이 Listener는 다음 역할을 수행합니다.

  • StepExecutionContext에 저장된 특정 key 값을 Step 종료 시점에 JobExecutionContext로 자동 승격(promote)

Step 내부에서 데이터 저장

Step 내부(Tasklet 등)에서는 StepExecutionContext에만 저장합니다.

ExecutionContext stepContext =
    chunkContext.getStepContext()
        .getStepExecution()
        .getExecutionContext();

stepContext.put("apiResult", apiResult);

이 시점에서는 JobExecutionContext에 직접 접근하지 않습니다. 

PromotionListener 설정

@Bean
public ExecutionContextPromotionListener promotionListener() {
    ExecutionContextPromotionListener listener =
        new ExecutionContextPromotionListener();

    listener.setKeys(new String[]{"apiResult"});
    return listener;
}
  • 승격 대상 key는 명시적으로 지정
  • 불필요한 데이터 공유 방지
  • JobExecutionContext 오염 방지

Step에 Listener 적용

@Bean
public Step apiCallStep(
        StepBuilderFactory stepBuilderFactory,
        ApiCallTasklet apiCallTasklet,
        ExecutionContextPromotionListener promotionListener) {

    return stepBuilderFactory.get("apiCallStep")
        .tasklet(apiCallTasklet)
        .listener(promotionListener)
        .build();
}

Step이 정상 종료되면 apiResult는 자동으로 JobExecutionContext로 복사됩니다. 

 

 다음 Step에서 데이터 조회

ExecutionContext jobContext =
    chunkContext.getStepContext()
        .getStepExecution()
        .getJobExecution()
        .getExecutionContext();

String apiResult = jobContext.getString("apiResult");

PromotionListener를 사용하면 다음 Step에서는 JobExecutionContext에서 바로 값을 조회하면 됩니다. 

 

StepExecutionListener 방식과 PromotionListener 방식 비교

  StepExecutionListener 직접 구현 PromotionListener
제어 유연성 높음 제한적
표준성 사용자 구현 ✔ 공식
코드 복잡도 높음 낮음
실수 가능성 있음 낮음
권장 용도 복잡한 로직 단순 전달

단순한 Step 간 데이터 전달이라면 PromotionListener를 기본 선택지로 고려하는 것이 바람직합니다.

 

실무 적용 시 고려사항

  • 직렬화 가능한 데이터만 저장
  • 대용량 데이터 저장 지양
  • → 외부 저장소에 저장 후 식별자만 ExecutionContext에 보관
  • 키 네이밍 규칙 명확화
  • Job 재시작 시 복원 가능성 고려
  • 병렬 Step 환경에서의 동시성 문제 주의 

ExecutionContext는 상태 관리 용도로 설계된 구조이므로, 데이터 저장소로 오용하지 않도록 주의해야 합니다.

 


 

맺음말 및 정리

Spring Batch에서 Step 간 변수 공유는 JobExecutionContext를 활용하면 공식적으로 지원되는 방식으로 안정적으로 구현할 수 있습니다. Listener와 ExecutionContext를 적절히 활용하면 각 Step의 독립성을 유지하면서도 필요한 데이터를 명확하게 전달할 수 있습니다.

 

ExecutionContext는 Spring Batch가 공식적으로 제공하는 상태 관리 메커니즘이므로, 별도의 커스텀 구현 없이도 재시작 가능하고 일관된 Step 간 데이터 전달이 가능합니다. 다만 직렬화, 데이터 크기, 동시성 문제를 고려하여 설계해야 하며, 필요한 데이터만 최소한으로 공유하는 것이 중요합니다.

 

Step 간 데이터 공유 역시 트랜잭션 관리와 마찬가지로 상황에 맞는 전략 선택이 핵심입니다. 각 Step의 책임을 명확히 하고 ExecutionContext를 적절히 활용하여 안정적이고 유지보수하기 쉬운 배치 시스템을 구성는 것에 도움이 되리라 생각합니다.