본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2026. 2. 3. · 4 Views
Logging, Observability & Debugging 완벽 가이드
AI 에이전트 시대에 필수적인 로깅, 관측성, 디버깅 기법을 다룹니다. 구조화된 로깅부터 분산 추적, 성능 프로파일링까지 실무에서 바로 적용할 수 있는 핵심 기술을 익혀봅니다.
목차
1. 구조화된 로깅 전략
김개발 씨는 새벽 3시에 긴급 호출을 받았습니다. 운영 중인 서비스에서 간헐적으로 오류가 발생한다는 것이었습니다.
로그 파일을 열어보니 수만 줄의 텍스트가 뒤섞여 있었고, 원하는 정보를 찾기가 마치 건초더미에서 바늘 찾기 같았습니다.
구조화된 로깅은 로그 메시지를 일정한 형식으로 기록하는 것입니다. 마치 도서관에서 책을 분류 체계에 따라 정리하는 것처럼, 로그도 검색과 분석이 쉬운 형태로 남겨야 합니다.
JSON 형식으로 로그를 기록하면 나중에 필터링, 집계, 시각화가 훨씬 수월해집니다.
다음 코드를 살펴봅시다.
import structlog
import logging
# 구조화된 로거 설정
structlog.configure(
processors=[
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.add_log_level,
structlog.processors.JSONRenderer()
],
wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
)
logger = structlog.get_logger()
# 컨텍스트와 함께 로깅
logger.info("user_login",
user_id="user_123",
ip_address="192.168.1.1",
login_method="oauth"
)
김개발 씨는 입사 6개월 차 백엔드 개발자입니다. 어느 날 새벽, 갑작스러운 장애 알림을 받고 노트북을 열었습니다.
로그 파일을 확인해보니 "Error occurred"라는 메시지만 덩그러니 있었습니다. 도대체 어떤 사용자가, 어떤 요청에서, 왜 오류가 났는지 알 수가 없었습니다.
선배 개발자 박시니어 씨가 슬랙으로 조언을 보내왔습니다. "로그에 컨텍스트가 없으니까 그런 거예요.
구조화된 로깅을 적용해보세요." 그렇다면 구조화된 로깅이란 정확히 무엇일까요? 쉽게 비유하자면, 구조화된 로깅은 마치 병원의 진료 기록과 같습니다.
의사가 환자를 진료할 때 "아팠음"이라고만 적지 않습니다. 환자명, 증상, 진료 일시, 처방 내용을 정해진 양식에 따라 기록합니다.
그래야 나중에 다른 의사가 봐도 상황을 파악할 수 있기 때문입니다. 구조화된 로깅이 없던 시절에는 어땠을까요?
개발자들은 print문이나 단순한 텍스트 로그에 의존했습니다. "사용자 로그인 실패"라는 메시지만 남기면, 어떤 사용자인지, 언제 시도했는지, 왜 실패했는지 알 길이 없었습니다.
장애가 발생하면 grep 명령어로 로그 파일을 뒤지며 밤을 새워야 했습니다. 바로 이런 문제를 해결하기 위해 구조화된 로깅이 등장했습니다.
JSON 형식으로 로그를 남기면 각 필드를 개별적으로 검색할 수 있습니다. "user_id가 user_123인 로그만 보여줘"라는 쿼리가 가능해지는 것입니다.
Elasticsearch나 Datadog 같은 로그 분석 도구와의 연동도 자연스러워집니다. 위의 코드를 살펴보겠습니다.
먼저 structlog 라이브러리를 설정하는 부분에서 TimeStamper는 모든 로그에 ISO 형식의 타임스탬프를 자동으로 추가합니다. JSONRenderer는 로그를 JSON 형식으로 출력합니다.
실제 로깅 부분을 보면, 단순히 메시지만 남기는 것이 아니라 user_id, ip_address, login_method 같은 컨텍스트 정보를 함께 기록합니다. 이 정보들이 나중에 문제를 추적할 때 결정적인 단서가 됩니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 결제 서비스를 운영한다고 가정해봅시다.
결제 실패 로그에 주문번호, 결제수단, 실패 사유 코드를 구조화하여 남기면, "오늘 카드 결제 실패율이 얼마인지", "특정 카드사에서만 오류가 나는지" 같은 분석이 가능해집니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 민감 정보를 로그에 그대로 남기는 것입니다. 비밀번호, 카드번호, 개인정보는 절대 로그에 기록하면 안 됩니다.
마스킹 처리를 하거나 아예 제외해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
구조화된 로깅을 적용한 후, 다음 장애 때는 상황이 달랐습니다. 로그 분석 도구에서 오류가 난 요청의 user_id로 검색하니, 해당 사용자의 모든 요청 흐름이 한눈에 보였습니다.
문제 원인을 30분 만에 파악할 수 있었습니다.
실전 팁
💡 - 로그 레벨(DEBUG, INFO, WARN, ERROR)을 적절히 구분하여 사용하세요
- 민감 정보는 반드시 마스킹 처리하거나 제외하세요
- 로그에 request_id를 포함시키면 요청 단위 추적이 쉬워집니다
2. 분산 추적
김개발 씨의 회사는 마이크로서비스 아키텍처를 도입했습니다. 사용자 요청 하나가 API Gateway, 인증 서비스, 주문 서비스, 결제 서비스를 거쳐 처리됩니다.
어느 날 "주문이 느려요"라는 고객 문의가 들어왔는데, 도대체 어느 서비스에서 병목이 생긴 건지 알 수가 없었습니다.
분산 추적은 여러 서비스를 거치는 요청의 전체 흐름을 추적하는 기술입니다. 마치 택배 배송 조회처럼, 요청이 어디서 시작해서 어느 서비스를 거쳐 어떻게 완료되었는지 전 과정을 볼 수 있습니다.
각 요청에 고유한 Trace ID를 부여하여 흩어진 로그를 하나로 연결합니다.
다음 코드를 살펴봅시다.
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
# 트레이서 프로바이더 설정
provider = TracerProvider()
processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="http://jaeger:4317"))
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("order-service")
# 분산 추적 적용
def process_order(order_id: str):
with tracer.start_as_current_span("process_order") as span:
span.set_attribute("order.id", order_id)
with tracer.start_as_current_span("validate_order"):
validate_order(order_id)
with tracer.start_as_current_span("process_payment"):
process_payment(order_id)
김개발 씨는 마이크로서비스 환경에서 일하는 개발자입니다. 예전에는 하나의 서버에서 모든 것을 처리했지만, 이제는 작은 서비스들이 서로 통신하며 일을 나눠서 합니다.
문제는 장애가 발생했을 때였습니다. "주문 API가 5초나 걸려요!" 고객지원팀에서 연락이 왔습니다.
김개발 씨는 각 서비스의 로그를 하나씩 열어봤습니다. 하지만 어느 서비스에서 시간이 오래 걸린 건지 알 수가 없었습니다.
서비스마다 로그 형식도 다르고, 같은 요청인지 구분할 방법도 없었습니다. 박시니어 씨가 해결책을 알려줬습니다.
"분산 추적을 적용해야 해요. OpenTelemetry를 써보세요." 분산 추적이란 무엇일까요?
택배를 주문하면 송장번호가 생깁니다. 이 번호로 조회하면 "물류센터 도착 → 상차 → 배송 중 → 배달 완료"처럼 전체 배송 과정을 볼 수 있습니다.
분산 추적도 마찬가지입니다. 사용자 요청에 Trace ID라는 고유 번호를 부여하고, 이 요청이 거치는 모든 서비스에서 같은 ID로 로그를 남깁니다.
분산 추적이 없던 시절에는 어땠을까요? 개발자들은 각 서비스의 로그를 따로따로 열어보며 타임스탬프를 비교했습니다.
"이 시간대에 A 서비스에서 뭐 했지? B 서비스는?" 이런 식으로 수동으로 연결해야 했습니다.
서비스가 10개, 20개로 늘어나면 이 작업은 사실상 불가능에 가까웠습니다. 바로 이런 문제를 해결하기 위해 분산 추적이 등장했습니다.
OpenTelemetry는 분산 추적의 표준으로 자리 잡은 오픈소스 프로젝트입니다. 코드에서 Span이라는 단위로 작업을 감싸면, 각 작업의 시작 시간, 종료 시간, 소요 시간이 자동으로 기록됩니다.
위 코드를 살펴보겠습니다. TracerProvider를 설정하고 Jaeger라는 추적 백엔드로 데이터를 보내도록 구성했습니다.
process_order 함수 안에서 start_as_current_span으로 Span을 생성합니다. 중첩된 Span은 부모-자식 관계를 형성하여 어떤 작업이 어떤 작업 안에서 실행되었는지 보여줍니다.
실제 현업에서는 Jaeger나 Zipkin 같은 도구로 추적 데이터를 시각화합니다. 요청 하나가 어떤 경로로 흘러갔는지 타임라인 형태로 볼 수 있고, 어느 서비스에서 시간이 오래 걸렸는지 한눈에 파악됩니다.
주의할 점도 있습니다. 모든 작업에 Span을 만들면 오버헤드가 생깁니다.
샘플링을 적용하여 전체 요청 중 일부만 추적하는 것이 일반적입니다. 또한 서비스 간 통신에서 Trace ID가 전파되도록 HTTP 헤더나 메시지 헤더에 포함시켜야 합니다.
김개발 씨는 분산 추적을 적용한 후, Jaeger 대시보드에서 느린 요청을 클릭해봤습니다. 결제 서비스에서 외부 API 호출이 4초나 걸리고 있었습니다.
원인을 찾는 데 5분도 걸리지 않았습니다.
실전 팁
💡 - 샘플링 비율을 적절히 조절하여 성능 영향을 최소화하세요
- 서비스 간 Context Propagation이 제대로 되는지 확인하세요
- Span에 의미 있는 태그(사용자 ID, 주문번호 등)를 추가하세요
3. 메트릭 수집 및 모니터링
김개발 씨의 서비스가 점점 커지면서 "지금 서버 상태가 어때?"라는 질문에 대답하기 어려워졌습니다. CPU 사용률은 얼마인지, 요청 처리량은 어떤지, 에러율은 증가하고 있는지 실시간으로 파악할 방법이 필요했습니다.
메트릭은 시스템의 상태를 숫자로 표현한 것입니다. 마치 자동차 계기판처럼, 속도, 연료량, 엔진 온도를 숫자로 보여주는 것과 같습니다.
메트릭을 수집하고 시각화하면 시스템의 건강 상태를 실시간으로 모니터링할 수 있습니다.
다음 코드를 살펴봅시다.
from prometheus_client import Counter, Histogram, Gauge, start_http_server
import time
# 메트릭 정의
REQUEST_COUNT = Counter(
'http_requests_total',
'Total HTTP requests',
['method', 'endpoint', 'status']
)
REQUEST_LATENCY = Histogram(
'http_request_duration_seconds',
'HTTP request latency',
['endpoint'],
buckets=[0.1, 0.5, 1.0, 2.0, 5.0]
)
ACTIVE_CONNECTIONS = Gauge('active_connections', 'Active connections')
# 메트릭 수집 예시
def handle_request(endpoint: str):
ACTIVE_CONNECTIONS.inc()
start_time = time.time()
try:
result = process_request(endpoint)
REQUEST_COUNT.labels(method='GET', endpoint=endpoint, status='200').inc()
return result
finally:
REQUEST_LATENCY.labels(endpoint=endpoint).observe(time.time() - start_time)
ACTIVE_CONNECTIONS.dec()
김개발 씨의 서비스가 성장하면서 트래픽이 10배로 늘었습니다. 어느 날 오후, 서비스가 갑자기 느려졌다는 신고가 들어왔습니다.
"지금 서버 상태가 어때요?" 팀장님의 질문에 김개발 씨는 제대로 대답할 수가 없었습니다. SSH로 서버에 접속해서 top 명령어를 치고 있을 때, 박시니어 씨가 다가왔습니다.
"메트릭 수집과 모니터링 시스템을 구축해야 해요. Prometheus와 Grafana를 써보세요." 메트릭이란 정확히 무엇일까요?
자동차를 운전할 때 계기판을 봅니다. 속도계는 현재 속도를, 연료 게이지는 남은 기름양을, RPM 게이지는 엔진 회전수를 보여줍니다.
이 숫자들만 봐도 차의 상태를 파악할 수 있습니다. 메트릭도 마찬가지입니다.
서버의 상태를 숫자로 표현한 것입니다. 메트릭 없이 운영하던 시절에는 어땠을까요?
개발자들은 문제가 생기면 서버에 직접 접속해서 로그를 뒤지고, 시스템 명령어를 실행해야 했습니다. "5분 전 CPU 사용률이 얼마였지?"라는 질문에는 답할 수 없었습니다.
이미 지나간 데이터는 남아있지 않았기 때문입니다. 바로 이런 문제를 해결하기 위해 메트릭 수집 시스템이 등장했습니다.
Prometheus는 가장 널리 쓰이는 메트릭 수집 도구입니다. 위 코드에서 세 가지 타입의 메트릭을 정의했습니다.
Counter는 누적 값으로, 요청 횟수처럼 계속 증가하는 값에 사용합니다. Histogram은 분포를 측정하며, 응답 시간의 분포를 파악하는 데 적합합니다.
Gauge는 현재 값으로, 활성 연결 수처럼 오르락내리락하는 값에 사용합니다. 코드를 자세히 보면, REQUEST_COUNT는 method, endpoint, status라는 레이블을 가집니다.
이 레이블 덕분에 "GET 요청 중 500 에러가 몇 번 났는지" 같은 세분화된 분석이 가능해집니다. REQUEST_LATENCY의 buckets 파라미터는 히스토그램의 구간을 정의합니다.
0.1초 미만, 0.5초 미만, 1초 미만... 이런 식으로 응답 시간이 어느 구간에 얼마나 분포하는지 알 수 있습니다.
실제 현업에서는 Prometheus로 수집한 메트릭을 Grafana로 시각화합니다. 대시보드에 그래프와 숫자를 배치하여 팀원 모두가 실시간으로 시스템 상태를 볼 수 있습니다.
특정 임계값을 넘으면 알림을 보내는 Alerting 설정도 가능합니다. 주의할 점도 있습니다.
레이블의 카디널리티가 너무 높으면 안 됩니다. 예를 들어 user_id를 레이블로 쓰면 사용자 수만큼 시계열이 생겨 Prometheus가 감당하지 못합니다.
레이블은 값의 종류가 제한된 것(status code, method 등)만 사용해야 합니다. 김개발 씨는 모니터링 시스템을 구축한 후, Grafana 대시보드를 항상 모니터에 띄워놓았습니다.
이제 "서버 상태가 어때요?"라는 질문에 "현재 RPS 3000, 평균 응답시간 150ms, 에러율 0.1%입니다"라고 자신 있게 대답할 수 있게 되었습니다.
실전 팁
💡 - RED 메트릭(Rate, Errors, Duration)을 기본으로 수집하세요
- 레이블 카디널리티를 항상 고려하세요
- 의미 있는 알림 임계값을 설정하고 정기적으로 검토하세요
4. 디버깅 워크플로우
김개발 씨는 금요일 오후 5시에 이상한 버그 리포트를 받았습니다. "특정 사용자만 로그인이 안 돼요." 재현 조건도 불분명하고, 로컬에서는 잘 되는데 운영 환경에서만 문제가 발생합니다.
어디서부터 어떻게 추적해야 할까요?
디버깅 워크플로우는 문제를 체계적으로 추적하고 해결하는 절차입니다. 마치 의사가 증상을 듣고, 검사하고, 진단하고, 치료하는 과정처럼, 개발자도 정해진 순서에 따라 문제에 접근해야 합니다.
무작정 코드를 뒤지기보다 로그, 메트릭, 추적 데이터를 종합적으로 분석하는 것이 핵심입니다.
다음 코드를 살펴봅시다.
import logging
from functools import wraps
from typing import Callable, Any
import traceback
import sys
logger = logging.getLogger(__name__)
def debug_context(func: Callable) -> Callable:
"""디버깅에 필요한 컨텍스트를 자동으로 수집하는 데코레이터"""
@wraps(func)
def wrapper(*args, **kwargs) -> Any:
# 함수 진입 로깅
logger.debug(f"Entering {func.__name__}", extra={
"function": func.__name__,
"args": str(args)[:200],
"kwargs": str(kwargs)[:200]
})
try:
result = func(*args, **kwargs)
logger.debug(f"Exiting {func.__name__}", extra={
"function": func.__name__,
"result_type": type(result).__name__
})
return result
except Exception as e:
# 예외 발생 시 풍부한 컨텍스트 로깅
logger.error(f"Exception in {func.__name__}", extra={
"function": func.__name__,
"exception_type": type(e).__name__,
"exception_message": str(e),
"traceback": traceback.format_exc(),
"args": str(args)[:200]
})
raise
return wrapper
김개발 씨는 금요일 저녁, 퇴근 직전에 긴급 버그 리포트를 받았습니다. "특정 사용자가 로그인을 못해요.
그런데 다른 사용자는 잘 돼요." 김개발 씨는 한숨을 쉬며 노트북을 다시 열었습니다. 처음에는 무작정 코드를 뒤지기 시작했습니다.
로그인 관련 코드를 열어보고, 이것저것 print문을 넣어봤습니다. 하지만 로컬 환경에서는 문제가 재현되지 않았습니다.
두 시간이 지났지만 진전이 없었습니다. 박시니어 씨가 화상 회의로 접속했습니다.
"체계적으로 접근해야 해요. 디버깅 워크플로우를 따라가 봅시다." 디버깅 워크플로우란 무엇일까요?
의사가 환자를 진료할 때를 생각해봅시다. "아파요"라는 말만 듣고 바로 수술을 하지 않습니다.
먼저 어디가 어떻게 아픈지 문진하고, 필요한 검사를 하고, 결과를 분석하여 진단을 내린 후에야 치료 방법을 결정합니다. 디버깅도 마찬가지입니다.
체계적인 워크플로우 없이 디버깅하면 어떻게 될까요? 개발자들은 "감"에 의존합니다.
"아마 여기가 문제일 거야"라며 코드를 수정하고, 안 되면 다른 곳을 고칩니다. 운이 좋으면 해결되지만, 대부분 시간만 낭비하게 됩니다.
더 큰 문제는 진짜 원인을 모른 채 임시방편으로 넘어가는 경우입니다. 박시니어 씨는 다음과 같은 순서로 접근했습니다.
첫째, 현상 파악입니다. 문제가 발생한 정확한 시간, 영향받은 사용자 수, 에러 메시지를 확인합니다.
모니터링 대시보드에서 그 시간대에 이상 징후가 있었는지 봅니다. 둘째, 로그 분석입니다.
해당 사용자의 요청 로그를 Trace ID로 추적합니다. 어느 서비스에서 어떤 에러가 났는지 확인합니다.
위 코드의 데코레이터처럼 함수 진입과 종료, 예외 발생 시 풍부한 컨텍스트가 남아있다면 분석이 훨씬 수월합니다. 셋째, 가설 수립입니다.
로그와 메트릭을 바탕으로 "아마 이것이 원인일 것이다"라는 가설을 세웁니다. 넷째, 검증입니다.
가설이 맞는지 테스트합니다. 재현이 가능하다면 가설이 맞는 것입니다.
다섯째, 수정 및 확인입니다. 원인을 수정하고 문제가 해결되었는지 확인합니다.
위 코드의 debug_context 데코레이터를 보면, 함수가 호출될 때 인자 값을, 예외가 발생할 때 스택 트레이스를 자동으로 로깅합니다. 이런 컨텍스트가 있어야 나중에 문제를 추적할 수 있습니다.
실제로 박시니어 씨의 안내에 따라 로그를 분석해보니, 해당 사용자의 세션 데이터가 Redis에서 만료되어 있었습니다. 세션 갱신 로직에 버그가 있었던 것입니다.
체계적으로 접근하니 30분 만에 원인을 찾을 수 있었습니다.
실전 팁
💡 - 문제 발생 시각과 영향 범위를 먼저 파악하세요
- 가설을 세우고 검증하는 과정을 반복하세요
- 해결 후에는 같은 문제가 재발하지 않도록 모니터링 알림을 추가하세요
5. 재현성 보장 기법
김개발 씨는 버그를 수정했다고 생각했는데, 같은 문제가 다시 발생했습니다. "분명히 고쳤는데 왜 또 나오는 거지?" 알고 보니 로컬 환경과 운영 환경의 설정이 달랐습니다.
같은 코드인데 다르게 동작하는 이유는 무엇일까요?
재현성은 같은 조건에서 같은 결과가 나오는 것을 보장하는 것입니다. 마치 요리 레시피처럼, 재료와 조리법이 같으면 누가 만들어도 비슷한 맛이 나와야 합니다.
로깅과 디버깅에서 재현성을 확보하면 "내 컴퓨터에서는 되는데"라는 악몽에서 벗어날 수 있습니다.
다음 코드를 살펴봅시다.
import hashlib
import json
from dataclasses import dataclass, asdict
from typing import Any, Dict
from datetime import datetime
@dataclass
class RequestSnapshot:
"""재현 가능한 요청 스냅샷"""
request_id: str
timestamp: str
endpoint: str
method: str
headers: Dict[str, str]
body: Any
environment: Dict[str, str]
def to_hash(self) -> str:
"""요청의 고유 해시 생성"""
content = json.dumps(asdict(self), sort_keys=True)
return hashlib.sha256(content.encode()).hexdigest()[:16]
def save_for_replay(self, path: str):
"""나중에 재현할 수 있도록 저장"""
with open(f"{path}/{self.request_id}.json", 'w') as f:
json.dump(asdict(self), f, indent=2)
def capture_request_context(request, env_vars: list) -> RequestSnapshot:
"""요청 컨텍스트를 재현 가능한 형태로 캡처"""
return RequestSnapshot(
request_id=request.headers.get('X-Request-ID', 'unknown'),
timestamp=datetime.utcnow().isoformat(),
endpoint=request.path,
method=request.method,
headers=dict(request.headers),
body=request.get_json(silent=True),
environment={k: os.getenv(k, '') for k in env_vars}
)
김개발 씨는 자신 있게 말했습니다. "이 버그, 제가 어제 수정했어요." 하지만 QA 팀에서 연락이 왔습니다.
"스테이징 환경에서 또 발생했어요." 김개발 씨는 당혹스러웠습니다. 분명히 로컬에서는 테스트를 통과했는데 말입니다.
박시니어 씨가 물었습니다. "로컬 환경과 스테이징 환경이 똑같아요?" 김개발 씨는 대답하지 못했습니다.
Python 버전, 라이브러리 버전, 환경 변수... 어느 하나라도 다르면 같은 코드가 다르게 동작할 수 있습니다.
재현성이란 무엇일까요? 요리를 생각해봅시다.
유명 셰프의 레시피를 그대로 따라 했는데 맛이 다르다면, 무언가가 다른 것입니다. 재료의 품질, 불의 세기, 조리 시간 중 하나라도 다르면 결과가 달라집니다.
소프트웨어도 마찬가지입니다. 코드 외에 수많은 환경 요소가 결과에 영향을 미칩니다.
재현성이 없으면 어떤 문제가 생길까요? "내 컴퓨터에서는 되는데"라는 말을 들어보셨을 것입니다.
개발자 A의 컴퓨터에서는 잘 되는데 개발자 B의 컴퓨터에서는 안 됩니다. 운영 환경에서는 또 다르게 동작합니다.
버그를 재현할 수 없으니 수정할 수도 없고, 수정했다고 생각해도 확신할 수 없습니다. 위 코드의 RequestSnapshot 클래스를 살펴봅시다.
요청이 들어올 때 단순히 endpoint와 body만 기록하는 것이 아니라, 환경 변수, 헤더, 타임스탬프까지 함께 저장합니다. 나중에 이 스냅샷을 로드하면 당시 상황을 그대로 재현할 수 있습니다.
to_hash 메서드는 요청의 고유 식별자를 생성합니다. 같은 조건의 요청은 같은 해시 값을 가지므로, "이 요청과 저 요청이 같은 조건인가?"를 판단할 수 있습니다.
실제 현업에서는 몇 가지 추가적인 기법을 사용합니다. 첫째, Docker를 사용하여 실행 환경을 코드화합니다.
Dockerfile에 Python 버전, 라이브러리 버전이 명시되어 있으므로 어디서든 같은 환경을 구축할 수 있습니다. 둘째, 시드(Seed) 값을 고정합니다.
랜덤 요소가 있는 코드는 시드를 기록해두면 같은 랜덤 시퀀스를 재현할 수 있습니다. 셋째, 외부 의존성을 모킹합니다.
외부 API 응답, 데이터베이스 상태 등을 스냅샷으로 저장해두면 네트워크 상태와 관계없이 테스트할 수 있습니다. 주의할 점도 있습니다.
스냅샷에 민감 정보가 포함되지 않도록 해야 합니다. 토큰, 비밀번호 같은 정보는 마스킹하거나 제외해야 합니다.
또한 스냅샷 파일이 너무 커지지 않도록 필요한 정보만 선별적으로 저장해야 합니다. 김개발 씨는 이후로 버그 리포트를 받으면 해당 요청의 스냅샷을 먼저 확보합니다.
같은 조건을 로컬에서 재현하여 디버깅하고, 수정 후 같은 스냅샷으로 테스트합니다. "내 컴퓨터에서는 되는데"라는 말은 더 이상 하지 않게 되었습니다.
실전 팁
💡 - 환경 변수, 의존성 버전을 로그에 함께 기록하세요
- 랜덤 요소가 있다면 시드 값을 고정하고 기록하세요
- Docker와 같은 컨테이너 기술로 환경을 코드화하세요
6. 성능 프로파일링
김개발 씨의 API가 점점 느려지고 있었습니다. 처음에는 100ms였던 응답 시간이 어느새 2초를 넘어가고 있었습니다.
코드를 봐도 어디가 문제인지 모르겠습니다. 느린 건 알겠는데, 정확히 어느 부분에서 시간이 오래 걸리는 걸까요?
성능 프로파일링은 코드의 어느 부분에서 시간과 자원이 소모되는지 측정하는 기술입니다. 마치 마라톤 선수의 구간별 기록을 측정하는 것처럼, 코드의 각 구간별 소요 시간을 파악하면 어디를 개선해야 할지 알 수 있습니다.
추측이 아닌 데이터에 기반한 최적화가 가능해집니다.
다음 코드를 살펴봅시다.
import cProfile
import pstats
import io
from functools import wraps
import time
from contextlib import contextmanager
from typing import Callable
@contextmanager
def profile_block(name: str):
"""코드 블록의 실행 시간을 측정하는 컨텍스트 매니저"""
start_time = time.perf_counter()
start_memory = get_memory_usage()
yield
elapsed = time.perf_counter() - start_time
memory_delta = get_memory_usage() - start_memory
print(f"[PROFILE] {name}: {elapsed:.4f}s, memory delta: {memory_delta:.2f}MB")
def profile_function(func: Callable) -> Callable:
"""함수의 상세 프로파일링 결과를 출력하는 데코레이터"""
@wraps(func)
def wrapper(*args, **kwargs):
profiler = cProfile.Profile()
profiler.enable()
result = func(*args, **kwargs)
profiler.disable()
stream = io.StringIO()
stats = pstats.Stats(profiler, stream=stream)
stats.sort_stats('cumulative')
stats.print_stats(10) # 상위 10개 함수만 출력
print(stream.getvalue())
return result
return wrapper
김개발 씨는 성능 개선 업무를 맡았습니다. "이 API 좀 빠르게 해줘." 팀장님의 요청이었습니다.
김개발 씨는 코드를 열어봤습니다. 수백 줄의 코드 중 어디가 느린 걸까요?
처음에는 "감"으로 접근했습니다. "아마 이 반복문이 문제일 거야." 반복문을 최적화했지만 속도는 거의 변하지 않았습니다.
"그럼 이 데이터베이스 쿼리가 문제겠지." 쿼리를 튜닝했지만 역시 별 효과가 없었습니다. 박시니어 씨가 조언했습니다.
"추측하지 말고 측정해요. 프로파일링을 해봅시다." 성능 프로파일링이란 무엇일까요?
마라톤 선수를 코칭한다고 생각해봅시다. 전체 기록이 느려졌다면, 어느 구간에서 시간을 잃었는지 알아야 합니다.
5km 지점, 10km 지점, 20km 지점... 구간별 기록을 측정하면 "하프 이후에 페이스가 떨어지는구나"라는 것을 알 수 있습니다.
코드도 마찬가지입니다. 프로파일링 없이 최적화하면 어떻게 될까요?
개발자들은 조기 최적화의 함정에 빠집니다. "이 부분이 느릴 것 같아"라는 추측으로 복잡한 최적화를 적용하지만, 실제로는 그 부분이 전체 성능에 미치는 영향이 미미한 경우가 많습니다.
시간을 낭비하고 코드만 복잡해집니다. 유명한 격언이 있습니다.
"추측하지 말고 측정하라." 도널드 커누스는 "조기 최적화는 만악의 근원"이라고도 했습니다. 위 코드를 살펴봅시다.
profile_block 컨텍스트 매니저는 특정 코드 블록의 실행 시간과 메모리 사용량을 측정합니다. with 문으로 감싸기만 하면 해당 블록의 성능을 확인할 수 있습니다.
profile_function 데코레이터는 더 상세한 정보를 제공합니다. Python의 cProfile 모듈을 사용하여 함수 내부에서 호출되는 모든 함수의 실행 시간을 측정합니다.
어떤 함수가 몇 번 호출되었고, 총 얼마나 시간이 걸렸는지 표로 보여줍니다. 실제로 프로파일링을 돌려보니 놀라운 사실이 드러났습니다.
김개발 씨가 의심했던 반복문이나 쿼리가 아니라, 로깅 함수가 전체 시간의 60%를 차지하고 있었습니다. 로그 레벨 설정이 DEBUG로 되어 있어서 수천 개의 불필요한 로그가 쌓이고 있었던 것입니다.
실제 현업에서는 py-spy, Pyinstrument 같은 더 강력한 도구를 사용하기도 합니다. py-spy는 실행 중인 프로세스에 붙어서 프로파일링할 수 있어 운영 환경에서도 사용 가능합니다.
플레임 그래프라는 시각화 형태로 결과를 보여줘서 병목 구간을 한눈에 파악할 수 있습니다. 주의할 점도 있습니다.
프로파일링 자체가 오버헤드를 발생시킵니다. 운영 환경에서 상시 프로파일링을 켜두면 성능이 오히려 저하됩니다.
문제가 발생했을 때 일시적으로, 또는 샘플링 방식으로 사용해야 합니다. 김개발 씨는 프로파일링 결과를 바탕으로 로그 레벨을 INFO로 변경했습니다.
그것만으로 응답 시간이 2초에서 200ms로 줄어들었습니다. 추측이 아닌 데이터에 기반한 최적화의 힘이었습니다.
실전 팁
💡 - 최적화 전에 반드시 프로파일링으로 병목을 확인하세요
- 전체 실행 시간의 80%를 차지하는 20%의 코드를 찾으세요
- 운영 환경에서는 샘플링 기반 프로파일러를 사용하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
vLLM 통합 완벽 가이드
대규모 언어 모델 추론을 획기적으로 가속화하는 vLLM의 설치부터 실전 서비스 구축까지 다룹니다. PagedAttention과 연속 배칭 기술로 GPU 메모리를 효율적으로 활용하는 방법을 배웁니다.
Web UI Demo 구축 완벽 가이드
Gradio를 활용하여 머신러닝 모델과 AI 서비스를 위한 웹 인터페이스를 구축하는 방법을 다룹니다. 코드 몇 줄만으로 전문적인 데모 페이지를 만들고 배포하는 과정을 초급자도 쉽게 따라할 수 있도록 설명합니다.
Sandboxing & Execution Control 완벽 가이드
AI 에이전트가 코드를 실행할 때 반드시 필요한 보안 기술인 샌드박싱과 실행 제어에 대해 알아봅니다. 격리된 환경에서 안전하게 코드를 실행하고, 악성 동작을 탐지하는 방법을 단계별로 설명합니다.
Voice Design then Clone 워크플로우 완벽 가이드
AI 음성 합성에서 일관된 캐릭터 음성을 만드는 Voice Design then Clone 워크플로우를 설명합니다. 참조 음성 생성부터 재사용 가능한 캐릭터 구축까지 실무 활용법을 다룹니다.
Tool Use 완벽 가이드 - Shell, Browser, DB 실전 활용
AI 에이전트가 외부 도구를 활용하여 셸 명령어 실행, 브라우저 자동화, 데이터베이스 접근 등을 수행하는 방법을 배웁니다. 실무에서 바로 적용할 수 있는 패턴과 베스트 프랙티스를 담았습니다.