Spring Batch 입문 45편 — Observability · Micrometer · Tracing

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

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 입문에서 운영까지 · 45편 — Observability · Micrometer · Tracing

이 글은 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;
}

작동:

  1. ObservationRegistry = Spring Batch 가 관찰 대상 등록
  2. DefaultMeterObservationHandler = Observation 을 MeterRegistry 의 Timer·Counter 등으로 변환
  3. MeterRegistry (메트릭 저장소 — Prometheus·Datadog·...) = 실제 metric backend

→ Spring Boot 환경 = MeterRegistryauto-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:

  1. Job 실행 시간spring.batch.job p50/p95/p99
  2. Job 성공/실패율spring.batch.job status 별 count
  3. 현재 실행 Jobspring.batch.job.active
  4. Step 별 throughputspring.batch.item.read count rate
  5. Skip · Retry ratespring.batch.chunk.write FAILURE 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 메서드:

  1. Observation.start(name, registry) — 시작
  2. observation.openScope()thread-local 활성 (try-with-resources)
  3. 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: 메트릭이 안 보임

원인 1ObservationRegistry bean 없음. 원인 2DefaultMeterObservationHandler 등록 누락. 원인 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:

  1. Job overview — count, success rate, current active
  2. Job duration — p50/p95/p99 by name
  3. Step throughputspring.batch.item.read rate
  4. Error rate — read/process/write FAILURE rate
  5. 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통합
  • 활성화 = ObservationRegistry bean + 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/prometheus endpoint + 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 에서 원문을 확인할 수 있어요.

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

이전 글:

다음 글:

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

답글 남기기

error: Content is protected !!