← 목록으로
📅 2026.06.15
파트 1. Spring Batch 핵심 아키텍처와 사용
SpringBootSpringBatchmeta-data주기성대용량TechStudy

본 문서는 Spring Batch 핵심 아키텍처와 도메인 모델에 대해 정리한 기록입니다.


1. Spring Batch는 언제 사용하면 좋은가?

스프링 배치는 사용자의 시각적 개입 없이(Non-interactive), 대량의 데이터를 주기적으로 처리하거나 복잡한 비즈니스 로직을 자동화해야 할 때 가장 강력한 효율을 발휘합니다. 구체적으로 아래와 같은 요구사항이 있을 때 도입합니다.

  • 대용량 데이터 처리 및 시스템 부하 분산
    수백만 건의 데이터를 한 번에 메모리에 올리면 OutOfMemoryError가 발생합니다. 데이터를 일정 단위(Chunk)로 나누어 읽고, 가공하고, 저장하여 메모리를 효율적으로 쓰고 싶을 때 사용합니다.
  • 트랜잭션 관리와 안정성(Robustness)이 필수적일 때
    작업 도중 일부 데이터에 에러가 나더라도 전체 작업을 롤백하거나, 반대로 에러가 난 데이터만 건너뛰고(Skip) 나머지는 정상 커밋하는 정밀한 트랜잭션 제어가 필요할 때 사용합니다.
  • 정기적 자동화 작업
    매일 밤 12시에 당일 매출을 집계하는 정산 시스템, 시스템 간 데이터 마이그레이션, 정기 리포트 생성 등에 최적화되어 있습니다.
  • 실패 후 재시작(Restart) 메커니즘
    밤새 돌던 배치가 새벽 3시에 뻗었을 때, 처음부터 다시 돌리는 것이 아니라 새벽 3시에 실패한 바로 그 지점부터 이어서 실행해야 하는 운영 환경의 필수 복구 기능이 필요할 때 사용합니다.

2. Spring Batch 핵심 용어 및 개념 구조도

제시된 용어들은 스프링 배치 프레임워크의 아키텍처 계층에 따라 다음과 같이 유기적으로 연결됩니다.

[ 인프라 / 실행 제어 계층 ]
Scheduler (Quartz, Spring Task 등) -> Trigger 발생 -> JobLauncher
                                                         |
                                                 JobRepository (DB 저장)
 
[ 배치 도메인 모델 계층 ]
Job (작업 단위) 
  └─ JobParameters (파라미터/식별자) -> JobInstance (논리적 단위) -> JobExecution (물리적 실행)
  └─ Step (단계별 작업) 
       ├─ StepExecution / ExecutionContext (상태 저장소)
       ├─ Scope (JobScope, StepScope)

       ├─ Tasklet 방식 (단일 작업)
       └─ Chunk 방식 (대용량 분할 처리)
            ├─ ItemReader (CursorReader / PageReader)
            ├─ ItemProcessor
            └─ ItemWriter
            └─ Commit Interval (청크 크기)
 
[ 운영 및 최적화 전략 계층 ]
Robustness : Skip, Retry, Restart
Scaling    : Multi-thread step, Partitioning (Master-Worker), Parallel step

3. 영속성 관리를 위한 메타데이터 테이블의 역할

스프링 배치는 배치 프로세스의 모든 상태와 이력을 데이터베이스에 저장합니다. 이를 통해 "어떤 배치가, 어떤 조건으로, 언제 실행되어, 어디까지 성공했는가"를 완벽하게 추적할 수 있으며, 시스템 장애 발생 시 안정적인 복구를 보장합니다.

  • JobInstance (BATCH_JOB_INSTANCE)
    Job과 JobParameters가 결합하여 생성되는 논리적인 작업 정의입니다. Job 이름과 최초 생성 시 부여된 고유 식별 키 등 인스턴스 고유 정보가 저장됩니다. 동일한 식별 키(파라미터)로 성공한 이력이 있다면 중복 실행을 원천 방지합니다.
  • JobParameters (BATCH_JOB_EXECUTION_PARAMS)
    배치를 구동할 때 외부에서 던져주는 파라미터 컬렉션입니다. 배치를 돌릴 때 던진 인자값들과 데이터 타입 등의 파라미터 이력이 저장되며, 동일한 Job을 서로 다르게 식별하는 고유 키 역할을 합니다.
  • JobExecution (BATCH_JOB_EXECUTION)
    JobInstance가 실제로 실행된 물리적인 시도 기록입니다. Job의 시작/종료 시간, 실행 상태(COMPLETED/FAILED), 종료 코드Job 실행 기록이 남습니다. 실패 시 동일한 인스턴스로 다시 실행하면 새로운 Execution이 생성됩니다.
  • JobExecutionContext (BATCH_JOB_EXECUTION_CONTEXT)
    Job 실행 도중 상태를 저장하는 Job 상태 저장소입니다. Job 범위(Scope)에서 여러 Step 간에 공유되어야 하는 자바 객체 및 상태 값(공유 데이터) 이 직렬화되어 저장됩니다.
  • StepExecution (BATCH_STEP_EXECUTION)
    Job 내부의 개별 Step이 실행된 기록입니다. 배치가 어디서 실패했는지 추적하는 가장 정밀한 단위이며, Step별 Read/Write/Commit/Filter/Rollback 횟수 및 상태 등 메트릭 정보가 Step 실행 기록으로 실시간 저장됩니다.
  • StepExecutionContext (BATCH_STEP_EXECUTION_CONTEXT)
    개별 Step 단위로 상태를 저장하는 Step 상태 저장소입니다. 대용량 처리 중 에러가 발생했을 때, Reader가 마지막으로 읽은 위치(Cursor 등)와 같은 실패 복구용 상태 값이 저장되어 재시작 시 해당 지점부터 이어서 처리할 수 있게 해줍니다.
상세 설정 코드 및 실무 포인트 보기

1. application.yml 상세 설정

메인 데이터를 다루는 primary 데이터소스와 배치 이력을 관리하는 batch 데이터소스를 나누어 등록하고, 스프링 배치의 기본 동작을 제어하는 옵션을 지정합니다.

spring:
  # 1. Spring Batch 프레임워크 핵심 제어 옵션
  batch:
    jdbc:
      # always: 기동 시 테이블이 없으면 자동 생성 (로컬/테스트 환경 추천)
      # never: 테이블을 자동 생성하지 않음 (DDL 권한이 제한된 운영 환경 추천)
      initialize-schema: always 
    job:
      # false: 어플리케이션이 켜질 때 모든 Job이 자동으로 실행되는 것을 방지합니다.
      # 운영 환경에서는 스케줄러(Jenkins, Quartz 등)가 원하는 시점에만 호출해야 하므로 필수 설정입니다.
      enabled: false 
 
  # 2. 멀티 데이터소스(Multi-DataSource) 설정
  datasource:
    primary:
      jdbc-url: jdbc:mysql://localhost:3306/business_db?serverTimezone=Asia/Seoul
      username: main_user
      password: main_password
      driver-class-name: com.mysql.cj.jdbc.Driver
      hikari:
        pool-name: HikariPool-Main
        maximum-pool-size: 10
 
    batch:
      jdbc-url: jdbc:mysql://localhost:3306/batch_meta_db?serverTimezone=Asia/Seoul
      username: batch_user
      password: batch_password
      driver-class-name: com.mysql.cj.jdbc.Driver
      hikari:
        pool-name: HikariPool-Batch
        maximum-pool-size: 5

2. BatchConfig 자바 상세 설정 (Spring Boot 3.x 기준)

application.yml에 선언한 두 개의 데이터소스를 자바 빈(Bean)으로 등록하고, Spring Batch 인프라(JobRepository, PlatformTransactionManager)가 오직 배치 전용 DB만 바라보도록 격리하는 설정 클래스입니다.

package com.example.batch.config;
 
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.repository.support.JobRepositoryFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
 
@Configuration
public class BatchDataSourceConfig {
 
    // 1. 비즈니스 로직용 메인 데이터소스
    @Bean
    @Primary // @Autowired 시 기본 채택되는 데이터소스 지정
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }
 
    // 2. 배치 메타데이터 테이블용 데이터소스
    @Bean(name = "batchDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.batch")
    public DataSource batchDataSource() {
        return DataSourceBuilder.create().build();
    }
 
    // 3. 배치 전용 트랜잭션 매니저 (배치 데이터소스 연동)
    @Bean(name = "batchTransactionManager")
    public PlatformTransactionManager batchTransactionManager(
            @Qualifier("batchDataSource") DataSource batchDataSource) {
        return new DataSourceTransactionManager(batchDataSource);
    }
 
    // 4. 커스텀 JobRepository 설정
    @Bean
    public JobRepository jobRepository(
            @Qualifier("batchDataSource") DataSource batchDataSource,
            @Qualifier("batchTransactionManager") PlatformTransactionManager batchTransactionManager) throws Exception {
        
        JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
        factory.setDataSource(batchDataSource);
        factory.setTransactionManager(batchTransactionManager);
        
        // [성능 및 격리 튜닝]
        // 대량의 배치 메타데이터가 적재될 때 데드락(Deadlock)을 방지하기 위해 격리 수준을 조정합니다.
        // 해당 DB 제품군이 지원하는 최적의 격리 수준을 선택합니다. (기본값: ISOLATION_SERIALIZABLE)
        factory.setIsolationLevelForCreate("ISOLATION_READ_COMMITTED");
        
        // 만약 한 DB 내에서 여러 서비스의 배치가 섞인다면 테이블 접두사를 변경할 수 있습니다. (기본값: BATCH_)
        factory.setTablePrefix("BATCH_"); 
        
        return factory.getObject();
    }
}

3. 설정 시 실무 관점 핵심 포인트

  • spring.batch.job.enabled: false 설정의 중요성: 이 옵션을 지정하지 않으면 톰캣이나 내부 서버가 구동될 때 스프링 컨텍스트에 등록된 모든 배치가 즉시 한 번씩 실행되어 버립니다. 운영 장애로 이어질 수 있으므로 반드시 false로 끄고 내부 스케줄러를 통해 실행하는 구성을 취해야 합니다.
  • 트랜잭션 매니저 분리: 배치 프로세스 도중 비즈니스 예외로 인해 비즈니스 데이터가 롤백되더라도, 배치 메타데이터 테이블의 실행 기록(FAILED 상태 등)은 롤백되지 않고 정상적으로 DB에 저장되어야 사후 추적이 가능합니다. 이를 위해 배치 인프라용 트랜잭션 매니저를 물리적으로 분리하는 것입니다.

이렇게 인프라 설정을 마치고 나면, 실제 Job을 개발할 때 해당 JobRepositorybatchTransactionManager를 주입받아 Step을 빌드하게 됩니다.


4. 메타데이터 등록 Flow

[배치 기동] 


[1. JobLauncher] ───> Job & JobParameters 수신


[2. JobRepository] ──> DB 조회 (동일 파라미터 성공 이력 검증)

   ├── [성공 이력 있음] ──> [실행 거부] JobInstanceAlreadyCompleteException 발생 (종료)

   └── [성공 이력 없음 / 기존 실패] 


[3. BATCH_JOB_INSTANCE] ──> 신규 인스턴스 등록 (기존 실패 이력 시 기존 ID 재사용)


[4. BATCH_JOB_EXECUTION] ──> 'STARTING' 상태로 물리 기록 생성


[5. BATCH_STEP_EXECUTION] ──> Step 실행 시 'STARTED' 상태 생성
           │                 (Chunk 커밋 수, 읽은 수 등 실시간 업데이트 반복)


[6. 배치 종료] ──> 결과에 따른 최종 상태 업데이트 (분기)

           ├── [성공 시] ──> JOB_EXECUTION & STEP_EXECUTION 상태 'COMPLETED' 업데이트
           └── [실패 시] ──> JOB_EXECUTION & STEP_EXECUTION 상태 'FAILED' 업데이트 + 오류 로그 저장
메타데이터 흐름 실행코드 예시

1. Chunk 지향 기반의 Batch 엔티티 구성 코드

package com.example.batch.job;
 
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobScope;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.support.ListItemReader;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
 
import java.util.Arrays;
 
@Slf4j
@Configuration
@RequiredArgsConstructor
public class UserSettlementJobConfig {
 
    // 1. Job 구성: JobRepository를 주입받아 메타데이터와 연동합니다.
    @Bean
    public Job userSettlementJob(JobRepository jobRepository, Step settlementStep) {
        return new JobBuilder("userSettlementJob", jobRepository)
                .start(settlementStep)
                .build();
    }
 
    // 2. Step 구성: Chunk 크기(Commit Interval)와 트랜잭션 매니저를 지정합니다.
    @Bean
    @JobScope
    public Step settlementStep(JobRepository jobRepository, 
                               PlatformTransactionManager batchTransactionManager,
                               ItemReader<String> settlementReader,
                               ItemProcessor<String, String> settlementProcessor,
                               ItemWriter<String> settlementWriter) {
        return new StepBuilder("settlementStep", jobRepository)
                .<String, String>chunk(5, batchTransactionManager) // Commit Interval = 5
                .reader(settlementReader)
                .processor(settlementProcessor)
                .writer(settlementWriter)
                .build();
    }
 
    // 3. ItemReader: Late Binding을 통해 JobParameters를 주입받아 활용합니다.
    @Bean
    @StepScope
    public ListItemReader<String> settlementReader(
            @Value("#{jobParameters['requestDate']}") String requestDate) {
        
        log.info("--> [Reader] BATCH_JOB_EXECUTION_PARAMS에서 requestDate 파라미터 읽기: {}", requestDate);
        
        // 실무에서는 여기서 CursorReader나 PagingReader를 사용해 DB를 조회합니다.
        return new ListItemReader<>(Arrays.asList("user1", "user2", "user3", "user4", "user5", "user6"));
    }
 
    // 4. ItemProcessor: 비즈니스 로직 가공 및 필터링
    @Bean
    @StepScope
    public ItemProcessor<String, String> settlementProcessor() {
        return item -> {
            log.info("--> [Processor] 데이터 가공 처리 중: {}", item);
            return item + " 완료";
        };
    }
 
    // 5. ItemWriter: Chunk 단위(5건)로 모인 리스트를 받아 일괄 저장합니다.
    @Bean
    @StepScope
    public ItemWriter<String> settlementWriter() {
        return chunk -> {
            log.info("--> [Writer] Chunk 단위 일괄 등록 착수 (Size: {})", chunk.size());
            for (String item : chunk) {
                log.info("--> [Writer] DB 영속화 진행: {}", item);
            }
            // 이 블록이 예외 없이 끝나면 batchTransactionManager에 의해 부모 트랜잭션이 커밋됩니다.
        };
    }
}

2. ExecutionContext 조작 코드 (중간 상태 저장 및 복구)

실패 시 복구나 Step 간 데이터 공유를 위해 ExecutionContext를 직접 제어하는 방법입니다. ItemStreamReader 인터페이스를 구현하거나, 아래와 같이 리스너(@BeforeStep, @AfterStep)를 활용하여 주입받을 수 있습니다.

package com.example.batch.listener;
 
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.annotation.AfterStep;
import org.springframework.batch.core.annotation.BeforeStep;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.stereotype.Component;
 
@Slf4j
@Component
public class SettlementStepListener {
 
    @BeforeStep
    public void beforeStep(StepExecution stepExecution) {
        // BATCH_STEP_EXECUTION_CONTEXT 조회 및 데이터 적재
        ExecutionContext stepContext = stepExecution.getExecutionContext();
        
        // 이전 실행 실패로 인해 기존 컨텍스트 데이터가 남아있는지 확인 (Restart 체크)
        if(stepContext.containsKey("lastProcessedIndex")) {
            int lastIndex = stepContext.getInt("lastProcessedIndex");
            log.info("--> [Restart 감지] 과거 실패 지점 Index 존재: {}", lastIndex);
        } else {
            stepContext.putInt("lastProcessedIndex", 0); // 초기값 세팅
            log.info("--> [최초 실행] ExecutionContext 내 lastProcessedIndex 초기화 완료");
        }
    }
 
    @AfterStep
    public ExitStatus afterStep(StepExecution stepExecution) {
        ExecutionContext stepContext = stepExecution.getExecutionContext();
        
        // Step 종료 직전 상태값 업데이트 -> BATCH_STEP_EXECUTION_CONTEXT 테이블에 직렬화되어 저장됨
        stepContext.putInt("lastProcessedIndex", 9999); 
        
        log.info("--> [Step 완료] 최종 누적 메트릭 - Read Count: {}, Write Count: {}", 
                stepExecution.getReadCount(), stepExecution.getWriteCount());
        
        return stepExecution.getExitStatus();
    }
}