2026년 7월 12일

마스터하기: Observability (관측 가능성) - 분산 시스템의 눈과 귀

10
마스터하기: Observability (관측 가능성) - 분산 시스템의 눈과 귀

마스터하기: Observability (관측 가능성) - 분산 시스템의 눈과 귀

마스터하기: Observability (관측 가능성) - 분산 시스템의 눈과 귀

1. 개념 소개: 정의, 탄생 배경, 왜 중요한지

1. 개념 소개: 정의, 탄생 배경, 왜 중요한지

안녕하세요! 10년 차 소프트웨어 엔지니어이자 기술 교육자로서, 저는 수많은 시스템의 흥망성쇠를 지켜보며 한 가지 변치 않는 진리를 깨달았습니다. 바로, "당신이 볼 수 없다면, 이해할 수 없고, 이해할 수 없다면, 제어할 수 없다"는 것입니다. 특히 현대의 복잡한 분산 시스템 환경에서는 이 진리가 더욱 강력하게 적용됩니다. 오늘 우리가 함께 마스터할 주제는 바로 이 "보는 능력"을 극대화하는 **Observability (관측 가능성)**입니다.

Observability란 무엇인가요?

Observability는 시스템의 외부 출력을 통해 시스템의 내부 상태를 얼마나 잘 추론할 수 있는지 나타내는 척도입니다. 쉽게 말해, "시스템이 왜 이렇게 동작하는가?"라는 질문에 명확하고 신속하게 답할 수 있는 능력이라고 할 수 있습니다. 시스템 내부를 들여다볼 수 있는 일종의 "초능력"인 셈이죠.

Observability는 왜 탄생했나요?

Observability의 중요성은 모놀리식 아키텍처에서 마이크로서비스, 클라우드 네이티브, 분산 시스템으로 전환되면서 폭발적으로 증가했습니다. 과거 모놀리식 애플리케이션에서는 하나의 코드베이스 내에서 모든 로직이 실행되었기 때문에, 문제가 발생하면 스택 트레이스나 간단한 로그만으로도 원인을 파악하기 비교적 쉬웠습니다.

하지만 현대 분산 시스템에서는 하나의 사용자 요청이 여러 마이크로서비스, 데이터베이스, 메시지 큐, 캐시 등을 거치며 처리됩니다. 각 서비스는 독립적으로 배포되고 운영되며, 서로 다른 언어와 프레임워크로 작성될 수도 있습니다. 이런 환경에서는 "내 서비스는 문제가 없는데, 왜 사용자 요청은 실패하지?"와 같은 질문에 답하기가 극도로 어려워집니다. 전통적인 모니터링 방식으로는 시스템 전체의 맥락을 파악하기 힘들었기에, 시스템의 복잡성을 이해하고 문제를 해결하기 위한 새로운 접근 방식, 즉 Observability가 필수적인 요소로 자리 잡게 되었습니다.

Observability가 왜 중요한가요?

Observability는 단순한 "있으면 좋은 것"이 아니라, 현대 소프트웨어 개발 및 운영의 핵심 성공 요인입니다.

  1. 신속한 문제 진단 및 해결: 장애 발생 시, 시스템의 내부 상태를 명확하게 파악하여 문제의 근본 원인을 빠르고 정확하게 찾아내고 해결할 수 있습니다. 이는 시스템 다운타임을 최소화하고 서비스 연속성을 보장하는 데 결정적인 역할을 합니다.
  2. 성능 최적화: 시스템의 병목 현상, 비효율적인 코드 경로, 리소스 낭비 지점 등을 식별하여 성능을 개선하고 비용을 절감할 수 있습니다.
  3. 시스템 이해도 증진: 개발자 및 운영팀이 시스템이 어떻게 작동하고, 각 구성 요소가 어떻게 상호작용하는지 명확하게 이해하는 데 도움을 줍니다. 이는 새로운 기능 개발, 아키텍처 개선, 온보딩 등 다양한 측면에서 긍정적인 영향을 미칩니다.
  4. 사용자 경험 개선: 잠재적 문제를 미리 감지하고 사용자에게 영향을 미치기 전에 선제적으로 대응할 수 있게 합니다. 이는 서비스의 신뢰도를 높이고 사용자 만족도를 향상시킵니다.
  5. 예측 및 예방: 과거 데이터를 기반으로 미래의 문제를 예측하고, 시스템 장애를 사전에 예방하기 위한 조치를 취할 수 있도록 지원합니다.

결론적으로, Observability는 현대 분산 시스템의 복잡성이라는 미로 속에서 길을 잃지 않고, 효율적으로 문제를 해결하며, 더 나아가 시스템을 발전시켜 나갈 수 있도록 돕는 나침반이자 지도와 같습니다.

2. 핵심 원리 설명 (비유와 다이어그램 활용)

2. 핵심 원리 설명 (비유와 다이어그램 활용)

Observability는 주로 세 가지 핵심 요소, 즉 **로그 (Logs), 메트릭 (Metrics), 트레이스 (Traces)**를 통해 구현됩니다. 이 세 가지는 "Observability의 세 기둥(Three Pillars of Observability)"이라고 불리며, 서로 보완하며 시스템의 전반적인 상태를 이해하는 데 필수적인 정보를 제공합니다.

Observability의 세 기둥

1. Logs (로그): "무슨 일이 일어났는가?"

  • 정의: 로그는 시스템에서 발생하는 모든 종류의 이벤트를 시간 순서대로 기록한 텍스트 데이터입니다. 특정 시점에 어떤 일이 발생했는지에 대한 상세한 기록을 제공합니다.
  • 비유: 비행기의 블랙박스 기록과 같습니다. 사고가 발생했을 때, 블랙박스에 기록된 모든 음성, 데이터, 센서 정보 등을 분석하여 사고의 원인과 과정을 재구성할 수 있습니다. 로그 또한 시스템의 특정 시점에서 발생한 모든 사건을 기록하여, 문제가 발생했을 때 그 원인을 추적하는 데 사용됩니다.
  • 주요 정보: 타임스탬프, 서비스명, 로그 레벨(DEBUG, INFO, WARN, ERROR 등), 메시지 내용, 요청 ID, 사용자 ID 등.
  • 중요성: 분산 시스템에서는 각 서비스가 독립적으로 로그를 생성하므로, 이 로그들을 중앙 집중식으로 수집하고 검색할 수 있는 시스템(예: ELK Stack(Elasticsearch, Logstash, Kibana), Grafana Loki)이 필수적입니다. 이를 통해 여러 서비스에 걸쳐 발생한 이벤트를 한곳에서 분석할 수 있습니다.

2. Metrics (메트릭): "얼마나 많이/자주/빠르게 일어나는가?"

  • 정의: 메트릭은 시간 경과에 따라 수집되는 측정 가능한 숫자 데이터입니다. 시스템의 전반적인 건강 상태와 성능 추세를 파악하는 데 사용됩니다.
  • 비유: 자동차의 계기판과 같습니다. 속도, RPM, 연료량, 엔진 온도 등은 현재 자동차의 상태를 숫자로 보여주며, 운전자는 이 정보를 통해 차량이 정상적으로 작동하는지, 연료가 부족한지 등을 즉시 파악할 수 있습니다.
  • 주요 종류:
    • Counter: 특정 이벤트 발생 횟수처럼 단조롭게 증가하는 값 (예: 총 요청 수, 에러 발생 수).
    • Gauge: 특정 시점의 현재 값 (예: CPU 사용률, 메모리 사용량, 현재 활성 사용자 수).
    • Histogram: 특정 값의 분포를 측정 (예: 요청 응답 시간의 분포).
    • Summary: Histogram과 유사하지만, 클라이언트 측에서 분위수(percentiles)를 계산합니다.
  • 중요성: 메트릭은 시스템의 추세를 파악하고, 임계값을 설정하여 알림을 발생시키는 데 매우 유용합니다. Prometheus, Grafana와 같은 도구를 사용하여 메트릭을 수집하고 시각화하며, 대시보드를 통해 시스템의 상태를 한눈에 모니터링할 수 있습니다.

3. Traces (트레이스): "어떻게 일이 진행되었는가?"

  • 정의: 트레이스는 분산 시스템 내에서 단일 요청의 전체 실행 흐름을 시각화한 데이터입니다. 요청이 여러 서비스를 거쳐 처리되는 과정을 보여주며, 각 단계(Span)의 시작 시간, 종료 시간, 소요 시간, 서비스 간의 호출 관계 등을 기록합니다.
  • 비유: 택배 송장 추적과 같습니다. 택배가 주문된 순간부터 고객에게 전달되기까지 어떤 물류센터를 거쳤고, 각 물류센터에서 얼마나 머물렀는지 등을 상세히 파악할 수 있습니다. 트레이스 또한 사용자 요청이 시스템에 들어와 응답으로 나갈 때까지 어떤 서비스를 거쳤고, 각 서비스에서 얼마나 시간이 소요되었는지 보여줍니다.
  • 주요 개념:
    • Trace ID: 단일 요청의 전체 흐름을 식별하는 고유 ID.
    • Span: Trace 내에서 하나의 작업 단위(예: 특정 서비스 호출, 데이터베이스 쿼리)를 나타냅니다. 각 Span은 고유한 Span ID와 부모 Span ID를 가집니다.
    • Parent Span ID: 현재 Span을 호출한 상위 Span의 ID를 나타내어 Span들 간의 계층 구조를 형성합니다.
  • 중요성: 분산 트레이싱은 마이크로서비스 아키텍처에서 특정 요청이 왜 느려졌는지, 어떤 서비스에서 오류가 발생했는지 등을 시각적으로 빠르게 파악할 수 있게 해줍니다. OpenTelemetry, Jaeger, Zipkin과 같은 도구들이 널리 사용됩니다.

다이어그램으로 보는 Observability

아래 다이어그램은 간단한 마이크로서비스 아키텍처에서 사용자의 요청이 처리될 때, 세 가지 Observability 데이터가 어떻게 생성되고 수집되는지를 보여줍니다.

+----------------+       +----------------+       +----------------+
|    User /      |       |    Service A   |       |    Service B   |
| API Gateway    |------>| (Auth, Order)  |------>| (Product, DB)  |
|                |<------|(Log, Metric,   |<------|(Log, Metric,   |
+----------------+       | Trace: Span1)  |       | Trace: Span2)  |
         |                       |                        |
         |                       |                        |
         |                       |                        |
         v                       v                        v
+----------------+    +------------------+    +------------------+
| Centralized    |    | Centralized      |    | Distributed      |
| Logging System |<---| Metrics System   |<---| Tracing System   |
| (ELK, Loki)    |    | (Prometheus,     |    | (Jaeger,         |
|                |    | Grafana)         |    | OpenTelemetry)   |
+----------------+    +------------------+    +------------------+

[사용자 요청 흐름]
1. 사용자 요청이 API Gateway를 통해 들어옴.
2. API Gateway는 Service A (인증/주문 처리)를 호출. (Span 1 시작)
3. Service A는 Service B (상품 정보/DB 처리)를 호출. (Span 2 시작, Span 1의 자식 Span)
4. Service B는 DB에서 데이터를 가져와 Service A로 응답. (Span 2 종료)
5. Service A는 받은 데이터를 처리하여 API Gateway로 응답. (Span 1 종료)
6. API Gateway는 최종 응답을 사용자에게 전달.

[Observability 데이터 생성 및 수집]
- 각 서비스 (API Gateway, Service A, Service B)는 자신에게 일어나는 이벤트를 상세 로그로 기록하고 중앙 집중식 로깅 시스템으로 전송합니다.
- 각 서비스는 CPU 사용량, 요청 처리 시간, 에러율 등의 메트릭을 주기적으로 수집하여 메트릭 시스템으로 전송합니다.
- 각 서비스는 요청의 시작과 끝, 그리고 다른 서비스 호출 시 트레이스 데이터를 생성(Span 생성)하고, Trace ID와 Parent Span ID를 통해 요청의 전체 흐름을 연결하여 분산 트레이싱 시스템으로 전송합니다.

이 다이어그램처럼, Observability의 세 기둥은 각기 다른 관점에서 시스템의 상태를 보여주지만, 궁극적으로는 "어떤 요청이 왜, 어떻게 동작했는지"에 대한 완전한 그림을 제공하기 위해 유기적으로 연결되어야 합니다.

3. 코드 예제 2개 (Python 또는 JavaScript, 주석 포함)

초중급 개발자분들을 위해 Python과 Node.js (JavaScript)를 사용하여 Observability의 핵심 요소를 구현하는 간단한 예제를 보여드리겠습니다.

예제 1: Python - 로깅 및 메트릭 (Flask 웹 애플리케이션)

이 예제는 Flask 웹 애플리케이션에서 기본적인 로깅과 Prometheus 메트릭을 설정하는 방법을 보여줍니다. requests_total 메트릭을 사용하여 총 요청 수를 기록하고, request_duration_seconds 메트릭으로 요청 처리 시간을 기록합니다.

# app.py
import logging
import time
import os
from flask import Flask, request, jsonify
from prometheus_client import generate_latest, Counter, Histogram, Gauge, Summary # prometheus_client 라이브러리 설치 필요
from prometheus_client.core import CollectorRegistry
from werkzeug.middleware.dispatcher import DispatcherMiddleware # Flask 앱과 Prometheus 익스포터를 연결하기 위함

# 1. 로깅 설정
# 모든 로그를 표준 출력(콘솔)으로 보냄
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(request_id)s - %(message)s')
logger = logging.getLogger(__name__)

# 커스텀 필드를 추가하기 위한 필터 (여기서는 request_id를 추가)
class RequestIdFilter(logging.Filter):
    def filter(self, record):
        record.request_id = getattr(request, 'request_id', 'N/A') # request 객체에 request_id가 없으면 'N/A'
        return True

logger.addFilter(RequestIdFilter())

app = Flask(__name__)

# 2. Prometheus 메트릭 설정
# CollectorRegistry: Prometheus 메트릭의 레지스트리 (기본 레지스트리를 사용해도 무방하나, 명시적으로 관리할 때 유용)
registry = CollectorRegistry()

# Counter: 요청이 들어올 때마다 1씩 증가하는 카운터
REQUESTS_TOTAL = Counter(
    'http_requests_total', # 메트릭 이름
    'Total HTTP Requests', # 설명
    ['method', 'endpoint', 'status'], # 레이블: 요청 메서드, 엔드포인트, 응답 상태 코드를 기준으로 집계
    registry=registry
)

# Histogram: 요청 처리 시간의 분포를 측정. 버킷을 지정하여 각 구간에 속하는 요청 수를 알 수 있음.
REQUEST_DURATION_SECONDS = Histogram(
    'http_request_duration_seconds',
    'HTTP Request Duration in seconds',
    ['method', 'endpoint'],
    buckets=(0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 10.0, float('inf')), # 응답 시간 버킷
    registry=registry
)

# 간단한 헬스체크 엔드포인트
@app.route('/health')
def health_check():
    return "OK", 200

# 메인 API 엔드포인트
@app.route('/api/items/<item_id>')
def get_item(item_id):
    start_time = time.time()
    request_id = request.headers.get('X-Request-ID', os.urandom(8).hex()) # 요청 ID 추출 또는 생성
    request.request_id = request_id # 로깅 필터에서 사용하도록 request 객체에 추가

    logger.info(f"Received request for item_id: {item_id}")

    try:
        # 가상의 작업 수행 (예: DB 조회, 다른 서비스 호출 등)
        time.sleep(0.1) # 100ms 지연 시뮬레이션
        if item_id == 'error':
            raise ValueError("Item not found or internal error")

        response_data = {"item_id": item_id, "name": f"Item {item_id}", "price": 100}
        status_code = 200
        logger.info(f"Successfully processed request for item_id: {item_id}")
        return jsonify(response_data), status_code

    except ValueError as e:
        status_code = 404 if "not found" in str(e) else 500
        logger.error(f"Error processing request for item_id {item_id}: {e}", exc_info=True) # exc_info=True로 스택 트레이스 포함
        return jsonify({"error": str(e)}), status_code
    except Exception as e:
        status_code = 500
        logger.critical(f"Critical error for item_id {item_id}: {e}", exc_info=True)
        return jsonify({"error": "Internal server error"}), status_code
    finally:
        duration = time.time() - start_time
        method = request.method
        endpoint = request.path

        # 메트릭 업데이트
        REQUESTS_TOTAL.labels(method, endpoint, status_code).inc() # 요청 수 증가
        REQUEST_DURATION_SECONDS.labels(method, endpoint).observe(duration) # 요청 처리 시간 기록
        logger.info(f"Request for {endpoint} took {duration:.4f} seconds with status {status_code}")

# Prometheus 메트릭을 노출하는 엔드포인트
@app.route('/metrics')
def metrics():
    return generate_latest(registry).decode('utf-8')

# Flask 앱을 Prometheus 익스포터와 함께 실행
# /metrics 경로로 오는 요청은 generate_latest()로 처리하고, 나머지 요청은 app으로 처리
application = DispatcherMiddleware(app, {
    '/metrics': lambda env, start_response: generate_latest(registry)(env, start_response)
})

# 직접 실행 시 (개발용)
if __name__ == '__main__':
    from werkzeug.serving import run_simple
    # pip install flask prometheus_client
    # python app.py
    # 접속: http://localhost:5000/api/items/123
    # 에러 테스트: http://localhost:5000/api/items/error
    # 메트릭 확인: http://localhost:5000/metrics
    run_simple('localhost', 5000, application)

실행 방법:

  1. pip install flask prometheus_client
  2. python app.py
  3. 브라우저에서 http://localhost:5000/api/items/123 또는 http://localhost:5000/api/items/error를 여러 번 호출합니다.
  4. http://localhost:5000/metrics에 접속하면 수집된 메트릭을 확인할 수 있습니다.
  5. 콘솔에는 request_id가 포함된 로그가 출력됩니다.

예제 2: JavaScript (Node.js) - 분산 트레이싱 (OpenTelemetry 사용)

이 예제는 Node.js Express 애플리케이션에서 OpenTelemetry를 사용하여 분산 트레이싱을 구현하는 방법을 보여줍니다. X-Request-ID 헤더를 통해 컨텍스트를 전파하고, 외부 서비스 호출 시 Span을 생성하여 트레이스를 연결합니다.

// service-a.js (첫 번째 서비스)
// npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/sdk-trace-base @opentelemetry/exporter-zipkin @opentelemetry/instrumentation-express express node-fetch
const opentelemetry = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin');
const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-base');
const express = require('express');
const fetch = require('node-fetch'); // HTTP 요청을 위한 라이브러리
const crypto = require('crypto'); // UUID 생성을 위한 모듈

// OpenTelemetry SDK 초기화
const sdk = new opentelemetry.NodeSDK({
  traceExporter: new ZipkinExporter({
    url: 'http://localhost:9411/api/v2/spans', // Zipkin 서버 주소 (로컬에서 Zipkin 실행 필요)
  }),
  spanProcessor: new BatchSpanProcessor(new ZipkinExporter({ url: 'http://localhost:9411/api/v2/spans' })),
  instrumentations: [getNodeAutoInstrumentations()], // Express, HTTP/HTTPS 등 자동 계측
});

sdk.start();
console.log('OpenTelemetry SDK initialized for Service A');

const