Spring Batch 입문 10편. Advanced Metadata Usage — JobExplorer 로 이력 조회·JobRegistry 로 동적 Job 등록·custom metadata 활용·운영 대시보드 패턴·이력 cleanup 정책까지 풀어쓴 학습 노트. Part 2 마무리.
이 글은 Spring Batch 입문에서 운영까지 시리즈 48편 중 10편이에요. Part 2 Job 설정·실행 의 마지막 글. 7~9편 으로 JobRepository(메타데이터 영속 저장소)·JobOperator(Job 실행 제어 API)·JobLauncher(Job 시작 진입점) 를 잡았다면, 이번 10편은 그 위 운영 도구 — Advanced Metadata Usage(메타데이터 운영 활용).
왜 Advanced Metadata?
운영 환경 = Job 실행 이력 조회·대시보드·재처리·cleanup 같은 부가 운영 작업 이 일상. Spring Batch 가 제공하는 세 가지 도구:
JobExplorer → 이력 read-only 조회
JobRegistry → 동적 Job 등록·이름 lookup
JobOperator → 실행 제어 (8편)
JobOperator 가 제어 라면 JobExplorer + JobRegistry = 조회·관리.
JobExplorer — Read-only 조회
public interface JobExplorer {
List<JobInstance> findJobInstancesByJobName(String jobName, int start, int count);
JobInstance getJobInstance(Long instanceId);
List<JobExecution> getJobExecutions(JobInstance jobInstance);
JobExecution getJobExecution(Long executionId);
Set<JobExecution> findRunningJobExecutions(String jobName);
long getJobInstanceCount(String jobName);
List<String> getJobNames();
// ...
}
자주 쓰는 자리
1. 최근 N개 instance 조회
@Autowired
private JobExplorer jobExplorer;
public List<JobInstanceSummary> recentInstances(String jobName, int count) {
List<JobInstance> instances = jobExplorer.findJobInstancesByJobName(jobName, 0, count);
return instances.stream()
.map(instance -> {
List<JobExecution> executions = jobExplorer.getJobExecutions(instance);
JobExecution latest = executions.stream()
.max(Comparator.comparing(JobExecution::getStartTime))
.orElse(null);
return new JobInstanceSummary(
instance.getId(),
instance.getJobName(),
latest != null ? latest.getStatus() : null,
latest != null ? latest.getStartTime() : null
);
})
.toList();
}
dashboard·monitoring 의 기본 query.
2. 실행 중인 Execution 찾기
Set<JobExecution> running = jobExplorer.findRunningJobExecutions("myJob");
for (JobExecution exec : running) {
log.info("Running: {} (started {})", exec.getId(), exec.getStartTime());
}
alert·stuck job 검출 에 자주.
3. Step 별 상세 통계
JobExecution exec = jobExplorer.getJobExecution(executionId);
for (StepExecution step : exec.getStepExecutions()) {
log.info("{}: read={} write={} skip={} duration={}ms",
step.getStepName(),
step.getReadCount(),
step.getWriteCount(),
step.getSkipCount(),
Duration.between(step.getStartTime(), step.getEndTime()).toMillis()
);
}
JobRegistry — 동적 Job 등록·lookup
이름으로 Job bean 을 찾는 registry.
public interface JobRegistry extends ListableJobLocator {
void register(JobFactory jobFactory) throws DuplicateJobException;
void unregister(String jobName);
Job getJob(String name) throws NoSuchJobException;
}
@Autowired 활용
Spring Boot 가 SimpleJobRegistry(기본 JobRegistry 구현체) 자동 등록:
@Autowired
private JobRegistry jobRegistry;
public Long runByName(String jobName, JobParameters params) throws Exception {
Job job = jobRegistry.getJob(jobName);
return jobOperator.start(job, params);
}
CLI·REST 에서 이름으로 Job 호출 의 표준 패턴.
Bean Auto-registration
@Configuration
public class JobRegistryConfig {
@Bean
public JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor(JobRegistry registry) {
JobRegistryBeanPostProcessor processor = new JobRegistryBeanPostProcessor();
processor.setJobRegistry(registry);
return processor;
}
}
JobRegistryBeanPostProcessor(Job bean 자동 등록기) = 모든 Job bean 을 자동으로 registry 에 등록. Spring Boot 가 자동 구성.
여기서 시험 함정이 하나 있어요 — 같은 이름 Job 두 개 = DuplicateJobException. Job 이름 unique 필요.
Custom Metadata 활용
JobParameters(Job 실행 식별 파라미터) 와 ExecutionContext(Job·Step 상태 저장소) 외에 자체 메타데이터 저장 패턴.
패턴 1: ExecutionContext 활용
// Step 안 Reader/Writer 등
@Override
public void update(ExecutionContext context) {
context.put("processedAt", LocalDateTime.now().toString());
context.put("itemsByCategory", Map.of("A", 100, "B", 50));
}
장점 — JobRepository 가 자동 영속, 재시작 시 사용 가능
단점 — 직렬화 필요 (Jackson JSON 기본)
패턴 2: 별도 DB 테이블
CREATE TABLE job_metadata (
job_execution_id BIGINT REFERENCES BATCH_JOB_EXECUTION(JOB_EXECUTION_ID),
metric_key VARCHAR(100),
metric_value VARCHAR(500),
PRIMARY KEY (job_execution_id, metric_key)
);
복잡한 metric·report data = 별도 table 권장. BATCH_ 테이블이 batch metadata 전용 으로 유지.
패턴 3: Listener 활용
@Component
public class MetricsCollector implements JobExecutionListener {
@Autowired
private MetricsRepository metricsRepo;
@Override
public void afterJob(JobExecution execution) {
Map<String, Object> metrics = collectMetrics(execution);
metricsRepo.save(execution.getId(), metrics);
}
}
Job 끝난 후 별도 시스템에 저장. ELK·Prometheus·custom DB 등.
운영 대시보드 — 자주 만나는 자리
항목 1: Job 별 최근 실행 status
@RestController
@RequestMapping("/api/batch")
public class BatchDashboardController {
@Autowired
private JobExplorer jobExplorer;
@GetMapping("/jobs")
public List<JobSummary> jobs() {
return jobExplorer.getJobNames().stream()
.map(name -> {
List<JobInstance> recent = jobExplorer.findJobInstancesByJobName(name, 0, 1);
JobExecution latest = recent.isEmpty()
? null
: jobExplorer.getJobExecutions(recent.get(0)).stream()
.max(Comparator.comparing(JobExecution::getStartTime))
.orElse(null);
return new JobSummary(name,
latest != null ? latest.getStatus() : "NEVER_RUN",
latest != null ? latest.getStartTime() : null);
})
.toList();
}
}
항목 2: 실행 중·실패 알람
@Scheduled(fixedRate = 60000) // 1분마다
public void checkStuckJobs() {
for (String name : jobExplorer.getJobNames()) {
Set<JobExecution> running = jobExplorer.findRunningJobExecutions(name);
for (JobExecution exec : running) {
Duration age = Duration.between(exec.getStartTime(), LocalDateTime.now());
if (age.toHours() > 2) {
alertingService.alert("Stuck Job", name, exec.getId(), age);
}
}
}
}
2시간 이상 실행 중 = 의심. 알람.
항목 3: Lag 추적
public Map<String, Long> jobLag(String jobName) {
List<JobInstance> instances = jobExplorer.findJobInstancesByJobName(jobName, 0, 100);
return instances.stream()
.collect(Collectors.toMap(
i -> i.getJobName(),
i -> {
JobExecution latest = jobExplorer.getJobExecutions(i).get(0);
long expectedStart = expectedStartTime(i);
long actualStart = latest.getStartTime().toEpochSecond(...);
return actualStart - expectedStart;
}
));
}
예상 시작 시각 vs 실제 시작 = lag. 정기 batch 의 지연 모니터링.
이력 Cleanup 정책
BATCH_* 테이블이 무한 누적. 운영 = 정리 필수.
직접 SQL Cleanup
-- 30일 전 데이터 삭제 (PostgreSQL)
DELETE FROM BATCH_STEP_EXECUTION_CONTEXT
WHERE STEP_EXECUTION_ID IN (
SELECT STEP_EXECUTION_ID FROM BATCH_STEP_EXECUTION
WHERE START_TIME < NOW() - INTERVAL '30 days'
);
DELETE FROM BATCH_STEP_EXECUTION
WHERE START_TIME < NOW() - INTERVAL '30 days';
DELETE FROM BATCH_JOB_EXECUTION_CONTEXT
WHERE JOB_EXECUTION_ID IN (
SELECT JOB_EXECUTION_ID FROM BATCH_JOB_EXECUTION
WHERE START_TIME < NOW() - INTERVAL '30 days'
);
DELETE FROM BATCH_JOB_EXECUTION_PARAMS
WHERE JOB_EXECUTION_ID IN (
SELECT JOB_EXECUTION_ID FROM BATCH_JOB_EXECUTION
WHERE START_TIME < NOW() - INTERVAL '30 days'
);
DELETE FROM BATCH_JOB_EXECUTION
WHERE START_TIME < NOW() - INTERVAL '30 days';
DELETE FROM BATCH_JOB_INSTANCE
WHERE JOB_INSTANCE_ID NOT IN (
SELECT DISTINCT JOB_INSTANCE_ID FROM BATCH_JOB_EXECUTION
);
순서 중요 — FK constraint(외래 키 제약) 때문에 child → parent 순서.
자체 Cleanup Job
@Configuration
public class CleanupBatchConfig {
@Bean
public Job cleanupBatchHistoryJob(JobRepository repo, Step cleanupStep) {
return new JobBuilder("cleanupBatchHistoryJob", repo)
.start(cleanupStep)
.build();
}
@Bean
public Step cleanupStep(JobRepository repo, PlatformTransactionManager tx,
CleanupTasklet cleanupTasklet) {
return new StepBuilder("cleanupStep", repo)
.tasklet(cleanupTasklet, tx)
.build();
}
}
@Component
public class CleanupTasklet implements Tasklet {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext context) {
int retentionDays = 30;
// ... DELETE SQL 들
return RepeatStatus.FINISHED;
}
}
Spring Batch 가 Spring Batch 의 이력을 정리 — 자기 참조 패턴. 별도 retention DB 권장.
Spring Cloud Data Flow 의 Cleanup
SCDF(Spring Cloud Data Flow 줄임) 가 batch 이력 cleanup task 내장. UI·CLI 에서 retention 정책 설정.
JobExecutionDecider 와 Metadata
20편 Controlling Flow 에서 깊이 다룰 자리. JobExecutionDecider(Step 다음 흐름을 판단하는 인터페이스). 미리 한 줄:
@Component
public class MyDecider implements JobExecutionDecider {
@Override
public FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) {
Object metadata = jobExecution.getExecutionContext().get("someKey");
return decideBasedOn(metadata);
}
}
ExecutionContext 의 metadata 기반 조건부 flow.
자주 쓰는 모니터링 Stack
Stack 1: Spring Actuator + Custom Endpoint
@Component
@Endpoint(id = "batch")
public class BatchActuatorEndpoint {
@Autowired
private JobExplorer jobExplorer;
@ReadOperation
public Map<String, Object> info() {
return Map.of(
"jobs", jobExplorer.getJobNames(),
"running", jobExplorer.findRunningJobExecutions(null).size()
);
}
}
Spring Actuator(앱 상태·메트릭 노출 모듈) 의 custom endpoint 로 /actuator/batch 에 custom info.
Stack 2: Micrometer + Prometheus
Micrometer(JVM 표준 메트릭 facade) + Prometheus(시계열 메트릭 DB) 조합. JFR(JDK Flight Recorder) + Micrometer = JFR 이벤트가 자동으로 metric. 45편 깊이.
@Bean
public MeterRegistry meterRegistry() {
return new PrometheusMeterRegistry(...);
}
/actuator/prometheus 에 job duration·step throughput·skip count 등 자동 노출.
Stack 3: ELK 로 Job log 집계
ELK(Elasticsearch·Logstash·Kibana 묶음). org.springframework.batch package 의 모든 로그를 Logstash → Elasticsearch → Kibana 로. 검색·대시보드 강력.
한계·실무 함정
1. JobExplorer 의 Performance
대량 instance 조회 시 DB query 가 수 초 단위. dashboard 가 polling 자주 하면 DB 부담. caching·indexing 필수.
2. JobRegistry 의 동적 등록
런타임 register() = 잠시 동작 가능. 단 대부분 환경 에서는 static @Bean Job 으로 충분.
3. ExecutionContext 직렬화 호환
Custom metadata 가 복잡 객체 = 직렬화 변경 시 재시작 실패. 간단 type·String·Map 권장.
4. Cleanup 의 FK constraint
순서 잘못 = constraint 위반. child first.
5. 대량 데이터의 BATCH_* 테이블 크기
수십만 instance × 수십 execution × 수만 step execution = BATCH_STEP_EXECUTION 이 수억 행. index·partitioning 검토.
6. Tab Prefix 변경 시 history 잃음
기존 데이터가 옛 prefix 라 새 prefix 가 못 봄. migration 필요.
Part 2 Job 설정·실행 마무리
6편 (5~10):
- 5 Infrastructure — Beans·
@EnableBatchProcessing·Resourceless - 6 Configuring a Job — JobBuilder·Validator·Incrementer·Listener
- 7 JobRepository — 영속화·Schema·Isolation
- 8 JobOperator — 실행·중지·재시작·CommandLineJobOperator
- 9 Running a Job — JobLauncher·Sync/Async·@Scheduled
- 10 Advanced Metadata — JobExplorer·JobRegistry·Dashboard·Cleanup
Part 2 = 모든 Job 의 실행·운영 기반. 다음 Part 3 부터 Step 의 내부·Chunk Processing 본격.
시험 직전 한 번 더 — Advanced Metadata 함정 압축 노트
- JobExplorer = read-only 조회 (JobInstance·Execution·StepExecution)
- 핵심 메서드 —
findJobInstancesByJobName·getJobExecutions·getJobExecution·findRunningJobExecutions·getJobInstanceCount·getJobNames - 자주 활용 = 최근 instance·실행 중 detection·step 통계
- JobRegistry = 이름 → Job bean lookup
SimpleJobRegistry= Spring Boot 자동 등록JobRegistryBeanPostProcessor=@Bean Job자동 등록- 같은 이름 =
DuplicateJobException - Custom Metadata 3가지 패턴 — ExecutionContext (자동 영속) · 별도 DB table · Listener + 외부 시스템
- 운영 대시보드 — Job 별 status·실행 중·실패 알람·lag 추적
- Stuck Job Alert =
@Scheduled+findRunningJobExecutions+ duration 검사 - 이력 Cleanup — 직접 SQL (FK 순서) · 자체 Cleanup Job · SCDF
- 순서 = STEP_EXECUTION_CONTEXT → STEP_EXECUTION → JOB_EXECUTION_CONTEXT → JOB_EXECUTION_PARAMS → JOB_EXECUTION → JOB_INSTANCE
- 모니터링 Stack — Spring Actuator custom endpoint · Micrometer + Prometheus · ELK 로그 집계
- 함정 — 대량 조회 performance (caching)
- 함정 — ExecutionContext 직렬화 호환 (간단 type)
- 함정 — Cleanup FK constraint 순서
- 함정 — BATCH_* 테이블 폭증 (수억 행 index·partitioning)
- 함정 — Table prefix 변경 시 history 잃음
- Part 2 Job 설정·실행 6편 마무리
공식 문서: Advanced Metadata Usage 에서 원문을 확인할 수 있어요.
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 5편 — Batch Infrastructure (@EnableBatchProcessing · Beans)
- 6편 — Configuring a Job (JobBuilder · Validator · Listener)
- 7편 — JobRepository (영속화 · Schema · Isolation)
- 8편 — JobOperator (실행 · 중지 · 재시작 · CommandLine)
- 9편 — Running a Job (JobLauncher · Sync/Async · Scheduler)
다음 글: