[Basic-3] 실전 비즈니스 메트릭: 마이크로미터 고급 활용과 운영 모니터링

2026. 1. 10. 22:57·Spring/Monitoring

1. 비즈니스 메트릭의 진정한 가치: 숫자 너머의 인사이트

 기초편에서 우리는 CPU, 메모리, HTTP 요청 같은 기술 메트릭을 다뤘다. 이제 한 걸음 더 나아가, "우리 비즈니스는 잘 돌아가고 있는가?"라는 질문에 답할 수 있는 비즈니스 메트릭을 설계하고 구현해보자.

1.1. 기술 메트릭의 한계와 비즈니스 메트릭의 필요성

기술 메트릭만으로 놓치는 것들:

# 기술 메트릭은 정상이지만...
✅ CPU 사용률: 45% (정상)
✅ 메모리 사용률: 60% (정상)
✅ HTTP 에러율: 0.5% (정상)
✅ 평균 응답 시간: 120ms (정상)

# 비즈니스 문제는 발생 중...
❌ 주문 취소율: 40% → (평소 5% 대비 8배 증가)
❌ 결제 실패율: 25% → (평소 2% 대비 12.5배 증가)
❌ 재고 소진률: 90% → (인기 상품 품절 위험)

실제 사례: "모든 시스템이 녹색인데 매출은 떨어졌다"

한 이커머스 회사에서 금요일 오후, 모든 기술 메트릭이 정상인데 갑자기 매출이 70% 급감했다. 모니터링 시스템은 아무런 경고도 보내지 않았다. 문제를 분석해보니:

  1. 기술 메트릭: 모두 정상
  2. 비즈니스 메트릭:
    • 카드사 연동 API 실패율: 85% (평소 1% 미만)
    • 결제 완료 주문 수: 시간당 15건 (평소 500건)
    • 장바구니 포기율: 95% (평소 30%)

문제는 카드사 시스템 장애였지만, 우리 시스템의 기술 메트릭은 정상이었다. 비즈니스 메트릭이 있었더라면 10분 만에 문제를 인지하고 대응할 수 있었다.

1.2. 좋은 비즈니스 메트릭의 조건

  1. 의사결정 지원: "무엇을 해야 할지" 알려줘야 함
  2. 조기 경보: 문제 발생 전에 이상 징후 감지
  3. 비즈니스 가치 연결: 기술 문제를 비즈니스 영향도로 변환
  4. 행동 유도: 데이터를 보고 즉시 조치를 취할 수 있어야 함

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
'Spring/Monitoring' 카테고리의 다른 글
  • [Basic-2] 마이크로미터, 프로메테우스, 그라파나: 운영 모니터링의 기초 이해
  • [Basic-1] 스프링 부트 액츄에이터(Actuator) 완벽 가이드
h6bro
h6bro
백엔드 개발자의 기술 블로그
  • h6bro
    Jun's Tech Blog
    h6bro
  • 전체
    오늘
    어제
    • 분류 전체보기 (250) N
      • Java (18)
        • Core (9)
        • Design Pattern (9)
      • Spring (80)
        • Core (24)
        • MVC (6)
        • DB (10)
        • JPA (26)
        • Monitoring (3)
        • Security (11)
        • WebSocket (0)
      • Database (33)
        • Redis (15)
        • MySQL (18)
      • MSA (25) N
        • MSA 기본 (11)
        • MSA 아키텍처 (14) N
      • Kafka (30)
        • Core (18)
        • Connect (12)
      • ElasticSearch (11)
        • Search (11)
        • Logging (0)
      • Test (4)
        • k6 (4)
      • Docker (9)
      • CI&CD (10)
        • GitHub Actions (6)
        • ArgoCD (4)
      • Kubernetes (18)
        • Core (12)
        • Ops (6)
      • Cloud Engineering (4)
        • AWS Infrastructure (3)
        • AWS EKS (1)
        • Terraform (0)
      • Project (8)
        • LinkFolio (1)
        • Secondhand Market (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • Cloud Engineering 포스팅 정리
  • 인기 글

  • 태그

    ㅈ
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
h6bro
[Basic-3] 실전 비즈니스 메트릭: 마이크로미터 고급 활용과 운영 모니터링
상단으로

티스토리툴바