Spring Batch 입문 9편 — Running a Job (JobLauncher · Sync/Async · Scheduler)

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

Spring Batch 입문 9편. Running a Job — JobLauncher 와 JobOperator 의 관계, 동기 vs 비동기 실행, JobParameters 의 valid type, Spring @Scheduled 통합, CommandLineJobRunner·HTTP launch·자주 만나는 함정까지 풀어쓴 학습 노트. Part 2 마무리 직전.

📚 Spring Batch 입문에서 운영까지 · 9편 — Running a Job (JobLauncher · Sync/Async · Scheduler)

이 글은 Spring Batch 입문에서 운영까지 시리즈 48편 중 9편이에요. 8편 까지 JobOperator 를 잡았다면, 이번 9편은 실제 실행 시 자주 마주치는 영역Running a Job.

JobLauncher vs JobOperator

JobLauncher    = 실제 Job 실행 엔진 (저수준)
JobOperator    = JobLauncher 위 통합 인터페이스 (고수준)

대부분의 환경에서는 JobOperator 만 사용하면 되고, JobLauncher 를 직접 다룰 일은 거의 없어요. 다만 sync/async 를 세밀하게 제어해야 할 때만 JobLauncher 를 직접 호출합니다.

JobLauncher 인터페이스

public interface JobLauncher {
    JobExecution run(Job job, JobParameters jobParameters)
        throws JobExecutionAlreadyRunningException,
               JobRestartException,
               JobInstanceAlreadyCompleteException,
               JobParametersInvalidException;
}

메서드는 run(Job, JobParameters) 단 하나입니다. JobExecution(Job 1회 실행 단위) 을 반환하고, JobOperator 의 start() 도 내부적으로 이 메서드를 호출합니다.

동기 vs 비동기 실행

동기 (기본)

@Bean
public JobLauncher syncJobLauncher(JobRepository repo) {
    TaskExecutorJobLauncher launcher = new TaskExecutorJobLauncher();
    launcher.setJobRepository(repo);
    launcher.setTaskExecutor(new SyncTaskExecutor());   // 동기
    launcher.afterPropertiesSet();
    return launcher;
}

SyncTaskExecutor(현재 thread 에서 즉시 실행) 를 쓰면 호출자가 Job 이 끝날 때까지 대기하고, run() 이 리턴되는 시점이 곧 Job 완료 시점입니다.

비동기

@Bean
public JobLauncher asyncJobLauncher(JobRepository repo) {
    TaskExecutorJobLauncher launcher = new TaskExecutorJobLauncher();
    launcher.setJobRepository(repo);
    launcher.setTaskExecutor(new SimpleAsyncTaskExecutor());   // 비동기
    launcher.afterPropertiesSet();
    return launcher;
}

SimpleAsyncTaskExecutor(요청마다 새 thread 띄움) 로 바꾸면 호출자에게 JobExecution 객체만 즉시 돌려주고, Job 은 별도 thread 에서 돌아갑니다.

Spring Boot 기본 = 동기

Spring Boot 의 자동 구성은 SyncTaskExecutor 를 씁니다. 그래서 Job 이 끝날 때까지 main thread 가 블로킹돼요.

여기서 시험 함정이 하나 있어요 — spring.batch.job.enabled=true (자동 실행) 에 동기 launcher 를 그대로 두면 애플리케이션이 Job 이 끝날 때까지 startup 자체를 기다립니다. CLI 환경에서는 자연스럽지만 REST API 서버 환경에서는 서버 시작이 그만큼 늦어지니, 이런 경우에는 비동기 launcher 를 권장합니다.

JobParameters — 모든 detail

JobInstance(같은 Job 의 논리적 실행 단위) 를 식별하는 핵심이니까 type 을 정확히 명시해야 해요.

Type 종류

JobParameters parameters = new JobParametersBuilder()
    .addString("name", "Alice")
    .addLong("age", 30L)
    .addDouble("score", 99.5)
    .addDate("createdAt", new Date())                     // java.util.Date
    .addLocalDate("targetDate", LocalDate.now())          // v5+
    .addLocalDateTime("now", LocalDateTime.now())         // v5+
    .toJobParameters();

Identifying vs Non-identifying

.addString("targetDate", "2026-05-17", true)      // identifying (기본)
.addString("runId", "abc-123", false)              // non-identifying

Identifying(JobInstance 식별에 쓰이는 표시) 으로 잡힌 파라미터만 JobInstance 식별에 기여합니다. identifying 값이 같으면 같은 JobInstance 로 묶여요.

자주 쓰이는 패턴은 이렇습니다. targetDate 같은 비즈니스 파라미터는 identifying 으로 두고, serverHost·runId 처럼 실행 환경을 표시하지만 식별에는 끼지 않는 런타임 파라미터는 non-identifying 으로 둡니다.

Type 변환 — CLI String → JobParameter

CLI 에서 param=value 형태로 받은 String 을 Long·Date 같은 타입으로 바꾸려면 다음처럼 type hint 문법을 씁니다.

$ java -jar batch.jar param1=value1 param2(long)=123 param3(date)=2026-05-17

CommandLineJobOperator(v6 의 CLI 실행 도구) 의 type hint 문법이고, 명시적으로 변환하고 싶다면 다음처럼 컨버터를 직접 호출해도 됩니다.

JobParameters params = new DefaultJobParametersConverter()
    .getJobParameters(properties);

Spring @Scheduled 통합

Spring Batch 는 Scheduler 가 아니라고 1편에서 강조했지만, 실무에서는 Spring 의 @Scheduled 와 함께 묶어 쓰는 일이 잦아요.

@Component
public class JobScheduler {

    @Autowired
    private JobOperator jobOperator;

    @Autowired
    private Job dailyReportJob;

    @Scheduled(cron = "0 0 2 * * *")        // 매일 새벽 2시
    public void runDailyReport() throws Exception {
        JobParameters params = new JobParametersBuilder()
            .addLocalDate("targetDate", LocalDate.now().minusDays(1))
            .toJobParameters();

        jobOperator.start(dailyReportJob, params);
    }
}

활성화는 @EnableScheduling 을 명시해서 켭니다.

@SpringBootApplication
@EnableScheduling          // 명시
public class MyApp { }

함정 — 비동기 launcher 필수

@Scheduled 는 cron pool thread 에서 호출되는데, 동기 launcher 를 쓰면 그 thread 가 Job 이 끝날 때까지 블로킹돼 다음 cron 까지 막힙니다. 그래서 비동기 launcher 가 사실상 필수예요. 또는 Job 안에서 @Async 메서드를 쓰는 방법도 있습니다.

외부 Scheduler 사용 시

Cron·Kubernetes CronJob·Airflow(파이썬 기반 워크플로 스케줄러)·Control-M(상용 배치 스케줄러) 같은 외부 스케줄러가 애플리케이션을 호출하는 구성이라면, 애플리케이션 자체에는 @Scheduled 가 필요 없습니다. 외부에서 CLI 명령이나 REST 호출로 트리거하면 끝이에요.

HTTP 로 Job 실행

8편에서 본 패턴이지만 자주 헷갈리는 자리니까 다시 한 번 짚어볼게요.

@PostMapping("/jobs/{name}")
public ResponseEntity<?> runJob(@PathVariable String name,
                                 @RequestBody Map<String, String> params) throws Exception {
    Job job = jobRegistry.getJob(name);
    JobParameters jobParameters = convertToJobParameters(params);

    // ★ 비동기 launcher 권장 — HTTP 응답 즉시 return
    Long executionId = jobOperator.start(job, jobParameters);

    return ResponseEntity.accepted().body(Map.of(
        "executionId", executionId,
        "status", "STARTED"
    ));
}

운영 환경에서는 비동기 launcher 로 띄우고 202 Accepted 와 함께 executionId 만 돌려준 다음, 호출자가 나중에 status 를 조회하게 합니다.

CommandLineJobRunner (옛) vs CommandLineJobOperator (v6)

v5 까지는 CommandLineJobRunner 가 표준 도구였지만, v6 부터는 8편에서 본 CommandLineJobOperator 를 권장합니다. v6 에서도 CommandLineJobRunner 는 하위 호환을 위해 그대로 쓸 수 있긴 한데, 신규 코드는 v6 도구로 가는 게 맞아요.

Job 자동 실행 — Spring Boot

spring:
  batch:
    job:
      enabled: true                    # default
      # name: job1,job2                # 특정 Job 만

spring-boot-starter-batch 의존성에 Job bean 만 정의되어 있으면 애플리케이션 시작 시 자동으로 실행됩니다. 운영 환경에서는 enabled: false 로 끄고, CLI 나 REST 로 명시적으로 실행하는 게 안전해요.

Job Parameter 의 함정 5가지

1. 같은 identifying parameters 두 번 → JobInstanceAlreadyComplete

jobOperator.start(myJob, paramA);   // OK
jobOperator.start(myJob, paramA);   // ✗ JobInstanceAlreadyCompleteException

해결은 RunIdIncrementer(매 실행마다 runId 자동 증가) 를 끼우거나, 매번 다른 파라미터 (예: timestamp) 를 넣는 방식입니다.

2. Implicit type → 다른 JobInstance

.addString("count", "100")           // String
vs
.addLong("count", 100L)              // Long

의미는 같지만 type 이 다르면 서로 다른 JobInstance 가 됩니다. 코드 전반에서 type 을 일관되게 가져가야 해요.

3. Date·LocalDate·LocalDateTime 혼동

.addDate("date", new Date())                  // java.util.Date
.addLocalDate("date", LocalDate.now())         // 다른 type!

같은 이름이라도 type 이 다르면 다른 JobInstance 로 잡힙니다. 프로젝트 안에서는 한 종류로 통일하는 게 좋아요.

4. Non-identifying 의 의도하지 않은 변경

.addString("runId", "abc", false)    // non-identifying
.addString("runId", "xyz", false)    // 같은 JobInstance (식별 X)

비즈니스 의미를 가진 파라미터를 non-identifying 으로 박아두면 실제로 다른 실행인데도 같은 JobInstance 로 묶여 의도와 다른 결과가 나오니, 식별 여부는 신중하게 정해야 합니다.

5. NULL parameter

null 값 파라미터도 넣을 수는 있지만 DB 컬럼이 NULL 로 들어가서 운영 모니터링에서 검색이 어색해져요. 기본값(default value) 을 채워두는 쪽을 권장합니다.

JobLauncher vs JobOperator 의 추천 사용

상황 권장
대부분 환경 JobOperator (start·stop·restart 통합)
Spring @Scheduled JobOperator + 비동기 launcher
단순 main 메서드 JobOperator (간단)
매우 세밀 제어 JobLauncher 직접
CLI CommandLineJobOperator (v6)
HTTP JobOperator + REST controller
Spring Cloud Data Flow SCDF 가 JobOperator wrap

운영 환경 권장 — Job 실행 패턴

패턴 1: Cron + CLI (단순)

# Crontab
0 2 * * * /opt/batch/run-daily-report.sh
#!/bin/bash
# run-daily-report.sh
TARGET_DATE=$(date -d "yesterday" +%Y-%m-%d)
java -jar /opt/batch/my-batch.jar \
    --job=dailyReport \
    targetDate=${TARGET_DATE}

패턴 2: Kubernetes CronJob

apiVersion: batch/v1
kind: CronJob
metadata:
  name: daily-report
spec:
  schedule: "0 2 * * *"
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      backoffLimit: 0
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: batch
              image: my-batch:1.0.0
              command: ["java", "-jar", "/app/my-batch.jar"]
              args:
                - --job=dailyReport
                - "targetDate=$(date -d yesterday +%Y-%m-%d)"

concurrencyPolicy: Forbid 옵션을 켜두면 이전 실행이 아직 안 끝났을 때 새 실행을 띄우지 않습니다.

패턴 3: API Trigger (HTTP)

@PostMapping("/jobs/{name}/trigger")
public ResponseEntity<?> trigger(@PathVariable String name) {
    Job job = jobRegistry.getJob(name);
    Long executionId = jobOperator.startNextInstance(job);
    return ResponseEntity.accepted().body(Map.of("executionId", executionId));
}

CI/CD 파이프라인이나 운영자 UI 가 이 엔드포인트를 호출하는 방식이에요.

패턴 4: Spring Cloud Data Flow

SCDF(Spring Cloud Data Flow, 배치·스트림 통합 관리 플랫폼) 안에서 GUI·CLI·REST 가 한 번에 묶여 제공되니까 운영 부담을 그만큼 덜 수 있습니다.

한계·실무 함정

1. Spring Boot 자동 실행

운영 환경에서는 spring.batch.job.enabled: false 로 두고 명시적으로만 실행합니다.

2. 동기 vs 비동기 launcher

REST·@Scheduled 환경에서는 비동기를 쓰고, CLI 일회성 실행이라면 동기로 가도 괜찮아요.

3. JobInstanceAlreadyComplete

매 실행마다 다른 identifying parameter 가 들어가야 합니다. Incrementer 를 끼우거나 timestamp 를 넣어 해결해요.

4. Multiple Job in 한 application

spring:
  batch:
    job:
      name: jobA       # 자동 실행할 Job 명시

job.name 을 명시하지 않으면 등록된 모든 Job 이 자동 실행돼서 의도와 다르게 동작할 수 있습니다.

5. JobParameter type 일관성

코드·CLI·REST 가 모두 같은 type 을 쓰도록 맞추고, 운영 도구를 거치는 과정에서 type drift(타입이 슬그머니 바뀜) 가 생기지 않게 신경 써야 해요.

6. Stop 후 process 종료

stop() 을 호출한다고 프로세스가 즉시 종료되지는 않습니다. 운영 자동화에서는 stop → polling → 확인 순서로 묶고 나서 종료까지 가져가야 안전합니다.

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

  • JobLauncher = 저수준 실행 엔진 (1 메서드 run)
  • JobOperator = 고수준 통합 인터페이스 (대부분 환경 권장)
  • Sync (기본) = SyncTaskExecutor, blocking
  • Async = SimpleAsyncTaskExecutor, 즉시 return + 별도 thread
  • Spring Boot 기본 = sync
  • @Scheduled·REST = 비동기 launcher 필수
  • JobParameters type = String·Long·Double·Date·LocalDate·LocalDateTime (v5+)
  • Identifying (기본) vs Non-identifying (addString(k, v, false))
  • 같은 identifying 두 번 = JobInstanceAlreadyCompleteException
  • @Scheduled + cron + JobOperator (Spring @EnableScheduling)
  • 외부 Scheduler (Cron·K8s·airflow) = application 외부에서 CLI·HTTP 호출
  • HTTP launch = 비동기 launcher + 202 Accepted + executionId
  • CommandLineJobOperator (v6) > CommandLineJobRunner (옛)
  • 자동 실행 = spring.batch.job.enabled: true (기본)
  • 운영 환경 = enabled: false + 명시 실행
  • 운영 패턴 — Cron + CLI · K8s CronJob · API Trigger · SCDF
  • K8s CronJob concurrencyPolicy: Forbid = 이전 실행 안 끝났으면 새 실행 X
  • 함정 — Spring Boot 자동 실행
  • 함정 — 동기 launcher + REST = 응답 blocking
  • 함정 — same identifying parameters → AlreadyComplete
  • 함정 — JobParameter type 혼합 (Date vs LocalDate)
  • 함정 — Multiple Job 자동 실행 (job.name 명시)
  • 함정 — Stop 후 polling 필요

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

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

이전 글:

다음 글:

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

답글 남기기

error: Content is protected !!