에러 처리와 디버깅 (Error Handling and Debugging)

목차


에러 처리와 디버깅을 왜 묻나

백엔드 면접에서 이 주제는 예외 문법을 아는지 확인하는 수준에서 끝나지 않습니다.

보통은 다음을 같이 봅니다.

  • 실패 인식: 실패를 정상 흐름과 구분해 다룰 수 있는가
  • 경계 설계: 어디서 잡고, 어디서 전파할지 판단할 수 있는가
  • 운영 대응: 장애가 나면 로그와 메트릭으로 원인을 좁혀 갈 수 있는가
  • 품질 연결: 버그를 고친 뒤 재현 테스트와 회귀 방지로 연결하는가

즉, 좋은 답변은 “try/except를 씁니다”보다
실패를 관찰 가능하게 만들고, 복구 가능한 것과 아닌 것을 구분한다는 흐름이어야 합니다.

관련된 테스트 전략은 테스트와 품질 (Testing & Quality), 실행 모델과 성능 문제는 메모리와 실행 모델 (Memory & Execution Model) 문서와 연결해서 보면 좋습니다.


에러를 어떻게 분류할까

에러를 전부 같은 방식으로 다루면 복구 정책이 섞여 버립니다.
보통은 다음처럼 나눠서 설명하는 편이 좋습니다.

분류 예시 대응 방향
사용자 입력 오류 잘못된 파라미터, 검증 실패 명확한 메시지와 4xx 응답
비즈니스 규칙 위반 재고 부족, 권한 없음 도메인 에러로 명시적 처리
일시적 인프라 오류 네트워크 timeout, 일시적 DB 장애 재시도, fallback, circuit breaker 검토
프로그래밍 오류 None 참조, 불변식 위반 빠르게 실패하고 수정 대상으로 취급

면접에서는 특히 다음 질문이 자주 이어집니다.

  • 이 에러는 재시도해도 되는가
  • 사용자에게 그대로 보여도 되는가
  • 로그에는 무엇을 남겨야 하는가
  • 장애로 연결되면 어디서 먼저 확인할 것인가

좋은 답변은 에러 이름을 나열하는 것이 아니라,
복구 가능성에 따라 처리 전략이 달라진다고 설명하는 답변입니다.


좋은 에러 처리의 원칙

1. 경계에서 의미를 바꾼다

하위 라이브러리의 원시 예외를 그대로 전파하기보다,
서비스 경계에서는 더 의미 있는 도메인 에러로 바꾸는 편이 좋습니다.

  • DB unique constraint 예외 → “이미 존재하는 사용자”
  • 외부 결제 timeout → “결제 서비스 일시 장애”

이렇게 해야 상위 계층이 구현 세부 대신 비즈니스 의미로 대응할 수 있습니다.

2. 복구 가능한 것과 불가능한 것을 구분한다

  • 복구 가능: 일시적 timeout, 네트워크 glitch, lock contention
  • 복구 불가능: 코드 버그, 데이터 불변식 위반, 잘못된 구성

복구 불가능한 오류를 무조건 재시도하면 문제만 더 오래 숨길 수 있습니다.

3. 너무 넓게 잡지 않는다

except Exception: 같은 광범위한 처리는 편해 보이지만,

  • 실제 원인을 숨길 수 있음
  • 잘못된 상태에서 계속 진행하게 만들 수 있음
  • 장애 분석 시 스택 트레이스 정보가 약해질 수 있음

즉, 에러 처리는 “다 잡기”보다
어디서 어떤 책임으로 잡을지 분명히 하는 것이 더 중요합니다.


재시도, 타임아웃, 실패 전파

에러 처리 답변이 실무적으로 들리려면 실패 전파 전략까지 같이 나와야 합니다.
다만 이 문서는 애플리케이션 코드에서 예외를 어떤 의미로 감싸고 어디서 끊을지에 집중합니다. retry 위치, timeout budget, circuit breaker 같은 분산 시스템 복원력 일반론은 복원력 패턴 (Timeout, Retry, Circuit Breaker) 문서가 더 직접적입니다.

재시도 (Retry)

재시도는 일시적 실패에 유효할 수 있지만, 조건 없이 넣으면 위험합니다.

  • 잘 맞는 경우: idempotent한 읽기 요청, 일시적 네트워크 timeout
  • 주의점: 중복 쓰기, 트래픽 증폭, 장애 확산

재시도는 보통 다음과 함께 설명하면 좋습니다.

  • 최대 재시도 횟수
  • 멱등성 보장 여부
  • 재시도 대상 오류 구분

타임아웃

기다림에도 상한이 있어야 합니다.

  • 외부 API timeout
  • DB query timeout
  • 전체 요청 timeout

타임아웃이 없으면 일부 느린 의존성 때문에 워커와 연결이 계속 묶일 수 있습니다.

좋은 답변은 모든 값을 외우는 것보다 서비스 경계마다 기다림의 상한을 둔다는 원칙을 설명하는 편이 좋습니다.

실패 전파

모든 에러를 최상위에서만 처리하면 원인 파악이 어렵고,
모든 계층에서 삼켜 버리면 장애가 숨어 버립니다.

그래서 보통은 다음처럼 나눕니다.

  • 하위 계층: 원인 보존, 필요한 래핑, 리소스 정리
  • 상위 계층: 사용자 응답 변환, retry/fallback 여부 결정, 로깅

즉, 이 문서에서는 에러의 의미와 전파 책임을 설명하고, backoff, jitter, circuit breaker는 복원력 패턴 (Timeout, Retry, Circuit Breaker) 문서로 넘기는 편이 좋습니다.


로그와 컨텍스트

디버깅에서 로그는 많다고 좋은 것이 아니라,
문제를 좁혀 갈 수 있는 정보가 구조적으로 남는가가 중요합니다.

좋은 로그에는 보통 다음이 같이 들어갑니다.

  • request id / trace id
  • 사용자나 주문 같은 핵심 식별자
  • 어떤 외부 의존성을 호출했는지
  • 어떤 입력이 실패를 만들었는지
  • 예외 타입과 스택 트레이스

주의할 점도 있습니다.

  • 비밀번호, 토큰, 주민번호 같은 민감 정보는 그대로 남기지 않기
  • 같은 오류를 너무 많이 찍어 log noise를 만들지 않기
  • 메시지는 사람이 읽을 수 있게, 필드는 시스템이 검색할 수 있게 남기기

면접에서는 “로그를 봅니다”보다
재현에 필요한 컨텍스트를 구조화해서 남기겠다고 말하는 편이 좋습니다.1


디버깅 접근법

좋은 디버깅 답변은 감이 아니라 절차로 설명됩니다.

  1. 증상 확인: 정확히 무엇이 깨졌는지 정리
  2. 재현 조건 파악: 언제, 어떤 입력, 어떤 환경에서 터지는지 확인
  3. 범위 축소: 최근 배포, 특정 모듈, 특정 의존성, 특정 데이터로 좁힘
  4. 관측 데이터 확인: 로그, 메트릭, trace, profile 확인
  5. 가설 검증: 추측이 아니라 실험으로 확인
  6. 수정 후 회귀 방지: 테스트와 모니터링으로 재발 방지

예를 들어 “응답이 느리다”는 문제도 바로 코드 수정으로 가지 않고,

  • DB 쿼리가 느린가
  • 외부 API timeout이 늘었는가
  • 메모리 압박으로 GC가 늘었는가
  • 최근 배포에서 특정 경로만 느려졌는가

같이 좁혀 가는 흐름이 필요합니다.


재현과 회귀 방지

디버깅은 원인을 찾는 데서 끝나지 않고, 같은 문제가 다시 안 나게 만드는 것까지 포함합니다.

  • 버그를 재현하는 최소 입력을 만든다
  • 재현 케이스를 테스트로 남긴다
  • 로그와 알림 규칙을 보강한다
  • 필요하면 timeout, retry, validation 규칙도 조정한다

즉, 좋은 디버깅 답변은 “원인을 찾았습니다”가 아니라
다음에는 더 빨리 찾고, 가능하면 다시 안 터지게 만들었다까지 이어집니다.


코드 예시

아래 예시는 외부 API 호출 실패를 도메인 의미가 있는 예외로 바꾸고, 상위 계층에서 로깅하는 기본 패턴입니다.

import logging


logger = logging.getLogger(__name__)


class PaymentGatewayTemporaryError(Exception):
    pass


def call_payment_gateway() -> None:
    raise TimeoutError("payment gateway timeout")


def process_payment() -> None:
    try:
        call_payment_gateway()
    except TimeoutError as error:
        raise PaymentGatewayTemporaryError("payment service is temporarily unavailable") from error


def handle_request() -> str:
    try:
        process_payment()
        return "ok"
    except PaymentGatewayTemporaryError:
        logger.exception("payment processing failed")
        return "retry later"

이 예시에서 중요한 포인트는 다음과 같습니다.

  • 하위 계층의 원시 예외를 의미 있는 서비스 예외로 변환합니다
  • raise ... from error로 원인 체인을 보존합니다
  • 사용자 응답과 내부 로그를 같은 메시지로 다루지 않습니다

면접 포인트

  • 에러는 입력 오류, 비즈니스 규칙 위반, 일시적 인프라 오류, 프로그래밍 오류처럼 복구 가능성 기준으로 나눠 설명하는 편이 좋습니다.
  • 좋은 에러 처리는 모든 예외를 잡는 것이 아니라, 어디서 의미를 바꾸고 어디서 전파할지 정하는 것입니다.
  • 재시도는 idempotency, backoff, 최대 횟수 없이 넣으면 오히려 장애를 키울 수 있습니다.
  • 디버깅은 로그를 많이 보는 것이 아니라, 증상 확인 → 재현 → 범위 축소 → 가설 검증의 절차로 설명하는 편이 좋습니다.
  • 좋은 답변은 수정 이후 회귀 테스트와 모니터링 보강까지 연결합니다.

참고 자료