🚨 이런 상황, 겪어보셨나요?
"서버에서 간헐적으로 500 에러가 나는데, 로그에 아무것도 없습니다."
"고객이 '결제가 안 된다'고 하는데, 재현이 안 됩니다. 로그라도 있으면..."
"로그가 너무 많이 쌓여서 디스크가 꽉 찼습니다."
"개발 환경에서는 콘솔에 다 보이는데, 프로덕션에서는 어디에 로그가 남는지 모르겠습니다."
Django 프로젝트에서 로그는 "잘 될 때는 필요 없지만, 문제가 생기면 가장 먼저 찾는 것"입니다. 그런데 정작 문제가 생겼을 때 로그가 없거나, 너무 많거나, 쓸모없는 내용만 있다면 장애 해결 시간이 기하급수적으로 늘어납니다.
🔥 실제 상황: 장애는 발생했는데 단서가 없다
새벽 3시, 모니터링 시스템에서 알람이 왔습니다. "주문 성공률 60%로 하락." 정상적으로 99% 이상이어야 할 주문 성공률이 갑자기 60%로 떨어진 것입니다.
개발자가 확인한 것:
- Django 콘솔 로그 — DEBUG=True일 때만 콘솔에 출력. 프로덕션은 DEBUG=False라 아무것도 없음
- 파일 로그 — 설정한 적 없어서 파일 자체가 없음
- Gunicorn 로그 — Worker timeout 메시지만 있고, 어떤 요청이 문제인지 정보 없음
- Sentry 등 에러 추적 — 연동하지 않았음
결국 개발자는 프로덕션 서버에 접속해서 DEBUG=True로 임시 변경하고, 에러를 재현하고, 콘솔에 찍힌 에러를 확인해야 했습니다. 프로덕션에서 DEBUG=True를 켜는 것은 보안상 매우 위험한 행동입니다.
장애 발생부터 원인 파악까지 2시간. 제대로 된 로그 설정이 있었다면 10분이면 해결할 수 있었던 문제였습니다.
🎯 로그 설정이 뭔데, 왜 이렇게 중요할까?
비행기 블랙박스 비유로 이해하기
로그를 이해하려면 비행기의 블랙박스(FDR, Flight Data Recorder)를 떠올려보세요.
비행기는 항상 블랙박스를 달고 다닙니다. 비행 중에는 아무도 안 보지만, 사고가 발생하면 블랙박스가 무슨 일이 있었는지, 어떤 순서로 일어났는지 알려줍니다.
블랙박스가 없는 비행기라면? 사고 원인을 영원히 알 수 없습니다. 로그가 없는 서버도 마찬가지입니다. 장애가 발생하면 미스터리 소설이 됩니다.
반면, 블랙박스에 모든 데이터를 다 기록하면? 저장 공간이 금방 차고 정작 필요한 데이터를 찾기 어렵습니다. 로그도 너무 적으면 쓸모없고, 너무 많으면 파묻혀서 쓸모없습니다.
📊 비즈니스 영향: 로그 부재가 가져오는 실질적 피해
1. 장애 해결 시간(MTTR) 증가
로그가 제대로 설정된 서버는 장애 발생 시 로그만 보면 원인을 파악할 수 있습니다. 평균 해결 시간 10분. 로그가 없는 서버는 재현 시도, 디버그 모드 활성화, 온갖 삽질을 거쳐야 합니다. 평균 해결 시간 2시간 이상.
서비스가 1시간 다운되면 매출 손실은 물론이고, 고객 신뢰까지 떨어집니다.
2. 디스크 공간 고갈
반대로 로그를 너무 많이 남기면 디스크가 금방 찹니다. DEBUG 레벨 로그를 프로덕션에서 파일로 남기면, 하루에 수 GB씩 쌓일 수 있습니다. 디스크가 꽉 차면 서버 자체가 멈춥니다.
3. 보안 위험
로그에 민감한 정보가 포함되면 보안 사고로 이어집니다. 비밀번호, 개인정보, API 키 등이 로그에 기록되면, 로그 파일이 유출될 때 큰 문제가 됩니다.
🔍 Django 로깅 시스템 이해하기
Django는 Python의 표준 logging 모듈을 그대로 사용합니다. 구성 요소를 이해하면 설정이 어렵지 않습니다.
4가지 구성 요소:
Logger는 "누가 로그를 만드는가"입니다. django, django.request, myapp 등 이름으로 구분됩니다. Handler는 "로그를 어디로 보낼 것인가"입니다. 콘솔, 파일, 이메일, Sentry 등. Formatter는 "로그를 어떤 형식으로 기록할 것인가"입니다. Filter는 "어떤 로그를 걸러낼 것인가"입니다.
로그 레벨 (5단계):
DEBUG → INFO → WARNING → ERROR → CRITICAL
개발 환경에서는 DEBUG부터, 프로덕션에서는 WARNING 이상만 기록하는 것이 일반적입니다.
🛠️ 실전 적용: Django 로깅 설정
1단계: 개발 환경 기본 설정
개발 중에는 콘솔에 상세한 로그를 보는 것이 편리합니다.
# settings/local.py
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '[{asctime}] {levelname} {name} {message}',
'style': '{',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
},
'root': {
'handlers': ['console'],
'level': 'DEBUG',
},
'loggers': {
'django': {
'handlers': ['console'],
'level': 'INFO', # Django 내부 로그는 INFO 이상만
'propagate': False,
},
'django.db.backends': {
'handlers': ['console'],
'level': 'DEBUG', # SQL 쿼리 보기 (필요시)
'propagate': False,
},
'myapp': {
'handlers': ['console'],
'level': 'DEBUG', # 내 코드는 DEBUG부터
'propagate': False,
},
},
}
2단계: 프로덕션 환경 설정
프로덕션에서는 파일 로그 + 로그 로테이션 + 적절한 레벨이 핵심입니다.
# settings/production.py
import os
LOG_DIR = os.path.join(BASE_DIR, 'logs')
os.makedirs(LOG_DIR, exist_ok=True)
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'production': {
'format': (
'[{asctime}] {levelname} {name} '
'{module}.{funcName}:{lineno} {message}'
),
'style': '{',
},
'json': {
'()': 'pythonjsonlogger.jsonlogger.JsonFormatter',
'format': '%(asctime)s %(levelname)s %(name)s %(message)s',
},
},
'handlers': {
'file_error': {
'class': 'logging.handlers.RotatingFileHandler',
'filename': os.path.join(LOG_DIR, 'error.log'),
'maxBytes': 10 * 1024 * 1024, # 10MB
'backupCount': 10, # 최대 10개 파일 유지
'formatter': 'production',
'level': 'ERROR',
},
'file_app': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': os.path.join(LOG_DIR, 'app.log'),
'when': 'midnight',
'interval': 1,
'backupCount': 30, # 30일치 보관
'formatter': 'production',
'level': 'INFO',
},
'console': {
'class': 'logging.StreamHandler',
'formatter': 'production',
},
},
'loggers': {
'django': {
'handlers': ['console', 'file_error'],
'level': 'WARNING',
'propagate': False,
},
'django.request': {
'handlers': ['file_error'],
'level': 'ERROR',
'propagate': False,
},
'myapp': {
'handlers': ['console', 'file_app', 'file_error'],
'level': 'INFO',
'propagate': False,
},
},
}
핵심 포인트:
RotatingFileHandler — 파일이 10MB를 넘으면 자동으로 새 파일로 로테이션합니다. backupCount=10이면 최대 100MB까지만 사용. 디스크 공간 고갈 방지.
TimedRotatingFileHandler — 매일 자정에 새 파일로 로테이션합니다. backupCount=30이면 30일치만 보관.
에러와 앱 로그 분리 — 에러만 보고 싶을 때 error.log만, 전체 흐름을 보고 싶을 때 app.log를 확인.
3단계: 애플리케이션 코드에서 로그 작성하기
설정을 했으면 이제 실제 코드에서 로그를 남겨야 합니다.
# services.py
import logging
logger = logging.getLogger(__name__) # 'myapp.services'
def process_payment(order):
logger.info(
f'결제 시작: order_id={order.id}, '
f'amount={order.total}, user={order.customer_id}'
)
try:
response = payment_gateway.charge(
amount=order.total,
token=order.card_token,
)
except PaymentGatewayError as e:
logger.error(
f'결제 실패: order_id={order.id}, '
f'error={str(e)}, token={order.card_token[:8]}...',
exc_info=True # 스택트레이스 포함
)
raise
if response.is_success:
logger.info(
f'결제 성공: order_id={order.id}, '
f'payment_id={response.payment_id}'
)
else:
logger.warning(
f'결제 거절: order_id={order.id}, '
f'reason={response.decline_reason}'
)
return response
로그 작성 원칙:
- 모듈 상단에 logger = logging.getLogger(__name__) — __name__을 사용하면 myapp.services처럼 모듈 경로가 자동으로 Logger 이름이 됩니다.
- 상황에 맞는 레벨 사용 — 정상 흐름은 info, 주의 필요한 상황은 warning, 실패는 error.
- 충분한 컨텍스트 포함 — order_id, user_id, amount 등 나중에 검색할 수 있는 식별자를 포함.
- 에러에는 exc_info=True — 스택트레이스가 함께 기록되어 디버깅이 훨씬 쉽습니다.
- 민감한 정보 주의 — 카드 토큰은 앞 8자리만 로그. 비밀번호, 개인정보는 절대 로그에 남기지 않습니다.
4단계: 요청/응답 로깅 미들웨어
모든 API 요청과 응답을 자동으로 로그로 남기는 미들웨어를 만들면 디버깅이 훨씬 쉽습니다.
# core/middleware.py
import logging
import time
import uuid
logger = logging.getLogger('myapp.request')
class RequestLoggingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# 요청 ID 생성 (요청 추적용)
request_id = str(uuid.uuid4())[:8]
request.request_id = request_id
# 요청 로그
logger.info(
f'[{request_id}] {request.method} {request.path} '
f'user={getattr(request.user, "id", "anonymous")}'
)
start_time = time.time()
response = self.get_response(request)
duration = time.time() - start_time
# 응답 로그
logger.info(
f'[{request_id}] {response.status_code} '
f'{duration:.3f}s'
)
# 느린 요청 경고
if duration > 3.0:
logger.warning(
f'[{request_id}] SLOW REQUEST: '
f'{request.method} {request.path} '
f'{duration:.3f}s'
)
return response
def process_exception(self, request, exception):
request_id = getattr(request, 'request_id', 'unknown')
logger.error(
f'[{request_id}] EXCEPTION: '
f'{request.method} {request.path} '
f'{type(exception).__name__}: {str(exception)}',
exc_info=True
)
# settings.py
MIDDLEWARE = [
'core.middleware.RequestLoggingMiddleware', # 최상단에 배치
'django.middleware.security.SecurityMiddleware',
# ...
]
이 미들웨어의 핵심:
요청 ID — 각 요청에 고유 ID를 부여해서, 로그에서 특정 요청의 전체 흐름을 추적할 수 있습니다. "주문 실패한 고객"의 요청 ID만 알면, 그 요청의 전체 과정을 로그에서 추적할 수 있습니다.
느린 요청 경고 — 3초 이상 걸리는 요청을 자동으로 경고합니다. 성능 문제를 조기에 발견할 수 있습니다.
예외 자동 로그 — 처리되지 않은 예외가 자동으로 로그에 기록됩니다.
🐛 자주 발생하는 실수와 디버깅
실수 1: 프로덕션에서 DEBUG 레벨 사용
# ❌ 프로덕션에서 DEBUG 레벨 파일 로그
'myapp': {
'handlers': ['file_app'],
'level': 'DEBUG', # 프로덕션에서 DEBUG는 로그 폭탄!
}
DEBUG 레벨은 모든 상세 정보를 기록합니다. 트래픽이 많은 프로덕션에서는 로그 파일이 순식간에 수 GB로 불어납니다.
# ✅ 프로덕션에서는 WARNING 또는 INFO
'myapp': {
'handlers': ['file_app', 'file_error'],
'level': 'INFO', # INFO 이상만 기록
}
실수 2: 로그 로테이션 없이 파일 로그 사용
# ❌ 로테이션 없이 파일에 계속 쌓음
'file': {
'class': 'logging.FileHandler', # 로테이션 없음!
'filename': 'app.log',
}
FileHandler는 파일이 무한정 커집니다. 반드시 RotatingFileHandler 또는 TimedRotatingFileHandler를 사용해야 디스크 공간 고갈을 방지할 수 있습니다.
실수 3: 민감한 정보를 로그에 기록
# ❌ 비밀번호, 카드 전체 번호가 로그에!
logger.info(f'로그인: user={username}, password={password}')
logger.info(f'결제: card={card_number}')
# ✅ 민감한 정보는 마스킹
def mask_sensitive(value, visible=4):
"""민감한 정보 마스킹"""
if not value:
return '***'
return f'{value[:visible]}{"*" * (len(value) - visible)}'
logger.info(f'로그인: user={username}') # 비밀번호는 절대 로그 금지
logger.info(f'결제: card={mask_sensitive(card_number)}') # 1234****
실수 4: disable_existing_loggers 함정
# ❌ 기존 로거를 비활성화
LOGGING = {
'version': 1,
'disable_existing_loggers': True, # Django 기본 로거까지 비활성화!
# ...
}
disable_existing_loggers: True로 설정하면 Django가 기본으로 제공하는 로거(django.request, django.security 등)가 비활성화됩니다. 보안 경고나 요청 에러 로그가 사라지는 것입니다.
항상 disable_existing_loggers: False를 사용하세요.
✅ 베스트 프랙티스 체크리스트
설계 단계
- [ ] 개발/프로덕션 환경별 로그 설정을 분리했는가?
- [ ] 프로덕션에서는 INFO 또는 WARNING 이상만 기록하는가?
- [ ] 로그 로테이션을 설정하여 디스크 공간을 관리하는가?
- [ ] 에러 로그와 앱 로그를 분리했는가?
개발 단계
- [ ] 모든 모듈에서 logging.getLogger(__name__)을 사용하는가?
- [ ] 상황에 맞는 로그 레벨을 사용하는가? (info/warning/error)
- [ ] 로그에 충분한 컨텍스트(ID, 상태, 값)를 포함하는가?
- [ ] 에러 로그에 exc_info=True로 스택트레이스를 포함하는가?
- [ ] 민감한 정보(비밀번호, 카드번호 등)를 마스킹하고 있는가?
운영 단계
- [ ] 요청/응답 로깅 미들웨어를 사용하고 있는가?
- [ ] 로그 모니터링 도구(Sentry, CloudWatch 등)를 연동했는가?
- [ ] 로그 파일 사이즈와 보관 주기를 모니터링하고 있는가?
- [ ] disable_existing_loggers가 False로 설정되어 있는가?
🎯 결론: 로그는 미래의 나에게 보내는 메시지입니다
로그 설정 문제는 장애가 발생하기 전까지는 느끼지 못하는 특징이 있습니다. 그래서 미리 준비해야 합니다.
기억해야 할 핵심 원칙
- 환경별로 로그 레벨을 분리하세요: 개발은 DEBUG, 프로덕션은 INFO 또는 WARNING. 프로덕션에서 DEBUG는 로그 폭탄입니다.
- 로그 로테이션은 필수: RotatingFileHandler나 TimedRotatingFileHandler로 디스크 공간을 보호하세요.
- 로그에 충분한 컨텍스트를 담으세요: 요청 ID, 사용자 ID, 주문 ID 등 추적에 필요한 정보를 포함하세요.
- 민감한 정보는 절대 로그에 남기지 마세요: 비밀번호, 카드번호, 개인정보는 마스킹 처리하세요.
서비스 규모별 추천
| 서비스 규모 | 추천 전략 |
| MVP/초기 스타트업 | 콘솔 로그 + Sentry 무료 플랜 연동 |
| 성장기 스타트업 | 파일 로그(로테이션) + 요청 로깅 미들웨어 + Sentry |
| 중대형 서비스 | JSON 구조화 로그 + ELK/CloudWatch + 요청 추적 + 알람 연동 |
💼 Django 로그/모니터링 컨설팅
제대로 된 로그 설정은 장애 해결 시간을 극적으로 단축합니다.
이런 고민이 있으시다면:
- 프로덕션에서 에러가 났는데 로그가 없어서 긴급 대응이 늘어진다면
- 로그 파일이 디스크를 잡아먹고 있다면
- Sentry나 모니터링 도구를 연동하고 싶다면
- 로그 기반 알람/모니터링 시스템을 구축하고 싶다면
크몽에서 Django 전문 컨설팅을 제공합니다. 실제 운영 경험을 바탕으로, 여러분의 서비스에 맞는 로그/모니터링 아키텍처를 함께 설계해 드립니다.
'프로그래밍 > Python' 카테고리의 다른 글
| 🎭 Mock 처리 부족: 외부 의존성 때문에 테스트가 불안정한 경우 (0) | 2026.02.17 |
|---|---|
| 🧪 테스트 DB 오염: 테스트 간 데이터가 남아서 실패하는 경우 (1) | 2026.02.16 |
| 📨 응답 형식 불일치: 프론트엔드에서 예상하는 JSON 구조와 다름 (0) | 2026.02.15 |
| 🔀 API 버전 관리 부족: 기존 클라이언트와 호환성 문제 (0) | 2026.02.14 |
| 📄 페이지네이션 누락: 대량 데이터 조회 시 성능 저하 (0) | 2026.02.13 |