Spring Batch 입문 9편. Running a Job — JobLauncher 와 JobOperator 의 관계, 동기 vs 비동기 실행, JobParameters 의 valid type, Spring @Scheduled 통합, CommandLineJobRunner·HTTP launch·자주 만나는 함정까지 풀어쓴 학습 노트. Part 2 마무리 직전.
이 글은 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 에서 원문을 확인할 수 있어요.
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 4편 — v6 What's New + Hello Job 5분 hands-on
- 5편 — Batch Infrastructure (@EnableBatchProcessing · Beans)
- 6편 — Configuring a Job (JobBuilder · Validator · Listener)
- 7편 — JobRepository (영속화 · Schema · Isolation)
- 8편 — JobOperator (실행 · 중지 · 재시작 · CommandLine)
다음 글: