Micrometer 입문 2편. Counter(단조 증가, 요청·에러 수)·Gauge(현재 순간 값, 큐·커넥션)·Timer(count+totalTime+max, latency 측정)·DistributionSummary(크기·건수 분포)·LongTaskTimer(진행 중 장기 작업)·FunctionCounter·FunctionTimer 등 Function 계열까지. 어느 Meter 타입을 언제 써야 하는지 비교표 포함.
이 글은 Micrometer 입문에서 운영까지 시리즈 2편이에요. 1편에서 Micrometer 의 전체 그림 — MeterRegistry, 태그, 지원 백엔드, Observation API 큰 그림 — 을 잡았다면, 이번 글은 "실제로 무엇을 어떻게 측정하느냐" 로 내려가요. Meter 타입 하나하나를 직접 다뤄야 하는 자리예요.
이번 글의 범위
Micrometer 가 제공하는 Meter 타입은 생각보다 다양해요. 종류만 나열하면 아래와 같아요.
| Meter 타입 | 핵심 용도 |
|---|---|
| Counter | 단조 증가 카운트 — 총 요청 수, 에러 수 |
| Gauge | 현재 순간의 값 — 큐 크기, 커넥션 수, 스레드 수 |
| Timer | 실행 시간 측정 — count + totalTime + max 자동 제공 |
| DistributionSummary | 시간 외의 분포 — 응답 크기(byte), 배치 건수 |
| LongTaskTimer | 아직 끝나지 않은 장기 작업 — active count + duration |
| FunctionCounter | 이미 존재하는 외부 카운트 누적값 노출 |
| FunctionTimer | 이미 존재하는 외부 시간 누적값 노출 |
| TimeGauge | 시간 단위의 Gauge — 시간 값을 다른 단위로 변환 노출 |
| MultiGauge | 여러 행(row)을 한 번에 Gauge 로 노출 |
하나씩 뜯어 볼게요.
Counter — 단조 증가만 한다
Counter 는 가장 단순한 Meter 예요. 숫자가 오직 올라가기만 해요. 음수로 내리는 게 불가능하고, 재설정도 안 돼요. 이 제약이 Counter 의 정체성이에요.
비유하자면 출퇴근 기록부 같은 거예요. "오늘 총 몇 명이 들어왔냐"는 올라가기만 하죠. "지금 몇 명이 안에 있냐"는 Gauge 가 할 일이고, Counter 는 그 질문에 대답을 못 해요.
// 기본 생성 패턴
Counter counter = Counter.builder("http.requests.total")
.tag("uri", "/api/orders")
.tag("method", "POST")
.tag("status", "200")
.description("Total HTTP requests handled")
.register(registry);
// 증가 방법
counter.increment(); // 1씩 증가
counter.increment(5.0); // 5만큼 증가 (double 허용)
// 현재 누적값 읽기
double total = counter.count(); // 예: 4802.0
여기서 중요한 포인트가 하나 있어요. Counter 가 주는 건 절대 누적값 이지, rate 가 아니에요. "초당 몇 건이냐"라는 질문은 Counter 가 아니라 Prometheus 의 rate() 함수가 답해요.
# Prometheus 쿼리 — 지난 5분 평균 초당 요청 수
rate(http_requests_total{uri="/api/orders"}[5m])
Counter 자체는 그냥 쌓아 놓고, 백엔드가 그 시계열에서 rate 를 계산하는 구조예요. 잊고 있다가 "Counter 값이 계속 늘어난다"고 놀라는 경우가 있어요 — 원래 그렇게 설계된 거예요.
Spring Boot 에서의 Counter 단축 표현
// registry.counter() 단축 호출 (builder 없이)
registry.counter("orders.failed", "reason", "validation").increment();
빌더가 번거로울 때 쓰는 편의 메서드예요. 단 description 이 없어서 메트릭 문서화 관점에서는 빌더 패턴이 더 나아요.
Gauge — 지금 이 순간의 값
Gauge 는 Counter 와 달리 올라갔다 내려갔다 해도 돼요. 큐에 메시지가 쌓이면 올라가고, 처리되면 내려가죠. 현재 순간의 상태 를 찍는 스냅샷 도구예요.
Gauge 를 만드는 방법이 Counter 와 조금 달라요. Gauge 는 "주기적으로 어딘가에서 값을 읽어 와야" 하기 때문에, 값 자체가 아니라 값을 읽어 올 함수 + 관찰할 객체 를 같이 넘겨요.
// 컬렉션 크기를 Gauge 로 노출 — 권장 패턴
Queue<String> orderQueue = new LinkedBlockingQueue<>();
Gauge.builder("order.queue.size", orderQueue, Collection::size)
.tag("queue", "processing")
.description("Current number of orders waiting")
.register(registry);
Collection::size 가 값을 읽어 오는 함수예요. Micrometer 는 이 함수를 주기적으로 호출해서 그 순간의 값을 기록해요.
Gauge 는 내부적으로 관찰 대상 객체를 약한참조(WeakReference) 로 잡아요. 그래야 GC 가 원래 객체를 정상적으로 수집할 수 있거든요. 그런데 람다 캡처나 강한참조를 실수로 넘기면, Micrometer 가 그 참조를 붙들고 있어서 GC 가 객체를 못 수거하는 메모리 누수가 생겨요.
특히 Gauge.builder("name", () -> someObj.getValue()) 처럼 람다가 외부 객체를 캡처할 때 발생해요. 항상 Gauge.builder("name", targetObj, obj -> obj.getValue()) 형태로 객체를 두 번째 인자로 명시하는 게 안전해요.
Gauge 의 약한참조 동작 확인
// ❌ 람다 캡처 패턴 — 누수 위험
AtomicInteger count = new AtomicInteger(0);
Gauge.builder("active.count", () -> count.get()) // count 강한참조 캡처
.register(registry);
// ✅ 객체 명시 패턴 — 안전
AtomicInteger count = new AtomicInteger(0);
Gauge.builder("active.count", count, AtomicInteger::get) // 약한참조로 감쌈
.register(registry);
약한참조로 감싼 경우, 만약 count 가 다른 곳에서 참조를 잃으면 Gauge 가 NaN 을 리포트하기 시작해요. 이게 "객체가 GC 됐다"는 신호예요. 반가운 신호는 아니지만 누수보다는 낫죠.
Gauge 단순 값 설정
외부 객체 없이 단순 숫자를 Gauge 로 박고 싶을 때는 AtomicInteger 나 AtomicLong 을 활용해요.
AtomicInteger activeConnections = new AtomicInteger(0);
Gauge.builder("db.connections.active", activeConnections, AtomicInteger::get)
.description("Active DB connections")
.register(registry);
// 다른 곳에서 값 변경
activeConnections.incrementAndGet(); // 커넥션 열림
activeConnections.decrementAndGet(); // 커넥션 닫힘
Timer — 얼마나 걸렸나
Timer 는 실행 시간을 측정하는 Meter 예요. Counter 와 Timer 를 합쳐 놓은 거라고 보면 돼요. 한 번 record() 할 때마다 내부적으로 세 가지 값이 동시에 갱신돼요.
count (실행 횟수) · totalTime (누적 실행 시간) · max (최근 구간 최댓값)
그래서 "초당 처리량"도 Timer 하나로 뽑아낼 수 있고, "평균 latency"도 totalTime / count 로 구해요. Prometheus 에서는 _count, _sum, _bucket suffix 로 분리돼서 나와요.
// 기본 Timer 사용 — record(Runnable)
Timer timer = Timer.builder("orders.processing.time")
.tag("type", "online")
.description("Order processing latency")
.register(registry);
// 방법 1: record(Runnable) — 가장 간단
timer.record(() -> processOrder(request));
// 방법 2: record(Callable<T>) — 반환값 필요할 때
Order result = timer.recordCallable(() -> processOrder(request));
// 방법 3: Timer.Sample — 시작·종료를 분리해야 할 때
Timer.Sample sample = Timer.start(registry);
try {
Order order = processOrder(request);
sample.stop(timer);
return order;
} catch (Exception e) {
sample.stop(Timer.builder("orders.processing.time")
.tag("type", "online")
.tag("error", "true")
.register(registry));
throw e;
}
Timer.Sample 은 시작과 종료 시점이 메서드 경계를 넘어야 할 때 유용해요. 예를 들어 요청 수신 시점에 Timer.start() 를 찍고, 응답을 다 보낸 다음에 sample.stop() 을 호출하는 패턴이 그래요.
Timer 의 나노초 정밀도
Timer 는 내부적으로 나노초(nanosecond) 정밀도로 시간을 기록해요. 수동으로 System.currentTimeMillis() 를 계산해서 넘기는 것보다 훨씬 정확하고, Exception 이 터졌을 때 finally 에서 처리하는 실수도 없어요.
// ❌ 이렇게 하지 마세요 — 수동 계산 + Exception 누락 위험
long start = System.currentTimeMillis();
try {
processOrder(request);
} finally {
long elapsed = System.currentTimeMillis() - start;
timer.record(elapsed, TimeUnit.MILLISECONDS); // ms 단위 정밀도 손실
}
// ✅ timer.record() 에 맡기세요
timer.record(() -> processOrder(request)); // 나노초 정밀도, Exception 안전
percentile(p50·p95·p99) 과 histogram 설정은 3편에서 자세히 다뤄요.
DistributionSummary — 분포를 잰다
DistributionSummary 는 Timer 와 구조가 비슷해요. 내부적으로 count · sum · max 를 관리해요. 차이는 딱 한 가지 — 시간이 아닌 수량 의 분포를 잴 때 써요.
"이번 API 응답이 몇 byte 였나", "이번 배치에서 몇 건을 처리했나", "이번 주문 금액이 얼마였나" — 이런 게 다 DistributionSummary 의 자리예요. Timer 는 내부적으로 나노초를 기준으로 설계돼 있어서 bytes 나 건수를 넣으면 단위가 맞지 않아요.
// 응답 크기(byte) 측정
DistributionSummary responseSizeBytes = DistributionSummary.builder("http.response.size")
.tag("uri", "/api/orders")
.baseUnit("bytes")
.description("Size of HTTP responses in bytes")
.register(registry);
// 응답 보낼 때마다 기록
responseSizeBytes.record(response.getContentLength());
// 배치 처리 건수 측정
DistributionSummary batchSize = DistributionSummary.builder("batch.processed.count")
.tag("job", "order-settlement")
.description("Number of records processed per batch execution")
.register(registry);
batchSize.record(processedCount); // 각 배치 실행 후
baseUnit("bytes") 는 Prometheus 메트릭 이름 접미사에 영향을 줘요. 설정하면 http_response_size_bytes_sum 처럼 단위가 이름에 붙어서 대시보드에서 헷갈림이 줄어요.
DistributionSummary 도 Timer 처럼 percentile 과 histogram 설정을 동일한 API 로 지원해요.
LongTaskTimer — 진행 중인 작업
Counter 나 Timer 는 "작업이 끝난 다음" 에 기록해요. 그런데 어떤 작업은 완료까지 몇 분, 심하면 몇 시간이 걸리기도 하잖아요. 배치 Job, 대용량 파일 처리, 외부 API 폴링 — 이런 작업이 지금 몇 개나 돌고 있는지, 각각 얼마나 됐는지 알아야 할 때가 있어요.
LongTaskTimer 는 그런 상황을 위해 설계됐어요. 작업이 시작되는 시점 에 기록하고, 끝나는 시점까지 active count 와 경과 시간을 실시간으로 리포트해요.
LongTaskTimer longTaskTimer = LongTaskTimer.builder("batch.settlement.active")
.tag("job", "daily-settlement")
.description("Long-running settlement batch tasks")
.register(registry);
// 작업 시작 시점에 sample 열기
LongTaskTimer.Sample sample = longTaskTimer.start();
try {
runSettlementBatch(); // 몇 분 걸리는 작업
} finally {
sample.stop(); // 완료 시 닫기
}
작업이 진행 중일 때 Prometheus 에서 읽으면 active_tasks 와 duration_sum 이 찍혀 있어요. 30분이 넘게 돌고 있다면 대시보드에서 즉시 보여서 조기에 알아챌 수 있죠.
여기서 주의할 점이 하나 있어요. LongTaskTimer 는 완료된 작업의 latency 를 집계하지 않아요. 완료된 건 Timer 로 따로 찍어야 해요. LongTaskTimer 는 순전히 "지금 얼마나 많은 작업이 얼마나 오래 돌고 있느냐"만 봐요.
Function 계열 — 외부 상태를 관찰
FunctionCounter, FunctionTimer, TimeGauge, MultiGauge 는 공통점이 있어요. 앱 내부에서 Micrometer 가 직접 측정하는 게 아니라, 이미 어딘가에 쌓여 있는 카운트·시간 값을 Micrometer 메트릭으로 노출 하는 역할이에요.
JVM 내장 MXBean, 서드파티 라이브러리 내부 카운터, DBCP 풀 통계 — 이런 것들이 이미 값을 들고 있어요. Micrometer 는 그 값을 주기적으로 읽어서 자기 레지스트리에 싣는 거예요.
FunctionCounter
// 서드파티 라이브러리의 누적 카운터를 Micrometer 로 노출
SomeThirdPartyClient client = ...; // 내부에 long processedCount 를 보유
FunctionCounter.builder("thirdparty.processed", client, SomeThirdPartyClient::getProcessedCount)
.description("Cumulative count from third-party client")
.register(registry);
Micrometer 는 getProcessedCount() 를 주기적으로 호출해서 이전 값과 차이를 Counter 처럼 다뤄요. 직접 increment() 를 호출할 수 없고, 외부에서 읽어 오기만 해요.
FunctionTimer
// 누적 호출 횟수와 누적 시간 둘 다 외부에서 읽어 오는 경우
FunctionTimer.builder("cache.get.time",
cache,
Cache::getHitCount, // count 함수
Cache::getTotalGetTime, // totalTime 함수 (나노초 또는 지정 단위)
TimeUnit.NANOSECONDS)
.description("Cache get operation timing")
.register(registry);
TimeGauge
일반 Gauge 와 같은데, 값의 단위가 시간인 경우예요. Micrometer 가 내부적으로 단위 변환을 처리해 줘요.
// 캐시 TTL 남은 시간을 초 단위로 노출
TimeGauge.builder("cache.ttl.remaining", cache, TimeUnit.SECONDS, Cache::getTtlSeconds)
.description("Remaining TTL of the cache in seconds")
.register(registry);
MultiGauge
데이터베이스 테이블별 행 수처럼, 동일한 구조의 Gauge 를 여러 레코드에 대해 한꺼번에 갱신 해야 할 때 써요.
MultiGauge tableRowCounts = MultiGauge.builder("table.row.count")
.description("Approximate row count per table")
.register(registry);
// 주기적으로 갱신
tableRowCounts.register(
Stream.of("orders", "users", "products")
.map(table -> MultiGauge.Row.of(Tags.of("table", table), getRowCount(table)))
.collect(Collectors.toList()),
true // overwrite 여부
);
각 Row 가 별도의 시계열이 돼요. 테이블이 추가되면 다음 갱신 시 자동으로 새 시계열이 생겨요.
어떤 타입을 언제 쓰나
Meter 타입 선택이 처음엔 애매하게 느껴지는데, 사실 판단 기준이 꽤 명확해요.
| 상황 | 추천 Meter |
|---|---|
| 요청 수, 에러 수, 처리 완료 수 (누적 총합) | Counter |
| 현재 큐 길이, 현재 커넥션 수, 현재 스레드 수 | Gauge |
| API 응답 시간, DB 쿼리 시간, 메서드 실행 시간 | Timer |
| 응답 크기(bytes), 배치 처리 건수, 요청 페이로드 크기 | DistributionSummary |
| 아직 진행 중인 배치·스케줄 작업의 현재 상태 | LongTaskTimer |
| 서드파티·JVM 내부 누적 카운터를 외부로 노출 | FunctionCounter |
| 서드파티·JVM 내부 누적 시간값을 외부로 노출 | FunctionTimer |
| 시간 값을 Gauge 로 노출 (단위 변환 필요) | TimeGauge |
| 동일 구조 Gauge 를 여러 레코드에 동시 노출 | MultiGauge |
가장 많이 헷갈리는 조합은 Counter vs Gauge 와 Timer vs DistributionSummary 예요.
Counter vs Gauge — "지금까지 총 몇 번?"이면 Counter, "지금 이 순간 몇 개?"이면 Gauge예요. 내려갈 수 있으면 Gauge, 올라가기만 하면 Counter.
Timer vs DistributionSummary — "단위가 시간이면" Timer, "시간 아닌 수량이면" DistributionSummary. 응답 바이트 수를 Timer 에 넣으면 백엔드가 나노초로 오해할 수 있어요.
함정 정리
사고 1: Counter 에 음수 increment 시도
원인 — "취소된 주문"을 표현하려고 counter.increment(-1.0) 을 호출해요.
해결 — Counter 는 음수 increment 를 허용하지 않아요(IllegalArgumentException 발생). "취소된 주문 수"는 별도 Counter 로 만들어야 해요. orders.cancelled Counter 를 따로 두는 게 맞는 설계죠.
사고 2: Gauge 람다 캡처 → 메모리 누수
원인 — Gauge.builder("size", () -> list.size()) 처럼 람다가 외부 객체를 강한참조로 캡처해요.
해결 — Gauge.builder("size", list, Collection::size) 로 객체를 두 번째 인자로 명시해요. Micrometer 가 약한참조로 잡아서 GC 정상 작동을 보장해요.
사고 3: Timer 를 수동으로 시간 계산해서 넘기기
원인 — System.currentTimeMillis() 차이를 구해 timer.record(elapsed, TimeUnit.MILLISECONDS) 로 넣어요.
해결 — timer.record(() -> {...}) 에 맡기면 됩니다. 나노초 정밀도, Exception 안전 처리, 시작·종료 관리 전부 Timer 가 해요.
사고 4: DistributionSummary 에 시간 단위 값을 넣음
원인 — "배치 처리 시간"을 DistributionSummary 로 기록해요. 값이 밀리초 단위라 숫자가 엄청 커요.
해결 — 처리 시간은 Timer 가 맞아요. Timer 는 내부적으로 나노초로 기록하고 백엔드가 단위를 알고 있어서 쿼리가 제대로 돼요. DistributionSummary 는 순수 수량(bytes, 건수 등) 전용이에요.
사고 5: LongTaskTimer 를 완료된 작업 latency 에 사용
원인 — 배치 작업 latency 를 LongTaskTimer 로 찍어요.
해결 — LongTaskTimer 는 "진행 중" 상태만 추적해요. 완료된 배치의 소요 시간을 알고 싶으면 Timer 를 함께 써야 해요. 둘을 같이 사용하면 "지금 몇 개가 돌고 있느냐"(LongTaskTimer) + "완료된 배치의 평균 시간은"(Timer) 두 질문을 동시에 답할 수 있어요.
사고 6: FunctionCounter 에 감소하는 값을 넘기기
원인 — 누적값이 아닌 현재 값(증감 가능한 값)을 FunctionCounter 에 연결해요.
해결 — FunctionCounter 는 단조 증가하는 누적 카운트를 읽어야 해요. 값이 내려갈 수 있는 상태는 Gauge 나 FunctionGauge 로 처리해야 해요. 값이 내려가면 Micrometer 가 전 주기보다 값이 작아졌다고 판단해 delta 를 0으로 처리해요.
사고 7: Gauge 약한참조로 객체가 GC 되어 NaN 리포트
원인 — Gauge 에 연결된 객체가 다른 곳에서 강한참조를 잃으면 GC 가 수거해요. 그 후 Gauge 가 NaN 을 리포트해요.
해결 — Gauge 가 관찰할 객체는 서비스·컴포넌트 필드처럼 생명주기가 명확한 곳에 보관해야 해요. 로컬 변수에만 있는 객체를 Gauge 에 연결하면 GC 에 금방 수거됩니다.
사고 8: Timer.Sample stop 을 finally 밖에서 호출
원인 — 정상 흐름에서만 sample.stop() 을 부르고, Exception 이 터지면 sample 이 닫히지 않아요.
해결 — sample.stop() 은 항상 finally 블록에서 호출해야 해요. 또는 처음부터 timer.record(() -> {...}) 를 쓰면 이 문제를 원천 차단할 수 있어요.
사고 9: DistributionSummary baseUnit 없이 byte 값 기록
원인 — DistributionSummary.builder("response.size") 에서 baseUnit("bytes") 를 빠뜨려요.
해결 — baseUnit("bytes") 를 붙이면 Prometheus 메트릭 이름이 response_size_bytes_sum 처럼 단위를 포함해요. 대시보드에서 단위 혼동이 줄어들고 팀 내 표준화에도 도움돼요.
사고 10: 같은 이름·같은 태그로 다른 Meter 타입 중복 등록
원인 — 코드 여러 군데에서 Counter.builder("orders.total") 와 Gauge.builder("orders.total") 를 같은 이름으로 등록해요.
해결 — MeterRegistry 는 이름+태그 조합이 같으면 기존 Meter 를 반환해요. 하지만 타입이 다르면 IllegalArgumentException 이 발생해요. Meter 이름은 타입을 반영하도록 설계하는 게 좋아요 — orders.created.count (Counter) vs orders.queue.size (Gauge) 처럼요.
운영 권장 패턴
Pattern 1: Counter + Gauge 세트로 처리 파이프라인 모니터링
@Service
public class OrderPipelineService {
private final Counter receivedCounter;
private final Counter processedCounter;
private final Counter failedCounter;
private final AtomicInteger inProgressGauge;
public OrderPipelineService(MeterRegistry registry) {
this.receivedCounter = Counter.builder("orders.received")
.description("Orders received into pipeline").register(registry);
this.processedCounter = Counter.builder("orders.processed")
.description("Successfully processed orders").register(registry);
this.failedCounter = Counter.builder("orders.failed")
.description("Failed order processing attempts").register(registry);
this.inProgressGauge = new AtomicInteger(0);
Gauge.builder("orders.in_progress", inProgressGauge, AtomicInteger::get)
.description("Orders currently being processed").register(registry);
}
public void process(Order order) {
receivedCounter.increment();
inProgressGauge.incrementAndGet();
try {
doProcess(order);
processedCounter.increment();
} catch (Exception e) {
failedCounter.increment();
throw e;
} finally {
inProgressGauge.decrementAndGet();
}
}
}
이 패턴은 "총 몇 개 받았나", "총 몇 개 성공했나", "총 몇 개 실패했나", "지금 처리 중인 게 몇 개나"를 하나의 서비스에서 한꺼번에 볼 수 있게 해줘요.
Pattern 2: Timer.Sample 로 비동기 흐름 측정
@Service
public class AsyncOrderService {
private final Timer orderTimer;
public AsyncOrderService(MeterRegistry registry) {
this.orderTimer = Timer.builder("orders.async.time")
.description("Async order processing latency")
.register(registry);
}
public CompletableFuture<Order> processAsync(CreateOrderRequest req) {
Timer.Sample sample = Timer.start(); // 시작점 저장
return CompletableFuture
.supplyAsync(() -> createOrder(req))
.whenComplete((order, ex) -> sample.stop(orderTimer)); // 완료 시 stop
}
}
비동기 흐름에서는 record(() -> {...}) 가 안 맞아요. Timer.Sample 을 시작점에 만들어 두고, 비동기 콜백에서 stop() 을 호출하는 패턴이 맞아요.
Pattern 3: MeterBinder 로 서드파티 라이브러리 메트릭 통합
@Component
public class HikariPoolMetricsBinder implements MeterBinder {
private final HikariDataSource dataSource;
public HikariPoolMetricsBinder(HikariDataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public void bindTo(MeterRegistry registry) {
HikariPoolMXBean pool = dataSource.getHikariPoolMXBean();
Gauge.builder("hikari.connections.active", pool, HikariPoolMXBean::getActiveConnections)
.description("Active HikariCP connections").register(registry);
Gauge.builder("hikari.connections.idle", pool, HikariPoolMXBean::getIdleConnections)
.description("Idle HikariCP connections").register(registry);
Gauge.builder("hikari.connections.pending", pool, HikariPoolMXBean::getThreadsAwaitingConnection)
.description("Threads waiting for a connection").register(registry);
}
}
MeterBinder 를 @Component 로 등록하면 Spring Boot 가 MeterRegistry 와 함께 자동 바인딩해요.
Pattern 4: DistributionSummary 로 SLO 위반 추적
DistributionSummary responseSizeKB = DistributionSummary.builder("api.response.size")
.baseUnit("kilobytes")
.tag("endpoint", "/api/export")
.sla(100, 500, 1000) // 100KB, 500KB, 1MB 버킷
.publishPercentiles(0.5, 0.95, 0.99)
.register(registry);
// 응답 보낼 때마다
responseSizeKB.record(response.getContentLength() / 1024.0);
sla() 버킷을 설정하면 Prometheus 에서 "100KB 이하 응답 비율"을 쿼리로 뽑을 수 있어요. 응답 크기 SLO 관리에 유용해요.
Pattern 5: 스케줄 작업에 LongTaskTimer + Timer 이중 적용
@Scheduled(cron = "0 0 2 * * *") // 매일 새벽 2시
public void runDailySettlement() {
LongTaskTimer.Sample longSample = longTaskTimer.start(); // 진행 중 추적
Timer.Sample timerSample = Timer.start(registry); // 완료 후 latency 기록
try {
doSettlement();
} finally {
longSample.stop();
timerSample.stop(completionTimer);
}
}
이중 적용이면 "지금 몇 개의 배치가 돌고 있냐"(LongTaskTimer)와 "완료된 배치는 평균 몇 분 걸렸냐"(Timer)를 동시에 알 수 있어요.
Pattern 6: Gauge 로 JVM 외부 리소스 상태 노출
@Component
public class ExternalResourceGauges implements MeterBinder {
private final RedisConnectionPool redisPool;
private final KafkaProducer<?, ?> kafkaProducer;
@Override
public void bindTo(MeterRegistry registry) {
// Redis 풀 상태
Gauge.builder("redis.pool.active", redisPool, RedisConnectionPool::getNumActive)
.description("Active Redis connections").register(registry);
// Kafka 프로듀서 버퍼 사용률
FunctionGauge.builder("kafka.producer.buffer.available.bytes",
kafkaProducer.metrics(), metrics ->
metrics.get(new MetricName("buffer-available-bytes", "producer-metrics", "", Collections.emptyMap()))
.map(Metric::metricValue)
.map(v -> ((Double) v))
.orElse(0.0))
.description("Available bytes in Kafka producer buffer")
.register(registry);
}
}
시험 직전 한 번 더 — Meter 타입 압축 노트
Counter
- 단조 증가만 허용 — 음수 increment 불가
- 총 요청 수·에러 수·처리 완료 수 등 누적 총합에 사용
Counter.builder("name").tag(...).register(registry),increment(),increment(n)- rate 계산은 Counter 가 아니라 Prometheus
rate()함수의 몫
Gauge
- 올라갔다 내려갔다 OK — 현재 순간의 상태 스냅샷
- 큐 크기·커넥션 수·스레드 수·메모리 사용률에 사용
Gauge.builder("name", targetObj, obj -> obj.getValue())— 객체 두 번째 인자 명시 필수- 람다 캡처 강한참조 → GC 못 수거 → 메모리 누수
- 약한참조 GC 시 Gauge 는
NaN리포트
Timer
- count + totalTime + max 세 값 자동 동시 관리
- 실행 시간 측정 전용 — 내부 단위 나노초
timer.record(Runnable),timer.recordCallable(Callable),Timer.SampleTimer.Sample은 비동기·시작·종료 분리 흐름에서 사용- percentile·histogram 설정은 3편 주제
DistributionSummary
- Timer 와 동일 구조(count·sum·max)이나 시간 아닌 수량 전용
- 응답 크기(bytes)·배치 건수·금액 분포에 사용
baseUnit("bytes")설정하면 Prometheus 이름에 단위 자동 포함- 시간 값을 DistributionSummary 에 넣는 실수 → 단위 맞지 않음
LongTaskTimer
- 진행 중인 작업의 active count + duration 실시간 리포트
- 완료된 작업 latency 는 추적 안 함 — Timer 와 함께 사용해야
LongTaskTimer.Sample sample = longTaskTimer.start(),sample.stop()
Function 계열
- FunctionCounter — 외부 누적 카운트 노출 (단조 증가 값만)
- FunctionTimer — 외부 누적 카운트 + 시간 동시 노출
- TimeGauge — 시간 단위 Gauge (단위 변환 처리)
- MultiGauge — 동일 구조 Gauge 를 여러 레코드에 한 번에
흔한 사고
- Counter 에 음수 → IllegalArgumentException
- Gauge 람다 캡처 → 강한참조 → 메모리 누수
- Timer 수동 시간 계산 → 단위 실수, Exception 누락
- DistributionSummary 에 시간 단위 값 → 단위 혼동
- LongTaskTimer 를 완료 latency 에 오용
- FunctionCounter 에 감소 가능한 값 → delta 0 처리
- Timer.Sample stop() 을 finally 밖에서 호출
- Gauge GC 수거 후 NaN 리포트 인지 못함
- DistributionSummary baseUnit 누락 → 단위 불명확
타입 선택 핵심 기준
- "총 몇 번?" → Counter / "지금 몇 개?" → Gauge
- "얼마나 걸렸나?" → Timer / "크기·건수 분포?" → DistributionSummary
- "아직 진행 중인 작업?" → LongTaskTimer
- "외부 값 노출?" → Function 계열
공식 레퍼런스: Micrometer 공식 docs — Concepts 에서 각 Meter 타입의 전체 옵션을 확인할 수 있어요.
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
다음 글: