이 글에서 얻는 것

  • APM(Application Performance Monitoring)이 무엇이고, 왜 필요한지 이해합니다.
  • 핵심 메트릭(응답 시간, 처리량, 오류율)을 모니터링할 수 있습니다.
  • Spring Boot Actuator로 헬스 체크와 메트릭을 노출합니다.
  • 분산 추적의 기본 개념을 이해합니다.
  • APM 도구 생태계를 비교하고 팀 상황에 맞는 도구를 선택할 수 있습니다.

0) APM은 “운영 중인 애플리케이션의 건강 상태"를 보여준다

APM이란?

APM (Application Performance Monitoring)
= 애플리케이션 성능을 실시간으로 모니터링

목적:
- 성능 병목 발견
- 장애 조기 감지
- 사용자 경험 최적화
- 리소스 사용량 추적

로깅 vs 모니터링 vs 트레이싱 (관측 3대 신호)

운영 시스템을 이해하기 위한 세 가지 신호는 각각 다른 질문에 답합니다.

신호질문예시도구
Logs (로그)“무슨 일이 일어났는가?”User alice login failed: bad passwordELK, Loki
Metrics (메트릭)“시스템이 얼마나 건강한가?”http_requests_total{status="500"} = 42Prometheus, Datadog
Traces (트레이스)“이 요청이 어디서 느려졌는가?”OrderAPI → PaymentService (320ms) → DB (280ms)Jaeger, Tempo
사용자 요청 하나를 추적한다고 생각해보세요:

[Metrics] → "지금 p95가 2초야, 뭔가 느려졌다" (감지)
[Traces]  → "느린 요청을 따라가보니 PaymentService에서 병목" (위치 파악)
[Logs]    → "PaymentService 로그: DB connection timeout" (원인 확인)

세 가지를 조합해야 문제를 빠르게 해결할 수 있습니다.

APM이 없으면 생기는 일

실제 장애 시나리오를 비교해보겠습니다.

APM 없는 팀:

  1. 고객 CS: “결제가 안 돼요” (발생 후 30분)
  2. 개발팀: 서버 접속 → 로그 grep → “어디 로그를 봐야 하지?”
  3. 2시간 후: “DB 커넥션 풀이 고갈됐었네”
  4. 근본 원인: 슬로우 쿼리 하나가 커넥션을 점유

APM 있는 팀:

  1. 알림: “결제 API p95 > 3초, 에러율 > 5%” (발생 즉시)
  2. 대시보드: 커넥션 풀 사용률 100% 확인
  3. 트레이스: 특정 쿼리가 8초 걸리는 것 확인
  4. 5분 만에 슬로우 쿼리 인덱스 추가로 해결

1) APM 핵심 메트릭

1-1) Golden Signals (핵심 지표 4가지)

Google SRE 팀이 정의한 4가지 핵심 지표입니다. “이것만 모니터링해도 대부분의 문제를 감지할 수 있다"는 뜻입니다.

Signal측정 대상예시 지표왜 중요한가
Latency요청 처리 시간p50, p95, p99사용자 체감 성능
Traffic요청량RPS, RPM용량 계획 기준
Errors실패한 요청 비율5xx rate, timeout rate서비스 품질
Saturation리소스 사용률CPU%, 커넥션 풀, 큐 길이한계 예측

1-2) RED 메서드 vs USE 메서드

Golden Signals 외에 두 가지 프레임워크가 자주 쓰입니다. 대상이 다릅니다.

RED 메서드 — 서비스(엔드포인트) 관점

항목설명PromQL 예시
Rate초당 요청 수rate(http_server_requests_seconds_count[5m])
Errors초당 에러 수rate(http_server_requests_seconds_count{status=~"5.."}[5m])
Duration요청 처리 시간histogram_quantile(0.95, rate(http_server_requests_seconds_bucket[5m]))

USE 메서드 — 리소스(인프라) 관점

항목설명예시
Utilization사용률CPU 70%, 메모리 80%
Saturation포화(큐/대기)스레드 풀 큐 길이, 디스크 I/O 대기
Errors리소스 에러디스크 에러, 네트워크 패킷 손실
💡 실무 팁:
- 마이크로서비스 → RED로 서비스 상태 모니터링
- 인프라/DB/캐시 → USE로 리소스 상태 모니터링
- 둘 다 함께 보는 게 이상적

1-3) 주요 메트릭 상세

응답 시간 (Response Time)

평균 응답 시간: 200ms
P50 (중앙값): 150ms    ← 절반의 요청이 이 시간 내 완료
P95 (95 백분위수): 500ms  ← 중요!
P99: 1000ms             ← 꼬리 지연(tail latency)

P95가 높다 = 일부 사용자가 느린 경험

⚠️ "평균"의 함정:
  요청 100개 중 99개가 10ms, 1개가 9,010ms → 평균 100ms
  → "평균 100ms"는 정상처럼 보이지만, 1%는 9초를 기다림
  → 반드시 P95/P99를 함께 봐야 합니다.

처리량 (Throughput)

RPS (Requests Per Second): 초당 요청 수
TPM (Transactions Per Minute): 분당 트랜잭션 수

예:
- 평균 RPS: 100
- 피크 RPS: 500 (트래픽 급증 시)

용량 산정 공식 (Little's Law):
  동시 요청 수 = RPS × 평균 응답 시간(초)
  
  100 RPS × 0.2초 = 20 동시 요청
  → 스레드 풀 20개면 이론상 커버 (여유분 고려해 40~50)

오류율 (Error Rate)

오류율 = (실패한 요청 / 전체 요청) × 100

예:
- 전체 요청: 10,000
- 실패: 50
- 오류율: 0.5%

목표: 오류율 < 0.1% (SLA에 따라 다름)

⚠️ 주의:
  - 5xx만 세면 안 됩니다. 타임아웃(504), 서킷브레이커(503)도 에러입니다.
  - 4xx 중에서도 429(Rate Limit), 408(Timeout)은 서버 측 문제 징후입니다.
  - 비즈니스 에러(결제 실패 등)도 별도 카운터로 추적해야 합니다.

리소스 사용률 임계값 가이드

리소스정상경고위험측정 방법
CPU< 60%60~80%> 80%system_cpu_usage
메모리(힙)< 70%70~85%> 85%jvm_memory_used_bytes / jvm_memory_max_bytes
커넥션 풀< 70%70~90%> 90%hikaricp_connections_active / hikaricp_connections_max
스레드 풀< 60%60~80%> 80%tomcat_threads_busy_threads / tomcat_threads_config_max_threads
디스크< 70%70~85%> 85%disk_free_bytes

2) APM 도구 생태계 비교

상용 vs 오픈소스

기준DatadogNew RelicElastic APMGrafana Stack
유형SaaSSaaSSelf-hosted/CloudSelf-hosted/Cloud
비용호스트당 $31/월~사용량 기반무료(OSS) ~ Cloud무료(OSS) ~ Cloud
장점올인원, UX 최고AI 이상 탐지, NRQLELK 통합, 로그 강점완전 오픈소스, 유연
단점비쌈가격 예측 어려움리소스 많이 씀직접 구축 필요
에이전트dd-agentNew Relic agentelastic-apm-agentOTel Collector
추천 상황예산 있는 중대형 팀AI 기반 분석 필요이미 ELK 운영 중벤더 락인 회피

오픈소스 Grafana Stack 구성도

┌──────────────────────────────────────────┐
│                Grafana                    │  ← 시각화/대시보드/알림
├───────────┬───────────┬──────────────────┤
│ Prometheus│   Loki    │     Tempo        │
│ (Metrics) │  (Logs)   │   (Traces)       │
├───────────┴───────────┴──────────────────┤
│          OpenTelemetry Collector          │  ← 수집/변환/라우팅
├──────────────────────────────────────────┤
│    Spring Boot + Micrometer + OTel SDK   │  ← 계측
└──────────────────────────────────────────┘

💡 선택 가이드: 초기 스타트업 → Grafana Stack(무료). 팀이 10명 이상이고 운영 인력이 부족하면 → Datadog/New Relic(관리형). 이미 ELK를 쓰고 있으면 → Elastic APM 추가.


3) Spring Boot Actuator 실전 설정

3-1) 의존성 및 기본 설정

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health, info, prometheus, metrics, env
      base-path: /actuator
  endpoint:
    health:
      show-details: when_authorized   # 인증된 사용자에게만 상세 정보
      show-components: when_authorized
      probes:
        enabled: true                  # K8s liveness/readiness 프로브
  metrics:
    tags:
      application: ${spring.application.name}
      environment: ${spring.profiles.active:local}
    distribution:
      percentiles-histogram:
        http.server.requests: true     # 히스토그램 활성화 (p95/p99 계산용)
      percentiles:
        http.server.requests: 0.5, 0.95, 0.99
      sla:
        http.server.requests: 100ms, 500ms, 1s, 5s  # SLO 버킷

3-2) Actuator 보안 설정

운영 환경에서 Actuator 엔드포인트를 그대로 노출하면 정보 유출 위험이 있습니다.

@Configuration
@EnableWebSecurity
public class ActuatorSecurityConfig {
    
    @Bean
    public SecurityFilterChain actuatorSecurity(HttpSecurity http) throws Exception {
        return http
            .securityMatcher("/actuator/**")
            .authorizeHttpRequests(auth -> auth
                // health와 prometheus는 내부 네트워크에서 인증 없이 접근 가능
                .requestMatchers("/actuator/health/liveness").permitAll()
                .requestMatchers("/actuator/health/readiness").permitAll()
                .requestMatchers("/actuator/prometheus").hasIpAddress("10.0.0.0/8")
                // 나머지는 ADMIN 역할 필요
                .anyRequest().hasRole("ADMIN")
            )
            .httpBasic(Customizer.withDefaults())
            .build();
    }
}

⚠️ 운영 환경 필수 점검: /actuator/env는 환경 변수(DB 비밀번호 등)가 노출될 수 있으므로 반드시 접근 제한하세요. 가능하면 Actuator를 별도 포트(management.server.port=9090)로 분리하는 것이 안전합니다.

3-3) 커스텀 Health Indicator

기본 헬스 체크(DB, Redis, Disk)만으로는 부족합니다. 비즈니스 의존성도 체크해야 합니다.

@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {
    
    private final PaymentGatewayClient paymentClient;
    private final CircuitBreaker circuitBreaker;
    
    @Override
    public Health health() {
        try {
            // 결제 게이트웨이 ping (타임아웃 2초)
            PaymentStatus status = paymentClient.healthCheck();
            
            if (status.isHealthy()) {
                return Health.up()
                    .withDetail("gateway", "PG사 연동 정상")
                    .withDetail("latency_ms", status.getLatencyMs())
                    .build();
            }
            
            return Health.down()
                .withDetail("gateway", "PG사 응답 이상")
                .withDetail("reason", status.getReason())
                .build();
                
        } catch (Exception e) {
            return Health.down()
                .withDetail("gateway", "PG사 연결 불가")
                .withException(e)
                .build();
        }
    }
}

결과 예시 (/actuator/health):

{
  "status": "UP",
  "components": {
    "db": { "status": "UP", "details": { "database": "PostgreSQL" } },
    "redis": { "status": "UP" },
    "paymentGateway": {
      "status": "UP",
      "details": { "gateway": "PG사 연동 정상", "latency_ms": 45 }
    }
  }
}

4) Micrometer 커스텀 메트릭 실전

4-1) 메트릭 타입별 활용

@Component
@RequiredArgsConstructor
public class OrderMetrics {
    
    private final MeterRegistry registry;
    
    // === Counter: 누적 횟수 (증가만 가능) ===
    public void recordOrderCreated(String paymentMethod) {
        registry.counter("orders.created",
            "payment_method", paymentMethod,  // 결제수단별 분류
            "channel", "web"                  // 채널별 분류
        ).increment();
    }
    
    // === Gauge: 현재 값 (증감 가능) ===
    // AtomicInteger 등을 바인딩하면 현재 상태를 추적
    private final AtomicInteger activeOrders = new AtomicInteger(0);
    
    @PostConstruct
    public void initGauges() {
        Gauge.builder("orders.active", activeOrders, AtomicInteger::get)
            .description("현재 처리 중인 주문 수")
            .register(registry);
    }
    
    // === Timer: 실행 시간 측정 ===
    public <T> T measureOrderProcessing(Supplier<T> task) {
        return registry.timer("orders.processing.duration",
            "type", "sync"
        ).record(task);
    }
    
    // === DistributionSummary: 값의 분포 ===
    public void recordOrderAmount(double amount) {
        DistributionSummary.builder("orders.amount")
            .description("주문 금액 분포")
            .baseUnit("won")
            .publishPercentiles(0.5, 0.95, 0.99)
            .publishPercentileHistogram()
            .maximumExpectedValue(10_000_000.0)
            .register(registry)
            .record(amount);
    }
}

4-2) 히스토그램 vs 서머리: 언제 뭘 쓰나?

기준HistogramSummary
Percentile 계산서버 측 (PromQL)클라이언트 측 (앱 내)
집계 가능✅ 여러 인스턴스 합산 가능❌ 인스턴스별로만 의미 있음
정확도버킷 경계에 따라 근사정확(windowed)
추천 상황다중 인스턴스 서비스단일 인스턴스, 정밀도 필요 시
💡 실무 원칙: 대부분의 경우 Histogram을 쓰세요.
  - 서비스가 여러 인스턴스로 스케일되면 Summary는 합산할 수 없습니다.
  - publishPercentileHistogram()을 켜면 Prometheus에서 
    histogram_quantile()로 p95/p99를 구할 수 있습니다.

4-3) 카디널리티 관리 (지표 폭발 방지)

메트릭 태그(라벨)를 남용하면 시계열이 기하급수적으로 늘어나 Prometheus가 OOM됩니다.

// ❌ 나쁜 예: 사용자 ID를 태그에 넣음 (카디널리티 폭발!)
registry.counter("api.requests", "user_id", userId);
// 사용자 100만명 × API 50개 = 5천만 시계열 → Prometheus 사망

// ✅ 좋은 예: 낮은 카디널리티 태그만 사용
registry.counter("api.requests", 
    "endpoint", "/api/orders",    // 50개 이내
    "method", "GET",              // 5종
    "status", "200"               // 10종 이내
);
// 50 × 5 × 10 = 2,500 시계열 → 안전

카디널리티 안전 기준:

태그안전 범위위험 신호
endpoint< 100URL에 path variable 포함
status< 20커스텀 비즈니스 코드 수백 개
user_id❌ 절대 금지사용자 수 = 시계열 수
request_id❌ 절대 금지요청마다 새 시계열 생성

5) 대시보드 설계 원칙

5-1) 3계층 대시보드 전략

┌─────────────────────────────────────┐
│ Level 1: Overview (전체 서비스 상태)  │  ← NOC/온콜 엔지니어가 항상 보는 화면
│   - 전체 에러율, p95, 서비스 맵      │
├─────────────────────────────────────┤
│ Level 2: Service (개별 서비스 상세)   │  ← 문제 발생 시 드릴다운
│   - RED 메트릭, 엔드포인트별 분포     │
├─────────────────────────────────────┤
│ Level 3: Resource (인프라/리소스)     │  ← 병목 원인 파악
│   - CPU/메모리/GC/커넥션 풀/슬로우쿼리│
└─────────────────────────────────────┘

5-2) Level 1 대시보드에 반드시 포함할 패널

1. Traffic Light (Red/Yellow/Green)
   - 각 서비스의 에러율 기반 상태 신호등
   
2. p95 Latency 추이 (시계열 그래프)
   - 엔드포인트별 p95, SLO 라인과 겹쳐서 표시
   
3. Error Rate 추이
   - 5xx rate, 서킷브레이커 트립 횟수
   
4. Saturation 게이지
   - CPU, 힙 메모리, 커넥션 풀 사용률

5. 최근 배포/변경 이벤트 (Annotation)
   - 배포 시점을 그래프에 표시 → 성능 변화와 배포의 상관관계 파악

6) APM 도입 로드맵

모든 것을 한꺼번에 하려면 실패합니다. 단계별로 접근하세요.

Day 1: 최소 관측성

✅ Spring Boot Actuator + Prometheus 엔드포인트 활성화
✅ /health, /prometheus 엔드포인트 확인
✅ Prometheus scrape 설정 추가
✅ Grafana에 Spring Boot 템플릿 대시보드 import (ID: 12900)

Week 1: 핵심 메트릭 + 알림

✅ 커스텀 비즈니스 메트릭 3~5개 추가 (주문 수, 결제 성공률 등)
✅ SLO 기반 알림 2~3개 설정 (p95 > 1s, error rate > 1%)
✅ 커넥션 풀/스레드 풀 메트릭 대시보드 추가
✅ 구조 로깅(Structured Logging) 전환 시작

Month 1: 전체 관측성 파이프라인

✅ 분산 트레이싱 도입 (OpenTelemetry + Tempo/Jaeger)
✅ Log ↔ Trace 연결 (traceId를 로그에 포함)
✅ 3계층 대시보드 완성
✅ 온콜 Runbook에 대시보드 링크 + 조치 가이드 포함
✅ 정기 리뷰: 월 1회 알림 품질 점검 (노이즈 비율 < 20%)

7) 흔한 실수와 안티패턴

#안티패턴왜 문제인가올바른 접근
1평균만 모니터링P99 문제를 놓침P95/P99 필수
2모든 것에 알림Alert Fatigue → 무시SLO 기반 핵심 알림만
3태그에 고카디널리티 값Prometheus OOMuser_id, request_id 금지
4Actuator 전체 공개정보 유출네트워크/인증 제한
5메트릭만 보고 로그/트레이스 무시원인 파악 불가3대 신호 통합
6배포 후 메트릭 확인 안 함성능 저하 뒤늦게 발견배포 이벤트 annotation + 자동 비교

운영 체크리스트

  • Actuator + Prometheus 엔드포인트 활성화 및 보안 설정 완료
  • Actuator를 별도 포트(management.server.port)로 분리했는가?
  • Golden Signals 4가지가 대시보드에 모두 있는가?
  • SLO 기반 알림이 2개 이상 설정되어 있는가?
  • 커스텀 비즈니스 메트릭이 3개 이상인가?
  • 카디널리티 검증: 태그 조합 시계열 수가 10,000 이하인가?
  • 로그에 traceId가 포함되어 있는가?
  • 배포 이벤트가 그래프에 annotation으로 표시되는가?
  • 월 1회 알림 품질 리뷰를 하고 있는가?

관련 글


👉 다음 편: APM 기본 (Part 2: Actuator, Prometheus 연동)