관측가능성(Observability): 현대 소프트웨어 시스템의 블랙박스를 밝히는 세 가지 기둥

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

안녕하세요! 10년 경력의 소프트웨어 엔지니어이자 기술 교육자로서, 오늘 여러분과 함께 현대 소프트웨어 시스템 운영에 있어 가장 중요하지만 종종 간과되는 개념 중 하나인 **관측가능성(Observability)**에 대해 깊이 있게 탐구하고자 합니다. 특히 최근 몇 년간 마이크로서비스, 클라우드 네이티브, 분산 시스템 아키텍처가 대세가 되면서, 관측가능성은 더 이상 선택이 아닌 필수가 되었습니다.
1.1. 관측가능성이란 무엇인가?
관측가능성(Observability)이란, 시스템의 외부 출력(로그, 메트릭, 트레이스)을 통해 시스템의 내부 상태를 얼마나 잘 추론할 수 있는지를 나타내는 척도입니다. 즉, "시스템이 왜 그렇게 동작하는가?"라는 질문에 답할 수 있는 능력을 의미합니다. 단순히 오류가 발생했는지 여부를 아는 것을 넘어, 오류가 어디서, 왜, 그리고 어떻게 발생했는지 파악하고, 심지어 문제가 발생하기 전에 잠재적인 문제를 감지할 수 있는 시스템의 특성을 말합니다.
이는 전통적인 모니터링(Monitoring) 개념을 확장한 것입니다. 모니터링은 "미리 정의된 지표"를 통해 시스템의 알려진 실패 모드를 감지하는 데 중점을 둡니다. 예를 들어, CPU 사용률이 90%를 넘으면 경고를 보내는 식이죠. 반면 관측가능성은 시스템의 "알 수 없는 실패 모드"까지도 탐지하고 진단할 수 있도록 시스템 자체를 설계하는 것에 가깝습니다. 시스템이 예상치 못한 방식으로 동작할 때, 충분한 정보가 있다면 원인을 밝혀낼 수 있는 능력이 바로 관측가능성입니다.
1.2. 탄생 배경: 복잡성과의 싸움
관측가능성 개념이 중요해진 가장 큰 배경은 바로 소프트웨어 시스템의 복잡성 증가입니다. 과거의 모놀리식(Monolithic) 아키텍처에서는 하나의 큰 애플리케이션 안에서 모든 로직이 실행되었기 때문에, 문제가 발생하면 해당 애플리케이션 코드만 분석하면 되는 경우가 많았습니다.
하지만 현대의 시스템은 다릅니다.
- 마이크로서비스 아키텍처: 수십, 수백 개의 작은 서비스들이 네트워크를 통해 서로 통신하며 하나의 기능을 수행합니다.
- 클라우드 네이티브 환경: 컨테이너, 서버리스 함수 등 휘발성 자원 위에서 애플리케이션이 동작하며, 온프레미스 환경보다 훨씬 동적입니다.
- 분산 시스템: 여러 서버에 걸쳐 데이터와 로직이 분산되어 있어, 하나의 요청이 여러 서비스를 거쳐 처리됩니다.
이러한 환경에서는 "내 웹 서비스가 느려졌는데, 도대체 어디가 문제지?"라는 질문에 답하기가 매우 어렵습니다. 데이터베이스가 느린 건지, 특정 마이크로서비스가 병목인지, 네트워크 지연인지, 아니면 의존하는 외부 API가 문제인지 파악하기가 쉽지 않죠. 마치 수많은 부품으로 이루어진 거대한 기계에서 오작동이 발생했을 때, 어느 부품이 고장 났는지 알기 위해 모든 부품을 분해해야 하는 것과 같습니다. 이러한 복잡성 속에서 시스템의 건강 상태를 파악하고, 문제를 신속하게 진단하며, 성능 병목을 찾아내기 위해 관측가능성이 필수적인 요소로 부상했습니다.
1.3. 왜 중요한가?
관측가능성은 개발팀과 운영팀(DevOps) 모두에게 다음과 같은 이점을 제공하며, 현대 소프트웨어 개발 및 운영의 핵심 역량으로 자리 잡고 있습니다.
- 빠른 문제 해결 (MTTR 단축): Mean Time To Resolution (문제를 해결하는 데 걸리는 평균 시간)을 획기적으로 줄여줍니다. 문제가 발생했을 때 "어디서" "왜" 발생했는지 즉시 파악할 수 있다면, 불필요한 디버깅 시간을 단축하고 서비스 중단 시간을 최소화할 수 있습니다.
- 성능 병목 식별: 시스템의 어떤 부분이 성능 저하를 일으키는지 정확히 파악하여 최적화할 수 있습니다. 예를 들어, 특정 API 호출이 느리다면, 해당 API 내부의 어떤 DB 쿼리나 외부 서비스 호출이 문제인지 밝혀낼 수 있습니다.
- 사용자 경험 개선: 시스템의 문제를 빠르게 해결하고 성능을 최적화함으로써 최종 사용자의 만족도를 높일 수 있습니다.
- 예측 및 예방: 과거 데이터를 분석하여 잠재적인 문제를 예측하고, 선제적으로 대응하여 장애를 예방할 수 있습니다.
- 개발 생산성 향상: 개발 단계에서부터 시스템의 동작을 명확히 이해하고 디버깅할 수 있어 개발 효율성을 높입니다.
- 비용 절감: 장애로 인한 비즈니스 손실을 최소화하고, 비효율적인 리소스 사용을 식별하여 인프라 비용을 절감할 수 있습니다.
관측가능성은 단순히 도구를 설치하는 것을 넘어, 시스템 설계 단계부터 "어떻게 시스템의 내부 상태를 외부에 노출할 것인가?"를 고민하는 문화와 접근 방식입니다. 이를 위해 흔히 **로깅(Logging), 모니터링(Monitoring), 트레이싱(Tracing)**이라는 세 가지 기둥을 활용합니다.
2. 핵심 원리 설명 (비유와 다이어그램 활용)

관측가능성의 세 가지 기둥인 로깅, 모니터링, 트레이싱은 각각 다른 종류의 정보를 제공하며, 이들이 결합될 때 비로소 시스템의 종합적인 그림을 얻을 수 있습니다.
2.1. 비유: 거대한 물류 센터
관측가능성을 이해하기 위해 거대한 물류 센터를 비유로 들어봅시다. 이 물류 센터는 수많은 작업자(마이크로서비스), 컨베이어 벨트(네트워크 통신), 창고(데이터베이스) 등으로 이루어져 있으며, 고객의 주문(요청)을 처리합니다.
-
로깅 (Logging): 각 작업자가 자신의 작업 내용을 상세히 기록하는 작업 일지와 같습니다. "오전 9시 30분, A 구역에서 1번 상품 포장 시작", "오전 9시 45분, B 구역으로 1번 상품 이동 완료" 등 개별적인 이벤트와 그 상세 내용을 기록합니다. 문제가 생겼을 때 이 작업 일지들을 모아보면 특정 작업자의 동작 흐름이나 오류 발생 시점의 상황을 파악할 수 있습니다. 하지만 모든 일지를 다 읽어야 해서 특정 문제의 원인을 찾기에는 시간이 오래 걸릴 수 있습니다.
-
모니터링 (Monitoring): 물류 센터 곳곳에 설치된 CCTV와 센서와 같습니다. 특정 구역의 작업 효율(처리량), 컨베이어 벨트의 속도(응답 시간), 창고의 재고량(DB 연결 수) 등을 실시간으로 측정하고 대시보드에 표시합니다. "현재 B 구역의 처리량이 평소보다 20% 감소했다!"와 같은 경고를 즉시 알려줄 수 있습니다. 센터 전체의 건강 상태를 한눈에 파악하고, 이상 징후를 빠르게 감지하는 데 탁월합니다. 하지만 특정 주문이 왜 지연되었는지 상세한 원인까지는 알기 어렵습니다.
-
트레이싱 (Tracing): 특정 고객의 주문서에 부착된 추적 태그와 같습니다. 고객의 주문이 물류 센터에 들어온 순간부터, 상품이 A 구역에서 포장되고, B 구역으로 이동하고, 최종적으로 배송 차량에 실리는 모든 과정을 이 태그를 통해 추적할 수 있습니다. "1번 고객의 주문이 A 구역에서 10분, B 구역에서 20분 지연되었다"처럼 하나의 요청이 시스템 내에서 어떻게 흘러가고, 각 단계에서 얼마나 시간이 소요되었는지, 어떤 오류가 발생했는지 전체적인 맥락에서 파악할 수 있습니다.
이 세 가지는 각기 다른 관점으로 물류 센터(시스템)의 상태를 보여주며, 서로 보완적으로 작용하여 시스템의 복잡한 문제를 해결하는 데 도움을 줍니다.
2.2. 관측가능성의 세 가지 기둥
2.2.1. 로깅 (Logging)
로깅은 시스템에서 발생하는 모든 이벤트(정보, 경고, 오류 등)를 시간 순서대로 기록하는 행위입니다. 이는 애플리케이션의 동작 흐름, 특정 기능의 실행 결과, 오류 발생 시점의 컨텍스트 등을 이해하는 데 필수적입니다.
- 특징:
- 이벤트 기반: 특정 시점에 발생한 사건에 대한 기록입니다.
- 상세 정보: 오류 메시지, 스택 트레이스, 변수 값 등 상세한 정보를 담을 수 있습니다.
- 비용: 너무 많은 로그는 저장 및 분석 비용을 증가시킬 수 있습니다.
- 모범 사례:
- 구조화된 로그: 단순히 텍스트 문자열이 아닌 JSON과 같은 구조화된 형태로 로그를 남겨야 검색 및 분석이 용이합니다.
- 로그 레벨 활용:
DEBUG,INFO,WARN,ERROR,CRITICAL등 적절한 로그 레벨을 사용하여 중요도를 구분합니다. - 상관관계 ID (Correlation ID): 하나의 요청이 여러 서비스를 거칠 때, 이 요청을 추적할 수 있도록
Trace ID와 같은 고유 ID를 로그에 포함시키는 것이 좋습니다.
2.2.2. 모니터링 (Monitoring)
모니터링은 시스템의 상태와 성능을 지속적으로 측정하고, 수집된 데이터를 시각화하며, 정의된 임계치를 벗어났을 때 경고를 발생시키는 과정입니다. 이는 시스템 전체의 건강 상태를 파악하고, 잠재적인 문제를 사전에 감지하는 데 사용됩니다.
- 특징:
- 메트릭 기반: CPU 사용률, 메모리 사용량, 네트워크 트래픽, 요청 처리량, 응답 시간, 오류율 등 숫자 형태의 시계열 데이터를 수집합니다.
- 집계 및 시각화: 수집된 메트릭은 대시보드(Grafana 등)를 통해 시각화되어 시스템의 추세를 한눈에 파악할 수 있게 합니다.
- 경고 (Alerting): 특정 지표가 임계치를 넘으면 관리자에게 알림(Slack, 이메일, SMS 등)을 보냅니다.
- 모범 사례:
- RED/USE/Four Golden Signals:
- RED (Request, Error, Duration): 요청률(Rate), 오류율(Errors), 응답 시간(Duration).
- USE (Utilization, Saturation, Errors): 자원 활용률, 자원 포화도, 오류.
- Four Golden Signals (Google SRE): Latency(지연 시간), Traffic(트래픽), Errors(오류), Saturation(포화도). 이러한 프레임워크를 기반으로 핵심 지표를 선정합니다.
- 베이스라인 설정: 정상적인 시스템의 기준선을 설정하고, 이를 벗어날 때 경고를 발생시킵니다.
- Runbook 준비: 경고 발생 시 어떻게 대처해야 하는지에 대한 절차(Runbook)를 문서화합니다.
- RED/USE/Four Golden Signals:
2.2.3. 트레이싱 (Tracing)
트레이싱은 하나의 요청(Request)이 분산 시스템 내의 여러 서비스와 컴포넌트를 거쳐 어떻게 처리되는지 그 전체 흐름을 시각적으로 추적하는 기술입니다. 마이크로서비스 환경에서 문제의 근원지를 파악하고, 성능 병목을 찾아내는 데 가장 효과적입니다.
- 특징:
- 분산 컨텍스트 전파: 각 서비스는 요청을 처리할 때 고유한
Trace ID와Span ID를 생성하고, 다음 서비스로 이 ID들을 함께 전달합니다. - Span: 요청 처리의 각 단계(예: 서비스 호출, DB 쿼리)를 나타내는 작업 단위입니다. 각 Span은 시작 시간, 종료 시간, 서비스 이름, 작업 이름, 메타데이터 등을 포함합니다.
- Trace: 여러 Span들이 계층적으로 연결되어 하나의 완전한 요청 흐름을 나타냅니다.
- 분산 컨텍스트 전파: 각 서비스는 요청을 처리할 때 고유한
- 모범 사례:
- OpenTelemetry: 벤더 중립적인 표준으로, 로깅, 메트릭, 트레이싱 데이터를 수집, 처리, 내보내는 데 필요한 API, SDK, 도구를 제공합니다.
- 모든 서비스에 적용: 부분적인 트레이싱은 무용지물이 될 수 있으므로, 시스템 내 모든 서비스에 트레이싱을 적용하는 것이 중요합니다.
- 샘플링 전략: 모든 요청을 트레이싱하면 오버헤드가 크므로, 중요한 요청이나 특정 비율의 요청만 샘플링하여 트레이싱할 수 있습니다.
2.3. 다이어그램: 분산 시스템에서의 관측가능성
그림: 분산 시스템에서 로깅, 모니터링, 트레이싱의 역할
위 다이어그램은 사용자의 요청이 API Gateway를 거쳐 Service A, Service B로 전달되고, Service B가 Database와 External API를 호출하는 일반적인 마이크로서비스 아키텍처를 보여줍니다.
- Log Files: 각 서비스는 자신의 로컬 파일 시스템이나 중앙 로깅 시스템(Elasticsearch, Loki 등)에 로그를 기록합니다. 이 로그들은
Log Aggregator를 통해 한 곳으로 모여 분석될 수 있습니다. - Metrics: 각 서비스는 CPU 사용률, 메모리, 요청 처리량, 응답 시간 등의 메트릭을
Metrics Collector(Prometheus 등)로 전송합니다. 이 데이터는Monitoring Dashboard(Grafana 등)에서 시각화되고,Alerting System을 통해 이상 징후 발생 시 경고를 보냅니다. - Traces: 사용자의 요청이
API Gateway에 도달하면 고유한Trace ID가 생성됩니다. 이Trace ID는 요청이Service A,Service B,Database,External API를 거칠 때마다 함께 전달됩니다. 각 서비스는 자신의 작업 단위를Span으로 기록하고, 이Span들은Trace Collector(Jaeger, Zipkin, OpenTelemetry Collector 등)로 전송됩니다.Trace Visualizer는 이Span들을 연결하여 요청의 전체 흐름을 시각적으로 보여줍니다.
이 세 가지 정보가 유기적으로 결합될 때, 우리는 시스템의 블랙박스를 열고 내부에서 무슨 일이 일어나고 있는지 명확하게 이해할 수 있습니다.
3. 코드 예제 2개 (Python)
여기서는 Python을 사용하여 로깅과 트레이싱의 기본 개념을 보여주는 간단한 예제를 소개합니다.
3.1. 예제 1: 구조화된 로깅 (Structured Logging)
일반적인 텍스트 로그보다 JSON과 같은 구조화된 로그는 로그 분석 도구에서 훨씬 효율적으로 검색, 필터링, 분석할 수 있습니다.
import logging
import json
import datetime
import sys
# JSON 포맷으로 로그를 출력하는 커스텀 Formatter
class JsonFormatter(logging.Formatter):
def format(self, record):
log_entry = {
"timestamp": datetime.datetime.fromtimestamp(record.created).isoformat(),
"level": record.levelname,
"name": record.name,
"message": record.getMessage(),
"pathname": record.pathname,
"lineno": record.lineno,
"process": record.process,
"thread": record.thread,
}
# record.args에 추가적인 키워드 인자가 있다면 함께 포함
if isinstance(record.args, dict):
log_entry.update(record.args)
# 예외 정보가 있다면 포함
if record.exc_info:
log_entry["exc_info"] = self.formatException(record.exc_info)
return json.dumps(log_entry)
# 로거 설정
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
# 콘솔 핸들러 설정
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(JsonFormatter())
logger.addHandler(handler)
# --- 로그 메시지 예제 ---
def process_order(order_id, item_count, user_id):
"""주문 처리 함수"""
logger.info("주문 처리 시작", extra={
"event": "order_processing_start",
"order_id": order_id,
"user_id": user_id
})
try:
if item_count > 100:
raise ValueError("주문 수량이 너무 많습니다.")
# 실제 주문 처리 로직 (생략)
processed_data = {"status": "success", "total_price": item_count * 1000}
logger.info("주문 처리 완료", extra={
"event": "order_processing_complete",
"order_id": order_id,
"result": processed_data
})
return processed_data
except ValueError as e:
logger.error("주문 처리 중 오류 발생", extra={
"event": "order_processing_error",
"order_id": order_id,
"error_message": str(e)
}, exc_info=True) # exc_info=True 로 스택 트레이스 포함
return {"status": "failed", "error": str(e)}
except Exception as e:
logger.critical("예상치 못한 심각한 오류 발생", extra={
"event": "unhandled_exception",
"order_id": order_id,
"error_message": str(e)
}, exc_info=True)
return {"status": "failed", "error": "Internal Server Error"}
if __name__ == "__main__":
print("--- INFO 로그 ---")
process_order("ORD-001", 5, "user_123")
print("\n--- ERROR 로그 (ValueError) ---")
process_order("ORD-002", 150, "user_456")
print("\n--- CRITICAL 로그 (예상치 못한 오류) ---")
# 강제로 예외 발생시키기
try:
raise ConnectionError("DB 연결 실패")
except ConnectionError as e:
process_order("ORD-003", 10, "user_789")
코드 설명:
JsonFormatter클래스를 정의하여logging모듈의 출력을 JSON 형식으로 변환합니다.extra인자를 통해order_id,user_id,event등 커스텀 필드를 추가할 수 있습니다.logger.info(),logger.error(),logger.critical()등의 메서드를 사용하여 다양한 레벨의 로그를 기록합니다.exc_info=True를 사용하면 예외 발생 시 스택 트레이스 정보가 로그에 포함되어 디버깅에 큰 도움이 됩니다.extra딕셔너리를 통해 로그에 추가적인 컨텍스트 정보를 쉽게 추가할 수 있으며, 이는 나중에 로그를 검색하고 필터링할 때 매우 유용합니다.
3.2. 예제 2: 간단한 분산 트레이싱 (OpenTelemetry Python SDK)
OpenTelemetry는 로깅, 메트릭, 트레이싱을 위한 표준이며, 다양한 언어의 SDK를 제공합니다. 여기서는 OpenTelemetry Python SDK를 사용하여 두 개의 가상 서비스 간의 요청 흐름을 트레이싱하는 예제를 보여줍니다. 실제 환경에서는 OpenTelemetry Collector를 통해 데이터가 수집되고 Jaeger나 Zipkin 같은 백엔드로 전송됩니다. 여기서는 콘솔에 트레이스 데이터를 출력하는 방식으로 구현합니다.
먼저, 필요한 라이브러리를 설치합니다:
pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-console
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
import time
import random
# 1. TracerProvider 설정: 트레이스를 생성하고 관리하는 객체
provider = TracerProvider()
# 콘솔에 Span 정보를 출력하는 Exporter 설정
span_processor = SimpleSpanProcessor(ConsoleSpanExporter())
provider.add_span_processor(span_processor)
trace.set_tracer_provider(provider)
# Tracer 가져오기
tracer = trace.get_tracer(__name__)
# 2. 서비스 1: "frontend-service"
def frontend_service(request_id):
# 'frontend-service'의 Span 시작
with tracer.start_as_current_span("frontend_request_handler") as span:
span.set_attribute("request.id", request_id)
print(f"[{span.context.trace_id:x}] Frontend Service: 요청 처리 시작 - {request_id}")
# 외부 서비스로 전파할 컨텍스트 생성 (HTTP 헤더에 해당)
carrier = {}
TraceContextTextMapPropagator().inject(carrier)
print(f" Frontend Service: 컨텍스트 전파 - {carrier}")
time.sleep(random.uniform(0.05, 0.1)) # 작업 시뮬레이션
# 백엔드 서비스 호출
backend_response = backend_service(request_id, carrier)
span.set_attribute("backend.response_status", backend_response.get("status"))
print(f"[{span.context.trace_id:x}] Frontend Service: 요청 처리 완료 - {request_id}")
return {"status": "success", "data": backend_response}
# 3. 서비스 2: "backend-service"
def backend_service(request_id, carrier):
# 전달받은 컨텍스트를 사용하여 Span 시작 (Trace ID를 이어받음)
ctx = TraceContextTextMapPropagator().extract(carrier)
with tracer.start_as_current_span("backend_data_processor", context=ctx) as span:
span.set_attribute("request.id", request_id)
print(f"[{span.context.trace_id:x}] Backend Service: 데이터 처리 시작 - {request_id}")
# 데이터베이스 작업 시뮬레이션 (새로운 Span으로)
with tracer.start_as_current_span("db_query") as db_span:
db_span.set_attribute("db.instance", "main_db")
db_span.set_attribute("db.statement", "SELECT * FROM users WHERE id = ?")
time.sleep(random.uniform(0.02, 0.08))
print(f" [{db_span.context.trace_id:x}] Backend Service: DB 쿼리 완료")
db_span.set_attribute("db.rows_affected", 1)
time.sleep(random.uniform(0.05, 0.15)) # 추가 작업 시뮬레이션
if random.random() < 0.1: # 10% 확률로 에러 발생
span.set_status(trace.Status(trace.StatusCode.ERROR, "Backend processing failed"))
span.record_exception(ValueError("데이터 처리 실패"))
print(f"[{span.context.trace_id:x}] Backend Service: 데이터 처리 실패 (오류 발생!) - {request_id}")
return {"status": "failed", "error": "데이터 처리 오류"}
print(f"[{span.context.trace_id:x}] Backend Service: 데이터 처리 완료 - {request_id}")
return {"status": "success", "data": "processed_data_from_db"}
if __name__ == "__main__":
print("--- 트레이싱 예제 시작 ---")
for i in range(3):
req_id = f"REQ-{i+1:03d}"
print(f"\n새로운 요청: {req_id}")
result = frontend_service(req_id)
print(f"최종 결과: {result}")
print("--- 트레이싱 예제 종료 ---")
코드 설명:
TracerProvider를 설정하고ConsoleSpanExporter를 사용하여 Span 정보를 콘솔에 출력하도록 합니다. 실제 환경에서는OTLPExporter등을 사용하여 OpenTelemetry Collector로 데이터를 보냅니다.tracer.start_as_current_span("span_name")을 사용하여 특정 코드 블록을 Span으로 정의하고,with문을 통해 Span의 시작과 끝을 자동으로 관리합니다.span.set_attribute()를 사용하여 Span에 추가적인 메타데이터(예:request.id,db.statement)를 추가합니다. 이는 나중에 트레이스를 분석할 때 유용합니다.TraceContextTextMapPropagator는 분산 시스템에서Trace ID와Span ID를 다음 서비스로 전달하는 역할을 합니다. 여기서는carrier딕셔너리를 사용하여 HTTP 헤더처럼 컨텍스트를 전달하는 방식을 시뮬레이션합니다.backend_service에서는 전달받은carrier에서 컨텍스트를 추출하여 기존 Trace에 새로운 Span을 연결합니다. 이를 통해 하나의 요청이 여러 서비스를 거치더라도 전체 흐름이 하나의 Trace로 연결됩니다.span.set_status(trace.Status(trace.StatusCode.ERROR, ...))와span.record_exception()을 통해 Span에 오류 정보를 기록할 수 있습니다.
이 코드를 실행하면 각 서비스에서 생성된 Span들이 하나의 Trace ID를 공유하며 계층적으로 출력되는 것을 볼 수 있습니다. 실제 트레이싱 시스템(Jaeger UI 등)에서는 이 데이터가 시각적으로 더 명확하게 표현됩니다.
4. 실무 적용 사례
관측가능성은 단순한 이론이 아니라, 실제 운영 환경에서 직면하는 복잡한 문제들을 해결하는 데 필수적인 도구입니다.
- 마이크로서비스 환경에서 장애 발생 시 트러블슈팅:
- 문제: 결제 서비스에서 오류
