본문 바로가기
클라우드/opentelemetry

💥 OpenTelemetry 예외 처리, 이거 모르면 큰일납니다! (성능과 안정성 둘 다 잡는 비법)

by gasbugs 2025. 11. 11.
"분명히 시스템에 장애가 발생했는데... 왜 내 대시보드에서는 모든 게 정상이라고 나올까?" 🧐

 

개발자라면 한 번쯤 겪어봤을 아찔한 순간입니다. 열심히 구축한 Observability 시스템이 정작 가장 중요한 순간에 침묵하는 상황이죠. 원인은 바로 OpenTelemetry의 예외 처리를 '제대로' 하지 않았기 때문일 수 있습니다.

 

오늘은 성능과 문제 해결 유용성, 두 마리 토끼를 모두 잡는 OpenTelemetry 예외 처리의 '골든 룰'을 알아보겠습니다.

 

🎯 핵심: 완벽한 예외 처리를 위한 3가지 황금률

핵심부터 말씀드리죠. OpenTelemetry 예외 처리의 정석은 바로 이 3가지 조합입니다.

  1. span.record_exception(): 예외의 상세 정보(스택 트레이스 등)를 기록한다.
  2. span.set_status(ERROR): 현재 스팬(Span)의 상태를 '실패'로 명확히 표시한다.
  3. raise (Re-throw): 예외를 다시 던져 애플리케이션의 원래 흐름을 방해하지 않는다.

이 세 가지를 함께 사용해야 비로소 완벽한 예외 추적이 가능해집니다. 하나씩 자세히 살펴볼까요?


📜 1단계: 자세한 단서 남기기 - record_exception()

예외가 발생했을 때 가장 먼저 해야 할 일은 "무슨 일이 있었는지" 상세하게 기록하는 것입니다. span.record_exception(e)는 바로 이 역할을 합니다.

  • 하는 일: 스팬에 'exception'이라는 특별한 이벤트(Event)를 추가합니다. 이 이벤트 안에는 다음과 같은 꿀 정보가 담겨있죠.
    • 예외 타입: ZeroDivisionError, NullPointerException 
    • 예외 메시지: 에러가 발생한 이유
    • 스택 트레이스(Stack Trace): 오류가 발생하기까지의 전체 함수 호출 경로 🗺️
  • 왜 이게 좋을까? (성능 🚀): 만약 이 모든 정보를 스팬의 속성(Attribute)에 저장한다면 어떨까요? 스택 트레이스처럼 용량이 큰 텍스트는 백엔드 시스템(e.g., Elasticsearch)의 인덱싱에 큰 부담을 줍니다. record_exception은 이를 '이벤트'로 처리하기 때문에, 검색 및 집계 성능 저하를 막아줍니다.

🚨 2단계: '나 문제 있어요!' 확실히 알리기 - set_status(ERROR)

예외 정보를 기록했다면, 이제 이 스팬이 '실패'했다는 것을 모두에게 알려야 합니다.

span.set_status(Status(StatusCode.ERROR, "에러 요약 메시지"))가 바로 그 확성기 역할을 합니다.

  • 하는 일: 해당 스팬을 시각적으로 '실패' 상태로 만듭니다. Grafana나 Jaeger 같은 Observability UI에서 빨간색으로 표시되어, 수천 개의 트레이스 속에서도 한눈에 문제를 발견할 수 있게 해줍니다.
  • 만약 이걸 안 쓰면?: record_exception만 사용하면 예외 기록은 남지만, 트레이스 목록에서는 '성공'으로 보일 수 있습니다. 마치 아픈데 아프다고 말을 안 하는 것과 같죠. 결국 문제 파악을 위한 골든타임을 놓치게 됩니다.

🌪️ 3단계: 내 역할은 끝, 원래 흐름대로 - 예외 다시 던지기 (raise)

가장 중요하고 가장 많이 하는 실수입니다. 예외를 기록한 뒤에는 반드시 다시 던져야 합니다.

  • 하는 일: catch 블록에서 처리한 예외를 상위 호출자에게 다시 전달합니다.
  • 왜 반드시 해야 할까?: OpenTelemetry는 애플리케이션의 '관찰자'이지, '조작자'가 아닙니다. 만약 예외를 catch하고 끝내버리면(swallow), 애플리케이션은 에러가 없는 줄 압니다.
    • 끔찍한 결과 😨:
      • DB 트랜잭션이 롤백되지 않고 커밋될 수 있습니다.
      • API 서버가 사용자에게 500 에러 대신 200 OK 응답을 보낼 수 있습니다.
      • 전역 예외 처리 로직이 동작하지 않아 후속 처리가 누락됩니다.

OpenTelemetry가 애플리케이션의 정상적인 오류 처리 흐름을 막아서는 안 됩니다.


🖥️ 전체 그림: 코드로 이해하기

이제 이 세 가지 원칙이 어떻게 조화롭게 작동하는지 Python 코드로 확인해 보겠습니다.

from opentelemetry import trace
from opentelemetry.trace.status import Status, StatusCode

tracer = trace.get_tracer(__name__)

def process_data(data):
    # 현재 Span 컨텍스트에서 새로운 Span 시작
    with tracer.start_as_current_span("process_data") as span:
        try:
            # 🕵️‍♂️ 디버깅에 유용한 컨텍스트 정보(속성)를 미리 추가해두면 좋습니다.
            span.set_attribute("data.size", len(data))

            # 💣 예외가 발생할 수 있는 위험한 작업 수행
            result = 100 / len(data) # len(data)가 0이면 ZeroDivisionError 발생
            span.set_attribute("result", result)

            # 성공 시에는 기본값이 OK이므로 생략해도 무방합니다.
            # span.set_status(Status(StatusCode.OK))
            return result

        except Exception as e:
            # ❗ 3단계 황금 룰 적용 ❗

            # 1️⃣ 단계: Span 상태를 ERROR로 설정
            error_message = f"Failed to process data: {e}"
            span.set_status(Status(StatusCode.ERROR, error_message))

            # 2️⃣ 단계: 예외 상세 정보(스택 트레이스 포함)를 이벤트로 기록
            span.record_exception(e)

            # 3️⃣ 단계: 애플리케이션의 정상적인 오류 처리를 위해 예외를 다시 던짐
            raise

# 함수 사용 예시
try:
    # 일부러 에러를 발생시키는 상황
    process_data("")
except ZeroDivisionError:
    # raise 덕분에 애플리케이션의 최상위 레벨에서도 예외를 인지하고 처리할 수 있습니다.
    print("Application handled the ZeroDivisionError correctly. ✅")

❌ 이런 방법은 왜 안될까요? (Anti-Patterns)

  • record_exception만 사용하는 경우: 스택 트레이스는 기록되지만, UI에서는 성공(녹색)으로 표시되어 문제를 놓치기 쉽습니다. 😴
  • set_status(ERROR)만 사용하는 경우: 스팬이 실패했다는 것은 알 수 있지만, '왜' 실패했는지 스택 트레이스가 없어 근본 원인을 찾기 어렵습니다. ❓
  • 예외를 raise하지 않는 경우 (최악!): 트레이스는 멀쩡해 보일 수 있지만, 실제로는 애플리케이션의 데이터 정합성이 깨지거나 사용자에게 잘못된 응답을 주는 등 심각한 버그를 유발합니다. 💣

✨ 정리: 완벽한 예외 처리를 위한 체크리스트

마지막으로, OpenTelemetry에서 예외를 처리할 때 이 표를 기억하세요.

항목 권장 전략 왜 해야 할까? (이유)
📜 예외 기록 span.record_exception(e) 성능+디버깅: 스택 트레이스를 이벤트로 기록해 백엔드 부하를 줄이고, 원인 분석에 필요한 모든 정보를 제공합니다.
🚨 상태 표시 span.set_status(Status(StatusCode.ERROR, ...)) 가시성: UI에서 실패한 트레이스를 즉시 식별하여 문제 해결 속도를 높입니다.
🌪️ 흐름 제어 예외를 다시 던지기 (raise) 안정성: 애플리케이션 고유의 오류 처리 로직(롤백, 에러 응답 등)이 정상적으로 동작하도록 보장합니다.
🕵️‍♂️ 컨텍스트 오류 발생 전 span.set_attribute()로 관련 정보 추가 디버깅: 오류 발생 시점의 user_id, request_id 등 주변 상황을 파악하여 디버깅에 결정적인 단서를 제공합니다.