2026년 3월 24일

Observability: 분산 시스템의 블랙박스를 밝히는 세 가지 빛 (로그, 메트릭, 트레이싱)

80
Observability: 분산 시스템의 블랙박스를 밝히는 세 가지 빛 (로그, 메트릭, 트레이싱)

Observability: 분산 시스템의 블랙박스를 밝히는 세 가지 빛 (로그, 메트릭, 트레이싱)

Observability: 분산 시스템의 블랙박스를 밝히는 세 가지 빛 (로그, 메트릭, 트레이싱)

안녕하세요, 10년 경력의 소프트웨어 엔지니어이자 기술 교육자입니다. 오늘(2026. 3. 24.)은 현대 소프트웨어 개발에서 가장 중요하지만, 종종 제대로 이해되지 못하는 개념 중 하나인 **Observability (관측 가능성)**에 대해 이야기해보려 합니다. 특히 마이크로서비스와 클라우드 환경이 보편화되면서, 시스템의 내부를 투명하게 들여다보고 문제를 빠르게 진단하는 능력은 개발자와 운영자 모두에게 필수적인 역량이 되었습니다.

1. 개념 소개

1. 개념 소개

정의: 시스템의 내부를 들여다보는 능력

Observability는 시스템의 외부 출력을 통해 시스템의 내부 상태를 얼마나 잘 추론할 수 있는지를 나타내는 척도입니다. 쉽게 말해, **"시스템이 왜 이렇게 동작하는가?"**라는 질문에 답할 수 있는 능력을 말합니다. 단순히 시스템이 '죽었는지 살았는지'를 넘어, '왜 죽었는지', '어떤 과정에서 문제가 발생했는지', '성능 저하의 원인이 무엇인지' 등을 심층적으로 파악할 수 있는 역량이죠.

탄생 배경: 복잡성 증가와 모니터링의 한계

과거 모놀리식(Monolithic) 아키텍처 시절에는 하나의 애플리케이션이 모든 기능을 담당했기 때문에, 문제가 발생하면 해당 애플리케이션의 로그나 자원 사용량을 확인하는 것만으로도 원인을 파악하기 비교적 쉬웠습니다.

하지만 현대 소프트웨어 시스템은 마이크로서비스, 클라우드 네이티브, 분산 시스템 아키텍처로 진화하면서 극도로 복잡해졌습니다. 수십, 수백 개의 서비스가 서로 통신하고, 컨테이너 오케스트레이션(Kubernetes), 서버리스(Serverless) 같은 동적인 환경에서 동작합니다. 이러한 환경에서는 단순히 CPU 사용률, 메모리 사용량 같은 '모니터링' 지표만으로는 예상치 못한 문제의 원인을 파악하기가 거의 불가능해졌습니다.

이러한 복잡성 속에서, 시스템의 '블랙박스'를 투명하게 들여다볼 수 있는 새로운 접근 방식이 필요해졌고, 그것이 바로 Observability의 탄생 배경입니다.

왜 중요한가: 빠르고 정확한 문제 해결의 열쇠

Observability는 현대 소프트웨어 시스템의 안정성과 효율성을 보장하는 핵심 요소입니다.

  • 문제 해결 시간 단축 (MTTR - Mean Time To Resolution): 시스템 장애 발생 시, 문제의 근원지를 빠르고 정확하게 찾아 해결함으로써 서비스 중단 시간을 최소화합니다. 이는 비즈니스 손실을 줄이고 사용자 신뢰를 유지하는 데 결정적입니다.
  • 성능 최적화: 시스템의 병목 현상, 불필요한 자원 소모 지점 등을 식별하여 성능을 최적화하고 운영 비용을 절감할 수 있습니다.
  • 운영 안정성 및 예측: 실시간으로 시스템 상태를 파악하고 트렌드를 분석하여 잠재적 위험을 미리 예측하고 예방할 수 있습니다.
  • 사용자 경험 개선: 문제 발생 시 사용자에게 미치는 영향을 최소화하고 빠른 복구를 통해 전반적인 서비스 신뢰도를 높입니다.
  • 개발 생산성 향상: 개발 단계에서부터 시스템의 동작을 쉽게 이해하고 디버깅할 수 있어 개발 생산성이 향상됩니다.

2. 핵심 원리 설명: 세 가지 기둥 (로그, 메트릭, 트레이싱)

2. 핵심 원리 설명: 세 가지 기둥 (로그, 메트릭, 트레이싱)

Observability는 주로 세 가지 핵심 기둥을 통해 구현됩니다. 이 세 가지는 각각 다른 질문에 답하며, 상호 보완적으로 작동하여 시스템의 전체적인 그림을 보여줍니다.

비유: 자동차의 계기판과 정비사의 진단 도구

Observability를 자동차에 비유하면 쉽게 이해할 수 있습니다.

  1. 로그 (Logs): 자동차의 블랙박스 기록 / 정비사의 작업 일지
    • "무슨 일이 일어났는가?" 에 대한 상세한 정보를 제공합니다.
    • 특정 시점에 시스템 내에서 발생한 사건(이벤트)의 기록입니다. 누가, 언제, 무엇을 했고, 그 결과가 어떠했는지 등 텍스트 형태로 상세한 정보를 담고 있습니다. 에러 메시지, 사용자 로그인 기록, 데이터베이스 쿼리 실행 기록 등이 여기에 해당합니다.
    • 특징: 불연속적이고, 이벤트 중심적이며, 상세합니다.
  2. 메트릭 (Metrics): 자동차의 속도계, 연료 게이지, 엔진 온도계
    • "시스템이 얼마나 잘/못하고 있는가?" 에 대한 측정 가능한 숫자 데이터를 제공합니다.
    • 시간의 흐름에 따라 측정 가능한 시스템의 상태를 나타내는 숫자 데이터입니다. CPU 사용률, 메모리 사용량, 네트워크 트래픽, 초당 요청 수(RPS), 에러율, 응답 시간 등이 대표적인 예입니다.
    • 특징: 연속적이고, 시간의 흐름에 따른 변화를 관찰하며, 집계 및 추세 분석에 용이합니다.
  3. 트레이싱 (Tracing): 자동차의 출고부터 소비자 인도까지의 이력 추적 번호
    • "요청이 시스템 내부에서 어떻게 이동했는가?" 에 대한 전체 흐름을 시각화합니다.
    • 분산 시스템에서 단일 사용자 요청이 여러 서비스와 컴포넌트를 거치면서 어떻게 처리되었는지 그 전체 흐름을 추적하고 시각화하는 기술입니다. 각 서비스 호출에 고유한 Trace ID를 부여하고, 각 호출 단계(Span)에서 걸린 시간과 정보를 기록하여 요청의 전체 경로를 파악합니다.
    • 특징: 분산 환경에서 요청의 전파 경로를 파악하며, 서비스 간의 의존성과 병목 현상을 식별하는 데 매우 유용합니다.

다이어그램 (개념 설명)

(실제 다이어그램을 삽입할 수 없으므로 텍스트로 개념을 설명합니다.)

사용자가 웹 브라우저에서 어떤 기능을 요청했다고 가정해 봅시다. 이 요청은 여러 단계를 거쳐 처리됩니다.

  1. 사용자 요청 (예: GET /api/users/123)
  2. API Gateway (서비스의 진입점)
    • Trace ID (예: abcde12345)를 생성하여 요청 헤더에 삽입합니다.
    • 요청이 들어왔다는 메트릭 (http_requests_total 증가)을 기록합니다.
    • 로그 (INFO: Request received for /api/users/123 with Trace ID abcde12345)를 기록합니다.
  3. User Service (사용자 정보를 처리하는 마이크로서비스)
    • Trace ID를 헤더에서 읽어 다음 호출로 전달합니다.
    • 요청 처리 시작/종료 시 메트릭 (user_service_latency 기록)을 업데이트합니다.
    • 로그 (INFO: Fetching user 123 from DB, Trace ID: abcde12345)를 기록합니다.
  4. Database (데이터베이스)
    • SQL 쿼리 실행 시간을 메트릭으로 기록합니다.
    • 쿼리 실행 로그를 남깁니다.
  5. User Service (DB 응답 처리 후)
    • 로그 (INFO: User 123 fetched successfully, Trace ID: abcde12345)를 기록합니다.
  6. API Gateway (최종 응답을 사용자에게 전달)
    • 요청 처리 완료 메트릭 (http_requests_total 증가, http_request_duration_seconds 기록)을 업데이트합니다.
    • 로그 (INFO: Responding to /api/users/123, Trace ID: abcde12345)를 기록합니다.

이렇게 모든 단계에서 Trace ID를 중심으로 로그, 메트릭이 연동되어 기록되면, 나중에 이 Trace ID 하나만으로 특정 요청의 전체 흐름(트레이스)을 재구성하고, 각 단계에서 어떤 일이 일어났는지(로그), 얼마나 시간이 걸렸는지(메트릭)를 한눈에 파악할 수 있게 됩니다.

3. 코드 예제 2개

예제 1: 구조화된 로깅 (Structured Logging) (Python)

일반적인 텍스트 로그는 사람이 읽기에는 편하지만, 기계가 파싱하고 분석하기 어렵습니다. 구조화된 로깅은 로그를 JSON과 같은 기계가 읽기 쉬운 형식으로 출력하여 중앙 집중식 로그 관리 시스템(ELK Stack, Grafana Loki 등)에서 쉽게 검색하고 분석할 수 있게 합니다.

import logging
import json
import sys
import uuid
from datetime import datetime

# JsonFormatter 클래스는 Python의 logging.Formatter를 상속받아
# 로그 레코드를 JSON 문자열로 변환하는 역할을 합니다.
class JsonFormatter(logging.Formatter):
    def format(self, record):
        # 로그 엔트리에 포함될 기본 정보들을 딕셔너리 형태로 정의합니다.
        log_entry = {
            "timestamp": datetime.fromtimestamp(record.created).isoformat(), # 로그 발생 시간 (ISO 8601 형식)
            "level": record.levelname,  # 로그 레벨 (INFO, WARNING, ERROR 등)
            "message": record.getMessage(), # 로그 메시지 본문
            "service": "my-awesome-service", # 어떤 서비스에서 발생한 로그인지 명시
            "module": record.module,    # 로그를 남긴 모듈 이름
            "funcName": record.funcName,# 로그를 남긴 함수 이름
            "lineno": record.lineno,    # 로그를 남긴 코드 라인 번호
        }

        # logging.Logger.info() 등의 메서드 호출 시, `extra` 인자를 통해
        # 추가적인 컨텍스트 정보를 전달할 수 있습니다.
        # 이 정보들은 `record` 객체의 속성으로 추가됩니다.
        # getattr(record, '속성명', '기본값')을 사용하여 안전하게 접근합니다.
        if hasattr(record, 'request_id'):
            log_entry["request_id"] = record.request_id
        if hasattr(record, 'user_id'):
            log_entry["user_id"] = record.user_id
        if hasattr(record, 'status'):
            log_entry["status"] = record.status
        if hasattr(record, 'error_type'):
            log_entry["error_type"] = record.error_type

        # 예외 정보가 있다면 로그 엔트리에 추가합니다.
        if record.exc_info:
            log_entry["exception"] = self.formatException(record.exc_info)
            
        # 최종적으로 딕셔너리를 JSON 문자열로 변환하여 반환합니다.
        return json.dumps(log_entry, ensure_ascii=False)

# 로거(logger)를 설정합니다.
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO) # INFO 레벨 이상의 로그만 처리하도록 설정

# 콘솔(표준 출력)로 로그를 내보낼 핸들러를 생성합니다.
handler = logging.StreamHandler(sys.stdout)
# 위에서 정의한 JsonFormatter를 핸들러에 연결합니다.
handler.setFormatter(JsonFormatter())
# 로거에 핸들러를 추가합니다.
logger.addHandler(handler)

# 요청을 처리하는 함수를 시뮬레이션합니다.
def process_request(user_id):
    # 각 요청에 고유한 ID를 부여하여 트레이싱 및 검색에 활용합니다.
    request_id = str(uuid.uuid4())
    
    # INFO 레벨 로그를 남기면서 `extra` 인자로 `request_id`와 `user_id`를 전달합니다.
    logger.info("Processing request", extra={"request_id": request_id, "user_id": user_id})

    try:
        # 시뮬레이션: 홀수 user_id는 에러 발생
        if user_id % 2 != 0:
            raise ValueError("Invalid user_id for processing")
        
        result = f"Request {request_id} processed successfully for user {user_id}"
        # 성공 로그를 남깁니다.
        logger.info(result, extra={"request_id": request_id, "user_id": user_id, "status": "success"})
        return result
    except ValueError as e:
        # 에러 발생 시 ERROR 레벨 로그를 남기고, `exc_info=True`로 예외 스택 트레이스를 포함합니다.
        # 추가적인 에러 관련 정보도 `extra`로 전달합니다.
        logger.error(f"Error processing request: {e}",
                     exc_info=True,
                     extra={"request_id": request_id, "user_id": user_id, "status": "failed", "error_type": "ValueError"})
        return f"Failed to process request for user {user_id}"

if __name__ == "__main__":
    print("--- 구조화된 로깅 예제 시작 ---")
    print("아래 출력은 JSON 형식의 로그입니다.")
    process_request(123) # 에러 발생
    process_request(124) # 성공