Spring Batch ExecutionContext 사이즈 초과 원인과 해결 방법

1. ExecutionContext 사이즈 초과 발생 원인

Spring Batch에서 아래와 같은 방식으로 대용량 데이터를 ExecutionContext에 저장할 경우, 직렬화된 데이터가 수십 KB ~ 수 MB까지 증가하면서 DB 저장에 실패하거나 OutOfMemoryError가 발생할 수 있습니다.


List<MyEntity> entityList = getMyEntityList();
stepExecution.getExecutionContext().put("myEntities", entityList); // ❌ 위험

2. 안전한 대체 전략

📌 2-1. Processor에서 get 후 즉시 remove


@StepScope
@Component
public class MyProcessor implements ItemProcessor<MyInput, MyOutput> {

    @Value("#{stepExecution.executionContext}")
    private ExecutionContext executionContext;

    private List<MyEntity> cachedList;

    @Override
    public MyOutput process(MyInput item) {
        if (cachedList == null) {
            cachedList = (List<MyEntity>) executionContext.get("myEntities");
            executionContext.remove("myEntities"); // ✅ 즉시 삭제
        }

        // cachedList로 처리
        return convert(item, cachedList);
    }
}

📌 2-2. StepExecutionListener로 꺼내고 바로 삭제


@Component
public class EntityCachingListener implements StepExecutionListener {

    private List<MyEntity> cachedEntities;

    @Override
    public void beforeStep(StepExecution stepExecution) {
        ExecutionContext ctx = stepExecution.getExecutionContext();
        cachedEntities = (List<MyEntity>) ctx.get("myEntities");
        ctx.remove("myEntities"); // ✅ 시작 시 제거
    }

    public List<MyEntity> getCachedEntities() {
        return cachedEntities;
    }
}

그리고 Processor에서 Listener 사용:


@Component
@StepScope
public class MyProcessor implements ItemProcessor<MyInput, MyOutput> {

    private final EntityCachingListener listener;

    public MyProcessor(EntityCachingListener listener) {
        this.listener = listener;
    }

    @Override
    public MyOutput process(MyInput item) {
        return processItemWith(listener.getCachedEntities(), item);
    }
}

3. SpEL에서 #{stepExecution}은 어디서 오는가?

Spring Batch는 @StepScope가 붙은 Bean에 대해 실행 시점에 StepExecution, JobExecution 등을 자동으로 SpEL 컨텍스트에 등록해줍니다.


@StepScope
@Bean
public MyProcessor myProcessor(@Value("#{stepExecution}") StepExecution stepExecution) {
    return new MyProcessor(stepExecution);
}

사용 가능한 SpEL 예시:

  • #{stepExecution}: 현재 StepExecution 객체
  • #{stepExecution.executionContext['key']}: StepExecutionContext의 특정 값
  • #{jobParameters['date']}: Job 파라미터

4. 마무리

ExecutionContext는 재시작이나 공유 데이터 처리에 유용하지만, 대용량 데이터를 직접 저장할 경우 오히려 장애의 원인이 됩니다. 꼭 필요한 정보만 저장하고, 불필요한 데이터는 빠르게 제거하는 전략이 필요합니다.

5. ExecutionContext 크기 확인 유틸

ExecutionContext에 저장된 객체가 실제로 얼마나 큰지 확인하고 싶다면, 아래와 같이 로그로 직렬화된 바이트 수를 확인할 수 있습니다.


import java.io.*;
import java.nio.charset.StandardCharsets;

public class ExecutionContextSizeUtil {

    public static int estimateSize(ExecutionContext ctx) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream out = new ObjectOutputStream(bos);
        out.writeObject(ctx);
        out.flush();
        return bos.toByteArray().length;
    }
}

// 사용 예시
int size = ExecutionContextSizeUtil.estimateSize(stepExecution.getExecutionContext());
log.info("ExecutionContext 직렬화 크기: {} bytes", size);

6. ExecutionContext 필터링 직렬화 전략 (선택 적용)

기본 직렬화는 모든 데이터를 저장하지만, 특정 키만 저장하거나 일부는 제외하고 싶다면 커스텀 ExecutionContextSerializer를 등록할 수 있습니다.


public class FilteringExecutionContextSerializer implements ExecutionContextSerializer {

    private final DefaultExecutionContextSerializer delegate = new DefaultExecutionContextSerializer();

    @Override
    public void serialize(ExecutionContext ctx, OutputStream out) throws IOException {
        ExecutionContext filtered = new ExecutionContext();

        for (Map.Entry<String, Object> entry : ctx.entrySet()) {
            String key = entry.getKey();
            if (!key.startsWith("exclude.")) { // 특정 접두어 제외
                filtered.put(key, entry.getValue());
            }
        }

        delegate.serialize(filtered, out);
    }

    @Override
    public ExecutionContext deserialize(InputStream in) throws IOException {
        return delegate.deserialize(in);
    }
}

이 클래스를 Spring Batch 설정에서 등록하면, 저장 시점에 불필요한 항목은 무시됩니다.


7. DB 컬럼 타입 변경 주의사항

ExecutionContext가 커지는 문제를 해결하기 위해 다음과 같은 컬럼 타입 변경이 필요할 수 있습니다:

MySQL 예시:


ALTER TABLE BATCH_STEP_EXECUTION_CONTEXT MODIFY SERIALIZED_CONTEXT LONGTEXT;
ALTER TABLE BATCH_JOB_EXECUTION_CONTEXT MODIFY SERIALIZED_CONTEXT LONGTEXT;

주의 사항 (LOCK)

  • 컬럼 타입 변경 시 Table-level Lock이 발생합니다
  • 운영 중인 테이블에서는 작업 시간대 조정 필요
  • 사전 백업, 테스트 DB에서 시뮬레이션 권장

✅ 마무리 체크리스트

  • ExecutionContext에 List, Entity 전체 저장은 금지
  • stepExecution.get().remove()로 조기에 제거
  • 가능하면 ID 목록이나 경량 데이터만 저장
  • 필요 시 DB 컬럼 타입을 LONGTEXT 또는 CLOB로 변경
  • 커스텀 직렬화 전략으로 필터링도 가능

이 전략을 통해 Spring Batch Job의 안정성과 유지보수성을 크게 개선할 수 있습니다.

관련 글

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다