./gradlew bootRun 실행 시 배치 작업이 자동으로 실행된다.Job과 JobParameters로 변환하여 JobLauncher에 전달./gradlew bootRun --args='--spring.batch.job.name=brutalizedSystemJob chaos=true,java.lang.Boolean'
public void run(ApplicationArguments args) throws Exception {
String[] jobArguments = args.getNonOptionArgs().toArray(new String[0]);
run(jobArguments);
}
public void run(String... args) throws JobExecutionException {
// "="를 delimiter로 사용해 key-value Properties로 변환
launchJobFromProperties(StringUtils.splitArrayElementsIntoProperties(args, "="));
}
run() 메서드를 호출getNonOptionArgs()로 잡 파라미터만 추출 (예: chaos=true,java.lang.Boolean)"=" 기준으로 Properties로 변환: { "chaos": "true,java.lang.Boolean" }Properties를 실제 JobParameters로 변환// getJobParameters()에서 각 Properties 엔트리를 JobParameter로 변환
for (Entry<Object, Object> entry : properties.entrySet()) {
String parameterName = (String) entry.getKey(); // "chaos"
String encodedJobParameter = (String) entry.getValue(); // "true,java.lang.Boolean"
JobParameter<?> jobParameter = decode(encodedJobParameter); // Boolean 타입의 true로 변환
jobParametersBuilder.addJobParameter(parameterName, jobParameter);
}
return jobParametersBuilder.toJobParameters();
decode() 메서드가 "true,java.lang.Boolean" 문자열을 분석하여 Boolean 타입의 true 값으로 변환이렇게 변환된 JobParameter는 JobParametersBuilder에 전달된다.
JobParameters가 준비되면 두 가지 경로로 Job을 실행한다.protected void launchJobFromProperties(Properties properties) throws JobExecutionException {
JobParameters jobParameters = this.converter.getJobParameters(properties);
executeLocalJobs(jobParameters);
executeRegisteredJobs(jobParameters);
}
@Configuration에서 빈으로 등록한 Job들 중 지정된 이름과 일치하는 Job만 실행private void executeLocalJobs(JobParameters jobParameters) throws JobExecutionException {
for (Job job : this.jobs) {
if (StringUtils.hasText(this.jobName)) {
if (!this.jobName.equals(job.getName())) {
continue; // 이름이 일치하지 않으면 건너뜀
}
}
execute(job, jobParameters);
}
}
jobs 변수에는 @Autowired를 통해 애플리케이션 컨텍스트의 모든 Job 타입 빈이 자동 주입// JobLauncherApplicationRunner
// ...
private Collection<Job> jobs = Collections.emptySet();
@Autowired(required = false)
public void setJobs(Collection<Job> jobs) {
this.jobs = jobs;
}
jobName 변수에는 --spring.batch.job.name=brutalizedSystemJob으로 전달한 실행할 Job의 이름이 설정된다.JobRegistry에 등록된 Job 중 로컬에 없는 Job을 찾아 실행JobRegistry는 Job 전용 저장소
MapJobRegistry(in-memory 구현체)가 사용됨private void executeRegisteredJobs(JobParameters jobParameters) throws JobExecutionException {
if (this.jobRegistry != null && StringUtils.hasText(this.jobName)) {
if (!isLocalJob(this.jobName)) { // 로컬 Job이 아닌 경우에만 실행
Job job = this.jobRegistry.getJob(this.jobName);
execute(job, jobParameters);
}
}
}
isLocalJob() 검사로 로컬 컨텍스트와 JobRegistry 양쪽에 같은 Job이 있어도 중복 실행 방지JobRegistry는 동적으로 Job을 등록해야 할 때 유용하다.
Job 정의를 읽어와 동적으로 등록하는 시나리오execute() 메서드에서 JobLauncher.run()을 호출하기 전에 getNextJobParameters()로 JobParameters를 한 번 더 가공한다.// execute(Job job, JobParameters jobParameters)
JobParameters parameters = getNextJobParameters(job, jobParameters);
JobExecution execution = this.jobLauncher.run(job, parameters);
// ...
private JobParameters getNextJobParameters(Job job, JobParameters jobParameters) {
if (jobRepository.isJobInstanceExists(job.getName(), jobParameters)) {
return getNextJobParametersForExisting(job, jobParameters); // 1) 이미 존재하는 JobInstance
}
if (job.getJobParametersIncrementer() == null) { // 2) Incrementer 없음
return jobParameters;
}
// 3) Incrementer 있음
JobParameters nextParameters = new JobParametersBuilder(jobParameters, this.jobExplorer)
.getNextJobParameters(job).toJobParameters();
return merge(nextParameters, jobParameters);
}
1) 동일한 JobInstance가 이미 존재하는 경우
JobExecution이 중단/실패이고 restartable인 경우, 이전 identifying 파라미터와 새 파라미터를 병합(merge)merge() 시 동일한 키는 새 파라미터 값으로 덮어쓰므로, 재시작 시 non-identifying 파라미터를 변경할 수 있다.2) JobParametersIncrementer가 없는 경우
JobParameters를 그대로 반환3) JobParametersIncrementer가 설정된 경우
RunIdIncrementer 등을 통해 이미 완료된 JobInstance를 동일 파라미터로 재실행할 수 있게 해준다.JobExplorer(메타데이터 저장소 읽기 전용 조회 컴포넌트)를 사용해 마지막 JobInstance의 실행 이력을 조회JobParameters를 JobParametersIncrementer.getNext()에 전달하여 증분된 파라미터를 생성한 후 기존 파라미터와 병합JobExplorer는 메타데이터 조회에 특화된 컴포넌트JobRepository는 저장/업데이트/삭제까지 담당하지만, JobExplorer는 오직 조회에만 집중하여 더 풍부한 조회 기능을 제공public interface JobExplorer {
List<JobInstance> getJobInstances(String jobName, int start, int count);
JobInstance getLastJobInstance(String jobName);
JobExecution getJobExecution(Long executionId);
JobExecution getLastJobExecution(JobInstance jobInstance);
Set<JobExecution> findRunningJobExecutions(String jobName);
List<String> getJobNames();
long getJobInstanceCount(String jobName);
/*...*/
}
jobExplorer.getLastJobInstance(name)으로 마지막 JobInstance를 조회한 후, 그 실행 이력의 JobParameters를 JobParametersIncrementer.getNext()에 전달한다.RunIdIncrementer는 run.id 파라미터 값을 1씩 증가시켜 매번 새로운 JobInstance 생성을 보장JobParameters가 결정되면 JobLauncher.run()을 호출하여 Job을 실행protected void execute(Job job, JobParameters jobParameters) {
JobParameters parameters = getNextJobParameters(job, jobParameters);
JobExecution execution = this.jobLauncher.run(job, parameters); // Job Squad 진입점
}
JobLauncherApplicationRunner.run()에서 시작하여 JobLauncher.run() 호출까지의 흐름이 완료된다.BatchAutoConfiguration을 자동으로 활성화시킨다.@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.batch.job", name = "enabled", havingValue = "true", matchIfMissing = true)
public JobLauncherApplicationRunner jobLauncherApplicationRunner(JobLauncher jobLauncher, JobExplorer jobExplorer,
JobRepository jobRepository, BatchProperties properties) {
JobLauncherApplicationRunner runner = new JobLauncherApplicationRunner(jobLauncher, jobExplorer, jobRepository);
String jobName = properties.getJob().getName();
if (StringUtils.hasText(jobName)) {
runner.setJobName(jobName);
}
return runner;
}
@ConditionalOnProperty(prefix = "spring.batch.job", name = "enabled", havingValue = "true", matchIfMissing = true)
spring.batch.job.enabled 프로퍼티가 true인 경우 JobLauncherApplicationRunner 빈을 자동 등록matchIfMissing = true 옵션으로 인해 별도로 프로퍼티를 설정하지 않아도 기본값 true가 적용JobLauncher, JobExplorer, JobRepository)를 전달properties.getJob().getName()으로 jobName을 추출해 setJobName()에 설정
--spring.batch.job.name=brutalizedSystemJob으로 전달한 실행할 Job의 이름이 여기서 설정된다.BatchAutoConfiguration 선언부에 @EnableConfigurationProperties(BatchProperties.class)가 선언되어 있다.
application.properties, application.yml, 커맨드 라인 파라미터로 전달된 설정값을 자동으로 BatchProperties 객체에 매핑@ConfigurationProperties(prefix = "spring.batch")
public class BatchProperties {
private final Job job = new Job(); // spring.batch.job.*
public static class Job {
private String name = ""; // spring.batch.job.name
}
/*...*/
}
@ConfigurationProperties(prefix = "spring.batch")로 spring.batch 접두사를 가진 설정값이 이 클래스에 매핑BatchAutoConfiguration이 JobLauncherApplicationRunner를 생성할 때 properties.getJob().getName()으로 이 값을 가져와 설정@Bean
@ConditionalOnMissingBean(ExitCodeGenerator.class)
public JobExecutionExitCodeGenerator jobExecutionExitCodeGenerator() {
return new JobExecutionExitCodeGenerator();
}
BatchAutoConfiguration 내부에 DefaultBatchConfiguration을 상속하는 SpringBootBatchConfiguration 클래스가 존재@Configuration(proxyBeanMethods = false)
static class SpringBootBatchConfiguration extends DefaultBatchConfiguration {
/*...*/
}
DefaultBatchConfiguration은 Spring Batch의 모든 핵심 인프라 컴포넌트를 생성하고 구성하는 역할JobRepository, JobExplorer, JobLauncher, JobRegistry, JobOperator, JobRegistryBeanPostProcessorStepScope, JobScope@Configuration(proxyBeanMethods = false)
@Import(ScopeConfiguration.class)
public class DefaultBatchConfiguration implements ApplicationContextAware {
@Bean
public JobRepository jobRepository() throws BatchConfigurationException { /*...*/ }
@Bean
public JobLauncher jobLauncher(JobRepository jobRepository) throws BatchConfigurationException { /*...*/ }
@Bean
public JobExplorer jobExplorer() throws BatchConfigurationException { /*...*/ }
@Bean
public JobRegistry jobRegistry() throws BatchConfigurationException { /*...*/ }
@Bean
public JobRegistrySmartInitializingSingleton jobRegistrySmartInitializingSingleton(JobRegistry jobRegistry) throws BatchConfigurationException { /*...*/ }
}
protected로 선언되어 있어 상속을 통한 커스터마이징이 가능하다.protected DataSource getDataSource() { /*...*/ }
protected PlatformTransactionManager getTransactionManager() { /*...*/ }
protected String getTablePrefix() { /*...*/ }
protected TaskExecutor getTaskExecutor() { /*...*/ }
DefaultBatchConfiguration을 상속한 @Configuration 클래스를 만들고 필요한 protected 메서드만 오버라이드하면 된다.@Configuration
public class KillBatchCustomConfiguration extends DefaultBatchConfiguration {
@Override
protected ExecutionContextSerializer getExecutionContextSerializer() {
return new Jackson2ExecutionContextStringSerializer(); // 기본 직렬화 대신 JSON 직렬화
}
}
DefaultBatchConfiguration 상속 없이 배치 코어 컴포넌트를 자동 구성할 수 있는 어노테이션// DefaultBatchConfiguration 상속 방식
@Configuration
public class BatchConfig extends DefaultBatchConfiguration { /*...*/ }
// @EnableBatchProcessing 방식
@Configuration
@EnableBatchProcessing
public class BatchConfig { /*...*/ }
@EnableBatchProcessing 내부를 살펴보면 ~Ref로 끝나는 속성들이 정의되어 있다.
@Import({ BatchRegistrar.class, ScopeConfiguration.class /*...*/ })
public @interface EnableBatchProcessing {
String dataSourceRef() default "dataSource";
String transactionManagerRef() default "transactionManager";
String executionContextSerializerRef() default "executionContextSerializer";
/*...*/
}
@Import로 가져오는 BatchRegistrar
@EnableBatchProcessing 어노테이션 정보를 읽어 배치 코어 컴포넌트 빈들을 동적으로 등록class BatchRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
EnableBatchProcessing batchAnnotation = importingClassMetadata.getAnnotations()
.get(EnableBatchProcessing.class)
.synthesize();
registerJobRepository(registry, batchAnnotation);
registerJobExplorer(registry, batchAnnotation);
registerJobLauncher(registry, batchAnnotation);
registerJobRegistry(registry);
/*...*/
}
}
register~ 메서드는 @EnableBatchProcessing의 ~Ref 속성을 사용해 빈을 동적으로 구성하고 등록DefaultBatchConfiguration과 다르다.
DefaultBatchConfiguration: protected 메서드 오버라이드@EnableBatchProcessing: 커스텀 빈을 정의하고 어노테이션 속성에 빈 이름을 지정~Ref 속성에는 기본값이 있으므로 기본 이름과 동일한 빈을 등록하면 속성 지정 없이도 커스터마이징 가능@Configuration
@EnableBatchProcessing(executionContextSerializerRef = "jacksonExecutionContextSerializer")
public class BatchConfig {
@Bean
public ExecutionContextSerializer jacksonExecutionContextSerializer() {
return new Jackson2ExecutionContextStringSerializer();
}
}
DefaultBatchConfiguration과 @EnableBatchProcessing 모두 ScopeConfiguration을 @Import한다.DefaultBatchConfiguration과 @EnableBatchProcessing이 공통으로 @Import하는 클래스StepScope와 JobScope 빈을 등록한다.@Configuration(proxyBeanMethods = false)
public class ScopeConfiguration {
private static final StepScope stepScope;
private static final JobScope jobScope;
static {
jobScope = new JobScope();
jobScope.setAutoProxy(false);
stepScope = new StepScope();
stepScope.setAutoProxy(false);
}
@Bean
public static StepScope stepScope() { return stepScope; }
@Bean
public static JobScope jobScope() { return jobScope; }
}
BatchScopeSupport의 postProcessBeanFactory()에서 수행
beanFactory.registerScope(name, this)JobScope/StepScope의 get() → getContext()가 호출된다.private StepContext getContext() {
StepContext context = StepSynchronizationManager.getContext();
if (context == null) {
throw new IllegalStateException("No context holder available for step scope");
}
return context;
}
StepSynchronizationManager.getContext()는 AbstractStep.execute()에서 register()로 등록했던 StepContext를 반환한다.
register 시점에 현재 스레드의 ThreadLocal에 StepExecution과 StepContext가 바인딩StepSynchronizationManager(JobSynchronizationManager)에서 유효한 컨텍스트를 가져올 수 있는 상태를 의미한다.
StepContext가 null → 아직 Step이 실행되지 않은 상태 → 예외 발생StepContext가 null이 아님 → Step이 실행 중이며 StepScope가 활성화된 상태StepContext는 단순한 스코프 활성화 지표가 아니라 런타임 배치 정보를 담고 있다.public Map<String, Object> getStepExecutionContext() { /*...*/ }
public Map<String, Object> getJobExecutionContext() { /*...*/ }
public Map<String, Object> getJobParameters() { /*...*/ }
StepContext 메서드의 매핑 관계:| SpEL 표현식 | StepContext 메서드 |
|---|---|
#{jobParameters} |
getJobParameters() |
#{stepExecutionContext} |
getStepExecutionContext() |
#{jobExecutionContext} |
getJobExecutionContext() |
StepScope.resolveContextualObject()가 이 연결을 담당한다.@Override
public Object resolveContextualObject(String key) {
StepContext context = getContext();
return new BeanWrapperImpl(context).getPropertyValue(key);
}
#{jobParameters['chaos']}의 실행 흐름:
resolveContextualObject("jobParameters") 호출StepContext를 확보하고 BeanWrapperImpl으로 프로퍼티 접근stepContext.getJobParameters()가 호출되어 Map<String, Object> 반환Map에서 ['chaos'] 키로 값 추출StepContext에는 이 외에도 getStepName(), getJobName(), getJobInstanceId(), getSystemProperties() 등이 정의되어 있다.@Value("#{stepName}") String stepName,
@Value("#{jobName}") String jobName,
@Value("#{jobInstanceId}") Long jobInstanceId,
@Value("#{systemProperties['os.name']}") String osName
@JobScope빈에서는stepName,jobInstanceId를 조회할 수 없다.JobScope는StepContext가 아닌JobContext를 사용하기 때문이다.
@ConditionalOnMissingBean(value = DefaultBatchConfiguration.class, annotation = EnableBatchProcessing.class)
public class BatchAutoConfiguration
BatchAutoConfiguration이 비활성화된다.
DefaultBatchConfiguration을 상속한 커스텀 컨피규레이션 클래스를 등록하는 경우@EnableBatchProcessing 어노테이션을 사용하는 경우BatchProperties, JobLauncherApplicationRunner, BatchDataSourceScriptDatabaseInitializer 등SpringBootBatchConfiguration(
DataSource dataSource,
@BatchDataSource ObjectProvider<DataSource> batchDataSource,
PlatformTransactionManager transactionManager,
@BatchTransactionManager ObjectProvider<PlatformTransactionManager> batchTransactionManager,
@BatchTaskExecutor ObjectProvider<TaskExecutor> batchTaskExecutor, BatchProperties properties,
ObjectProvider<BatchConversionServiceCustomizer> batchConversionServiceCustomizers,
ObjectProvider<ExecutionContextSerializer> executionContextSerializer) {
// ...
}
BatchAutoConfiguration은 DefaultBatchConfiguration을 상속한 SpringBootBatchConfiguration을 자동으로 빈으로 등록한다.
JobRepository, JobLauncher, JobExplorer 등을 사용할 수 있었던 이유DefaultBatchConfiguration의 protected 메서드들을 오버라이드
DefaultBatchConfiguration을 직접 상속하지 않아도 원하는 부분만 커스터마이징 가능// DefaultBatchConfiguration 직접 상속 없이 빈만 정의하면 자동 적용
@Configuration
public class KillBatchCustomConfiguration {
@Bean
public ExecutionContextSerializer executionContextSerializer() {
return new Jackson2ExecutionContextStringSerializer();
}
}
ExecutionContextSerializer 빈만 정의해도 SpringBootBatchConfiguration에 의해 자동으로 적용된다.BatchAutoConfiguration의 다른 자동 구성 기능(JobLauncherApplicationRunner 등)도 그대로 유지된다.@BatchDataSource, @BatchTransactionManager, @BatchTaskExecutor는 한정자(Qualifier) 역할
@Bean
@BatchTaskExecutor
public TaskExecutor taskExecutor() {
return new SimpleAsyncTaskExecutor();
}
@BatchTaskExecutor를 선언하면 애플리케이션 내에 여러 TaskExecutor가 있더라도 해당 빈만 TaskExecutorJobLauncher에서 사용된다.별도 구성이 없을 경우
TaskExecutorJobLauncher는SyncTaskExecutor(동기식)를 사용한다.
TransactionManager를 별도로 구성해야 한다.@Configuration
public class TransactionManagerConfig {
@Bean
@Primary
public DataSource dataSource() { /*...*/ } // 비즈니스 데이터용
@Bean
@BatchDataSource
public DataSource batchDataSource() { /*...*/ } // 배치 메타데이터용
@Bean
@Primary
public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
return new JpaTransactionManager(emf); // 비즈니스 데이터용
}
@Bean
@BatchTransactionManager
public PlatformTransactionManager batchTransactionManager(@BatchDataSource DataSource dataSource) {
return new JdbcTransactionManager(dataSource); // 배치 메타데이터용
}
}
main() 메서드로는 Job이 실패해도 종료 코드가 항상 0이 반환된다.// 기본 - Job 실패해도 exit code 0
public static void main(String[] args) {
SpringApplication.run(KillBatchSystemApplication.class, args);
}
// 수정 - Job 실패 시 적절한 exit code 반환
public static void main(String[] args) {
System.exit(SpringApplication.exit(SpringApplication.run(KillBatchSystemApplication.class, args)));
}
SpringApplication.exit()는 애플리케이션 컨텍스트를 종료하면서 Job 실행 결과에 따른 종료 코드를 반환System.exit()를 통해 운영체제에 정확한 실행 결과를 전달
SpringApplication.exit()는 내부적으로 ExitCodeGenerator 빈들을 조회하여 종료 코드를 결정한다.BatchAutoConfiguration이 자동 등록하는 JobExecutionExitCodeGenerator가 이 역할을 담당@Override
public int getExitCode() {
for (JobExecution execution : this.executions) {
if (execution.getStatus().ordinal() > 0) {
return execution.getStatus().ordinal();
}
}
return 0;
}
JobExecution의 BatchStatus enum의 ordinal 값이 종료 코드로 사용된다.| BatchStatus | ordinal (종료 코드) |
|---|---|
COMPLETED |
0 |
STARTING |
1 |
STARTED |
2 |
STOPPING |
3 |
STOPPED |
4 |
FAILED |
5 |
ABANDONED |
6 |
UNKNOWN |
7 |
BatchStatus는 enum이라 확장할 수 없다.
ExitStatus를 활용할 수 있다.ExitStatus는 enum인 BatchStatus와 달리 문자열 기반이므로 자유롭게 정의하고 확장할 수 있다."SECURITY_BREACH", "DATA_INTEGRITY_VIOLATION", "DISK_FULL" 같은 커스텀 종료 상태를 정의 가능Tasklet에서 커스텀 ExitStatus 설정
StepContribution을 통해 사용자 정의 ExitStatus를 전달// Tasklet 내에서 예외 타입별로 커스텀 ExitStatus 설정
catch (IllegalStateException e) {
contribution.setExitStatus(new ExitStatus("SKULL_FRACTURE", e.getMessage()));
throw e;
} catch (ValidationException e) {
contribution.setExitStatus(new ExitStatus("SYSTEM_BRUTALIZED", e.getMessage()));
throw e;
}
StepContribution에 설정한 ExitStatus는 stepExecution.apply(contribution)을 통해 StepExecution에 반영커스텀 ExitCodeGenerator 구현
ExitCodeGenerator와 JobExecutionListener를 함께 구현하여 ExitStatus를 종료 코드로 변환@Component
public class BrutalizedSystemExitCodeGenerator implements JobExecutionListener, ExitCodeGenerator {
private final SimpleJvmExitCodeMapper exitCodeMapper = new SimpleJvmExitCodeMapper();
private int exitCode = 0;
public BrutalizedSystemExitCodeGenerator() {
exitCodeMapper.setMapping(Map.of(
"SKULL_FRACTURE", 3,
"SYSTEM_BRUTALIZED", 4,
"UNKNOWN_CHAOS", 5));
}
@Override
public void afterJob(JobExecution jobExecution) {
String exitStatus = jobExecution.getExitStatus().getExitCode();
this.exitCode = exitCodeMapper.intValue(exitStatus);
}
@Override
public int getExitCode() { return exitCode; }
}
SimpleJvmExitCodeMapper: 문자열 ExitStatus를 정수형 종료 코드로 매핑하는 유틸리티
COMPLETED → 0, FAILED → 1 매핑이 등록되어 있음setMapping()으로 커스텀 매핑 추가 가능afterJob()에서 JobExecution의 ExitStatus를 종료 코드로 변환@Component로 빈 등록 시 @ConditionalOnMissingBean 조건에 의해 기존 JobExecutionExitCodeGenerator 대신 사용됨Job에 리스너로 등록 필요@Bean
public Job brutalizedSystemJob() {
return new JobBuilder("brutalizedSystemJob", jobRepository)
.start(brutalizedSystemStep())
.listener(brutalizedSystemExitCodeGenerator) // 리스너 등록
.build();
}