1. 비즈니스 메트릭의 진정한 가치: 숫자 너머의 인사이트
기초편에서 우리는 CPU, 메모리, HTTP 요청 같은 기술 메트릭을 다뤘다. 이제 한 걸음 더 나아가, "우리 비즈니스는 잘 돌아가고 있는가?"라는 질문에 답할 수 있는 비즈니스 메트릭을 설계하고 구현해보자.
1.1. 기술 메트릭의 한계와 비즈니스 메트릭의 필요성
기술 메트릭만으로 놓치는 것들:
# 기술 메트릭은 정상이지만...
✅ CPU 사용률: 45% (정상)
✅ 메모리 사용률: 60% (정상)
✅ HTTP 에러율: 0.5% (정상)
✅ 평균 응답 시간: 120ms (정상)
# 비즈니스 문제는 발생 중...
❌ 주문 취소율: 40% → (평소 5% 대비 8배 증가)
❌ 결제 실패율: 25% → (평소 2% 대비 12.5배 증가)
❌ 재고 소진률: 90% → (인기 상품 품절 위험)
실제 사례: "모든 시스템이 녹색인데 매출은 떨어졌다"
한 이커머스 회사에서 금요일 오후, 모든 기술 메트릭이 정상인데 갑자기 매출이 70% 급감했다. 모니터링 시스템은 아무런 경고도 보내지 않았다. 문제를 분석해보니:
- 기술 메트릭: 모두 정상
- 비즈니스 메트릭:
- 카드사 연동 API 실패율: 85% (평소 1% 미만)
- 결제 완료 주문 수: 시간당 15건 (평소 500건)
- 장바구니 포기율: 95% (평소 30%)
문제는 카드사 시스템 장애였지만, 우리 시스템의 기술 메트릭은 정상이었다. 비즈니스 메트릭이 있었더라면 10분 만에 문제를 인지하고 대응할 수 있었다.
1.2. 좋은 비즈니스 메트릭의 조건
- 의사결정 지원: "무엇을 해야 할지" 알려줘야 함
- 조기 경보: 문제 발생 전에 이상 징후 감지
- 비즈니스 가치 연결: 기술 문제를 비즈니스 영향도로 변환
- 행동 유도: 데이터를 보고 즉시 조치를 취할 수 있어야 함
2. MeterRegistry 심층 이해: 마이크로미터의 핵심 엔진
기초편에서 MeterRegistry가 "메트릭의 등록소"라고 배웠다. 이제 그 내부를 파헤쳐보자.
2.1. MeterRegistry의 다양한 구현체와 선택 기준
// 다양한 MeterRegistry 구현체들
MeterRegistry registry1 = new SimpleMeterRegistry(); // 메모리 기반, 테스트용
MeterRegistry registry2 = new JmxMeterRegistry(...); // JMX 통합
MeterRegistry registry3 = new PrometheusMeterRegistry(...); // Prometheus 전용
MeterRegistry registry4 = new DatadogMeterRegistry(...); // Datadog 전용
// 가장 강력한: CompositeMeterRegistry
CompositeMeterRegistry compositeRegistry = new CompositeMeterRegistry();
compositeRegistry.add(registry1); // 로컬 테스트용
compositeRegistry.add(registry3); // Prometheus로 전송
compositeRegistry.add(registry4); // Datadog로도 전송 (이중화)
// Spring Boot에서의 자동 구성
@Configuration
public class MetricsConfig {
@Bean
public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> {
// 모든 메트릭에 공통 태그 추가
registry.config().commonTags(
"application", "order-service",
"cluster", System.getenv().getOrDefault("CLUSTER_NAME", "local"),
"version", "1.2.0"
);
// Registry별 특화 설정
if (registry instanceof PrometheusMeterRegistry) {
// Prometheus 전용 설정
} else if (registry instanceof DatadogMeterRegistry) {
// Datadog 전용 설정
}
};
}
}
2.2. MeterFilter: 메트릭의 변형과 필터링
MeterFilter는 메트릭이 등록되기 전/후에 가공할 수 있는 강력한 도구다.
@Configuration
public class MeterFilterConfiguration {
@Bean
public MeterFilter renameMeterFilter() {
// 메트릭 이름 변경 (레거시 호환성)
return MeterFilter.renameTag("http.server.requests", "uri", "path");
}
@Bean
public MeterFilter ignoreCommonTagsFilter() {
// 특정 태그 값 무시 (예: health-check는 모니터링에서 제외)
return MeterFilter.deny(id -> {
String uri = id.getTag("uri");
return uri != null && uri.contains("health");
});
}
@Bean
public MeterFilter minimumExpectedValueFilter() {
// Timer의 최소 예상값 설정 (히스토그램 버킷 최적화)
return MeterFilter.minimumExpectedValue("http.server.requests",
Duration.ofMillis(10));
}
@Bean
public MeterFilter maximumExpectedValueFilter() {
// Timer의 최대 예상값 설정
return MeterFilter.maximumExpectedValue("http.server.requests",
Duration.ofSeconds(30));
}
@Bean
public MeterFilter percentileHistogramFilter() {
// 특정 메트릭에 대해 백분위수 히스토그램 활성화
return MeterFilter.enablePercentileHistogram("custom.timer");
}
@Bean
public MeterFilter slaFilter() {
// 서비스 수준 협약(SLA) 설정
return MeterFilter.enableSla("custom.timer",
Duration.ofMillis(100),
Duration.ofMillis(500),
Duration.ofSeconds(1));
}
}
2.3. Tag(태그) 설계 전략: 카디널리티의 함정
태그는 메트릭을 분류하는 강력한 도구지만, 잘못 사용하면 시스템을 마비시킬 수 있다.
// ❌ 위험한 태그 설계: 높은 카디널리티
Counter.builder("api.requests")
.tag("user_id", "user-123456789") // 수천~수만 개의 고유값
.tag("session_id", "sess-987654321") // 더 많은 고유값
.tag("request_id", "req-abcdef12345") // 무한에 가까운 고유값
.register(registry);
// ⚠️ 문제점:
// 1. 메모리 폭발: 각 고유 조합별로 메트릭 시계열 생성
// 2. 성능 저하: 쿼리 속도 급감
// 3. 저장소 부하: 프로메테우스 시계열 수 폭발
// ✅ 안전한 태그 설계: 낮은 카디널리티
Counter.builder("api.requests")
.tag("http_method", "GET") // 4-5개 값 (GET, POST, PUT, DELETE)
.tag("http_status", "200") // 10개 내외 값 (200, 404, 500 등)
.tag("endpoint_group", "user-api") // 10-20개 값 (도메인별 그룹)
.tag("environment", "production") // 3-4개 값 (dev, staging, prod)
.register(registry);
// ✅ 중간 카디널리티 관리: 적절한 그룹화
Counter.builder("business.orders")
.tag("customer_tier", "premium") // 3-4개 값 (basic, premium, vip)
.tag("product_category", "electronics") // 20-30개 값 (카테고리 수준)
.tag("region", "seoul") // 10-20개 값 (지역 수준)
.register(registry);
태그 카디널리티 가이드라인:
| 카디널리티 | 예시 | 권장 사용처 | 주의사항 |
| 낮음 (1-10) | environment, tier, status | 필터링, 그룹화 | 안전함 |
| 중간 (10-100) | endpoint, category, region | 세분화 분석 | 모니터링 필요 |
| 높음 (100-1000) | user_segment, product_id | 제한적 사용 | 주의 필요 |
| 매우 높음 (1000+) | user_id, session_id, order_id | 사용 금지 | 시스템 마비 유발 |
3. 메트릭 등록 패턴: 직접 등록 vs 어노테이션
3.1. 직접 등록 패턴: 최대의 유연성
@Service
@Slf4j
public class OrderService {
private final MeterRegistry registry;
private final Counter orderCounter;
private final Timer orderProcessingTimer;
private final Gauge inventoryGauge;
// 비즈니스 상태
private final AtomicInteger inventoryLevel = new AtomicInteger(1000);
private final Map<String, Integer> categoryInventory = new ConcurrentHashMap<>();
public OrderService(MeterRegistry registry) {
this.registry = registry;
// 1. Counter: 주문 관련 메트릭
this.orderCounter = Counter.builder("business.orders.total")
.description("전체 주문 수")
.tag("service", "order-service")
.baseUnit("orders")
.register(registry);
// 2. Timer: 주문 처리 시간 (히스토그램 + 백분위수)
this.orderProcessingTimer = Timer.builder("business.orders.processing.time")
.description("주문 처리 소요 시간")
.publishPercentileHistogram() // 히스토그램 활성화
.publishPercentiles(0.5, 0.95, 0.99) // P50, P95, P99
.sla(Duration.ofMillis(100), Duration.ofMillis(500), Duration.ofSeconds(1))
.minimumExpectedValue(Duration.ofMillis(1))
.maximumExpectedValue(Duration.ofSeconds(10))
.register(registry);
// 3. Gauge: 재고 수준
this.inventoryGauge = Gauge.builder("business.inventory.level",
inventoryLevel,
AtomicInteger::get)
.description("현재 재고 수량")
.strongReference(true) // 가비지 컬렉션 방지
.register(registry);
// 4. 여러 Gauge를 한 번에 등록 (MeterBinder 패턴)
registerCategoryInventoryGauges();
}
private void registerCategoryInventoryGauges() {
// 카테고리별 재고 게이지 등록
categoryInventory.put("electronics", 200);
categoryInventory.put("clothing", 300);
categoryInventory.put("books", 150);
categoryInventory.forEach((category, stock) -> {
Gauge.builder("business.inventory.by.category",
stock,
Integer::doubleValue)
.tag("category", category)
.description(category + " 카테고리 재고")
.register(registry);
});
}
public Order processOrder(OrderRequest request) {
// 타이머 샘플 시작
Timer.Sample sample = Timer.start(registry);
try {
log.info("주문 처리 시작: {}", request.getOrderId());
// 재고 확인
if (inventoryLevel.get() <= 0) {
throw new OutOfStockException("재고 부족");
}
// 재고 차감
inventoryLevel.decrementAndGet();
// 주문 처리 비즈니스 로직
Order order = executeBusinessLogic(request);
// 성공 메트릭 기록
orderCounter.increment();
recordSuccessMetrics(request);
return order;
} catch (Exception e) {
// 실패 메트릭 기록
recordFailureMetrics(request, e);
throw e;
} finally {
// 타이머 샘플 정지 (항상 실행 보장)
sample.stop(orderProcessingTimer);
}
}
private void recordSuccessMetrics(OrderRequest request) {
// 성공 세부 메트릭
Counter.builder("business.orders.success")
.tag("payment_method", request.getPaymentMethod())
.tag("customer_tier", request.getCustomerTier())
.register(registry)
.increment();
}
private void recordFailureMetrics(OrderRequest request, Exception e) {
// 실패 세부 메트릭
Counter.builder("business.orders.failed")
.tag("failure_reason", e.getClass().getSimpleName())
.tag("payment_method", request.getPaymentMethod())
.register(registry)
.increment();
}
}
3.2. 어노테이션 기반 패턴: 선언적 프로그래밍
@Service
@Slf4j
@Timed(value = "business.order.service",
description = "주문 서비스 전체 실행 시간",
percentiles = {0.5, 0.95, 0.99})
public class OrderServiceAnnotated {
@Counted(value = "business.orders.count",
description = "주문 처리 횟수",
extraTags = {"type", "annotated"})
@Timed(value = "business.orders.timed",
description = "주문 처리 시간",
longTask = false)
public Order processOrder(OrderRequest request) {
// 비즈니스 로직
return executeOrder(request);
}
@Counted(value = "business.orders.count",
description = "주문 취소 횟수",
recordFailuresOnly = true) // 실패한 경우만 기록
public void cancelOrder(String orderId) {
// 취소 로직
throwIfCannotCancel(orderId);
executeCancel(orderId);
}
@Timed(value = "business.orders.long.running",
description = "장시간 실행 주문 처리",
longTask = true) // 장시간 실행 작업 표시
@SneakyThrows
public Order processBatchOrder(List<OrderRequest> requests) {
// 배치 처리 (오래 걸림)
Thread.sleep(5000);
return batchProcess(requests);
}
}
// AOP 활성화 구성
@Configuration
@EnableAspectJAutoProxy
public class MetricsAspectConfiguration {
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
@Bean
public CountedAspect countedAspect(MeterRegistry registry) {
return new CountedAspect(registry);
}
@Bean
public MeterRegistryCustomizer<MeterRegistry> addCommonTags() {
return registry -> registry.config()
.commonTags("region", "asia-northeast3", "zone", "zone-a");
}
}
3.3. 두 패턴의 비교와 선택 기준
| 기준 | 직접 등록 | 어노테이션 기반 |
| 유연성 | ⭐⭐⭐⭐⭐ (완전 제어) | ⭐⭐ (제한적) |
| 가독성 | ⭐⭐ (로직과 혼합) | ⭐⭐⭐⭐⭐ (분리) |
| 일관성 | ⭐ (개발자 의존) | ⭐⭐⭐⭐⭐ (표준화) |
| 세부 제어 | ⭐⭐⭐⭐⭐ | ⭐ |
| 에러 처리 | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| 학습 곡선 | 높음 | 낮음 |
추천 가이드:
- 비즈니스 핵심 지표: 직접 등록 (세부 제어 필요)
- CRUD 작업: 어노테이션 (일관성 중요)
- 외부 API 호출: 직접 등록 (타임아웃, 재시도 메트릭)
- 배치 작업: 직접 등록 + @Timed(longTask=true)
4. 비즈니스 메트릭 설계 실전: 전자상거래 시스템 예제
4.1. 비즈니스 도메인 분석
// 도메인 모델
public enum OrderStatus {
CREATED, PAYMENT_PENDING, PAYMENT_COMPLETED,
SHIPPED, DELIVERED, CANCELLED, REFUNDED
}
public enum PaymentMethod {
CREDIT_CARD, BANK_TRANSFER, MOBILE_PAYMENT, PAYPAL
}
public enum CustomerTier {
BRONZE, SILVER, GOLD, PLATINUM
}
4.2. 핵심 비즈니스 메트릭 정의
@Component
public class EcommerceMetrics {
private final MeterRegistry registry;
// 1. 판매 메트릭
private final Counter totalSalesCounter;
private final Counter failedSalesCounter;
private final Gauge dailySalesGauge;
// 2. 재고 메트릭
private final Gauge inventoryLevelGauge;
private final Counter lowStockAlertCounter;
// 3. 고객 메트릭
private final Counter newCustomerCounter;
private final Gauge customerSatisfactionGauge;
// 4. 배송 메트릭
private final Timer deliveryTimer;
private final Counter delayedDeliveryCounter;
// 5. 재무 메트릭
private final Counter revenueCounter;
private final Counter refundCounter;
// 비즈니스 상태
private final AtomicInteger dailySales = new AtomicInteger(0);
private final AtomicInteger customerSatisfaction = new AtomicInteger(100);
private final Map<String, AtomicInteger> inventory = new ConcurrentHashMap<>();
public EcommerceMetrics(MeterRegistry registry) {
this.registry = registry;
initializeInventory();
initializeMetrics();
}
private void initializeInventory() {
inventory.put("smartphone", new AtomicInteger(500));
inventory.put("laptop", new AtomicInteger(200));
inventory.put("headphones", new AtomicInteger(1000));
inventory.put("monitor", new AtomicInteger(150));
}
private void initializeMetrics() {
// 1. 판매 메트릭 초기화
this.totalSalesCounter = Counter.builder("ecommerce.sales.total")
.description("전체 판매 건수")
.tag("business_unit", "online-store")
.register(registry);
this.failedSalesCounter = Counter.builder("ecommerce.sales.failed")
.description("실패한 판매 건수")
.tag("business_unit", "online-store")
.register(registry);
this.dailySalesGauge = Gauge.builder("ecommerce.sales.daily",
dailySales, AtomicInteger::get)
.description("오늘 판매 건수")
.register(registry);
// 2. 재고 메트릭 초기화
this.inventoryLevelGauge = Gauge.builder("ecommerce.inventory.level",
() -> calculateTotalInventory())
.description("전체 재고 수준")
.register(registry);
this.lowStockAlertCounter = Counter.builder("ecommerce.inventory.low_stock_alerts")
.description("재고 부족 알림 발생 횟수")
.register(registry);
// 카테고리별 재고 게이지 등록
inventory.forEach((product, stock) -> {
Gauge.builder("ecommerce.inventory.by_product",
stock, AtomicInteger::get)
.tag("product", product)
.description(product + " 재고 수량")
.register(registry);
});
// 3. 고객 메트릭 초기화
this.newCustomerCounter = Counter.builder("ecommerce.customers.new")
.description("신규 고객 수")
.register(registry);
this.customerSatisfactionGauge = Gauge.builder("ecommerce.customers.satisfaction",
customerSatisfaction, AtomicInteger::get)
.description("고객 만족도 지수 (0-100)")
.register(registry);
// 4. 배송 메트릭 초기화
this.deliveryTimer = Timer.builder("ecommerce.delivery.time")
.description("주문부터 배송 완료까지 시간")
.publishPercentileHistogram()
.register(registry);
this.delayedDeliveryCounter = Counter.builder("ecommerce.delivery.delayed")
.description("지연 배송 건수")
.register(registry);
// 5. 재무 메트릭 초기화
this.revenueCounter = Counter.builder("ecommerce.finance.revenue")
.description("총 매출액 (원)")
.baseUnit("KRW")
.register(registry);
this.refundCounter = Counter.builder("ecommerce.finance.refunds")
.description("환불 건수")
.register(registry);
}
private double calculateTotalInventory() {
return inventory.values().stream()
.mapToInt(AtomicInteger::get)
.sum();
}
// 비즈니스 메서드들...
public void recordSale(Sale sale) {
// 판매 기록
totalSalesCounter.increment();
dailySales.incrementAndGet();
// 재고 감소
AtomicInteger productStock = inventory.get(sale.getProductId());
if (productStock != null) {
productStock.decrementAndGet();
// 재고 부족 알림
if (productStock.get() < 10) {
lowStockAlertCounter.increment();
alertLowStock(sale.getProductId(), productStock.get());
}
}
// 매출 기록
revenueCounter.increment(sale.getAmount());
// 신규 고객인 경우
if (sale.isNewCustomer()) {
newCustomerCounter.increment();
}
}
public void recordDelivery(Delivery delivery) {
// 배송 시간 기록
deliveryTimer.record(delivery.getDeliveryDuration());
// 지연 배송인 경우
if (delivery.isDelayed()) {
delayedDeliveryCounter.increment();
customerSatisfaction.decrementAndGet(); // 만족도 하락
}
}
public void recordRefund(Refund refund) {
refundCounter.increment();
customerSatisfaction.decrementAndGet(); // 만족도 하락
}
public void recordCustomerFeedback(Feedback feedback) {
// 고객 피드백에 따른 만족도 조정
if (feedback.isPositive()) {
customerSatisfaction.incrementAndGet();
} else {
customerSatisfaction.decrementAndGet();
}
}
}
4.3. 비즈니스 KPI 대시보드 설계
{
"dashboard": {
"title": "전자상거래 비즈니스 KPI 대시보드",
"panels": [
{
"title": "실시간 매출 현황",
"targets": [
{
"expr": "increase(ecommerce_finance_revenue_total[1h])",
"legendFormat": "시간당 매출"
}
],
"type": "stat",
"fieldConfig": {
"defaults": {
"unit": "currencyKRW"
}
}
},
{
"title": "주요 제품 재고 현황",
"targets": [
{
"expr": "ecommerce_inventory_by_product",
"legendFormat": "{{product}}"
}
],
"type": "bargauge",
"fieldConfig": {
"defaults": {
"min": 0,
"max": 1000,
"thresholds": {
"steps": [
{"color": "red", "value": 0},
{"color": "yellow", "value": 50},
{"color": "green", "value": 100}
]
}
}
}
},
{
"title": "고객 만족도 추이",
"targets": [
{
"expr": "ecommerce_customers_satisfaction",
"legendFormat": "만족도 지수"
}
],
"type": "gauge",
"fieldConfig": {
"defaults": {
"min": 0,
"max": 100,
"thresholds": {
"steps": [
{"color": "red", "value": 0},
{"color": "orange", "value": 60},
{"color": "yellow", "value": 80},
{"color": "green", "value": 90}
]
}
}
}
},
{
"title": "판매 성공률",
"targets": [
{
"expr": "1 - (increase(ecommerce_sales_failed_total[1h]) / increase(ecommerce_sales_total_total[1h]))",
"legendFormat": "성공률"
}
],
"type": "gauge",
"fieldConfig": {
"defaults": {
"unit": "percentunit",
"min": 0,
"max": 1,
"thresholds": {
"steps": [
{"color": "red", "value": 0},
{"color": "yellow", "value": 0.95},
{"color": "green", "value": 0.99}
]
}
}
}
}
]
}
}
5. 고급 모니터링 패턴과 최적화
5.1. 분산 추적과 메트릭 통합
@Component
public class DistributedMetrics {
private final MeterRegistry meterRegistry;
private final Tracer tracer; // OpenTelemetry 또는 Brave Tracer
public DistributedMetrics(MeterRegistry meterRegistry, Tracer tracer) {
this.meterRegistry = meterRegistry;
this.tracer = tracer;
}
public <T> T traceAndMeasure(String operationName,
Map<String, String> tags,
Supplier<T> operation) {
// 분산 추적 Span 시작
Span span = tracer.nextSpan()
.name(operationName)
.start();
// 메트릭 측정 시작
Timer.Sample timerSample = Timer.start(meterRegistry);
try (Scope scope = tracer.withSpan(span)) {
// 비즈니스 로직 실행
T result = operation.get();
// 성공 메트릭 기록
recordSuccess(operationName, tags);
return result;
} catch (Exception e) {
// 실패 메트릭 기록
recordFailure(operationName, tags, e);
// Span에 에러 정보 추가
span.error(e);
throw e;
} finally {
// 타이머 정지
timerSample.stop(Timer.builder("distributed.operation.time")
.tags(tags)
.tag("operation", operationName)
.register(meterRegistry));
// Span 종료
span.finish();
}
}
private void recordSuccess(String operationName, Map<String, String> tags) {
Counter.builder("distributed.operation.success")
.tags(tags)
.tag("operation", operationName)
.register(meterRegistry)
.increment();
}
private void recordFailure(String operationName,
Map<String, String> tags,
Exception e) {
Counter.builder("distributed.operation.failure")
.tags(tags)
.tag("operation", operationName)
.tag("error_type", e.getClass().getSimpleName())
.register(meterRegistry)
.increment();
}
}
5.2. 메트릭 샘플링과 집계 최적화
@Configuration
public class MetricsOptimizationConfig {
@Bean
public MeterFilter samplingFilter() {
return new MeterFilter() {
private final Random random = new Random();
@Override
public MeterFilterReply accept(Meter.Id id) {
String name = id.getName();
// 고빈도 메트릭 샘플링
if (name.startsWith("http.server.requests")) {
return random.nextDouble() < 0.1 ? // 10% 샘플링
MeterFilterReply.ACCEPT :
MeterFilterReply.DENY;
}
// 비즈니스 메트릭은 전수 조사
if (name.startsWith("business.") ||
name.startsWith("ecommerce.")) {
return MeterFilterReply.ACCEPT;
}
return MeterFilterReply.NEUTRAL;
}
};
}
@Bean
public MeterFilter cardinalityLimitFilter() {
return new MeterFilter() {
private final Map<String, Integer> tagValueCounts = new ConcurrentHashMap<>();
private static final int MAX_VALUES_PER_TAG = 50;
@Override
public Meter.Id map(Meter.Id id) {
List<Tag> filteredTags = new ArrayList<>();
for (Tag tag : id.getTags()) {
String tagKey = tag.getKey();
String tagValue = tag.getValue();
// 태그 값 수 제한
String countKey = id.getName() + "." + tagKey;
int currentCount = tagValueCounts.getOrDefault(countKey, 0);
if (currentCount >= MAX_VALUES_PER_TAG) {
// 값 수 제한 초과 시 기본값 사용
filteredTags.add(Tag.of(tagKey, "other"));
} else {
filteredTags.add(tag);
tagValueCounts.put(countKey, currentCount + 1);
}
}
return id.withTags(filteredTags);
}
};
}
@Bean
public MeterFilter metricsPrefixFilter() {
// 메트릭 이름에 접두사 추가 (네임스페이스 분리)
return MeterFilter.renameTag(
MeterFilter.denyNameStartsWith("jvm."),
"jvm.",
"java."
);
}
}
5.3. 동적 메트릭 등록과 제거
@Component
public class DynamicMetricsManager {
private final MeterRegistry registry;
private final Map<String, Meter> dynamicMeters = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(1);
public DynamicMetricsManager(MeterRegistry registry) {
this.registry = registry;
}
public void registerDynamicGauge(String name,
Supplier<Number> valueSupplier,
Map<String, String> tags) {
String meterKey = generateKey(name, tags);
if (!dynamicMeters.containsKey(meterKey)) {
Gauge gauge = Gauge.builder(name, valueSupplier)
.tags(tags)
.register(registry);
dynamicMeters.put(meterKey, gauge);
// 일정 시간 후 자동 제거 (메모리 누수 방지)
scheduler.schedule(() -> removeDynamicMeter(meterKey),
1, TimeUnit.HOURS);
}
}
public void registerEphemeralCounter(String name,
Map<String, String> tags,
Runnable onExpire) {
String meterKey = generateKey(name, tags);
Counter counter = Counter.builder(name)
.tags(tags)
.register(registry);
dynamicMeters.put(meterKey, counter);
// 5분 후 제거
scheduler.schedule(() -> {
removeDynamicMeter(meterKey);
onExpire.run();
}, 5, TimeUnit.MINUTES);
}
private void removeDynamicMeter(String meterKey) {
Meter meter = dynamicMeters.remove(meterKey);
if (meter != null) {
registry.remove(meter);
}
}
private String generateKey(String name, Map<String, String> tags) {
return name + ":" + tags.toString();
}
@PreDestroy
public void cleanup() {
scheduler.shutdown();
dynamicMeters.clear();
}
}
6. 프로덕션 운영을 위한 고급 구성
6.1. 다중 환경 메트릭 분리
# application-prod.yml
management:
metrics:
export:
prometheus:
enabled: true
step: 30s
tags:
environment: production
region: ${AWS_REGION}
availability-zone: ${AWS_AVAILABILITY_ZONE}
distribution:
percentiles-histogram:
http.server.requests: true
sla:
http.server.requests: 100ms, 250ms, 500ms, 1s, 2s
# application-dev.yml
management:
metrics:
export:
prometheus:
enabled: true
step: 1m # 개발 환경은 덜 자주
tags:
environment: development
developer: ${USER}
distribution:
percentiles-histogram:
http.server.requests: false # 개발 환경에서는 비활성화
6.2. 보안 강화 메트릭 필터링
@Component
public class SecurityAwareMetricsFilter implements MeterFilter {
private final List<Pattern> sensitivePatterns = Arrays.asList(
Pattern.compile(".*password.*", Pattern.CASE_INSENSITIVE),
Pattern.compile(".*token.*", Pattern.CASE_INSENSITIVE),
Pattern.compile(".*secret.*", Pattern.CASE_INSENSITIVE),
Pattern.compile(".*key.*", Pattern.CASE_INSENSITIVE)
);
@Override
public Meter.Id map(Meter.Id id) {
// 민감한 정보가 태그에 포함되어 있는지 확인
List<Tag> safeTags = id.getTags().stream()
.map(this::sanitizeTag)
.collect(Collectors.toList());
return id.withTags(safeTags);
}
private Tag sanitizeTag(Tag tag) {
String key = tag.getKey();
String value = tag.getValue();
// 키가 민감한 정보인지 확인
if (isSensitive(key) || isSensitive(value)) {
return Tag.of(key, "****"); // 값 마스킹
}
// URL 파라미터에서 민감 정보 제거
if ("uri".equals(key)) {
String sanitizedUri = sanitizeUri(value);
return Tag.of(key, sanitizedUri);
}
return tag;
}
private boolean isSensitive(String text) {
if (text == null) return false;
return sensitivePatterns.stream()
.anyMatch(pattern -> pattern.matcher(text).find());
}
private String sanitizeUri(String uri) {
// 쿼리 파라미터에서 민감 정보 제거
try {
URI parsedUri = new URI(uri);
String query = parsedUri.getQuery();
if (query != null) {
String sanitizedQuery = Arrays.stream(query.split("&"))
.map(param -> {
if (param.contains("=")) {
String[] parts = param.split("=", 2);
String paramName = parts[0];
String paramValue = parts.length > 1 ? parts[1] : "";
if (isSensitive(paramName)) {
return paramName + "=****";
}
}
return param;
})
.collect(Collectors.joining("&"));
return parsedUri.getPath() +
(sanitizedQuery.isEmpty() ? "" : "?" + sanitizedQuery);
}
} catch (Exception e) {
// URI 파싱 실패 시 원본 반환
}
return uri;
}
}
7. 모니터링 성숙도 모델: 레벨 0에서 레벨 3까지
레벨 0: 무감시 상태
- 상태: 로그 파일만 존재
- 문제: 장애는 사용자가 먼저 발견
- 다음 단계: 기본 헬스 체크 구현
레벨 1: 반응적 모니터링
- 기술 메트릭: CPU, 메모리, 디스크
- 알람: 임계값 초과 시 이메일 알림
- 문제: "무엇이 문제인지"는 여전히 수동 분석
- 다음 단계: 비즈니스 메트릭 추가
레벨 2: 예측적 모니터링
- 비즈니스 메트릭: 주문 수, 취소율, 재고 수준
- 대시보드: 실시간 KPI 시각화
- 알람: 비즈니스 영향도 기반 에스컬레이션
- 문제: "왜 발생했는지"는 여전히 수동 분석
- 다음 단계: 근본 원인 분석 자동화
레벨 3: 사전 예방적 모니터링
- 머신 러닝: 이상 패턴 자동 감지
- 근본 원인 분석: 문제 자동 진단
- 자동화: 자동 복구 스크립트 실행
- 목표: 장애 방지, 가용성 99.99%+
'Spring > Monitoring' 카테고리의 다른 글
| [Basic-2] 마이크로미터, 프로메테우스, 그라파나: 운영 모니터링의 기초 이해 (0) | 2026.01.10 |
|---|---|
| [Basic-1] 스프링 부트 액츄에이터(Actuator) 완벽 가이드 (0) | 2026.01.10 |
