Micrometer란?
💡 Micrometer란?
- Micrometer는 Java 애플리케이션의 메트릭을 수집하기 위한 계측 라이브러리이다.
- 메트릭을 직접 장기간 저장하거나 대시보드로 시각화하지는 않는다.
- Prometheus, Datadog 등 다양한 모니터링 시스템에 연결할 수 있는 공통 API를 제공한다.
- 동일한 계측 코드를 유지하면서 모니터링 시스템을 변경할 수 있도록 설계되었다.
메트릭(Metric)
- 애플리케이션 상태를 숫자로 표현한 측정 데이터이다.
- 예: HTTP 요청 수, HTTP 응답 시간, JVM Heap 사용량, DB Connection 수
Micrometer가 해결하는 문제
- Prometheus와 Datadog 같은 모니터링 시스템은 서로 다른 계측 방식을 사용한다.
- Micrometer는 공통 API를 제공하여 애플리케이션 코드의 변경을 줄인다.
- 이를 통해 특정 모니터링 시스템에 강하게 종속되는 문제인 Vendor Lock-in을 줄일 수 있다.
역할을 구분하면
Micrometer는 애플리케이션 안에서 값을 측정하고, Prometheus는 그 값을 주기적으로 수집·저장하며, Grafana는 저장된 값을 그래프로 보여준다.
📖 공식 문서
- 문서명: Micrometer Documentation
- 위치: Introduction
- 근거: Micrometer는 여러 관측 시스템의 계측 클라이언트 위에 공통 facade를 제공한다.
- 링크: Micrometer Documentation 열기
💡 Meter란?
Meter는 Micrometer에서 메트릭을 측정하는 기본 단위이다. 무엇을 측정할 것인지에 따라 서로 다른 Meter를 사용하며, 모든 Meter는 MeterRegistry에 등록되어 관리된다.
| Meter | 측정하는 값 | 대표 사용 예시 |
|---|---|---|
| Counter | 누적 횟수 | 회원가입 수, 예외 발생 횟수 |
| Timer | 실행 시간과 실행 횟수 | HTTP 요청, DB 조회 시간 |
| Gauge | 현재 상태 값 | 메모리 사용량, Connection Pool 수 |
| DistributionSummary | 값의 크기와 분포 | 파일 크기, 응답 크기 |
| LongTaskTimer | 진행 중인 긴 작업의 시간 | 배치, 대용량 데이터 처리 |
| FunctionCounter | 함수가 반환하는 누적값 | 기존 객체의 누적값 노출 |
| FunctionTimer | 함수가 반환하는 횟수와 총 시간 | 기존 객체의 count와 time 노출 |
즉, Meter는 “무엇을 측정할 것인가”를 나타내는 추상적인 개념이고, Counter와 Timer는 Meter의 구체적인 종류이다.
📖 공식 문서
- 문서명: Micrometer Meters
- 위치: Concepts → Meters
- 링크: Micrometer Meters 열기
Timer란?
💡 Timer란?
- Timer는 Micrometer가 제공하는 Meter 중 하나이다.
- 짧게 실행되는 작업의 실행 시간(Latency)과 실행 횟수(Frequency)를 함께 측정한다.
- 한 번 시간을 재고 끝나는 스톱워치가 아니라, 같은 작업이 여러 번 실행될 때 측정 결과를 계속 누적하는 Meter이다.
- HTTP 요청 처리, DB 조회, Redis 조회, 외부 API 호출, 락 대기 시간 등에 사용할 수 있다.
Latency(지연 시간)는 작업이 시작된 순간부터 종료될 때까지 걸린 시간이다. 요청 처리에 120ms가 걸렸다면 해당 요청의 latency는 120ms이다.
Frequency(발생 빈도)는 동일한 작업이 몇 번 수행되었는지를 뜻한다. 상품 조회가 1분 동안 500번 실행되었다면 해당 구간의 실행 횟수는 500회이다.
📖 공식 문서
- 문서명: Micrometer Timers
- 위치: Concepts → Timers
- 근거: Timer는 짧은 작업의 latency와 발생 빈도를 측정하기 위한 Meter이다.
- 링크: Micrometer Timers 열기
💡 Timer가 기록하는 값
| 값 | 설명 |
|---|---|
| count | 측정이 완료된 횟수 |
| totalTime | 모든 실행 시간을 더한 누적 시간 |
| max | 관찰 시간 창 안에서 가장 오래 걸린 실행 시간 |
| Histogram | 실행 시간을 구간(bucket)별 누적 개수로 기록한 데이터(설정 시) |
| Percentile | P50, P95, P99 같은 백분위 값(설정 시) |
실행 시간이 100ms, 200ms, 300ms였다면 다음과 같이 누적된다.
- count = 3
- totalTime = 600ms
- mean = 600ms ÷ 3 = 200ms
- max = 300ms
Histogram과 Percentile은 Timer를 등록했다고 항상 자동으로 생성되는 값이 아니다. 분포 통계 설정을 활성화해야 하며, 실제 노출 방식은 사용하는 MeterRegistry 구현체에 따라 달라질 수 있다.
💡 왜 Timer를 사용하는가?
1. 기능 성공 여부와 성능은 서로 다른 문제이다.
두 요청이 모두 성공하더라도 하나는 80ms, 다른 하나는 2,300ms가 걸릴 수 있다. 사용자는 성공 여부뿐 아니라 기다린 시간도 체감하므로 실행 시간을 별도로 측정해야 한다.
2. 로그와 메트릭은 목적이 다르다.
long start = System.currentTimeMillis();
productService.findById(id);
long elapsed = System.currentTimeMillis() - start;
log.info("elapsed={}ms", elapsed);
로그는 특정 요청의 상세 상황을 확인할 때 유용하다. 그러나 전체 요청의 평균, P95, 최대값과 시간에 따른 변화를 보려면 로그를 다시 집계해야 한다. Timer는 반복 실행 결과를 메트릭으로 누적해 이러한 집계를 쉽게 만든다.
3. 시간을 잴 수 있는 작업은 Counter를 중복 생성할 필요가 없는 경우가 많다.
Counter는 횟수만 측정하지만 Timer는 시간과 횟수를 함께 기록한다. 단, timeout처럼 별도의 사건 자체를 명확하게 세어야 한다면 별도 Counter가 유용할 수 있다.
4. 평균만 보면 일부 매우 느린 요청이 가려질 수 있다.
99개가 100ms이고 1개가 10초라면 평균은 약 199ms이다. 평균만 보면 한 사용자가 10초를 기다렸다는 사실이 잘 드러나지 않으므로 P95, P99, max 같은 지표를 함께 본다.
Timer 등록과 MeterRegistry
💡 Timer.builder()로 등록하기
Timer를 사용하려면 먼저 MeterRegistry에 등록해야 한다. Builder는 Timer의 이름, 설명, 태그, 분포 통계 설정을 구성하고, register(meterRegistry)가 실제 Timer를 등록하고 반환한다.
Timer lockWaitTimer = Timer.builder("stock.lock.wait")
.description("재고 락 획득 대기 시간")
.tag("lock.type", "redisson")
.publishPercentiles(0.5, 0.95, 0.99)
.minimumExpectedValue(Duration.ofMillis(1))
.maximumExpectedValue(Duration.ofSeconds(3))
.register(meterRegistry);
위 설정의 의미는 다음과 같다.
| 설정 | 설명 |
|---|---|
stock.lock.wait |
메트릭 이름 |
description() |
메트릭의 목적을 설명한다. |
tag() |
값의 종류가 제한적인 분류 정보를 추가한다. |
publishPercentiles() |
이 애플리케이션 인스턴스에서 P50, P95, P99를 계산한다. |
minimumExpectedValue() |
예상 최소 시간을 분포 통계 설정에 제공한다. |
maximumExpectedValue() |
예상 최대 시간을 분포 통계 설정에 제공한다. |
register() |
설정된 Timer를 MeterRegistry에 등록하고 반환한다. |
예시 이름 주의http.server.requests는 Spring Boot가 자동으로 제공하는 HTTP 서버 메트릭 이름과 충돌하거나 혼동될 수 있다. 직접 만든 락 메트릭에는 stock.lock.wait처럼 목적이 분명한 별도 이름을 사용하는 편이 안전하다.
Prometheus에서 여러 인스턴스의 분포를 합칠 계획이라면 다음처럼 histogram을 등록할 수 있다.
Timer lockWaitTimer = Timer.builder("stock.lock.wait")
.description("재고 락 획득 대기 시간")
.publishPercentileHistogram()
.serviceLevelObjectives(
Duration.ofMillis(10),
Duration.ofMillis(50),
Duration.ofMillis(100),
Duration.ofMillis(300),
Duration.ofSeconds(1)
)
.minimumExpectedValue(Duration.ofMillis(1))
.maximumExpectedValue(Duration.ofSeconds(3))
.register(meterRegistry);
serviceLevelObjectives()는 “100ms 이하 요청 비율”처럼 팀이 중요하게 보는 경계의 bucket을 추가한다. 이는 percentile 자체를 지정하는 설정이 아니라 특정 시간 기준을 직접 관찰하기 위한 설정이다.
💡 왜 register()가 필요한가?
- Builder는 아직 Timer의 설정 정보를 구성하는 단계이다.
register()를 호출해야 MeterRegistry에서 관리되는 실제 Timer를 얻는다.- 같은 이름과 같은 태그 조합으로 다시 등록하면 Registry는 일반적으로 이미 등록된 Meter를 반환한다.
- 이름은 같아도 태그 값이 다르면 별도의 Meter ID로 관리된다.
Timer getTimer = Timer.builder("product.lookup")
.tag("method", "GET")
.register(meterRegistry);
Timer postTimer = Timer.builder("product.lookup")
.tag("method", "POST")
.register(meterRegistry);
위 두 Timer는 이름은 같지만 태그가 다르므로 서로 다른 Meter이다.
💡 MeterRegistry란?
MeterRegistry는 Meter를 등록하고 이름과 태그 조합으로 관리하며, Registry 구현체에 맞는 형태로 측정값을 제공하는 중심 컴포넌트이다.
- Counter, Timer, Gauge 같은 Meter를 생성·등록·조회한다.
- 같은 Meter ID가 불필요하게 중복 생성되지 않도록 관리한다.
- 각 Meter가 기록한 현재 누적 상태를 애플리케이션 메모리에서 관리한다.
- Prometheus Registry라면 scrape 가능한 형식으로 값을 노출할 수 있게 연결한다.
“MeterRegistry가 매 요청마다 Prometheus에 메트릭을 전송한다”라고 이해하면 안 된다. 일반적인 Prometheus 구성에서는 요청 처리 중 Timer가 메모리의 값을 갱신하고, Prometheus가 별도의 주기로 메트릭 엔드포인트를 조회한다.
📖 공식 문서
- 문서명: Micrometer Registry
- 위치: Concepts → Registry
- 링크: Micrometer Registry 열기
Percentile과 Histogram
💡 Percentile이란?
Percentile(백분위수)은 여러 측정값을 작은 값부터 정렬했을 때 전체 중 일정 비율이 어느 값 이하에 위치하는지 보여준다.
- P50 = 10ms: 요청의 약 50%가 10ms 이내에 완료되었다.
- P95 = 100ms: 요청의 약 95%가 100ms 이내에 완료되었다.
- P99 = 500ms: 요청의 약 99%가 500ms 이내에 완료되었다.
P95가 100ms라는 말은 평균이 100ms라는 뜻이 아니다. 요청의 약 95%가 100ms 이내에 완료되었고 나머지 약 5%는 그보다 오래 걸렸다는 뜻이다.
중요한 구분
요청 한 건에는 P95가 없다. 요청 한 건에는 10ms, 200ms처럼 실행 시간 하나만 있다. P95는 여러 요청의 실행 시간을 모은 분포에서 계산한다.
💡 publishPercentiles()
publishPercentiles()는 Micrometer가 애플리케이션 인스턴스 내부에서 지정된 percentile을 계산해 내보내도록 설정한다.
Timer.builder("stock.lock.wait")
.publishPercentiles(0.5, 0.95, 0.99)
.register(meterRegistry);
- 0.5는 P50, 0.95는 P95, 0.99는 P99를 의미한다.
- 사용자별 P95를 구한 뒤 평균 내는 방식이 아니다.
- 해당 인스턴스가 측정한 여러 요청의 분포에서 percentile을 계산한다.
- 단일 Spring Boot 인스턴스의 로컬 성능 실험에서 결과를 바로 확인하기 편하다.
한계: 서버별 percentile은 전체 percentile로 다시 합칠 수 없다.
| 서버 | 요청 수 | P95 |
|---|---|---|
| 서버 A | 10,000건 | 100ms |
| 서버 B | 10건 | 1,000ms |
두 P95의 평균 550ms는 전체 10,010건의 P95가 아니다. Percentile은 이미 분포를 하나의 경계값으로 압축한 결과이므로 단순 합산이나 평균으로 원래 분포를 복원할 수 없다.
💡 Bucket과 publishPercentileHistogram()
Bucket은 측정된 시간을 분류하는 시간 구간이다. Prometheus의 classic histogram bucket은 보통 누적형이므로 “100ms 이하” bucket에는 10ms, 50ms 이하 요청도 모두 포함된다.
| 요청 | 락 대기 시간 |
|---|---|
| A | 8ms |
| B | 40ms |
| C | 80ms |
| D | 250ms |
| E | 700ms |
| 누적 Bucket | 포함 요청 | 개수 |
|---|---|---|
| 10ms 이하 | A | 1 |
| 50ms 이하 | A, B | 2 |
| 100ms 이하 | A, B, C | 3 |
| 300ms 이하 | A, B, C, D | 4 |
| 1초 이하 | A, B, C, D, E | 5 |
stock_lock_wait_seconds_bucket{le="0.01"} 1
stock_lock_wait_seconds_bucket{le="0.05"} 2
stock_lock_wait_seconds_bucket{le="0.1"} 3
stock_lock_wait_seconds_bucket{le="0.3"} 4
stock_lock_wait_seconds_bucket{le="1.0"} 5
le는 less than or equal, 즉 “이 값 이하”라는 뜻이다.
publishPercentileHistogram()은 이러한 bucket을 모니터링 시스템에 노출할 수 있게 한다.
Timer.builder("stock.lock.wait")
.publishPercentileHistogram()
.register(meterRegistry);
- 각 애플리케이션 인스턴스는 같은 경계의 bucket별 누적 개수를 제공한다.
- Prometheus는 같은 bucket끼리 합산할 수 있다.
- 합산된 전체 분포를 기반으로 다중 인스턴스의 P95, P99 근삿값을 계산할 수 있다.
- Bucket 경계 사이의 실제 값은 알 수 없으므로 histogram percentile은 근삿값이다.
| 구분 | publishPercentiles() | publishPercentileHistogram() |
|---|---|---|
| 계산 주체 | 애플리케이션 | Prometheus 같은 백엔드 |
| 노출 데이터 | 계산된 P50, P95, P99 | Bucket별 누적 개수 |
| 서버 간 집계 | 전체 percentile로 재집계 불가 | 동일 bucket을 합산해 재집계 가능 |
| 적합한 경우 | 단일 인스턴스 실험, 개별 인스턴스 확인 | 다중 인스턴스 운영 집계 |
두 설정을 동시에 사용할 수도 있지만 분포 통계 메모리 비용과 노출 시계열 수가 늘 수 있다. 단일 인스턴스 포트폴리오 실험이라면 publishPercentiles()만으로 시작하고, 다중 인스턴스 및 장기 시계열 운영이 필요해질 때 histogram과 Prometheus를 도입하는 선택도 충분히 타당하다.
📖 공식 문서
- 문서명: Micrometer Histograms and Percentiles
- 위치: Concepts → Histograms and Percentiles
- 링크: Micrometer Histograms and Percentiles 열기
Timer 시간 측정 방법
Timer를 등록한 다음에는 실제 작업 시간을 기록해야 한다. 대표적인 방법은 record(), Timer.Sample, @Timed 세 가지이다.
| 방법 | 측정 범위 | 적합한 상황 |
|---|---|---|
| record() | 하나의 코드 블록 | Timer가 미리 정해져 있고 시작·종료가 붙어 있을 때 |
| Timer.Sample | 개발자가 정한 시작부터 종료까지 | 결과별 태그 또는 세부 구간 측정이 필요할 때 |
| @Timed | 메서드 전체 | 메서드 경계를 간단히 자동 측정할 때 |
💡 1. record()
record()는 Timer가 코드 블록 실행 전후의 시간을 측정해 같은 Timer에 기록하는 가장 단순한 방법이다.
Timer databaseTimer = Timer.builder("stock.database.decrease")
.publishPercentiles(0.5, 0.95, 0.99)
.register(meterRegistry);
databaseTimer.record(() -> {
stockRepository.decrease(productId, quantity);
});
반환값이 있는 작업은 recordCallable()을 사용할 수 있다.
Product product = databaseTimer.recordCallable(() ->
productRepository.findById(productId)
.orElseThrow()
);
개념적인 동작 순서는 다음과 같다.
- 시작 시간을 확인한다.
- 현재 호출 스레드에서 전달받은 작업을 실행한다.
- 성공하거나 예외로 종료되면 경과 시간을 계산한다.
- Timer의 count와 totalTime 등에 결과를 기록한다.
주의: record()는 작업을 비동기로 바꾸지 않는다. 내부 작업이 2초 걸리면 호출한 스레드도 그 작업이 끝날 때까지 실행을 계속하지 못한다. Timer가 추가하는 비용과 실제 측정 대상 작업의 실행 시간은 구분해야 한다.
💡 2. Timer.Sample
Timer.Sample은 시작 시각을 보관했다가, 작업이 끝난 뒤 선택한 Timer에 경과 시간을 기록하는 측정용 객체이다. Sample 자체는 count나 totalTime을 누적하는 Timer가 아니다.
Timer.Sample = 시작 시각을 들고 있는 스톱워치
Timer = 측정 결과를 계속 누적하는 기록장
Sample은 결과를 알기 전에 측정을 시작하고, 결과를 확인한 뒤 태그를 선택해야 할 때 특히 유용하다.
Timer.Sample waitSample = Timer.start(meterRegistry);
String result = "error";
try {
boolean acquired = lock.tryLock(
1,
3,
TimeUnit.SECONDS
);
result = acquired ? "acquired" : "timeout";
if (!acquired) {
throw new StockLockTimeoutException();
}
} catch (InterruptedException e) {
result = "interrupted";
Thread.currentThread().interrupt();
throw new IllegalStateException("락 획득 중 인터럽트 발생", e);
} finally {
waitSample.stop(
meterRegistry.timer(
"stock.lock.wait",
"result",
result
)
);
}
위 코드는 tryLock() 직전부터 성공, 타임아웃 또는 인터럽트 결과가 정해질 때까지의 락 대기 시간을 기록한다.
락을 실제로 보유한 전체 시간은 락 획득 직후 Sample을 시작하고 unlock() 완료 뒤 중지한다.
Timer.Sample holdSample = Timer.start(meterRegistry);
try {
stockRepository.decrease(productId, quantity);
} finally {
try {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
} finally {
holdSample.stop(
meterRegistry.timer("stock.lock.hold")
);
}
}
이 측정 범위는 다음과 같다.
락 획득 성공
→ 임계 구역의 재고 차감 작업
→ unlock() 실행 완료
→ stock.lock.hold에 기록
- Sample을 시작하고
stop()하지 않으면 아무 Timer에도 기록되지 않는다. - 예외 경로에서도 정확히 한 번 중지되도록 흐름을 설계해야 한다.
- 동일한 Sample을 여러 번 중지하는 구조는 피해야 한다.
- 결과 태그는
acquired,timeout,interrupted처럼 종류가 제한된 값만 사용한다.
💡 3. @Timed
@Timed는 애너테이션이 붙은 메서드의 실행 시간을 AOP로 측정한다. 개발자가 메서드 안에서 Timer의 시작과 종료 코드를 직접 작성하지 않아도 된다는 장점이 있다.
@Timed(
value = "stock.decrease.total",
percentiles = {0.5, 0.95, 0.99}
)
public void decreaseStock(Long productId, int quantity) {
// 락 획득, 재고 차감, 락 반환
}
호출자
↓
Spring AOP Proxy
↓ 시작 시간 기록
실제 @Timed 메서드 실행
↓ 종료 시간 기록
Timer에 경과 시간 누적
@Timed가 실제로 동작하려면 현재 프로젝트의 Spring Boot와 Micrometer 구성에 맞는 애너테이션 계측 설정이 필요하다. Micrometer의 TimedAspect를 직접 사용할 경우에는 다음처럼 Bean으로 등록할 수 있다.
@Configuration
public class MetricsConfig {
@Bean
public TimedAspect timedAspect(MeterRegistry meterRegistry) {
return new TimedAspect(meterRegistry);
}
}
버전과 구성 확인 필요
애너테이션 계측 활성화 방식과 필요한 의존성은 Spring Boot 및 Micrometer 버전에 따라 달라질 수 있다. 실제 프로젝트에서는 사용 중인 버전의 공식 문서를 기준으로 확인해야 한다.
@Timed의 측정 범위
메서드 진입
→ 락 획득 대기
→ 락 획득
→ 재고 차감
→ 락 반환
→ 메서드 종료
따라서 @Timed 하나만 사용하면 전체 작업이 느리다는 사실은 알 수 있지만, 락 대기와 락 점유 중 어느 구간이 병목인지는 분리하기 어렵다.
Self-invocation 주의
@Service
public class StockService {
public void processOrder() {
decreaseStock(); // 같은 객체 내부 호출
}
@Timed("stock.decrease.total")
public void decreaseStock() {
// 재고 차감
}
}
같은 객체 안에서 직접 호출하면 Spring AOP Proxy를 거치지 않으므로 @Timed가 적용되지 않을 수 있다. 다른 Spring Bean이 프록시를 통해 호출하는 구조인지 확인해야 한다.
비동기 작업 주의
메서드가 CompletableFuture, Reactor Mono·Flux 등을 실제 완료 전에 반환한다면 단순 메서드 경계 측정과 실제 비동기 완료 시간은 다를 수 있다. 비동기 작업은 사용하는 계측 방식이 완료 신호까지 추적하는지 별도로 확인해야 한다.
💡 record(), Timer.Sample, @Timed 비교
| 구분 | record() | Timer.Sample | @Timed |
|---|---|---|---|
| 범위 | 코드 블록 | 직접 지정 | 메서드 전체 |
| Timer 선택 | 시작 전 | 종료 시 선택 가능 | 애너테이션 설정 |
| 동적 결과 태그 | 직접 분기 필요 | 적합 | 세밀한 제어는 제한적 |
| AOP 필요 | 아니오 | 아니오 | 예 |
| Self-invocation 영향 | 없음 | 없음 | 있음 |
| 대표 사례 | DB 호출 한 구간 | 락 대기·점유 시간 | 서비스 메서드 전체 |
동시성 처리 구간 측정
락 성능을 분석하려면 락 대기 시간과 락 점유 시간을 구분해야 한다.
| 메트릭 | 측정 범위 | 권장 방식 |
|---|---|---|
stock.decrease.total |
메서드 진입부터 종료까지 | @Timed 또는 기존 HTTP 자동 계측 |
stock.lock.wait |
락 획득 시도부터 결과 반환까지 | Timer.Sample |
stock.lock.hold |
락 획득부터 unlock 완료까지 | Timer.Sample |
락 획득 시도
↓ stock.lock.wait 시작
다른 요청의 락 반환까지 대기
↓ stock.lock.wait 종료
락 획득
↓ stock.lock.hold 시작
재고 조회 및 차감
락 반환 완료
↓ stock.lock.hold 종료
결과 해석 예시 1
- 전체 작업 P95 = 900ms
- 락 대기 P95 = 800ms
- 락 점유 P95 = 70ms
락을 잡은 뒤의 작업은 비교적 빠르지만, 여러 요청이 같은 락을 얻기 위해 오래 기다리는 상황으로 해석할 수 있다. 같은 키에 트래픽이 집중되는지, 락 범위가 너무 넓은지, 재시도가 경쟁을 늘리는지 확인한다.
결과 해석 예시 2
- 전체 작업 P95 = 900ms
- 락 대기 P95 = 20ms
- 락 점유 P95 = 850ms
락 경쟁은 심하지 않지만 락 안의 DB 쿼리, 트랜잭션 또는 비즈니스 로직이 오래 걸리는 상황으로 해석할 수 있다. 임계 구역에 외부 API 호출이나 불필요한 작업이 포함됐는지 확인한다.
시간만으로는 부족하다.
락 획득 timeout의 횟수나 비율도 함께 확인해야 한다. Timer의 result=timeout count를 이용하거나 사건 자체를 명확히 분리해야 한다면 Counter를 추가할 수 있다.
주의 사항
💡 전체 작업과 세부 작업은 목적에 맞게 측정한다.
- 하나의 Timer로 전체 시간을 측정하면 어느 단계가 느린지 알기 어렵다.
- 모든 메서드에 Timer를 붙이면 비용과 복잡도가 커진다.
- 성능상 중요하거나 외부 의존성이 있거나 실제 병목 가능성이 있는 경계를 우선 측정한다.
- HTTP 요청 전체 시간은 Spring Boot 자동 계측이 이미 제공하는지 먼저 확인한다.
💡 태그는 값의 종류가 제한적인 경우에만 사용한다.
- Meter는 이름과 태그 조합으로 식별된다.
result=success|failure, HTTP method처럼 값의 종류가 제한된 분류가 적절하다.- userId, orderId, traceId, 이메일, 전체 query string은 태그로 사용하지 않는다.
- 태그 값의 종류가 많아지는 고카디널리티는 Meter 수, 메모리 사용량, 저장 비용을 급격히 늘릴 수 있다.
💡 Timer만으로 병목 원인을 확정할 수 없다.
- Timer는 어느 구간이 얼마나 느린지 보여준다.
- 왜 느린지는 로그, 분산 추적, 프로파일러, SQL 실행 계획, 스레드 덤프, Connection Pool 상태 등을 함께 분석해야 알 수 있다.
- Timer는 성능 저하를 발견하고 범위를 좁히는 도구이지 원인을 자동 진단하는 도구는 아니다.
💡 현재 프로젝트 범위에 맞는 선택을 한다.
- 단일 Spring Boot 인스턴스에서 k6로 통제된 성능 실험을 한다면
publishPercentiles(0.5, 0.95, 0.99)로도 목적을 달성할 수 있다. - 다중 인스턴스의 값을 통합하거나 시간에 따른 변화를 저장하고 경보를 만들 필요가 생기면 histogram과 중앙 메트릭 시스템 도입 가치가 커진다.
- MSA라고 해서 Prometheus라는 특정 제품이 필수인 것은 아니다. 다만 분산된 서비스의 메트릭, 로그, 트레이스를 중앙에서 관찰할 체계는 중요하다.
핵심 정리
- Timer는 짧은 작업의 실행 시간과 실행 횟수를 함께 기록한다.
- MeterRegistry는 이름과 태그 조합으로 Meter를 등록하고 관리한다.
publishPercentiles()는 한 인스턴스가 자신의 측정 분포에서 percentile을 계산한다.publishPercentileHistogram()은 bucket별 누적 개수를 노출해 여러 인스턴스의 분포를 합칠 수 있게 한다.record()는 하나의 코드 블록,Timer.Sample은 개발자가 정한 세부 구간,@Timed는 메서드 전체 측정에 적합하다.- 락 성능은 대기 시간과 점유 시간을 분리해야 원인을 좁힐 수 있다.
- Timer는 병목을 보여주지만 원인을 자동으로 확정하지는 않는다.
참고 자료
'Projects > hub-eleven' 카테고리의 다른 글
| [부하테스트] 재고 API - (2) k6 부하 테스트 환경과 개발 환경 분리 (0) | 2026.07.29 |
|---|---|
| [부하테스트] 재고 API - (1) k6로 테스트 해보기 (0) | 2026.07.28 |
| [동시성 처리] Redisson은 어떻게 Redis Pub/Sub을 이용해 재시도(Retry) 로직을 구현할 수 있을까? (0) | 2026.06.24 |
| [트러블슈팅] Redisson 분산 락 적용 중 트랜잭션 커밋 순서 문제를 해결한 과정 (2) (0) | 2026.06.23 |
| [트러블슈팅] Redisson 분산 락 적용 중 트랜잭션 커밋 순서 문제를 해결한 과정 (1) (0) | 2026.06.22 |