Spring Batch 입문 6편 — Configuring a Job (JobBuilder · Validator · Listener)

2026-05-17Spring Batch 입문에서 운영까지

Spring Batch 입문 6편. Configuring a Job — JobBuilder 의 모든 것. Step 연결·flow·restartable·Validator·Incrementer·JobExecutionListener·meta-data·운영 권장 패턴까지 풀어쓴 학습 노트.

📚 Spring Batch 입문에서 운영까지 · 6편 — Configuring a Job (JobBuilder · Validator · Listener)

이 글은 Spring Batch 입문에서 운영까지 시리즈 48편 중 6편이에요. 5편 에서 infrastructure 자동 구성 을 잡았다면, 이번 6편은 실제 Job 을 어떻게 정의하나JobBuilder 의 모든 것.

JobBuilder 의 핵심

@Bean
public Job myJob(JobRepository repo, Step step1, Step step2) {
    return new JobBuilder("myJob", repo)
        .start(step1)
        .next(step2)
        .build();
}

3편에서 본 Job 의 골격 이에요. JobBuilder 가 이 모든 옵션 을 제공해요.

기본 옵션

1. Step 연결 — start·next

.start(stepA)        // 첫 Step
.next(stepB)         // 그 다음
.next(stepC)
.next(stepD)

위에서 아래로 차례대로 실행돼요.

2. 조건부 flow

.start(stepA)
.on("COMPLETED").to(stepB)
.from(stepA).on("FAILED").to(errorStep)
.from(stepB).on("*").end()
.build();

각 step 의 ExitStatus (Step 종료 상태 코드) 에 따라 분기돼요. 20편 Controlling Step Flow 에서 더 깊이 다뤄요.

3. Restartable 설정

new JobBuilder("myJob", repo)
    .preventRestart()
    .start(step)
    .build();

preventRestart() 를 박으면 실패해도 재시작이 막혀요. 어떤 Job 은 idempotency (같은 작업 두 번 실행해도 결과 동일한 성질) 가 깨질 수 있어 명시적으로 차단하는 거예요.

기본 = restartable=true.

Validator — JobParameter 검증

public class MyJobParametersValidator implements JobParametersValidator {
    @Override
    public void validate(JobParameters parameters) throws JobParametersInvalidException {
        if (parameters.getString("targetDate") == null) {
            throw new JobParametersInvalidException("targetDate is required");
        }
        // ...
    }
}
new JobBuilder("myJob", repo)
    .validator(new MyJobParametersValidator())
    .start(step)
    .build();

Job 이 시작되기 전에 필수 파라미터를 검증해요. 잘못 박힌 파라미터가 있으면 즉시 실패해요.

DefaultJobParametersValidator 활용

DefaultJobParametersValidator validator = new DefaultJobParametersValidator(
    new String[] {"targetDate", "outputPath"},   // required
    new String[] {"limit", "debug"}              // optional
);

new JobBuilder(...)
    .validator(validator)
    .start(step)
    .build();

코드를 따로 짜지 않아도 필수·옵션 파라미터를 명시 할 수 있어요.

Incrementer — 매번 새 JobParameters

3편의 함정 — 같은 JobParameters 두 번 실행 시 두 번째 skip. 매 실행마다 자동으로 다른 parameter 가 들어가게 해줘요:

new JobBuilder("myJob", repo)
    .incrementer(new RunIdIncrementer())
    .start(step)
    .build();

RunIdIncrementer = 매 실행마다 run.id parameter +1. 항상 새 JobInstance 가 만들어져요.

또는 JobLauncher.run() 시 직접 추가:

JobParameters parameters = new JobParametersBuilder()
    .addLong("run.id", System.currentTimeMillis())
    .toJobParameters();

Custom Incrementer

public class DateIncrementer implements JobParametersIncrementer {
    @Override
    public JobParameters getNext(JobParameters parameters) {
        String last = parameters.getString("targetDate");
        LocalDate next = (last == null ? LocalDate.now() : LocalDate.parse(last)).plusDays(1);
        return new JobParametersBuilder(parameters)
            .addString("targetDate", next.toString())
            .toJobParameters();
    }
}

날짜 자동 +1 같은 도메인 incrementer 예요.

JobExecutionListener — Job 라이프사이클 hook

public class MyJobListener implements JobExecutionListener {
    @Override
    public void beforeJob(JobExecution jobExecution) {
        log.info("Job starting: {}", jobExecution.getJobInstance().getJobName());
        // 알림·통계 초기화 등
    }

    @Override
    public void afterJob(JobExecution jobExecution) {
        if (jobExecution.getStatus() == BatchStatus.COMPLETED) {
            log.info("Job completed");
            sendSuccessNotification(jobExecution);
        } else {
            log.error("Job failed");
            sendFailureAlert(jobExecution);
        }
    }
}
new JobBuilder(...)
    .listener(new MyJobListener())
    .start(step)
    .build();

자주 쓰는 자리:

  • before = 통계 초기화·외부 시스템 lock·시작 알림
  • after = 결과 통지·외부 시스템 unlock·정리

listener 는 여러 개 박을 수 있어요 (.listener(a).listener(b)).

Annotation 기반 Listener (대안)

@Component
public class MyJobListener {
    @BeforeJob
    public void before(JobExecution execution) { ... }

    @AfterJob
    public void after(JobExecution execution) { ... }
}

annotation 만으로 동일하게 동작해요. Bean 자동 등록.

자주 쓰는 Pattern — Job Configuration

패턴 1: Simple Linear Job

@Bean
public Job dailyReportJob(JobRepository repo,
                          Step extractStep, Step transformStep, Step loadStep,
                          JobExecutionListener listener) {
    return new JobBuilder("dailyReportJob", repo)
        .incrementer(new RunIdIncrementer())
        .listener(listener)
        .validator(new DefaultJobParametersValidator(
            new String[]{"targetDate"}, new String[]{}))
        .start(extractStep)
        .next(transformStep)
        .next(loadStep)
        .build();
}

ETL (추출·변환·적재 데이터 흐름) 의 표준 형태예요.

패턴 2: Conditional Flow

@Bean
public Job dataLoadJob(JobRepository repo,
                       Step downloadStep, Step processStep, Step retryDownloadStep,
                       Step notifyFailureStep) {
    return new JobBuilder("dataLoadJob", repo)
        .start(downloadStep)
        .on("COMPLETED").to(processStep)
        .from(downloadStep).on("RETRY").to(retryDownloadStep)
        .next(processStep)
        .from(downloadStep).on("FAILED").to(notifyFailureStep)
        .end()
        .build();
}

다음 step 이 이전 결과 에 따라 갈라져요.

패턴 3: Split (병렬)

@Bean
public Job parallelJob(JobRepository repo, Step step1, Step step2, Step step3, Step mergeStep) {
    Flow flow1 = new FlowBuilder<SimpleFlow>("flow1").start(step1).build();
    Flow flow2 = new FlowBuilder<SimpleFlow>("flow2").start(step2).build();
    Flow flow3 = new FlowBuilder<SimpleFlow>("flow3").start(step3).build();

    return new JobBuilder("parallelJob", repo)
        .start(flow1).split(new SimpleAsyncTaskExecutor()).add(flow2, flow3)
        .next(mergeStep)
        .end()
        .build();
}

여러 Step 을 동시에 돌려놓고 다 끝나면 다음으로 넘어가요. 37편 scaling 에서 더 깊이 다뤄요.

Meta-Data 활용

JobExecution 의 모든 metadata = JobRepository (DB) 에 영속. 직접 조회:

@Autowired
private JobExplorer jobExplorer;

public List<JobInstance> getRecentInstances(String jobName, int count) {
    return jobExplorer.findJobInstancesByJobName(jobName, 0, count);
}

public List<JobExecution> getExecutions(JobInstance instance) {
    return jobExplorer.getJobExecutions(instance);
}

10편 Advanced Metadata Usage 에서 더 깊이 다뤄요.

Job 의 ExitStatus

Job 이 끝나면 ExitStatus 가 찍혀요:

new JobBuilder(...)
    .listener(new JobExecutionListener() {
        @Override
        public void afterJob(JobExecution execution) {
            execution.setExitStatus(new ExitStatus("CUSTOM_EXIT_CODE", "Description"));
        }
    })
    .build();

기본은 COMPLETED·FAILED 같은 표준 값이에요. Custom ExitStatus 를 박으면 외부 시스템 (scheduler 등) 과 통신 할 수 있어요.

자주 헷갈리는 자리

Job 은 thread-safe 한가

Job bean 은 stateless·thread-safe. 여러 JobExecution 이 동일 Job bean 으로 동시 실행 가능. 단 같은 JobInstance 의 동시 실행 은 X (3편).

JobBuilder 의 chaining 순서

.start(step)
.next(...)
.listener(...)        // listener 는 chain 어디든
.incrementer(...)
.validator(...)
.build();

대부분 순서 무관. 단 .start() 가 먼저 호출 권장 (graph 의 시작점).

Restart 동작

preventRestart() 안 박으면 기본 restartable. 실패한 JobExecution = 같은 JobInstance 의 새 JobExecution 으로 재시작 가능.

jobOperator.restart(failedExecutionId);

restartable=false 면 RestartException.

운영 권장 — Job 정의 체크리스트

  • [ ] incrementer 박기 (매 실행 다른 parameter) 또는 명시 timestamp
  • [ ] validator 박기 (필수 parameter 검증)
  • [ ] listener 박기 (시작·종료 알림)
  • [ ] Step 들에 의미 있는 이름 (운영 로그에 표시)
  • [ ] Conditional flow 가 너무 복잡하면 별도 Job 으로 분리
  • [ ] preventRestart()진짜 필요한 자리만 (대부분 restartable 권장)

한계·실무 함정

1. Incrementer 없이 같은 parameter

RunIdIncrementer 없으면 같은 parameter 두 번째 실행 skip. CI/CD·운영자 혼란.

2. Validator 누락

필수 parameter 없이 Job 실행 = Step 중간에 NullPointerException. validator 로 시작 전 차단.

3. Listener 안 예외

afterJob 안에서 알림 발송 같은 외부 호출 실패 = Job 자체 영향. try-catch + 알림 실패 별도 로그.

4. 너무 복잡한 Flow

.from()·.on()·.to() 가 5개+ 이상 = 가독성 폭망. 별도 Job 분리 + Scheduler 가 chaining.

5. Step 재사용 시 이름 충돌

여러 Job 이 같은 Step bean 공유 = 같은 이름. Step 의 이름이 unique* 해야 JobRepository 추적 정확. 단, 같은 step 을 다른 Job 에서 재사용은 OK (logical step).

시험 직전 한 번 더 — Configuring a Job 함정 압축 노트

  • JobBuilder 기본 = new JobBuilder(name, repo).start(step).next(step).build()
  • .start() = 첫 Step
  • .next() = 순차 다음 Step
  • .on(status).to(step) = 조건부 flow (20편)
  • .from(step).on(status).to(step) = 다른 시작점 분기
  • .split(executor).add(flow, ...) = 병렬
  • .preventRestart() = 재시작 차단 (idempotent X 환경)
  • 기본 restartable = true
  • Validator = JobParametersValidator 구현, validate() 안 예외
  • DefaultJobParametersValidator(requiredKeys, optionalKeys) = 손쉬운 검증
  • Incrementer = JobParametersIncrementer.getNext(prev)
  • RunIdIncrementer = run.id 자동 +1
  • Custom Incrementer = DateIncrementer 같이 도메인 incrementer
  • JobExecutionListener = beforeJob·afterJob
  • 자주 쓰는 자리 — 통계·외부 lock·알림
  • Annotation = @BeforeJob·@AfterJob (대안)
  • Job bean = stateless·thread-safe — 여러 JobExecution 동시 실행 OK (단 같은 JobInstance X)
  • 운영 권장 — incrementer + validator + listener + 의미 있는 step 이름 + flow 단순화
  • 함정 — Incrementer 누락 = 같은 parameter skip
  • 함정 — Validator 누락 = 중간 NPE
  • 함정 — Listener 안 예외 = Job 영향, try-catch
  • 함정 — 너무 복잡한 flow = 가독성 폭망 (분리 + Scheduler)
  • 함정 — Step 이름 unique 권장
  • ExitStatus = COMPLETED·FAILED 기본, custom 도 가능

공식 문서: Configuring a Job 에서 원문을 확인할 수 있어요.

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!