[Python 시리즈 9편] 예외 처리와 로깅 전략

English version

클래스로 로직을 구조화했다면 이제 실패했을 때의 흐름을 설계해야 합니다. 우선은 "무슨 일이 벌어졌는지 알기"만 해결하면 됩니다. 이번 글에서는 print 수준의 단순한 확인에서 시작해, 예외 처리 패턴과 Python logging 모듈로 실행 과정을 차근차근 추적하는 방법을 정리합니다.

이번 글에서 새로 나오는 용어

  1. 예외(Exception): 코드 실행 중 문제가 발생했음을 알리는 신호로 try/except로 잡을 수 있음
  2. 스택 추적(traceback): 예외가 발생할 때 어떤 함수들이 호출됐는지 순서를 보여 주는 기록
  3. 로거(Logger): logging 모듈에서 메시지를 남기는 주체로 이름과 레벨을 설정해 사용
  4. 핸들러(Handler): 로그를 콘솔, 파일, 원격 서비스 등 원하는 위치로 보내는 구성 요소

핵심 개념

학습 메모

  • 소요 시간: 60분
  • 준비물: requests 호출, 함수·클래스 구조 이해
  • 학습 목표: 사용자 정의 예외와 로거를 구성해 실패 원인을 명확히 남기기

아래 순서대로 따라가면 자연스럽게 난도를 올릴 수 있습니다.

  1. 핵심 흐름: try/except로 안전하게 실패를 감싸기
  2. 보강: 사용자 정의 예외로 맥락 남기기
  3. 확장: logging 모듈로 기록 자동화하기

예외 처리 흐름과 로그 메시지가 맞물리면 실패 지점을 빠르게 찾을 수 있습니다.

코드로 따라하기

try/except의 기본 형태 (Core)

print로 확인하던 시절과 다르게, try/except는 실패를 예상하고 잡는 공식 문법입니다.

from requests import RequestException

try:
    response = requests.get("https://status.mathbong.com", timeout=3)
    response.raise_for_status()
except (RequestException, TimeoutError) as exc:
    print("상태 확인 실패", exc)
else:
    print("정상", response.status_code)
finally:
    print("리소스 정리")

else 블록은 예외가 없을 때만 실행되고, finally는 성공/실패와 상관없이 리소스를 정리할 때 사용합니다. 복잡한 개념처럼 보이지만, "성공했을 때"와 "어떻게 끝나든" 두 가지 경우를 명시하는 단순한 구분이라고 생각하면 편합니다.

사용자 정의 예외로 맥락 추가 (Core → Plus)

기본 예외만으로는 "왜" 실패했는지가 잘 드러나지 않습니다. 이름이 있는 새 예외 클래스를 만들어 두면 메시지 하나만 봐도 상황이 떠올라 디버깅이 쉬워집니다.

class MealFetchError(RuntimeError):
    def __init__(self, school_code: str, original: Exception):
        super().__init__(f"급식 조회 실패: {school_code}")
        self.original = original


def fetch_meal(school_code: str) -> dict:
    try:
        response = requests.get(f"https://api.school/{school_code}", timeout=4)
        response.raise_for_status()
        return response.json()
    except Exception as exc:
        raise MealFetchError(school_code, exc) from exc

from exc 구문은 원래 예외 스택을 보존해 디버깅에 도움이 됩니다. "새 에러를 던지되, 원인을 잊지 않는다"는 간단한 규칙으로 기억하세요.

logging 기본 설정 (Core)

print로 찍는 대신 logging을 쓰면 같은 문장 구조로 메시지를 남길 수 있고, 나중에 필터링도 가능합니다. 처음에는 딱 한 줄의 메시지로 시작하세요.


logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s %(message)s",
)

logger = logging.getLogger("mealbot")
logger.info("서비스 시작")

basicConfig는 한 번만 호출해야 하며, 문자열 서식을 통해 UTC 시간, 모듈 이름 같은 메타데이터를 기록할 수 있습니다. 너무 많은 필드를 넣기보다, 필요한 최소 정보(시간, 레벨, 이름)만 먼저 채움으로써 겁먹지 않는 것이 포인트입니다.

구조적 로그와 핸들러 분리 (Optional)

여기부터는 추가 학습 구간입니다. 로그를 JSON처럼 구조화하면 다른 도구로 쉽게 읽을 수 있고, 핸들러를 나누면 출력 위치를 원하는 만큼 늘릴 수 있습니다.


class JsonFormatter(logging.Formatter):
    def format(self, record):
        payload = {
            "level": record.levelname,
            "message": record.getMessage(),
            "module": record.name,
            "extras": getattr(record, "extra", {}),
        }
        return json.dumps(payload, ensure_ascii=False)


handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())

logger = logging.getLogger("mealbot")
logger.setLevel(logging.INFO)
logger.addHandler(handler)

핸들러를 분리하면 콘솔에는 요약 로그, 파일에는 상세 로그 등 다양한 출력을 동시에 구성할 수 있습니다. 아직 필요하지 않다면 이 섹션은 훑어보고 넘어가도 괜찮습니다.

예외 로깅 패턴 (Core)

예외를 잡은 뒤에는 메시지를 남겨야 다음에 같은 상황이 왔을 때 금방 대응할 수 있습니다. try -> except -> logger 순서를 습관처럼 연결해 두세요.

try:
    meal = fetch_meal("demo-high")
except MealFetchError:
    logger.exception("급식 조회 실패", extra={"school": "demo-high"})
else:
    logger.info("급식 데이터 수신", extra={"menu_count": len(meal["menu"])})

logger.exception은 현재 스택 정보를 자동으로 덧붙이므로 except 블록 안에서만 호출해야 합니다. "예외를 잡았다 → 즉시 logger.exception"이라는 단순 규칙만 기억해도 실무에서 크게 도움이 됩니다.

운영 팁

  • uvicorn이나 gunicorn 환경에서는 로거 이름을 서비스별로 나눠 노이즈를 줄입니다.
  • 외부 서비스 키나 개인정보가 로그에 남지 않도록 extra에 필터 함수 적용을 고려합니다.
  • 장기 실행 작업은 logging.handlers.RotatingFileHandler로 파일 용량을 제한합니다.

왜 중요한가

운영 중인 스크립트는 언제든 실패할 수 있습니다. 로그에 컨텍스트를 남기고 예외를 감싸두면 복구 시간을 크게 줄이고, 팀원과 실패 지점을 공유하기도 쉽습니다.

실습

  • 따라 하기: MealFetchError 예제를 따라 하면서 from exc 덕분에 체인된 스택이 어떻게 출력되는지 확인합니다.
  • 확장하기: JSON 포맷 로거를 파일과 콘솔 핸들러 두 개로 나눠 서로 다른 포맷을 적용합니다.
  • 디버깅: 의도적으로 404 URL을 호출해 예외를 만들고 logger.exception 메시지에 extra 데이터를 붙여 힌트를 남깁니다.
  • 완료 기준: try/except → 사용자 정의 예외 → 구조적 로그까지 연결된 실행 흐름을 테스트로 재현할 수 있을 때입니다.

마무리

탄탄한 예외 처리와 로깅은 서비스 장애를 재현하고 복구하는 시간을 획기적으로 줄여 줍니다. 다음 편에서는 이런 안정성을 기반으로 가상환경과 패키지 메타데이터를 관리하며 프로젝트 위생을 지키는 방법을 살펴봅니다.

💬 댓글

이 글에 대한 의견을 남겨주세요