본 문서는 Spring Batch 핵심 아키텍처와 도메인 모델에 대해 정리한 기록입니다.
스프링 배치는 사용자의 시각적 개입 없이(Non-interactive), 대량의 데이터를 주기적으로 처리하거나 복잡한 비즈니스 로직을 자동화해야 할 때 가장 강력한 효율을 발휘합니다. 구체적으로 아래와 같은 요구사항이 있을 때 도입합니다.
OutOfMemoryError가 발생합니다. 데이터를 일정 단위(Chunk)로 나누어 읽고, 가공하고, 저장하여 메모리를 효율적으로 쓰고 싶을 때 사용합니다.제시된 용어들은 스프링 배치 프레임워크의 아키텍처 계층에 따라 다음과 같이 유기적으로 연결됩니다.
[ 인프라 / 실행 제어 계층 ]
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스프링 배치는 배치 프로세스의 모든 상태와 이력을 데이터베이스에 저장합니다. 이를 통해 "어떤 배치가, 어떤 조건으로, 언제 실행되어, 어디까지 성공했는가"를 완벽하게 추적할 수 있으며, 시스템 장애 발생 시 안정적인 복구를 보장합니다.
BATCH_JOB_INSTANCE)BATCH_JOB_EXECUTION_PARAMS)BATCH_JOB_EXECUTION)BATCH_JOB_EXECUTION_CONTEXT)BATCH_STEP_EXECUTION)BATCH_STEP_EXECUTION_CONTEXT)메인 데이터를 다루는 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: 5application.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();
}
}spring.batch.job.enabled: false 설정의 중요성: 이 옵션을 지정하지 않으면 톰캣이나 내부 서버가 구동될 때 스프링 컨텍스트에 등록된 모든 배치가 즉시 한 번씩 실행되어 버립니다. 운영 장애로 이어질 수 있으므로 반드시 false로 끄고 내부 스케줄러를 통해 실행하는 구성을 취해야 합니다.FAILED 상태 등)은 롤백되지 않고 정상적으로 DB에 저장되어야 사후 추적이 가능합니다. 이를 위해 배치 인프라용 트랜잭션 매니저를 물리적으로 분리하는 것입니다.이렇게 인프라 설정을 마치고 나면, 실제 Job을 개발할 때 해당 JobRepository와 batchTransactionManager를 주입받아 Step을 빌드하게 됩니다.
[배치 기동]
│
▼
[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' 업데이트 + 오류 로그 저장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에 의해 부모 트랜잭션이 커밋됩니다.
};
}
}실패 시 복구나 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();
}
}