비유로 시작하기

비행기 조종석을 생각해보세요. 고도계, 속도계, 연료계, 엔진 온도계 등 수십 개의 계기판이 있습니다. 조종사는 이 계기판들을 통해 비행기 상태를 실시간으로 파악하고, 이상 징후가 발생하면 경보음이 울립니다.

모니터링 시스템은 소프트웨어의 조종석입니다. 시스템이 얼마나 잘 동작하고 있는지를 측정하고(Metrics), 무슨 일이 일어났는지 기록하며(Logs), 요청이 어디를 거쳤는지 추적합니다(Traces).


Observability 3대 요소

graph TD OBS[Observability] OBS --> METRICS[Metrics
숫자로 측정 - CPU, TPS, 에러율] OBS --> LOGS[Logs
이벤트 기록 - 오류 상세, 감사] OBS --> TRACES[Traces
요청 흐름 추적 - 분산 트레이싱] METRICS --> PROM[Prometheus + Grafana] LOGS --> ELK[ELK Stack / Loki] TRACES --> JAEGER[Jaeger / Zipkin]
요소 질문 도구
Metrics 지금 시스템이 얼마나 바쁜가? Prometheus, Datadog
Logs 에러가 왜 발생했나? ELK, Loki
Traces 어느 서비스에서 지연이 발생했나? Jaeger, Zipkin, Tempo

Prometheus + Grafana

가장 널리 쓰이는 오픈소스 메트릭 모니터링 스택입니다.

Prometheus 동작 원리

sequenceDiagram participant APP as Spring App
/actuator/prometheus participant PROM as Prometheus Server participant ALERT as Alertmanager participant GRAF as Grafana participant SLACK as Slack PROM->>APP: Scrape (15초마다 Pull) APP-->>PROM: 메트릭 데이터 PROM->>PROM: TSDB에 저장 GRAF->>PROM: PromQL 쿼리 PROM-->>GRAF: 데이터 반환 GRAF-->>GRAF: 대시보드 렌더링 PROM->>ALERT: 알림 규칙 위반 ALERT->>SLACK: Slack 알림 발송

Pull 방식: Prometheus가 각 서비스에서 메트릭을 수집합니다. Push 방식(Datadog Agent)과 대비됩니다.

Prometheus 설정

# prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

rule_files:
  - "alert_rules.yml"

alerting:
  alertmanagers:
    - static_configs:
        - targets: ["alertmanager:9093"]

scrape_configs:
  - job_name: 'spring-app'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['app1:8080', 'app2:8080']
    # Kubernetes 환경에서는 자동 디스커버리

  - job_name: 'kubernetes-pods'
    kubernetes_sd_configs:
      - role: pod
    relabel_configs:
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
        action: keep
        regex: true
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
        action: replace
        target_label: __metrics_path__

알림 규칙

# alert_rules.yml
groups:
  - name: application
    rules:
      - alert: HighErrorRate
        expr: |
          rate(http_server_requests_seconds_count{status=~"5.."}[5m])
          / rate(http_server_requests_seconds_count[5m]) > 0.01
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "에러율 1% 초과"
          description: " 에러율: "

      - alert: HighLatency
        expr: |
          histogram_quantile(0.95,
            rate(http_server_requests_seconds_bucket[5m])
          ) > 2
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "P95 응답시간 2초 초과"

      - alert: PodCrashLooping
        expr: rate(kube_pod_container_status_restarts_total[15m]) > 0
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Pod 재시작 감지: "

PromQL 주요 쿼리

# 초당 요청 수 (TPS)
rate(http_server_requests_seconds_count[5m])

# P95 응답시간
histogram_quantile(0.95, rate(http_server_requests_seconds_bucket[5m]))

# 에러율
rate(http_server_requests_seconds_count{status=~"5.."}[5m])
/ rate(http_server_requests_seconds_count[5m])

# JVM Heap 사용률
jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"}

# DB 커넥션 풀 사용률
hikaricp_connections_active / hikaricp_connections_max

# CPU 사용률 (컨테이너)
rate(container_cpu_usage_seconds_total[5m]) * 100

Spring Actuator + Micrometer

Spring Boot 애플리케이션의 메트릭을 Prometheus로 내보내는 설정입니다.

의존성

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'io.micrometer:micrometer-registry-prometheus'
}

설정

# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health, info, prometheus, metrics
  endpoint:
    health:
      show-details: always
      probes:
        enabled: true  # liveness, readiness 분리
  metrics:
    tags:
      application: ${spring.application.name}
      environment: ${spring.profiles.active}
    distribution:
      percentiles-histogram:
        http.server.requests: true  # 히스토그램 활성화 (P95 계산용)
      slo:
        http.server.requests: 100ms, 500ms, 1s, 2s

커스텀 메트릭

@Component
@RequiredArgsConstructor
public class OrderMetrics {

    private final MeterRegistry registry;
    private final Counter orderCreatedCounter;
    private final Counter orderCancelledCounter;
    private final Timer orderProcessingTimer;
    private final Gauge pendingOrdersGauge;

    @PostConstruct
    public void init() {
        orderCreatedCounter = Counter.builder("order.created.total")
            .description("생성된 주문 수")
            .tag("type", "all")
            .register(registry);

        orderCancelledCounter = Counter.builder("order.cancelled.total")
            .description("취소된 주문 수")
            .register(registry);

        orderProcessingTimer = Timer.builder("order.processing.duration")
            .description("주문 처리 시간")
            .publishPercentiles(0.5, 0.95, 0.99)
            .register(registry);
    }

    public void recordOrderCreated() {
        orderCreatedCounter.increment();
    }

    public void recordOrderProcessing(Runnable task) {
        orderProcessingTimer.record(task);
    }

    // 게이지: 현재 상태를 반영 (람다로 실시간 값 제공)
    public void registerPendingOrdersGauge(OrderRepository repository) {
        Gauge.builder("order.pending.count", repository, r -> r.countByStatus(OrderStatus.PENDING))
            .description("처리 대기 중인 주문 수")
            .register(registry);
    }
}

ELK Stack

Elasticsearch + Logstash + Kibana. 로그 수집, 저장, 시각화 스택입니다.

graph LR APP[Spring App
Logback] -->|JSON 로그| FB[Filebeat] FB -->|전송| LS[Logstash
파싱/필터링] LS -->|저장| ES[(Elasticsearch)] ES -->|쿼리| KI[Kibana
시각화/검색] SLACK[Slack] -.->|알림| KI

Spring Logback JSON 설정

<!-- logback-spring.xml -->
<configuration>
    <appender name="JSON_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
            <includeMdcKeyName>traceId</includeMdcKeyName>
            <includeMdcKeyName>spanId</includeMdcKeyName>
            <includeMdcKeyName>userId</includeMdcKeyName>
            <customFields>{"application":"myapp","environment":"prod"}</customFields>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="JSON_CONSOLE"/>
    </root>
</configuration>

출력 결과:

{
  "@timestamp": "2026-05-01T10:00:00.000Z",
  "level": "ERROR",
  "message": "주문 처리 실패",
  "logger": "com.example.OrderService",
  "traceId": "abc123def456",
  "spanId": "789xyz",
  "userId": "user-42",
  "application": "myapp",
  "environment": "prod",
  "exception": "com.example.OrderNotFoundException: Order not found: 12345"
}

Logstash 파이프라인

# logstash.conf
input {
  beats {
    port => 5044
  }
}

filter {
  json {
    source => "message"
  }

  # 에러 로그 태깅
  if [level] == "ERROR" {
    mutate {
      add_tag => ["error"]
    }
  }

  # IP 지역 정보 추가
  geoip {
    source => "clientIp"
  }
}

output {
  elasticsearch {
    hosts => ["elasticsearch:9200"]
    index => "myapp-logs-%{+YYYY.MM.dd}"
    ilm_enabled => true
    ilm_rollover_alias => "myapp-logs"
    ilm_policy => "myapp-logs-policy"
  }
}

Datadog

SaaS 형태의 통합 모니터링 플랫폼. 메트릭/로그/트레이싱을 하나의 플랫폼에서 제공합니다.

장점:

  • 설치가 매우 간단 (Agent 하나로 모든 것)
  • UI/UX가 우수함
  • APM(Application Performance Monitoring) 내장
  • 이상 감지(Anomaly Detection) AI 내장

단점:

  • 비용이 높음 (Host당 월 $15~$35)
  • 데이터가 외부로 나감 (금융권 제약)
# Kubernetes Datadog Agent
apiVersion: v1
kind: ConfigMap
metadata:
  name: datadog-config
data:
  datadog.yaml: |
    api_key: YOUR_API_KEY
    logs_enabled: true
    apm_config:
      enabled: true
    process_config:
      enabled: true
// Spring Boot Datadog 통합 (dd-java-agent 사용)
// JVM 옵션 추가만으로 자동 계측
// -javaagent:/path/to/dd-java-agent.jar
// -Ddd.service=myapp
// -Ddd.env=production
// -Ddd.version=1.0.0
// -Ddd.logs.injection=true

모니터링 전략

4 Golden Signals (SRE 기준)

신호 설명 예시 메트릭
Latency 요청 처리 시간 P50, P95, P99 응답시간
Traffic 요청량 TPS (초당 트랜잭션 수)
Errors 오류율 5xx 응답 비율
Saturation 자원 포화도 CPU, 메모리, 커넥션 풀

SLI / SLO / SLA

SLI (Service Level Indicator): 측정 지표
  예) 지난 30일간 가용성 = 성공 요청 수 / 전체 요청 수 = 99.95%

SLO (Service Level Objective): 내부 목표
  예) 가용성 ≥ 99.9%, P95 응답시간 ≤ 500ms

SLA (Service Level Agreement): 고객과의 계약
  예) 가용성 < 99.9% 시 서비스 크레딧 제공

알림 전략 (Alert Fatigue 방지)

P0 (즉시 대응, 24시간):
  - 서비스 완전 다운
  - 에러율 > 5%
  → PagerDuty / 전화

P1 (1시간 내 대응):
  - P95 응답시간 > 2초
  - DB 커넥션 풀 90% 이상
  → Slack #incidents

P2 (다음 근무일):
  - 디스크 80% 초과
  - 배포 실패
  → Slack #alerts (채널)

극한 시나리오

시나리오: 메트릭은 정상인데 사용자 불만 폭발

원인: 비즈니스 메트릭 없이 기술 메트릭만 모니터링

# 기술 메트릭 (정상): HTTP 200 응답률 99.9%
rate(http_requests_total{status="200"}[5m]) / rate(http_requests_total[5m])

# 비즈니스 메트릭 (이상): 결제 성공률 60%
rate(payment_success_total[5m]) / rate(payment_attempt_total[5m])

HTTP 200이어도 결제 로직 내부에서 실패할 수 있습니다. 비즈니스 메트릭을 반드시 추가하세요.

시나리오: 로그 스토리지 폭발

일일 로그 100GB 발생 → 한 달 3TB → 비용 폭발

해결책:
1. Log Level 조정: INFO → WARN (프로덕션)
2. 샘플링: 정상 요청의 1%만 DEBUG 로깅
3. ILM (Index Lifecycle Management): 7일 → 콜드 → 30일 후 삭제
4. 중요도별 보존 기간 분리: ERROR 90일, INFO 7일