파트 4. 예외 처리와 안정성 확보 및 메타데이터 제어 (Robustness & Status)
본 문서는 배치 처리 도중 발생하는 예외 상황에 대비하여 어떻게 안정성(Robustness)을 확보할 수 있는지, 그리고 메타데이터 병목 현상을 제어하는 전략에 대해 정리한 기록입니다.
운영 환경의 배치는 언제든지 예외(네트워크 순단, 데이터 포맷 오류 등)가 발생할 수 있으며, 수천만 건의 데이터를 처리할 때 메타 데이터 백엔드 부하로 인한 병목이 발생할 수 있습니다. Spring Batch는 이를 제어하기 위한 상태 전이 메커니즘과 안정성 확보 전략을 제공합니다.
1. Meta-Data 테이블의 상태(Status) 관리 메커니즘
Spring Batch는 실행 중인 Job과 Step의 상태를 BATCH_JOB_EXECUTION 및 BATCH_STEP_EXECUTION 테이블에 실시간으로 기록하며 흐름을 제어합니다. 이때 가장 중요한 두 가지 상태 개념이 BatchStatus와 ExitStatus입니다.
① BatchStatus (프레임워크 내부 제어용 상태)
- 의미: JVM 내에서 실행 중인 배치의 물리적인 실행 상태를 나타내며,
BATCH_JOB_EXECUTION테이블 등에 명확히 기록됩니다. - 주요 값:
STARTING,STARTED,STOPPING,STOPPED,FAILED,COMPLETED,ABANDONED - 메타데이터 업데이트 시점:
- Step 시작 시: 메타 테이블에
STARTED상태로 레코드가 갱신됩니다. - Chunk 커밋 시: 트랜잭션이 커밋될 때마다
COMMIT_COUNT,READ_COUNT,WRITE_COUNT등의 지표가 실시간으로 누적 업데이트됩니다. - 종료 시: 내부 로직이 예외 없이 완수되면
COMPLETED로, 복구하지 못한 예외가 발생하여 Step이 터지면FAILED로 마킹됩니다.
② ExitStatus (Job 흐름 제어용 상태)
- 의미: Step의 실행이 끝났을 때, 다음 조건부 흐름(Flow)을 결정하기 위해 컴포넌트가 반환하는 프로세스 종료 코드입니다.
- 주요 값:
EXECUTING,COMPLETED,NOOP,FAILED,STOPPED,UNKNOWN - 특징: BatchStatus와 달리 개발자가 리스너 등을 통해 임의의 문자열로 커스텀할 수 있습니다. 예를 들어, 실제 물리적 상태는 실패(BatchStatus = FAILED)했더라도 후속 복구 Step을 실행시키기 위해 종료 코드를
ExitStatus = "FAILED_BUT_RECOVERABLE"형태로 조작하여 다음 실행 경로를 바인딩할 수 있습니다.
2. 레벨별 예외 처리 전략 및 구현 (Job, Step, Tasklet, Chunk)
운영 환경의 배치는 언제든지 예외(네트워크 순단, 데이터 포맷 오류 등)가 발생할 수 있습니다. Spring Batch는 작업을 중단시키지 않고 끝까지 수행하기 위한 강력한 예방책을 제공합니다.
① Skip (건너뛰기)
대량의 데이터를 처리하는 도중, 단 1건의 데이터 포맷 오류 때문에 전체 배치가 멈추는 것은 비효율적입니다. SkipPolicy를 설정하면 특정 Exception이 발생했을 때 해당 Item만 무시하고 다음 Item으로 넘어가도록 처리할 수 있습니다. 치명적이지 않은 비즈니스 예외에 주로 적용합니다.
② Retry (재시도)
외부 API를 호출하거나 DB 데드락이 발생했을 때 등 일시적인 네트워크 장애나 일시적 시스템 오류가 원인이라면, 즉시 실패 처리하지 않고 재시도하는 것이 좋습니다. Chunk 설정의 .retryLimit()을 사용하여 재시도 횟수와 간격(BackOff)을 지정할 수 있습니다.
③ Restart (재시작 기능)
배치가 50% 진행된 상황에서 뻗었다면, 다음 실행 시 처음부터 다시 시작하는 것이 아니라 실패한 지점(Chunk)부터 이어서 시작(Restart)할 수 있습니다. 이는 앞서 배운 BATCH_JOB_EXECUTION, BATCH_STEP_EXECUTION 메타 테이블이 현재까지 성공한 상태를 완벽하게 기록하고 있기 때문에 가능합니다. 이를 통해 장애 발생 시 복구 시간을 획기적으로 단축합니다.
Job & Step 레벨 예외 처리 예시
@Bean
public Job robustJob(JobRepository jobRepository, Step mainStep, Step recoveryStep) {
return new JobBuilder("robustJob", jobRepository)
.start(mainStep)
.on("FAILED") // ExitStatus가 FAILED일 때
.to(recoveryStep) // 복구 Step으로 분기
.on("*").end()
.from(mainStep)
.on("*").end() // 성공 시 종료
.end()
.build();
}Chunk 레벨 Skip & Retry 예시
@Bean
public Step chunkExceptionStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new StepBuilder("chunkExceptionStep", jobRepository)
.<String, String>chunk(1000, transactionManager)
.reader(customReader())
.processor(customProcessor())
.writer(customWriter())
.faultTolerant() // 예외 처리 기능 활성화
.skip(IllegalArgumentException.class) // 특정 비즈니스 예외 건너뛰기
.skipLimit(100)
.retry(DeadlockLoserDataAccessException.class) // DB 데드락 발생 시 재시도
.retryLimit(3)
.build();
}3. 대용량 처리 시 메타데이터 병목 현상과 올바른 해결 방안
실시간 처리가 불가능한 이유와 병목의 본질
Spring Batch는 청크 단위로 트랜잭션을 커밋할 때마다 데이터의 무결성과 추적성을 위해 BATCH_STEP_EXECUTION 테이블에 동기식(Synchronous)으로 쓰기 연산을 수행합니다.
데이터가 수천만 건 이상으로 대형화되면 비즈니스 로직의 연산 속도나 실제 비즈니스 DB에 데이터를 밀어 넣는 속도가 메타데이터 테이블을 업데이트하고 쓰기 락(Write Lock)을 획득하는 속도보다 빨라집니다. 이로 인해 메타 DB 쪽에서 베스트 아웃(Bottleneck) 현상이 발생하며, 배치 전체가 멈칫거리거나 지연되는 현상이 발생합니다. 그렇다고 데이터를 한 번에 메모리에 다 올리는 것은 OOM으로 가는 지름길입니다.
대용량을 위한 아키텍처적 올바른 해결 전략
데이터 유실 리스크 없이, 그리고 메모리를 과도하게 쓰지 않으면서 이 메타데이터 병목을 해결하는 표준 실무 전략은 크게 두 가지입니다.
전략 A: 메타데이터 전용 데이터소스 분리 (Multi-DataSource)
가장 권장되는 방식입니다. 비즈니스 원천 데이터가 저장되는 Production DB와 배치의 상태를 기록하는 Meta DB 인스턴스(혹은 스키마)를 완전히 물리적으로 분리합니다. 메타데이터 테이블의 잦은 갱신과 락 경쟁이 비즈니스 트랜잭션 풀에 간섭하지 않도록 인프라 레벨에서 격리하는 것입니다.
전략 B: Chunk Size 수치 조정 및 JobRepository 격리수준 완화
Chunk 단위를 안전한 범위 내에서 최대치(예: 1000~5000)로 높여 메타데이터 테이블에 쓰기 요청을 보내는 빈도 자체를 줄이고, JobRepository가 메타데이터 바인딩할 때 사용하는 트랜잭션 격리 수준(Isolation Level)을 낮추어 락 범위를 최소화합니다.
자바 설정 예시 (Multi-DataSource 및 JobRepository 최적화)
@Configuration
public class BatchDataSourceConfig {
// 1. 비즈니스 DB와 메타 DB를 분리하여 주입받음
@Bean
public JobRepository customJobRepository(
@Qualifier("metaDataSource") DataSource metaDataSource,
PlatformTransactionManager metaTransactionManager) throws Exception {
JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
factory.setDataSource(metaDataSource);
factory.setTransactionManager(metaTransactionManager);
// 2. 메타데이터 테이블 갱신 시 발생하는 블로킹을 줄이기 위해 격리 수준 완화
factory.setIsolationLevelForCreate("ISOLATION_READ_COMMITTED");
factory.afterPropertiesSet();
return factory.getObject();
}
}
댓글
GitHub 계정으로 의견이나 질문을 남길 수 있습니다.