상위 목록: 하위 목록: 작성 날짜: 읽는 데 10 분 소요

오류 발생(raise)

오류 발생(raise)은 프로그램이 허용된 범위 안에서 작동하지 않을 때 강제로 오류(예외)를 발생시키는 역할을 합니다.

코드상에서 문제는 없지만 알고리즘상에서 문제가 발생하거나 허용하지 않는 경우 강제로 오류를 발생시킵니다.

발생시킨 오류는 try-except 구문으로 예외 처리할 수 있으며, 처리하지 않으면 프로그램이 종료됩니다.


제 18강에서는 Python이 발생시킨 오류를 try-except로 처리하는 방법을 다뤘습니다.

이번 강좌는 반대로, 작성한 코드가 직접 오류를 발생시키는 방법을 다룹니다.

함수를 작성하다 보면 나이에 음수가 전달되거나, 존재하지 않는 계정으로 로그인을 시도하는 것처럼 문법적으로는 문제가 없지만 처리할 수 없는 상황을 만나게 됩니다.

이때 raise로 오류를 발생시키면, 함수를 호출한 쪽에 “이 요청은 처리할 수 없다”는 사실과 그 이유를 명확하게 전달할 수 있습니다.

이번 강좌에서는 raise의 기본 사용법, 오류 메시지 전달, 상황에 맞는 오류 선택, 사용자 정의 오류, 오류 다시 발생, 예외 연결을 차례대로 다룹니다.


def divide(a, b):
    if b == 0:
        return None
    return a / b


result = divide(10, 0)
print(result * 2)
결과
TypeError: unsupported operand type(s) for *: ‘NoneType’ and ‘int’

오류를 발생시키지 않고 None 같은 특별한 값을 반환하는 방법도 있지만, 위 예제처럼 문제가 생깁니다.

divide(10, 0)은 처리할 수 없는 요청인데도 None을 반환하고 조용히 넘어갑니다.

호출한 쪽에서 None인지 확인하지 않으면, 문제는 엉뚱한 곳인 result * 2에서 TypeError로 드러나므로 원인을 찾기 어렵습니다.

처리할 수 없는 상황에서 바로 오류를 발생시키면, 문제가 생긴 바로 그 위치에서 원인을 알려 줄 수 있습니다.



강제 오류 발생

raise 구문은 raise 오류 형태로 작성하며, 실행되는 순간 함수나 구문의 나머지 코드를 실행하지 않고 즉시 오류를 발생시킵니다.

발생한 오류는 가장 가까운 try-except 구문에서 처리되며, 처리할 except가 없다면 프로그램이 종료됩니다.


fruit = ["apple", "banana", "grape", "watermelon"]
print(fruit)

while True:
    index = input("과일 색인 값 선택: ")

    try:
        index = int(index)

        if index == 1:
            raise NameError

        else:
            print(fruit[index])

    except NameError:
        print("바나나는 불가능합니다.")

    except IndexError:
        print("해당 색인은 호출할 수 없습니다.")

    except ValueError:
        print("정수만 입력할 수 있습니다.")

    print("------------")
결과
[‘apple’, ‘banana’, ‘grape’, ‘watermelon’]
과일 색인 값 선택: 0
apple
————
과일 색인 값 선택: 1
바나나는 불가능합니다.
————
과일 색인 값 선택: 5
해당 색인은 호출할 수 없습니다.
————
과일 색인 값 선택: 9.5
정수만 입력할 수 있습니다.
————

raise 오류를 통해 강제로 오류를 발생시킬 수 있습니다.

index가 1일 때 정상적으로 출력되는 것이 맞지만 허용하지 않는다면 raise를 통해 강제로 오류를 발생시킵니다.

1을 입력했을 때 NameError가 발생하며, except 구문으로 넘어갑니다.

9.5나 abc처럼 정수로 변환할 수 없는 값을 입력하면, int() 함수에서 ValueError가 발생합니다.


결과를 입력 순서대로 보면, 0은 오류 없이 apple을 출력합니다.

1은 raise NameError가 실행되어 print(fruit[index])까지 가지 않고 곧바로 except NameError로 이동합니다.

5는 fruit[5]가 존재하지 않으므로 Python이 IndexError를 발생시키고, 9.5는 int("9.5")에서 Python이 ValueError를 발생시킵니다.

즉, raise로 직접 발생시킨 오류와 Python이 발생시킨 오류는 같은 방식으로 except에서 처리됩니다.

어떤 경우든 try-except 구문이 끝나면 print("------------")가 실행되고 다음 입력을 받습니다.


  • Tip : 예제는 무한 반복문이므로, Ctrl + C를 눌러 종료합니다.

  • Tip : NameError는 원래 정의되지 않은 이름(변수)을 사용할 때 발생하는 내장 오류입니다. 실제 코드에서는 상황에 맞는 내장 오류를 사용하거나, 아래와 같이 사용자 정의 오류를 만들어 사용하는 것이 좋습니다.


def set_age(age):
    if age < 0:
        raise ValueError(f"나이는 음수일 수 없습니다: {age}")
    return age


print("시작")
set_age(-5)
print("끝")
결과
시작
Traceback (most recent call last):
  File “main.py”, line 8, in <module>
    set_age(-5)
    ~~~~~~~^^^^
  File “main.py”, line 3, in set_age
    raise ValueError(f”나이는 음수일 수 없습니다: {age}”)
ValueError: 나이는 음수일 수 없습니다: -5

발생시킨 오류를 처리하지 않으면, Python이 발생시킨 오류와 마찬가지로 Traceback을 출력하고 프로그램이 종료됩니다.

set_age(-5)에서 오류가 발생했으므로, 그 다음 줄의 print("끝")은 실행되지 않습니다.

Traceback은 오류가 전달된 경로를 위에서 아래로 보여 줍니다. 8번째 줄에서 set_age를 호출했고, set_age 안의 3번째 줄에서 raise가 실행되었다는 의미입니다.

마지막 줄에는 오류의 종류와 메시지가 출력되므로, 오류 메시지를 작성할 때는 무엇이 왜 잘못되었는지 알 수 있도록 작성합니다.

  • Tip : 실제 오류 메시지에는 파일의 전체 경로가 표시되며, 예제에서는 main.py로 줄여 표기했습니다.



오류 메시지 전달

오류의 종류만으로는 무엇이 잘못되었는지 충분히 알기 어렵습니다.

ValueError가 발생했다는 사실보다 “나이는 음수일 수 없습니다: -5”처럼 잘못된 값과 이유를 함께 전달하면, 오류를 받은 쪽에서 원인을 바로 알 수 있습니다.


def set_age(age):
    if age < 0:
        raise ValueError(f"나이는 음수일 수 없습니다: {age}")
    return age


try:
    set_age(-5)
except ValueError as e:
    print(type(e).__name__, e)
    print(e.args)
결과
ValueError 나이는 음수일 수 없습니다: -5
(‘나이는 음수일 수 없습니다: -5’,)

raise 오류(메시지)의 형태로 오류와 함께 메시지를 전달할 수 있습니다.

except 오류 as 변수로 발생한 오류 객체를 변수에 할당하며, 변수를 출력하면 전달한 메시지가 출력됩니다.

오류.args에는 오류를 발생시킬 때 전달한 인자가 튜플로 저장됩니다.

type(e).__name__은 오류 클래스의 이름을 문자열로 반환하므로, 여러 종류의 오류를 함께 처리할 때 어떤 오류인지 출력하는 데 사용합니다.

  • Tip : raise ValueError와 같이 클래스만 작성하면, 인자가 없는 인스턴스(ValueError())가 생성되어 발생합니다.



상황에 맞는 오류 선택

raise에는 원하는 오류를 사용할 수 있지만, 아무 오류나 사용하면 오류를 처리하는 쪽에서 혼란이 생깁니다.

Python의 내장 오류는 각각 정해진 의미가 있으므로, 상황과 의미가 맞는 오류를 선택해야 합니다.


def set_age(age):
    if not isinstance(age, int):
        raise TypeError(f"나이는 정수여야 합니다: {type(age).__name__}")
    if age < 0:
        raise ValueError(f"나이는 음수일 수 없습니다: {age}")
    return age


for value in [20, -5, "스물"]:
    try:
        print(set_age(value))
    except (TypeError, ValueError) as e:
        print(type(e).__name__, e)
결과
20
ValueError 나이는 음수일 수 없습니다: -5
TypeError 나이는 정수여야 합니다: str

20은 올바른 값이므로 그대로 반환됩니다.

-5는 자료형은 맞지만 값이 허용 범위를 벗어났으므로 ValueError를 발생시킵니다.

"스물"은 자료형 자체가 잘못되었으므로 TypeError를 발생시킵니다.

except (TypeError, ValueError) as e처럼 소괄호로 묶으면 여러 오류를 하나의 except에서 처리할 수 있습니다.


오류 사용하는 상황
ValueError 자료형은 맞지만 값이 올바르지 않을 때
TypeError 자료형이 올바르지 않을 때
KeyError / IndexError 사전의 키 / 목록의 색인이 없을 때
FileNotFoundError 파일이 존재하지 않을 때
NotImplementedError 아직 구현하지 않은 기능일 때
RuntimeError 다른 분류에 해당하지 않는 실행 중 오류일 때


try:
    raise "오류 발생"
except TypeError as e:
    print("TypeError:", e)
결과
TypeError: exceptions must derive from BaseException

raise에는 오류 클래스나 오류 인스턴스만 사용할 수 있습니다.

문자열처럼 오류가 아닌 값을 raise하면, 의도한 오류 대신 exceptions must derive from BaseException이라는 TypeError가 발생합니다.

모든 오류는 BaseException 클래스를 상속받아야 하며, 직접 오류를 만들 때는 다음 섹션처럼 Exception을 상속받습니다.



사용자 정의 오류 발생

내장 오류만으로는 프로그램에서 발생하는 상황을 정확하게 표현하기 어려울 때가 있습니다.

예를 들어, “관리자 계정이 아님”이나 “잔액 부족”은 ValueError로 표현할 수도 있지만, 다른 ValueError와 구분하여 처리하기 어렵습니다.

이때 제 27강에서 다룬 클래스 상속을 이용하여 직접 오류 클래스를 정의합니다.


class AdminError(Exception):
    pass


while True:

    admin = input("관리자 계정 입력: ")

    try:
        if admin != "yundaehee":
            raise AdminError()
        else:
            print("관리자 계정입니다.")

    except AdminError:
        print("관리자 계정이 아닙니다.")

    print("------------")
결과
관리자 계정 입력: admin
관리자 계정이 아닙니다.
————
관리자 계정 입력: yundaehee
관리자 계정입니다.
————

클래스가 Exception을 상속 받는다면 사용자 정의 오류를 발생시킬 수 있습니다.

입력된 값이 yundaehee가 아니라면, raise 구문으로 넘어가게 되고, AdminError 클래스로 오류가 발생합니다.

except 구문에서 사용자 정의 오류를 예외 처리할 수 있습니다.

AdminError는 본문이 pass뿐이지만, Exception을 상속받았으므로 메시지 저장, Traceback 출력 등 오류에 필요한 기능을 모두 물려받습니다.

  • Tip : 사용자 정의 오류의 이름은 내장 오류와 같이 Error로 끝나도록 작성하는 것이 관례입니다.

  • Tip : 사용자 정의 오류는 BaseException이 아닌 Exception을 상속받습니다. BaseException을 직접 상속받은 KeyboardInterrupt, SystemExit 등은 프로그램 종료와 관련된 특수한 오류이므로, 일반적인 except Exception:에서 잡히지 않습니다.


class AdminError(Exception):
    def __init__(self, account):
        super().__init__(f"관리자 계정이 아닙니다: {account}")
        self.account = account


try:
    raise AdminError("guest")
except AdminError as e:
    print(e)
    print(e.account)
결과
관리자 계정이 아닙니다: guest
guest

사용자 정의 오류에 __init__ 메서드를 정의하면, 오류 메시지를 만들거나 추가 정보를 속성으로 저장할 수 있습니다.

super().__init__(메시지)로 부모 클래스인 Exception에 메시지를 전달합니다.

print(e)는 Exception에 전달한 메시지를 출력하고, e.account는 직접 저장한 계정 이름을 반환합니다.

오류를 처리하는 쪽에서는 메시지 문자열을 분석하지 않고도 e.account로 어떤 계정에서 문제가 발생했는지 바로 알 수 있습니다.


class BankError(Exception):
    pass


class InsufficientBalanceError(BankError):
    pass


class AccountLockedError(BankError):
    pass


def withdraw(balance, amount, locked=False):
    if locked:
        raise AccountLockedError("잠긴 계좌입니다.")
    if amount > balance:
        raise InsufficientBalanceError(f"잔액이 {amount - balance}원 부족합니다.")
    return balance - amount


for args in [(1000, 500), (1000, 3000), (1000, 100, True)]:
    try:
        print(withdraw(*args))
    except BankError as e:
        print(type(e).__name__, e)

print(issubclass(InsufficientBalanceError, Exception))
결과
500
InsufficientBalanceError 잔액이 2000원 부족합니다.
AccountLockedError 잠긴 계좌입니다.
True

사용자 정의 오류가 여러 개라면, 공통 부모 오류 클래스를 만들고 계층 구조로 정의하는 것이 좋습니다.

예제의 InsufficientBalanceError와 AccountLockedError는 모두 BankError를 상속받습니다.

except BankError는 BankError뿐만 아니라 이를 상속받은 모든 오류를 처리하므로, 두 오류를 하나의 except로 처리할 수 있습니다.

반대로 잔액 부족만 따로 처리하고 싶다면 except InsufficientBalanceError로 작성합니다.

이처럼 오류를 계층으로 정의하면, 사용하는 쪽에서 넓게 잡을지 좁게 잡을지 선택할 수 있습니다.



오류 다시 발생

오류를 처리하는 위치에서 모든 일을 해결할 수 없는 경우가 있습니다.

예를 들어, 함수 안에서는 오류를 기록만 하고, 사용자에게 메시지를 보여 주는 일은 함수를 호출한 쪽에서 처리하고 싶을 수 있습니다.

이때 오류를 잡았다가 다시 발생시켜 위쪽으로 전달합니다.


def parse(text):
    try:
        return int(text)
    except ValueError:
        print("변환 실패 기록")
        raise


try:
    parse("abc")
except ValueError as e:
    print("다시 잡은 예외:", e)
결과
변환 실패 기록
다시 잡은 예외: invalid literal for int() with base 10: ‘abc’

except 구문 안에서 raise만 단독으로 작성하면, 현재 처리 중인 오류를 그대로 다시 발생시킵니다.

오류를 기록하는 등의 작업만 수행하고, 실제 처리는 호출한 쪽으로 넘길 때 사용합니다.

실행 순서로 보면, int("abc")에서 ValueError가 발생하여 parse 안의 except가 변환 실패 기록을 출력하고, raise가 같은 오류를 다시 발생시켜 바깥쪽 except가 메시지를 출력합니다.

오류 객체가 그대로 전달되므로 메시지와 Traceback의 발생 위치가 바뀌지 않습니다.


except 구문에서 오류를 처리한 뒤 다시 발생시키지 않으면, 오류는 그 자리에서 사라집니다.

호출한 쪽이 실패 사실을 알아야 하는데도 오류를 출력만 하고 넘어가면, 잘못된 결과로 프로그램이 계속 실행될 수 있으므로 주의합니다.

  • Tip : except ValueError as e: 안에서 raise e로 작성해도 같은 오류가 발생하지만, 현재 처리 중인 오류를 다시 발생시킬 때는 raise만 작성하는 것이 일반적입니다.



예외 연결(raise from)

저수준의 오류를 그대로 전달하면, 호출한 쪽에서 오류의 의미를 이해하기 어려울 수 있습니다.

예를 들어, 설정 파일을 읽는 함수에서 ValueError: invalid literal for int()가 발생하면, 사용자는 어떤 설정이 잘못되었는지 알기 어렵습니다.

이때 프로그램에 맞는 의미 있는 오류로 바꿔 발생시키면서, 원래 오류를 원인으로 함께 기록할 수 있습니다.


class ConfigError(Exception):
    pass


def load_port(text):
    try:
        return int(text)
    except ValueError as e:
        raise ConfigError("포트 설정이 올바르지 않습니다.") from e


try:
    load_port("eighty")
except ConfigError as e:
    print(e)
    print(repr(e.__cause__))
결과
포트 설정이 올바르지 않습니다.
ValueError(“invalid literal for int() with base 10: ‘eighty’”)

raise 새 오류 from 원인 오류를 사용하면, 새로운 오류에 원인이 된 오류를 연결할 수 있습니다.

이를 예외 연결(Exception Chaining)이라 부르며, 저수준의 오류를 프로그램에 맞는 의미 있는 오류로 바꿔 발생시킬 때 사용합니다.

연결된 원인 오류는 오류.__cause__ 속성에 저장됩니다.

호출한 쪽은 ConfigError만 처리하면 되고, 자세한 원인이 필요하면 __cause__로 원래의 ValueError를 확인할 수 있습니다.


예외 처리를 하지 않고 load_port("eighty")를 실행하면 아래와 같이 두 오류가 함께 출력됩니다.

결과
Traceback (most recent call last):
  File “main.py”, line 7, in load_port
    return int(text)
ValueError: invalid literal for int() with base 10: ‘eighty’

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File “main.py”, line 12, in <module>
    load_port(“eighty”)
    ~~~~~~~~~^^^^^^^^^^
  File “main.py”, line 9, in load_port
    raise ConfigError(“포트 설정이 올바르지 않습니다.”) from e
ConfigError: 포트 설정이 올바르지 않습니다.

The above exception was the direct cause of the following exception은 위의 오류가 아래 오류의 직접적인 원인이라는 의미입니다.

Traceback은 원인 오류를 먼저, 새로 발생시킨 오류를 나중에 출력합니다. 그러므로, 가장 아래의 ConfigError가 최종적으로 발생한 오류이고, 위의 ValueError가 그 원인입니다.


class ConfigError(Exception):
    pass


def load_port(text):
    try:
        return int(text)
    except ValueError as e:
        raise ConfigError("포트 설정이 올바르지 않습니다.")


try:
    load_port("eighty")
except ConfigError as e:
    print(repr(e.__cause__))
    print(repr(e.__context__))
    print(e.__suppress_context__)
결과
None
ValueError(“invalid literal for int() with base 10: ‘eighty’”)
False

만약 from 없이 except 구문 안에서 새로운 오류를 발생시키면, During handling of the above exception, another exception occurred 메시지가 출력됩니다.

이는 오류를 처리하는 도중에 다른 오류가 발생했다는 의미로, 의도한 연결인지 코드의 실수인지 구분하기 어렵습니다.

그러므로, 오류를 바꿔 발생시킬 때는 from을 명시하는 것이 좋습니다.


예제처럼 from을 생략해도 Python은 처리 중이던 오류를 __context__ 속성에 자동으로 기록합니다.

from으로 명시한 원인은 __cause__에, 자동으로 기록된 오류는 __context__에 저장됩니다. 예제는 from을 사용하지 않았으므로 __cause__가 None입니다.

__suppress_context__는 __context__를 출력하지 않을지를 나타내며, 아래의 from None을 사용하면 True가 됩니다.


class ConfigError(Exception):
    pass


def load_port(text):
    try:
        return int(text)
    except ValueError:
        raise ConfigError("포트 설정이 올바르지 않습니다.") from None


load_port("eighty")
결과
Traceback (most recent call last):
  File “main.py”, line 12, in <module>
    load_port(“eighty”)
    ~~~~~~~~~^^^^^^^^^^
  File “main.py”, line 9, in load_port
    raise ConfigError(“포트 설정이 올바르지 않습니다.”) from None
ConfigError: 포트 설정이 올바르지 않습니다.

raise 새 오류 from None을 사용하면 원인 오류를 숨기고 새로운 오류만 출력합니다.

원인 오류의 정보가 사용자에게 불필요하거나 혼란을 줄 수 있을 때 사용합니다.

다만, 원인 오류를 숨기면 디버깅할 때 필요한 정보도 사라지므로, 특별한 이유가 없다면 from e로 원인을 남겨 두는 편이 좋습니다.

  • Tip : 실제 오류 메시지에는 파일의 전체 경로가 표시되며, 예제에서는 main.py로 줄여 표기했습니다.

  • Tip : 오류에 추가 정보를 덧붙이는 add_note 메서드와 여러 오류를 묶어 발생시키는 ExceptionGroup은 제 18강에서 확인할 수 있습니다.



정리

구문 설명 특징
raise 오류 오류 발생 클래스만 쓰면 인자 없는 인스턴스 생성
raise 오류(메시지) 메시지와 함께 오류 발생 메시지는 str(e), e.args로 확인
class 이름(Exception): 사용자 정의 오류 이름은 Error로 끝내는 것이 관례
부모 오류 클래스 사용자 정의 오류의 계층 except 부모로 자식 오류도 처리
raise (단독) 처리 중인 오류 다시 발생 except 구문 안에서 사용
raise 새 오류 from e 예외 연결 원인은 __cause__에 저장
from 생략 암시적 연결 처리 중이던 오류는 __context__에 저장
raise 새 오류 from None 원인 오류 숨김 __suppress_context__가 True


raise는 함수가 처리할 수 없는 상황을 호출한 쪽에 알리는 방법입니다. None 같은 특별한 값을 반환하는 것보다 문제가 생긴 위치와 이유를 명확하게 전달할 수 있습니다.

오류는 상황에 맞는 내장 오류를 선택하고, 프로그램 고유의 상황이라면 Exception을 상속받은 사용자 정의 오류를 만들어 사용합니다.

저수준의 오류를 의미 있는 오류로 바꿀 때는 raise ... from e로 원인을 함께 남겨, 디버깅에 필요한 정보가 사라지지 않도록 합니다.

댓글 남기기