개발 · 자동화핵심 · 16분 읽기

Python 예외·자원 정리와 실패 기록

예외를 숨기지 말고 처리 가능한 실패만 구체적으로 다룹니다. 오류 원인과 정리 절차, 재시도 범위를 분리합니다.

개념부터 찾아보기

정의·역할·동작 원리를 독립 페이지에서 확인하세요. 본문의 밑줄 용어에도 설명이 있습니다.

이 노트에서 설명할 수 있게 되는 것

  1. 예외가 발생한 뒤 except·else·finally로 흐름이 바뀌는 순서를 설명한다.
  2. 처리 가능한 오류와 전파해야 할 오류를 나눈다.
  3. 정리 과정·재시도·로그에서 원래 실패 정보를 보존한다.
먼저 알면 좋은 개념Python 함수·인수·반복과 모듈

01예외 객체가 호출 경로를 거슬러 올라간다

  • 실행 중 int("abc")가 숫자 변환에 실패하면 ValueError 인스턴스가 발생한다.
  • 현재 코드 위치에서 맞는 except를 찾고 없으면 호출자 쪽으로 전파한다.
  • 처리되지 않으면 traceback과 예외 정보가 출력되며 일반 프로그램 실행이 중단될 수 있다. traceback은 호출 위치를 이어 보여 주므로 마지막 오류 줄만 읽기보다 어떤 입력이 어느 함수를 거쳐 왔는지 추적하는 자료다.
  • Exception 클래스 안에 발생 가능한 모든 오류가 목록으로 들어 있다는 설명은 정확하지 않다.
  • 예외들은 클래스 상속 계층을 이루고 except SomeError는 그 클래스 또는 하위 클래스의 인스턴스를 처리한다.
  • 아래의 KeyboardInterrupt와 SystemExit 등은 일반 Exception과 구분되므로 무조건 bare except로 삼키면 종료 요청까지 무시할 수 있다.
  • 문법 오류와 실행 중의 변환·접속·파일 오류를 구분한다.
  • traceback에는 경로와 요청 값 등이 포함될 수 있으므로 외부 응답과 내부 진단 기록의 범위를 나누고 운영 화면에 상세 traceback을 그대로 노출하지 않는다.

02try·except·else·finally의 정상과 실패 분기

  • 의 문장을 순서대로 실행하다 예외가 생기면 남은 문장을 건너뛰고 맞는 except로 이동한다.
  • 예외가 없으면 else를 수행하며 finally는 해당 구조를 빠져나갈 때 정리 작업을 수행한다.
  • else 안에서 생긴 예외는 앞의 에 대응하는 except가 자동으로 처리하는 범위가 아니므로 보호하려는 작업 범위를 좁게 정하는 데 의미가 있다.
  • 아래 예시에서 숫자로 바뀌면 else가 출력하고 잘못된 형식이면 except가 설명한다.
    • 어느 경로든 finally의 끝 메시지를 거친다.
  • finally에서 return하거나 새 예외를 발생시키면 원래 결과·예외를 가릴 수 있어 정리 코드에는 이런 동작을 신중하게 둔다.
  • -except는 오류를 무조건 정상값으로 바꾸는 장치가 아니라 프로그램이 감당할 실패를 명시하는 제어 흐름이다.
text = "8080"
try:
    port = int(text)
except ValueError:
    print("정수 형식이 아니다")
else:
    print("변환 결과:", port)
finally:
    print("변환 시도 종료")
# 8080 입력: 변환 결과: 8080 -> 변환 시도 종료

코드 동작을 설명하는 교육 예시. 주석의 결과는 언어 규칙으로 설명한 예상값이며 이 페이지 작성 중 실행하지 않았다.

03형식 변환 성공과 업무 조건 만족은 다르다

  • int("70000")은 숫자 형식으로 변환할 수 있어 ValueError가 생기지 않는다.
    • 하지만 / 포트 번호로는 허용 범위 밖이다.
  • 자료형 변환 후 업무 범위를 별도로 검사하고 잘못된 상태에 ValueError 또는 의미 있는 사용자 예외를 발생시키면 호출자가 구분하여 대응할 수 있다.
  • 입력 검증과 외부 시스템 장애를 같은 메시지로 합치지 않는다.
  • 사용자 정의 예외는 보통 Exception의 하위 클래스로 만들고 필요한 문맥을 담는다.
    • 예외 메시지에는 개인키·암호·토큰을 넣지 않는다.
  • 잘못된 호출을 숨기기 위해 무조건 False나 빈 list를 반환하면 진짜 빈 결과와 실패를 구분하기 어려워진다.
    • 함수의 계약에 따라 정상값·예외 중 어떤 방식으로 전달할지 정한다.
class PortError(ValueError):
    pass

def parse_port(text):
    port = int(text)
    if not 1 <= port <= 65535:
        raise PortError("port must be between 1 and 65535")
    return port

# parse_port("abc"): ValueError
# parse_port("70000"): PortError

코드 동작을 설명하는 교육 예시. 주석의 결과는 언어 규칙으로 설명한 예상값이며 이 페이지 작성 중 실행하지 않았다.

04예외를 다시 발생시키고 원인을 연결하기

  • except 안의 raise만 사용하면 현재 처리 중인 예외를 다시 전파할 수 있다.
  • raise NewError(...) from exc는 바깥 계층에 맞는 오류를 만들면서 원래 원인을 명시적으로 연결한다.
    • 예를 들어 파일을 읽는 저수준 OSError를 설정 로딩 실패라는 프로그램 의미로 감싸되 원래 경로·원인 추적을 유지하는 방식이다.
  • from None은 문맥 표시를 줄이지만 원인을 아예 모른 척하는 설계의 대체가 되어서는 안 된다.
  • 구체적 예외를 먼저 두고 넓은 Exception을 뒤에 두어야 더 세밀한 처리 경로가 실행된다.
  • ValueError와 OSError는 입력 형식과 외부 I/O 실패라는 다른 범주다.
  • 잡은 예외를 아무 기록 없이 pass하면 이후 코드가 불완전한 상태로 계속되어 원인과 증상이 멀어질 수 있다.
def load_config(path):
    try:
        with open(path, encoding="utf-8") as stream:
            return stream.read()
    except OSError as exc:
        raise RuntimeError("configuration could not be loaded") from exc

코드 동작을 설명하는 교육 예시. 주석의 결과는 언어 규칙으로 설명한 예상값이며 이 페이지 작성 중 실행하지 않았다.

원리 심화

05예외 전파는 스택 정리와 원인 연결을 포함한다

  • 예외가 발생하면 오류 객체와 traceback이 실패 경로를 나타낸다. 호출자에 처리기가 없으면 더 바깥으로 전파되며 중간 함수의 finally 등 정리 경로를 거친다.
  • else는 가 예외 없이 정상적으로 끝난 경우의 후속 작업을 분리한다. 에서 return·break·continue로 빠져나가는 흐름까지 모두 else로 이어지는 것은 아니다.
  • 에서 return할 값을 정했어도 finally 정리가 먼저 실행될 수 있다. 정리가 정상 종료되면 원래 반환 또는 예외 전파가 이어진다.
  • finally에서 새 return이나 예외를 발생시키면 기존 흐름을 대체하거나 가릴 수 있다. .14부터 finally를 빠져나가는 return 등에는 SyntaxWarning이 나올 수 있어 버전별 진단도 구분한다.
  • raise AppError(...) from exc는 상위 오류와 직접 원인을 연결한다. 오류를 새 이름으로 바꿀 때 원인까지 잃으면 실제 실패 계층을 역추적하기 어렵다.
예제로 추적

06변환 오류를 업무 오류로 바꾸고 정리하는 흐름

  1. 아래 예제의 int("bad")가 ValueError를 발생시키면 다음 정상 문장은 실행하지 않고 except ValueError로 이동한다.
  2. 처리기는 설정값 해석 실패를 RuntimeError로 바꾸되 from exc로 원래 ValueError를 직접 원인으로 연결한다. 정상값을 만들지 않고 실패를 유지한다.
  3. 새 오류가 전파되기 전에 finally의 cleanup이 출력된다. cleanup이 보인다고 설정 변환이나 함수 호출이 성공한 것은 아니다.
  4. 바깥 except RuntimeError가 새 예외를 받고 그 __cause__가 ValueError임을 읽는다. 예상 출력 순서는 cleanup 다음 RuntimeError ValueError다.
  5. finally 자체에서 오류가 생기면 결과가 달라질 수 있다. 이 예제는 정리 출력이 정상 완료된다는 조건을 두고 원인 연결을 추적한다.
def parse_setting():
    try:
        return int("bad")
    except ValueError as exc:
        raise RuntimeError("invalid setting") from exc
    finally:
        print("cleanup")

try:
    parse_setting()
except RuntimeError as exc:
    print(type(exc).__name__, type(exc.__cause__).__name__)
# cleanup
# RuntimeError ValueError

실행하지 않은 교육 예시. 주석의 출력은 언어 규칙으로 추적한 예상값이다.

검증·예외

07처리됨·전파됨·결과 불명을 따로 기록한다

  • except Exception을 먼저 두면 그 아래의 구체적인 ValueError 처리기가 실행될 기회를 잃을 수 있다. 메시지 문자열 비교보다 타입과 상속 관계로 처리 범위를 정한다.
  • 자료가 정말 0건인 경우와 조회가 실패해 빈 목록을 반환한 경우를 구별한다. 빈 값을 정상값으로 선택하려면 호출 계약에 그 의미를 명시해야 한다.
  • 외부 변경 요청이 시간 초과되면 요청이 미실행인지 이미 반영됐는지 로컬 예외만으로 확정하지 못할 수 있다. 결과 불명 상태를 성공·실패와 별도로 남긴다.
  • 반복해도 결과가 달라지지 않는 조회와 새 주문·계정 생성 같은 변경은 재시도 조건이 다르다. 작업 식별자와 상태 조회로 중복 여부를 판단한다.
  • 내부에서는 예외 타입·연결된 원인·필요한 호출 위치를 남기고 사용자 응답에서는 민감 경로나 입력값을 제한한다. 오류를 숨기는 것과 공개 정보를 줄이는 것은 다른 작업이다.

08자원 정리와 재시도는 실패 종류에 맞춘다

  • 열린 파일·SSH 연결·잠금은 정상 완료와 오류 경로 양쪽에서 해제되어야 한다.
  • with를 지원하면 로 수명을 표현하고, 그렇지 않으면 생성 성공 뒤 /finally에 close를 둔다.
  • 자원 생성 자체가 실패한 경우 닫을 객체가 없다는 점까지 코드의 범위에 반영한다.
    • 정리와 원래 오류 보고가 서로 방해하지 않게 한다.
    • 다만 강제 종료나 시스템 장애까지 finally의 실행과 자원 정리가 보장되는 것은 아니다.
  • 재시도는 연결 시간 초과처럼 일시적 원인에 의미가 있을 수 있지만 인증 실패나 잘못된 설정을 계속 반복해도 해결되지 않는다.
  • 쓰기 요청은 응답만 잃었어도 서버에서 이미 수행되었을 수 있으므로 무조건 다시 실행하면 중복 작업이 된다.
  • 최대 시도 수·간격·멱등성 키·중간 상태 조회로 불확실성을 관리한다. 예외 처리는 성공으로 위장하기보다 시스템의 실제 상태를 정확히 전달하는 역할을 한다.

09실패를 결과로 관리하기

  • 자동화에서는 성공·실패·미실행을 구분하고 대상, 시각, 오류 유형, 종료 코드를 필요한 범위에서 기록한다.
    • 일시적인 연결 실패와 입력 오류는 대응이 다르다.
  • 재시도 횟수와 대기 시간을 제한하고 이미 반영된 변경을 반복해도 안전한지 확인한다.
value = "unknown"
try:
    count = int(value)
except ValueError:
    print("정수 형식이 아닙니다.")
else:
    print(count)

예상 가능한 자료 변환 오류만 구체적으로 처리하는 예시다.

10실패한 한 줄 다음에 무엇이 실행되는가

  • 같은 안에서 문자열을 정수로 바꾸고 그 값을 사용하는 코드를 생각한다.
  • int("bad")에서 ValueError가 발생하면 의 남은 문장을 계속 실행하지 않고 대응하는 except를 찾는다.
  • except ValueError가 있으면 그 처리로 이동하고, 이 에 대한 else는 실행하지 않는다.
  • 같은 구조의 finally는 일반적인 제어 흐름에서 예외 처리 이후 정리 단계로 실행된다.
  • 예외가 처리되지 않으면 필요한 정리를 거쳐 바깥 호출자로 전파된다.
  • else를 쓰면 변환이 성공한 뒤 수행할 작업에서 생긴 예외를 앞의 변환 오류 처리와 섞지 않도록 범위를 나눌 수 있다.
    • 예를 들어 파일 읽기까지 큰 하나에 넣고 모든 오류를 값이 잘못됐다고 보고하면 실제 권한 문제를 숨긴다.
  • 여러 예외를 처리할 때는 구체적인 유형과 의도를 분명히 하고 예외 메시지의 문자열 내용만으로 종류를 판정하지 않는다.
  • 라이브러리의 상세 오류를 상위 업무 오류로 바꿀 때는 원인 예외를 연결해 진단 경로를 유지한다.
  1. int("bad")는 정상 정수 값을 반환하지 못한다.
  2. 안의 다음 문장은 실행하지 않는다.
  3. 일치하는 except가 처리하고 이 의 else는 건너뛴다.
  4. finally가 정리 작업을 수행한다.
  5. 이 사례에서는 ValueError를 처리했으므로 finally 뒤의 후속 코드로 진행한다. 처리되지 않은 다른 예외라면 호출자로 전파된다.

설명과 함께 보는 보조 도식

처리 과정

ValueError가 발생한 호출의 제어 흐름

try·except ValueError·else·finally가 같은 구조에 있는 실패 경로를 보여 준다.

전체 단계를 한 번에 읽기
  1. 01

    변환 시도

    int("bad")는 정상 정수 값을 반환하지 못한다.

    입력
    bad
  2. 02

    예외 발생

    try 안의 다음 문장은 실행하지 않는다.

    예외 유형
    ValueError
  3. 03

    구체적 처리

    일치하는 except가 처리하고 이 try의 else는 건너뛴다.

    else
    실행하지 않음
  4. 04

    정리

    finally가 정리 작업을 수행한다.

    주의
    여기서 원래 예외를 덮어쓰지 않기
  5. 05

    이후 흐름

    이 사례에서는 ValueError를 처리했으므로 finally 뒤의 후속 코드로 진행한다. 처리되지 않은 다른 예외라면 호출자로 전파된다.

    이 사례
    예외 처리 후 계속 진행
학습 모식도. 선택한 입력과 조건에서 발생하는 처리·상태 변화를 보여 주며 실제 명령은 실행하지 않는다.

11오류를 숨기는 처리와 정상 복구 구분하기

  • except에서 아무것도 하지 않고 지나가면 실패한 자료가 정상 결과처럼 이어질 수 있다.
    • 기본값으로 대체하는 것도 업무에서 허용된 경우에만 해야 한다.
  • 필수 설정을 못 읽었는데 빈 설정으로 계속 실행하면 이후 단계에서 더 큰 오류를 만들 수 있다.
    • 반대로 사용자 입력 한 항목의 변환 실패처럼 해당 항목만 보고하고 다른 입력을 계속 처리할 수 있는 경우도 있다.
  • 처리 범위는 예외 문법보다 실제 작업의 원자성과 실패 허용 조건으로 결정한다.
  • 정리 함수 자체가 실패하면 원래 예외와 정리 예외를 구분해 기록해야 한다.
  • finally에서 return하면 원래 반환값이나 예외 전달을 바꿀 수 있어 특별한 이유 없이 사용하지 않는다.
  • 재시도 로그에는 시도 횟수·대상·오류 유형·종료 상태를 남기고 비밀번호나 전체 요청 본문은 제외한다.
    • 무한 재시도는 일시 장애를 자원 고갈로 키울 수 있다.
  • 이미 변경이 적용된 뒤 응답만 잃은 경우에는 재시도 전에 상태를 조회해 중복 수행 가능성을 판단한다.
구성·관찰 대상확인하는 값구분할 문제
예외 위치처음 실패한 연산 범위가 너무 넓은지
처리 분기유형·대체값·전파무시와 정상 복구의 차이
정리 단계연결·파일·잠금 해제원래 오류를 덮어쓰는지
재시도횟수·간격·멱등성중복 변경과 일시 오류
학습 확인

이해했는지 확인하기

  • 처리할 예외 유형과 try 범위를 좁힌다.
  • 실패해도 자원이 정리되는지 확인한다.
  • 재시도 가능 오류와 입력·논리 오류를 구분한다.

함께 확인할 공식 자료