Spring Batch 입문 45편. Observability 의 두 축 — Micrometer 메트릭 + Observation API 분산 트레이싱. 기본 제공 메트릭 8가지 (`spring.batch.job`·`step`·`item.read`·`item.process`·`chunk.write`·`job.active`·`step.active`·`job.launch.count`), ObservationRegistry · DefaultMeterObservationHandler · TracingAwareMeterObservationHandler, Custom Observation, BatchObservabilityBeanPostProcessor, Prometheus·Grafana 연동까지 정리한 학습 노트.
이 글은 Spring Batch 입문에서 운영까지 시리즈 48편 중 45편이에요. 44편 의 분산 처리 다음 — batch 가 잘 돌아가는지 어떻게 아는가 — Observability (운영 가시성). Part 10 의 마지막.
Observability 의 두 축
| 축 | 답하는 질문 |
|---|---|
| Metrics | "지금 batch 가 어떻게 돌아가는가?" (count·duration·rate) |
| Tracing | "한 Job 의 각 단계 가 어디서 얼마나 걸렸는가?" |
Spring Batch 6 = Micrometer Observation API (관찰 표준 API) 로 두 축 통합. 메트릭과 트레이싱이 같은 API.
활성화 — ObservationRegistry Bean
ObservationRegistry = 관찰 대상 등록소.
Metrics collection is disabled by default. To enable it, define a Micrometer ObservationRegistry bean. — 공식 reference
@Bean
public ObservationRegistry observationRegistry(MeterRegistry meterRegistry) {
ObservationRegistry registry = ObservationRegistry.create();
registry.observationConfig()
.observationHandler(new DefaultMeterObservationHandler(meterRegistry));
return registry;
}
작동:
ObservationRegistry= Spring Batch 가 관찰 대상 등록DefaultMeterObservationHandler= Observation 을 MeterRegistry 의 Timer·Counter 등으로 변환MeterRegistry(메트릭 저장소 — Prometheus·Datadog·...) = 실제 metric backend
→ Spring Boot 환경 = MeterRegistry 가 auto-configured. Spring Batch metric 도 자동 활성.
기본 제공 메트릭 8가지
Spring Batch metric prefix = spring.batch.
| Metric | Type | Tags | 의미 |
|---|---|---|---|
spring.batch.job |
TIMER | name, status |
Job 실행 시간 |
spring.batch.job.active |
LONG_TASK_TIMER | name |
현재 실행 중 Job |
spring.batch.step |
TIMER | name, job.name, status |
Step 실행 시간 |
spring.batch.step.active |
LONG_TASK_TIMER | name |
현재 실행 중 Step |
spring.batch.item.read |
TIMER | job.name, step.name, status |
item read 시간 |
spring.batch.item.process |
TIMER | job.name, step.name, status |
item process 시간 |
spring.batch.chunk.write |
TIMER | job.name, step.name, status |
chunk write 시간 |
spring.batch.job.launch.count |
COUNTER | N/A | Job launch 횟수 |
Tag 값:
- Job/Step
status= ExitStatus (COMPLETED·FAILED·STOPPED) - read/process/write
status=SUCCESS또는FAILURE
TIMER vs LONG_TASK_TIMER
- TIMER = 완료된 작업의 duration 누적 (개수 · 평균 · 백분위)
- LONG_TASK_TIMER = 현재 진행 중 작업의 wall-clock duration
→ spring.batch.job = "끝난 Job 평균 5분", spring.batch.job.active = "지금 실행 중인 Job 3분째".
Prometheus 연동
Prometheus = 시계열 메트릭 수집·저장 엔진.
# application.yml
management:
metrics:
export:
prometheus:
enabled: true
endpoints:
web:
exposure:
include: prometheus,health,metrics
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
/actuator/prometheus endpoint 가 자동 노출 → Prometheus scrape.
Prometheus query 예제
# Job 평균 실행 시간
rate(spring_batch_job_seconds_sum[5m]) / rate(spring_batch_job_seconds_count[5m])
# Job 실패율
rate(spring_batch_job_seconds_count{status="FAILED"}[5m])
/ rate(spring_batch_job_seconds_count[5m])
# 현재 실행 중 Job 개수
spring_batch_job_active_active_count
# Step 별 read 처리 속도
rate(spring_batch_item_read_seconds_count{status="SUCCESS"}[1m])
Grafana 대시보드
Grafana = 메트릭 시각화 대시보드 도구.
표준 panels:
- Job 실행 시간 —
spring.batch.jobp50/p95/p99 - Job 성공/실패율 —
spring.batch.jobstatus 별 count - 현재 실행 Job —
spring.batch.job.active - Step 별 throughput —
spring.batch.item.readcount rate - Skip · Retry rate —
spring.batch.chunk.writeFAILURE rate
Custom Metrics — Tasklet 예제
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
public class MyTimedTasklet implements Tasklet {
private final ObservationRegistry observationRegistry;
public MyTimedTasklet(ObservationRegistry observationRegistry) {
this.observationRegistry = observationRegistry;
}
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
Observation observation = Observation.start("my.tasklet.step", observationRegistry);
try (Observation.Scope scope = observation.openScope()) {
// 비즈니스 로직
doWork();
return RepeatStatus.FINISHED;
} catch (Exception e) {
observation.error(e);
throw e;
} finally {
observation.stop();
}
}
}
핵심 3 메서드:
Observation.start(name, registry)— 시작observation.openScope()— thread-local 활성 (try-with-resources)observation.stop()— 종료
observation.error(e) = 예외 발생 시 status FAILURE.
→ Micrometer Observation API = 메트릭 + 트레이싱 동시에.
Tracing — 분산 트레이싱
Tracing = 한 요청이 거친 단계별 시간·경로 추적.
As of version 5, Spring Batch provides tracing through Micrometer's Observation API. — 공식 reference
분산 트레이싱 활성화:
@Bean
public ObservationRegistry observationRegistry(MeterRegistry meterRegistry, Tracer tracer) {
DefaultMeterObservationHandler meterHandler =
new DefaultMeterObservationHandler(meterRegistry);
ObservationRegistry registry = ObservationRegistry.create();
registry.observationConfig()
.observationHandler(new TracingAwareMeterObservationHandler<>(meterHandler, tracer));
return registry;
}
TracingAwareMeterObservationHandler = MeterRegistry 동시에 Tracer 도 활용.
Tracer 구현
- Brave (Zipkin 호환 트레이서) —
io.micrometer:micrometer-tracing-bridge-brave - OpenTelemetry (OTel — 관찰 데이터 오픈 표준) —
io.micrometer:micrometer-tracing-bridge-otel
자동 trace 구조
[Trace] Job: importPaymentsJob
├─ [Span] Step: readStep (3s)
│ ├─ [Span] item.read (slow records detail)
│ └─ [Span] chunk.write
├─ [Span] Step: processStep (45s)
└─ [Span] Step: writeStep (12s)
각 Step·chunk·item 단위 span. 어디가 느린지 한눈에.
Zipkin · Jaeger 연동
Zipkin·Jaeger = 분산 트레이스 저장·조회 백엔드.
management:
tracing:
sampling:
probability: 1.0 # 100% sampling (운영 = 0.1 권장)
zipkin:
tracing:
endpoint: http://zipkin:9411/api/v2/spans
또는 OpenTelemetry Collector 활용.
BatchObservabilityBeanPostProcessor
If you do not use @EnableBatchProcessing or DefaultBatchConfiguration, you need to register a BatchObservabilityBeanPostProcessor. — 공식 reference
표준 환경 (@EnableBatchProcessing · DefaultBatchConfiguration) = 자동 등록.
수동 설정 환경:
@Bean
public static BatchObservabilityBeanPostProcessor batchObservabilityBeanPostProcessor() {
return new BatchObservabilityBeanPostProcessor();
}
→ Batch artifact 들에 ObservationRegistry 자동 주입.
Custom Observation 의 활용
ItemProcessor 의 외부 호출 추적
public class ApiEnrichmentProcessor implements ItemProcessor<Customer, EnrichedCustomer> {
private final ApiClient client;
private final ObservationRegistry observationRegistry;
@Override
public EnrichedCustomer process(Customer customer) {
return Observation.createNotStarted("api.enrich", observationRegistry)
.lowCardinalityKeyValue("customer.country", customer.getCountry())
.observe(() -> {
CustomerDetails details = client.fetch(customer.getId());
return new EnrichedCustomer(customer, details);
});
}
}
lowCardinalityKeyValue = tag 추가 (cardinality 낮은 값 — 국가 코드 등).
observe(supplier) = start + scope + stop 한 줄로.
→ 각 외부 호출이 별도 span 으로 trace. 어느 customer 의 API 호출이 느린지 추적.
Custom metric 추가 — Counter
@Component
public class SkipCounter implements SkipListener<Customer, Customer> {
private final Counter skipCounter;
public SkipCounter(MeterRegistry registry) {
this.skipCounter = Counter.builder("batch.custom.skip")
.description("Custom skip events")
.tag("type", "validation")
.register(registry);
}
@Override
public void onSkipInRead(Throwable t) {
skipCounter.increment();
}
@Override
public void onSkipInProcess(Customer item, Throwable t) {
skipCounter.increment();
}
@Override
public void onSkipInWrite(Customer item, Throwable t) {
skipCounter.increment();
}
}
직접 MeterRegistry 활용 — Spring Batch 기본 외 custom counter · timer · gauge.
Gauge — 외부 상태 노출
@Component
public class QueueDepthGauge {
public QueueDepthGauge(MeterRegistry registry, ExternalQueue queue) {
Gauge.builder("batch.external.queue.depth", queue, ExternalQueue::depth)
.description("Pending items in external queue")
.register(registry);
}
}
Gauge = current value snapshot. 외부 시스템 상태 모니터링.
Cardinality 주의
Cardinality = 한 tag 가 가질 수 있는 unique value 개수.
| Tag 종류 | Cardinality | 영향 |
|---|---|---|
| Low (status·country·env) | <100 | 안전 |
| Medium (job.name·step.name) | 10~1000 | OK |
| High (customer.id·order.id) | 1M+ | 메모리 폭발 위험 |
→ High cardinality tag 절대 X. 개별 customer ID 같은 값을 tag 로 박으면 Prometheus 메모리 폭발.
높은 cardinality 가 필요한 정보 = logs 또는 traces 활용.
Logging 과 결합 — Trace ID 주입
logging:
pattern:
level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"
MDC (Mapped Diagnostic Context — 로그용 thread-local 저장소) 에 traceId·spanId 자동 주입 (Micrometer Tracing). 로그가 어느 trace 의 일부 인지 추적.
자주 만나는 사고
사고 1: 메트릭이 안 보임
원인 1 — ObservationRegistry bean 없음.
원인 2 — DefaultMeterObservationHandler 등록 누락.
원인 3 — @EnableBatchProcessing 없는 환경에서 BatchObservabilityBeanPostProcessor 누락.
해결 — 위 3가지 검증.
사고 2: Prometheus 가 metric 못 가져옴
원인 — /actuator/prometheus endpoint 노출 안 됨.
해결 — management.endpoints.web.exposure.include=prometheus.
사고 3: 메모리 폭발
원인 — high cardinality tag (customer.id 등).
해결 — tag 제거 + logs/traces 활용.
사고 4: Tracing span 누락
원인 — Tracer bean 없음 또는 TracingAwareMeterObservationHandler 미사용.
해결 — Brave/OTel bridge dependency + handler 교체.
사고 5: Async/Remote 환경 span 깨짐
원인 — Async TaskExecutor 가 trace context propagation 안 함.
해결 — ContextPropagatingTaskDecorator 또는 TaskExecutor 설정 — Spring Boot 기본 TaskExecutionAutoConfiguration 활용.
사고 6: Tracing sampling 100% 운영 부하
원인 — probability: 1.0 으로 모든 trace 저장.
해결 — 운영 = 0.05~0.1 (5~10%) 또는 adaptive sampling.
사고 7: Custom Observation 의 try-with-resources 누락
원인 — openScope() 만 호출 후 close 안 함 → thread-local 누수.
해결 — 반드시 try-with-resources.
운영 권장 패턴
Pattern 1: 표준 ObservationRegistry
@Bean
public ObservationRegistry observationRegistry(MeterRegistry meterRegistry) {
ObservationRegistry registry = ObservationRegistry.create();
registry.observationConfig()
.observationHandler(new DefaultMeterObservationHandler(meterRegistry));
return registry;
}
Spring Boot + Micrometer 기본 환경. 자동 metric.
Pattern 2: Tracing 활성 (운영 환경)
@Bean
public ObservationRegistry observationRegistry(MeterRegistry meterRegistry, Tracer tracer) {
DefaultMeterObservationHandler meterHandler =
new DefaultMeterObservationHandler(meterRegistry);
ObservationRegistry registry = ObservationRegistry.create();
registry.observationConfig()
.observationHandler(new TracingAwareMeterObservationHandler<>(meterHandler, tracer));
return registry;
}
management:
tracing:
sampling:
probability: 0.1 # 10% sampling
분산 트레이싱 + 메트릭 결합.
Pattern 3: Custom Observation in ItemProcessor
@Override
public EnrichedCustomer process(Customer customer) {
return Observation.createNotStarted("processor.api.enrich", observationRegistry)
.lowCardinalityKeyValue("country", customer.getCountry())
.lowCardinalityKeyValue("tier", customer.getTier())
.observe(() -> {
return apiClient.enrich(customer);
});
}
각 enrich 호출이 별도 span + tag 로 분류.
Pattern 4: Alert 설정
# Prometheus alert rule
groups:
- name: batch
rules:
- alert: BatchJobFailed
expr: increase(spring_batch_job_seconds_count{status="FAILED"}[1h]) > 0
labels:
severity: critical
annotations:
summary: "Batch job {{ $labels.name }} failed"
- alert: BatchJobSlow
expr: histogram_quantile(0.95, rate(spring_batch_job_seconds_bucket[5m])) > 3600
labels:
severity: warning
annotations:
summary: "Batch job p95 > 1 hour"
PromQL (Prometheus query 언어) 기반 알람. Slack · PagerDuty 연동.
Pattern 5: Dashboard 표준 panel
Grafana 대시보드 권장 5 panels:
- Job overview — count, success rate, current active
- Job duration — p50/p95/p99 by
name - Step throughput —
spring.batch.item.readrate - Error rate — read/process/write FAILURE rate
- Skip/Retry — custom counter
Pattern 6: 로그 trace 연결
logging:
pattern:
level: "%5p [${spring.application.name:-},%X{traceId:-},%X{spanId:-}]"
logs 의 traceId = traces 와 연결. ELK (Elasticsearch + Logstash + Kibana 로그 스택) /Loki + Jaeger 연동.
Spring Boot Actuator 와 결합
management:
endpoint:
health:
show-details: always
endpoints:
web:
exposure:
include: health,info,metrics,prometheus,batchjobs
management.endpoint.health.group.batch.include: batchJobsHealth
/actuator/health = 전체 health (Batch job repository 도 자동 포함).
/actuator/metrics = 모든 메트릭 (spring.batch.* 포함).
/actuator/prometheus = Prometheus 형식 노출.
시험 직전 한 번 더 — Observability · Micrometer 함정 압축 노트
- 2 축 = Metrics (count·duration·rate) + Tracing (per-step duration)
- Spring Batch 6 = Observation API 로 통합
- 활성화 =
ObservationRegistrybean +DefaultMeterObservationHandler등록 MeterRegistry(Prometheus·Datadog 등) 자동 주입 (Spring Boot)- 기본 metric 8가지 = job · job.active · step · step.active · item.read · item.process · chunk.write · job.launch.count
- prefix =
spring.batch - TIMER vs LONG_TASK_TIMER — 완료 vs 진행 중
- Tags —
name·job.name·step.name·status(job/step = ExitStatus, item = SUCCESS/FAILURE) - Prometheus 연동 =
/actuator/prometheusendpoint +management.endpoints.web.exposure.include=prometheus - Grafana 5 panels = overview · duration p99 · throughput · error rate · skip/retry
- Custom Observation =
Observation.start(name, registry)+openScope()+stop() - 또는
observe(supplier)한 줄 lowCardinalityKeyValue= tag 추가 (cardinality 낮은 값)observation.error(e)= 예외 시 status FAILURE- Tracing (v5+) =
TracingAwareMeterObservationHandler사용 - Tracer 구현 — Brave (Zipkin) · OpenTelemetry
- 자동 span — Job → Step → chunk → item 계층
- Sampling probability = 운영 0.05~0.1 권장 (100% = 부하)
BatchObservabilityBeanPostProcessor=@EnableBatchProcessing없을 때 수동 등록- Custom Counter =
Counter.builder("batch.custom.skip").register(registry) - Gauge = current value snapshot (외부 queue depth 등)
- Cardinality 주의 — high cardinality tag (customer.id) = 메모리 폭발
- low = status·country, high = id (logs/traces 활용)
- Trace ID 로그 주입 =
%X{traceId:-},%X{spanId:-} - 함정 — ObservationRegistry bean 없음 → metric 안 보임
- 함정 — Prometheus endpoint 노출 누락
- 함정 — high cardinality tag 메모리 폭발
- 함정 — Tracer bean 없음 → span 누락
- 함정 — Async/Remote 환경 trace context propagation
- 함정 — sampling 100% 운영 부하
- 함정 — Custom Observation try-with-resources 누락
- 패턴 — 표준 ObservationRegistry (개발 환경)
- 패턴 — Tracing 활성 + 10% sampling (운영)
- 패턴 — Custom Observation in Processor (외부 호출 추적)
- 패턴 — Prometheus alert rule (failed · slow job)
- 패턴 — Grafana 5 표준 panel
- 패턴 — logs trace ID 연결 (ELK/Loki + Jaeger)
- Spring Boot Actuator 와 자연 결합
공식 문서: Spring Batch Observability · Micrometer Support 에서 원문을 확인할 수 있어요.
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 40편 — Testing · @SpringBatchTest · End-to-End
- 41편 — Common Patterns · 흔한 운영 패턴 카탈로그
- 42편 — Spring Batch Integration · 두 프레임워크 경계
- 43편 — Launching via Messages · JobLaunchingGateway · Informational
- 44편 — Async Processors · Remote Chunking · Partitioning
다음 글: