파이썬의 모든 데이터는 객체이며, 이는 오류에 관한 데이터도 마찬가지다. 파이썬에는 다양한 오류 상황이 예외 클래스로 범주화되어 있다. 이 범주를 살펴보면 어떤 코드에서 어떤 오류가 발생할지 대략적으로 예상할 수 있다. 그리고 예외의 종류가 클래스라면, 실제로 일어나는 예외는 그 클래스에 속하는 객체로서 존재할 것이다. 파이썬에서 실행시간 오류가 발생한다는 것은 그 상황에 대응하는 예외 클래스에 속하는 객체가 생성된다는 뜻이다. 클래스와 객체를 다루는 법은 앞 장에서 이미 학습했으므로, 여러분이 예외의 종류(클래스)를 직접 정의하거나 예외(객체)를 직접 생성하고 조작할 수도 있다.

이 절에서는 파이썬에 정의되어 있는 예외의 분류를 간단히 살펴보고, 필요한 예외 유형을 직접 정의하고, 상황에 맞는 예외를 일으키는 방법도 알아본다.

9.4.1 예외의 분류

파이썬의 예외 클래스들은 상속을 이용해 계층적으로 구성되어 있다. 모든 예외 클래스는 BaseException 클래스의 하위 클래스이며, 프로그램을 종료시키는 예외를 제외한 대부분의 예외 클래스는 Exception 클래스의 하위 클래스다.

다음은 중요한 예외와 비교적 접하기 쉬운 예외를 꼽아 그 의미와 계층을 정리한 것이다. 양이 많아서 당황할 수 있는데, 지금 다 외우거나 억지로 이해할 필요는 없다. 언제 어떤 오류가 발생하는지는 프로그래밍 경험을 쌓으면서 점차 파악할 정보다. 한 번 훑어보고, 나중에 예외를 처리해야할 일이 생겼을 때 참고하면 된다.

BaseException
├── SystemExit
├── KeyboardInterrupt
└── Exception
    ├── ArithmeticError
    │   └── ZeroDivisionError
    ├── AssertionError
    ├── AttributeError
    ├── EOFError
    ├── ImportError
    │   └── ModuleNotFoundError
    ├── LookupError
    │   ├── IndexError
    │   └── KeyError
    ├── NameError
    ├── OSError
    │   ├── ChildProcessError
    │   ├── FileExistsError
    │   ├── FileNotFoundError
    │   ├── IsADirectoryError
    │   ├── NotADirectoryError
    │   ├── PermissionError
    │   └── TimeoutError
    ├── ReferenceError
    ├── RuntimeError
    │   ├── NotImplementedError
    │   └── RecursionError
    ├── SyntaxError
    │   └── IndentationError
    │       └── TabError
    ├── TypeError
    ├── ValueError
    │   └── UnicodeError
    └── Warning

그림 9-4 주요 예외 클래스의 계층 (준비중)

예외 클래스 의미 또는 예외 발생 원인
BaseException 모든 예외의 최상위 예외
SystemExit 프로그램을 종료하는 명령이 실행되었을 때
KeyboardInterrupt Control-C 키가 입력되었을 때
Exception (시스템 종료를 제외한) 대부분의 예외의 상위 예외
ArithmeticError 수의 연산과 관련된 문제
ZeroDivisionError 수를 0으로 나누려 할 때
AssertionError assert 문(9.4.6 assert 문으로 검증하기)
AttributeError (모듈, 클래스, 인스턴스에서) 잘못된 속성을 가리킬 때
EOFError (파일, 스트림 등에서) 읽어들일 데이터가 더이상 없을 때
ImportError 모듈을 임포트할 수 없을 때
ModuleNotFoundError 임포트할 모듈을 찾을 수 없을 때
LookupError (시퀀스, 매핑에서) 잘못된 인덱스, 키를 인덱싱할 때
IndexError (시퀀스에서) 잘못된 인덱스를 인덱싱할 때
KeyError (매핑에서) 잘못된 키를 인덱싱할 때
NameError 잘못된 변수(이름)를 가리킬 때
OSError 컴퓨터 시스템(운영 체제)의 동작과 관련된 다양한 문제
ChildProcessError 하위 프로세스(프로그램이 실행한 별도의 프로그램)에서 오류 발생
FileExistsError 이미 존재하는 파일/디렉터리를 새로 생성하려 할 때
FileNotFoundError 존재하지 않는 파일/디렉터리에 접근하려 할 때
IsADirectoryError 파일을 위한 명령을 디렉터리에 실행할 때
NotADirectoryError 디렉터리를 위한 명령을 파일에 실행할 때
PermissionError 명령을 실행할 권한이 없을 때
TimeoutError 명령이 수행되는 시간이 시스템이 허용한 기준을 초과했을 때
RuntimeError 다른 분류에 속하지 않는 실행시간 오류
NotImplementedError 내용 없는 메서드가 호출되었을 때
RecursionError 함수의 재귀 호출 단계가 허용한 깊이를 초과했을 때
SyntaxError 구문 오류
IndentationError 들여쓰기가 잘못되었을 때
TabError 들여쓰기에 탭과 스페이스를 번갈아가며 사용했을 때
TypeError 연산/함수가 계산해야 할 데이터가 잘못된 유형일 때
ValueError 연산/함수가 계산해야 할 데이터가 유형은 올바르나 값이 부적절할 때
UnicodeError 유니코드와 관련된 오류
Warning 심각한 오류는 아니나 주의가 필요한 사항에 관한 경고

표 9-3 주요 예외 클래스의 의미와 발생 원인

9.4.2 여러 예외를 동일하게 처리하기

여러 종류의 예외가 동일한 방식으로 처리되어야 할 때도 있다. 튜플과 사전에 존재하지 않는 인덱스와 키를 인덱싱하는 경우를 생각해 보자.

코드 9-30 튜플과 사전에 존재하지 않는 인덱스와 키를 인덱싱하는 예외

def get(key, dataset):
    """데이터 집합(dataset)에서 인덱스(키)에 해당하는 값을 반환한다.
    데이터 집합에 해당하는 인덱스(키)가 존재하지 않는 경우,
    None을 반환한다.
    """
    try:
        value = dataset[key]
    except IndexError:  # 인덱스가 잘못된 예외
        return None
    except KeyError:    # 키가 잘못된 예외
        return None
    else:
        return value

print(get(3, (1, 2, 3)))               # 범위를 벗어난 인덱스를 인덱싱
print(get('age', {'name': '박연오'}))  # 사전에 없는 키 인덱싱

실행 결과:

None
None

위 코드처럼 여러 종류의 예외를 동일한 방식으로 처리해야 할 때는 except 절에 처리할 예외를 괄호로 감싸고 콤마로 구별해 나열하면 된다. (이 경우에는 괄호를 생략할 수 없다.) 따라서 다음과 같이 수정할 수 있다.

코드 9-31 여러 예외 동일하게 처리하기

def get(key, dataset):
    """데이터 집합(dataset)에서 인덱스(키)에 해당하는 값을 반환한다.
    데이터 집합에 해당하는 인덱스(키)가 존재하지 않는 경우,
    None을 반환한다.
    """
    try:
        value = dataset[key]
    except (IndexError, KeyError):  # 두 예외를 함께 처리
        return None
    else:
        return value

print(get(3, (1, 2, 3)))               # 범위를 벗어난 인덱스를 인덱싱
print(get('age', {'name': '박연오'}))  # 사전에 없는 키 인덱싱

실행 결과:

None
None

이 방법으로 여러 개의 예외를 일괄 처리할 수 있다. 물론, 예외마다 처리해야 하는 방법이 다른 경우에는 이 방법을 쓸 수 없다.

상위 범주 예외 처리하기

except 절에서 상위 범주의 예외를 처리하면 그 범주에 속하는 하위 범주의 예외도 함께 처리된다. IndexError 예외와 KeyError 예외는 상위 범주 예외 LookupError에 속한다. 따라서 코드 9-31은 다음과 같이 LookupError를 처리하도록 수정할 수도 있다.

코드 9-32 상위 범주 예외 처리하기

def get(key, dataset):
    """데이터 집합(dataset)에서 인덱스(키)에 해당하는 값을 반환한다.
    데이터 집합에 해당하는 인덱스(키)가 존재하지 않는 경우,
    None을 반환한다.
    """
    try:
        value = dataset[key]
    except LookupError:  # 상위 범주 예외 처리
        return None
    else:
        return value

print(get(3, (1, 2, 3)))               # 범위를 벗어난 인덱스를 인덱싱
print(get('age', {'name': '박연오'}))  # 사전에 없는 키 인덱싱

실행 결과:

None
None

모든 예외를 처리해버리면 어떨까?

최상위 예외 클래스인 BaseException이나 그에 준하는 Exception 등을 except 절에서 처리하면 발생하는 모든 예외를 처리할 수 있다.

코드 9-33 상위 범주 예외 처리하기

try:
    print(1 / 0)
except BaseException:
    print('종류는 모르겠지만 하여튼 예외가 발생했다.')

하지만 이런 식으로 모든 예외를 똑같이 처리해버리는 것은 확실한 이유가 없다면 피해야 한다. 만약 오류 메시지를 보기 싫다는 이유만으로 모든 예외를 조용히 묻어둔 채 프로그램이 진행되도록 한다면, 실행시간 오류를 논리적 오류로 만들어버리는 더욱 큰 문제가 될 것이다. 오류가 명시적으로 일어나는 것은 프로그래머를 돕기 위한 것이다. 조용히 발생하는 논리적 오류는 잡아내기가 힘들며, 프로그램을 미궁에 빠트릴 수 있다.

9.4.3 예외 객체 전달받기

예외 객체는 함수의 실행 호출과 코드 블록을 따라 한 단계씩 밖으로 전달된다. 그 과정에서 코드 블록 중 try 문 블록이 있고, 예외 객체에 해당되는 except 이 있다면 except 절이 예외 객체를 전달받아 처리한다. 이 때, except 절 안에서 예외 객체에 담긴 오류 정보를 이용할 수 있다. except 절에서 예외 객체를 이용하려면 except 예외클래스 as 변수이름:과 같이, 처리할 예외 클래스 오른쪽에 as 변수이름을 추가하면 된다. ‘as’는 ‘~로서’라는 뜻이며, 전달받은 예외를 지정한 변수 이름으로 부르겠다는 뜻이다. 예외를 가리키는 이름은 ‘e’, ‘err’, ‘error’, ‘exception’ 등이 주로 사용된다.

대화식 셸에서 예외 객체를 전달받아 그 내용을 확인해 보자. try 문에서 예외를 발생시키고 except 절에서 발생한 예외를 전달받아, 전역변수에 대입한다.

코드 9-34 예외 객체 전달받아 확인하기

>>> try: 
...     1 / 0 
... except ZeroDivisionError as e:  # 예외 객체를 변수 e에 대입
...     exception = e    # 예외 객체를 전역변수 exception에 대입
... 
>>> type(exception)  # 예외 객체의 유형 확인
<class 'ZeroDivisionError'>

>>> isinstance(exception, ZeroDivisionError)  # ZeroDivisionError의 인스턴스인가?
True

>>> isinstance(exception, BaseException)  # BaseException의 인스턴스인가?
True

>>> str(exception)  # 문자열로 변환하면 오류의 발생원인을 나타내는 문자열이 된다
'division by zero'

실험에서 확인할 수 있듯이 예외 객체는 해당되는 예외 클래스의 인스턴스이며, 오류의 발생원인을 나타내는 문자열도 포함하고 있다.

예외 객체가 코드 블록을 다 빠져나올 때까지 처리되지 않는다면 오류 메시지가 출력되고 프로그램이 중단된다. 이는 프로그램의 가장 바깥쪽에서 except BaseException as e:와 같이 모든 예외를 잡아내 처리하는 것으로 생각할 수 있다. except 절이 전달받은 예외 객체에는 오류에 관한 여러가지 정보가 포함되므로, 이를 활용해 파이썬의 기본 오류 처리를 흉내낼 수도 있다. 하지만 프로그래밍 언어의 기본 동작을 재정의하는 것은 바람직하지 않으며, 입문서에서 다룰 내용도 아니다.

9.4.4 예외 유형 정의하기

여러분의 프로그램에서만 발생할 가능성이 있는 어떤 예외가 있는데, 이 예외가 파이썬이 제공하는 어떠한 예외 유형에도 들어맞지 않는 경우가 있을 수 있다. 예외 유형이 없으면 예외 처리도 불가능할 것이므로 곤란할 것이다. 그럴 때는 여러분이 예외 유형을 직접 정의하면 된다. 예외의 유형은 클래스이기 때문에 class 문을 통해 직접 정의할 수 있다. 직접 정의하는 예외 클래스는 Exception 클래스를 상속해야 한다. BaseException을 직접 상속하지 않는 점을 주의하자.

문을 자동으로 여닫는 사물 인터넷 프로그램을 만든다고 생각해보자. 문이 열린 상태에서 문을 열거나, 문이 닫힌 상태에서 문을 닫는 것은 예외 상황이다. 이를 나타내는 예외 유형을 다음과 같이 정의하기로 하자.

BaseException
└── Exception
    └── DoorException (문 관련 예외)
        ├── DoorOpenedException (문 열림 예외)
        └── DoorClosedException (문 닫힘 예외)

그림 9-5 문 관련 예외의 범주 (준비중)

코드 9-35 문 관련 예외 정의하기

class DoorException(Exception):
    """문과 관련된 예외"""
    pass  # 본문에 포함할 내용이 없을 때 pass 문을 사용

class DoorOpenedException(DoorException):
    """문 열림 예외"""
    pass

class DoorClosedException(DoorException):
    """문 닫힘 예외"""
    pass

문의 조작과 관련된 예외(DoorException)를 상위 범주로 정의하고, 그 하위 범주로 문 열림 예외(DoorOpenedException)와 문 닫힘 예외(DoorClosedException)를 정의했다. 상위 범주는 Exception을 상속했고, 하위 범주는 상위 범주 클래스를 상속해 정의했다. 클래스를 정의하는 방법과 상속에 관해서는 8장에서 이미 알아보았으니 잘 모르겠다면 다시 살펴보도록 하자.

새로 정의한 예외 유형은 except DoorException:과 같이 except 절에서 처리할 수 있다. 그런데 이 예외는 언제 발생할까? 문이 열리고 닫히는 것을 파이썬이 자동으로 확인하여 예외를 발생시켜줄까? 이에 관해서는 이어지는 내용에서 소개된다.

연습문제

연습문제 9-5 은행 계좌 관련 예외 1

은행 계좌와 관련된 예외를 다음과 같이 정의해 보아라.

BaseException
└── Exception
    └── AccountException (계좌 관련 예외)
        ├── AccountBalanceException (계좌 잔고 예외)
        ├── FrozenAccountException (동결 계좌 예외)
        └── InvalidTransactionException (잘못된 입출금 예외)

9.4.5 raise 문으로 예외 발생시키기

파이썬의 기본 예외는 파이썬 인터프리터가 자동으로 감지하여 발생시킨다. 하지만 모든 경우에 그런 것은 아니어서 프로그래머가 오류를 직접 확인해 예외를 발생시켜야 할 때도 있다. 또, 여러분이 직접 정의한 예외 유형은 여러분이 직접 오류를 확인하고 발생시켜야 할 것이다.

(실제로 오류를 일으키는 것을 제외하고) 예외를 발생시키는 방법에는 raise 문을 이용하는 방법과 assert 문을 이용하는 방법이 있다. raise 문부터 알아보자.

‘raise’는 ‘일으키다’라는 뜻이며, raise 문은 특정 예외를 명시적으로 발생시킨다. raise 예약어 오른쪽에 발생시킬 예외 객체를 표기하면 그 예외가 즉시 발생한다.

raise 예외객체

예외 객체를 생성하는 방법은 다른 객체를 생성하는 방법과 다르지 않다. 예외 클래스를 인스턴스화하여 생성하면 된다. 인스턴스화할 때, 첫번째 매개변수를 오류의 원인을 나타내는 문자열로 지정하는 것이 관례다. IDLE의 대화식 셸에서 직접 오류를 발생시켜보자.

코드 9-36 raise 문으로 예외 발생시키기

>>> raise ZeroDivisionError('0으로 나눌 수 없음')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: 0으로 나눌 수 없음

오류 원인이 “division by zero” 대신 “0으로 나눌 수는 없죠.”로 바뀐 점을 제외하면, 위 코드에서 출력된 오류 메시지는 1 / 0을 연산했을 때 발생하는 오류 메시지와 완전히 같다. 1 / 0을 연산하도록 하면 파이썬은 연산의 분모가 0 임을 검사하여 감지하여 raise ZeroDivisionError('division by zero')를 실행하는 셈이다.

raise 문으로 발생시키는 예외는 파이썬 인터프리터가 자동으로 발생시키는 예외와 다른 점이 없다. try 문으로 처리하는 것도 당연히 가능하다.

코드 9-37 raise 문으로 발생시킨 예외 처리하기

>>> try:
...     raise ZeroDivisionError('0으로 나눌 수 없음')
... except ZeroDivisionError:
...     print('0으로 나누는 예외가 발생했습니다.')
... 
0으로 나누는 예외가 발생했습니다.

발생시키는 예외 객체에 추가 정보를 담을 수도 있다. 예외 객체를 먼저 만든 후 추가로 포함할 속성을 대입하여 raise 문으로 발생시키면 된다.

코드 9-38 예외 객체에 추가 정보를 속성으로 대입하기

>>> try:
...     exception = ZeroDivisionError('0으로 나눌 수 없음')
...     exception.user = '박연오'
...     raise exception
... except ZeroDivisionError as e:
...     print('오류 원인:', str(e))
...     print('오류를 일으킨 사용자:', e.user)
... 
오류 원인: 0으로 나눌 수 없음
오류를 일으킨 사용자: 박연오

raise 문으로 논리적 오류 해결하기

raise 문을 활용하면 조용히 발생하는 논리적 오류를 명시적으로 발생하는 실행시간 오류로 완화할 수 있다. 논리적 오류가 발생할 가능성이 있는 코드에서 상태를 검사하여 문제가 있는 경우 예외를 발생시키면 된다. 다음은 문을 나타내는 클래스를 정의한 것이다. 문이 이미 열려있는 상태에서 다시 열지 않도록, 코드 9-35에서 정의한 문 관련 예외를 적절하게 발생시키도록 했다. if 문으로 논리적 오류를 검사한 후, raise 문으로 예외를 발생시키면 된다.

코드 9-39 문 관련 예외를 적절히 처리하는 문 클래스

class Door:
    """문을 나타내는 클래스"""
    def __init__(self):
        self.is_opened = True  # 문이 열려있는지를 나타내는 상태

    def open(self):
        # 문이 이미 열린 경우, 예외를 일으킨다
        if self.is_opened:
            raise DoorOpenedException('문이 이미 열려 있음')
        # 그렇지 않다면, 문을 연다
        else:
            print('문을 엽니다.')
            self.is_opened = True

    def close(self):
        # 문이 이미 닫힌 경우, 예외를 일으킨다
        if not self.is_opened:
            raise DoorClosedException('문이 이미 닫혀 있음')
        # 그렇지 않다면, 문을 닫는다
        else:
            print('문을 닫습니다.')
            self.is_opened = False

오류가 제대로 처리되는지 인스턴스를 생성하여 확인해 보자.

코드 9-40 문 클래스의 예외 발생 시험하기

door = Door()  # 문 인스턴스 생성
door.close()   # 문 닫기
door.open()    # 문 열기
door.open()    # 문 열린 상태에서 문 열기

실행 결과:

문을 닫습니다.
문을 엽니다.
Traceback (most recent call last):
  File "example_9_38.py", line 39, in <module>
    door.open()    # 문 열린 상태에서 문 열기
  File "example_9_38.py", line 21, in open
    raise DoorOpenedException('문이 이미 열려 있음')
__main__.DoorOpenedException: 문이 이미 열려 있음

문이 열린 상태에서 닫거나, 문이 닫힌 상태에서 열 때는 정상 동작하고, 문이 열린 상태에서 문을 열자 DoorOpenedException이 발생하는 것을 확인할 수 있다.

연습문제

연습문제 9-6 은행 계좌 관련 예외 2

은행 계좌를 관리하는 클래스를 다음과 같이 정의하였다.

class Account():
    """은행 계좌"""
    def __init__(self, balance, is_frozen):
        """인스턴스를 초기화한다."""
        self.balance = balance      # 계좌 잔액
        self.is_frozen = is_frozen  # 계좌 동결 여부
    
    def check(self):
        """계좌의 잔고를 조회한다."""
        print('계좌 잔액은', self.balance, '원 입니다.')
    
    def deposit(self, amount):
        """계좌에 amount 만큼의 금액을 입금한다."""
        self.balance += amount
    
    def withdraw(self, amount):
        """계좌에서 amount 만큼의 금액을 인출한다."""
        self.balance -= amount

그런데 이 클래스를 시험하는 중 다음과 같은 논리적 오류가 발견되었다.

  • 계좌의 잔액을 초과하는 액수가 출금되어서는 안 된다.
  • 동결된 계좌에서 출금되어서는 안 된다.
  • 0 이하의 액수는 입금 또는 출금할 수 없다.

연습문제 9-5에서 정의한 예외 클래스를 활용해, 위의 논리적 오류 상황에서 적절한 예외가 발생되도록 deposit() 메서드와 withdraw() 메서드를 수정해라.

9.4.6 assert 문으로 검증하기

논리적 오류를 해결하는 또다른 방법은 assert 문을 이용하는 방법이다. assert 문은 예외를 일으킨다는 점에서 raise 문과 비슷한 명령이다. 하지만 언제, 왜, 어떤 예외를 발생시키는지가 raise 문과 다르다.

비교항목 raise 문 assert 문
용도 예외의 발생 상태의 검증
언제 예외를 일으키는가? 항상 검증식이 거짓일 때만
어떤 예외를 일으키는가? 지정한 예외 AssertionError

표 9-4 raise 문과 assert 문의 비교

raise 문은 오류를 이미 발견한 상황에서 예외를 발생시키기 위한 명령이다. 따라서 무조건 예외를 발생시키며, 프로그래머가 가르쳐준 예외를 발생시킨다. 반면, assert 문은 어떤 상태를 검증하기 위한 명령이다. 따라서 입력된 식을 계산하여 결과가 참일 때는 아무 문제를 일으키지 않고, 결과가 거짓일 때만 AssertionError 예외를 발생시킨다.

assert 문을 작성하는 양식은 다음과 같다.

assert 검증식, 오류메시지

‘assert’는 ‘단언하다’라는 뜻이며, assert 문은 “이 식은 올바르다!”라고 강하게 주장하는 셈이다. 입력된 식이 거짓이라면 크게 부끄러울 것이므로 예외가 발생한다. 오류 메시지는 선택사항이므로 검증식만 입력해도 된다. 한 번 확인해 보자.

코드 9-41 assert 문으로 식 검사하기

>>> assert True         # 식이 올바르면 문제 없다
>>> assert 1 + 1 == 2   # 식이 올바르면 문제 없다
>>> assert 1 - 1 == 2   # 식이 거짓이면 AssertionError가 발생한다
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

>>> assert False, '뭔가 잘못됐군요'   # 오류 메시지 지정하기
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError: 뭔가 잘못됐군요

assert 문으로 논리적 오류 해결하기

assert 문을 활용하면 논리적 오류를 간단히 검사할 수 있다. 계산된 결과가 올바른지 검산할 때, 어떤 중요한 명령을 실행하기 전에 준비가 잘 갖춰졌는지 확인할 때 등 무언가 ‘확인이 필요한’ 상황이라면 assert 문을 사용하자.

assert 문을 사용하면 코드 9-39의 문 클래스를 더 간결하게 정의할 수 있다.

코드 9-42 assert 문으로 클래스의 상태 검사하기

class Door:
    """문을 나타내는 클래스"""
    def __init__(self):
        self.is_opened = True  # 문이 열려있는지를 나타내는 상태

    def open(self):
        # 문이 닫혀 있어야 한다
        assert not self.is_opened

        # 그렇지 않다면, 문을 연다
        print('문을 엽니다.')
        self.is_opened = True

    def close(self):
        # 문이 열려 있어야 한다
        assert self.is_opened

        # 그렇지 않다면, 문을 닫는다
        print('문을 닫습니다.')
        self.is_opened = False

실행 결과:

문을 닫습니다.
문을 엽니다.
Traceback (most recent call last):
  File "example_9_40.py", line 25, in <module>
    door.open()    # 문 열린 상태에서 문 열기
  File "example_9_40.py", line 8, in open
    assert not self.is_opened
AssertionError

assert 문은 사용하기 간편하지만, 발생시킬 예외를 직접 지정할 수 없다는 점이 단점이다. DoorException 같은 예외 클래스를 정의하더라도 assert 문에서는 활용하지 못한다. 간단한 오류 검사에는 assert 문을 활용하고, 오류를 체계적으로 관리해야 할 때는 예외 클래스를 적절히 정의하고 raise 문을 활용하도록 하자.

이 절에서 예외를 분류하는 방법과 예외를 발생시키는 방법을 알아보았다. 예외를 직접 정의해야 할 일은 개인 수준의 작은 프로젝트에서는 흔한 일은 아니다. 예외를 일으켜야 할 때는 그보다 많다. 예외를 일으킨다는 게 이상하게 생각될 수도 있다. 그러나 조용히 문제를 덮고 넘어가겠다는 생각은 프로그래머가 취할 태도가 아니다. 심각한 병일수록 조용히 자라나는 법, 적극 검사하고 드러내야 곪기 전에 치료할 수 있다.

연습문제

연습문제 9-7 은행 계좌 관련 예외 3

연습문제 9-6에서는 if 문과 raise 문을 이용해 논리적 오류를 처리했다. 이를 assert 문을 이용해 처리하는 방식으로 수정해라.