9.3.1 예외 처리란 무엇인가

항상 동일하게 발생하는 오류는 프로그램을 고쳐 해결하면 되는데, 입력 데이터나 실행 환경 등 프로그램의 외부 요인에 의해 발생하는 실행 오류는 어떻게 해결해야 할까? 입력 데이터와 실행 환경은 프로그래머가 아니라 사용자가 통제한다. “F1 키를 절대 누르지 마시오”, “프로그램 사용중 인터넷 연결을 끊지 마시오” 같은 권고에 사용자가 따른다는 보장은 없다. 잘못된 입력, 잘못된 실행 환경은 발생한다.

외부 요인을 제거할 수 없다면, 그 대응책을 프로그램 안에 마련해두면 된다. 오류가 없을 때의 정상적인 실행 흐름과 오류가 발생할 예외적 상황에 대응하는 별도의 실행 흐름을 함께 작성해두는 것이다. 이를 예외 처리(exception handling)라고 한다.

“냄비에 육수 1리터를 담고 센 불로 끓인다.”

이 조리법은 조리도구와 식재료가 갖추어져 있을 것을 전제로 한다. 만약 냄비가 없다면 “냄비가 없다”라는 ‘예외’ 상황이 발생하여 조리를 수행할 수 없을 것이다. 조리법을 수정하여 이를 ‘처리’해보자.

“냄비에 육수 1리터를 담고 센 불로 끓인다. 만약 냄비가 없을 경우 주전자를 대신 사용한다.”

수정한 조리법은 “냄비가 없다”라는 예외를 처리할 수 있다. “만약 냄비가 없을 경우 …” 라는 표현에서 나타나듯, 예외 처리는 환경이라는 ‘조건’에 따라 실행할 지시를 ‘선택’하는 것이다. 따라서 if 문을 사용해 예외 처리를 할 수 있다. 그러나 if 문은 예외 처리를 위해 쓰기에는 불편한 점이 있어서 예외 처리를 위한 전용 문법이 따로 있다. 이어지는 내용에서 이에 관해 자세히 알아볼 것이다.

예외 처리는 어디까지?

위의 조리법에서 일어날 수 있는 예외가 “냄비가 없다”뿐일까? 냄비가 있더라도 용량이 1리터에 못미칠 수도 있고, 냄비가 없을 뿐 아니라 주전자도 없을 경우도 있다. 조리법을 다시 수정하면 이런 예외도 처리할 수 있겠으나, 언제나 미처 생각하지 못한 새로운 예외가 발생할 수 있다. 즉, 모든 예외를 예견하는 것은 불가능하다.

예외 처리를 어디까지 할 것인지는 프로그램의 안정성이 어느 정도까지 요구되느냐에 달렸다. 의학, 항공 관제 등을 다루는 정밀 프로그램이라면 충분한 테스트를 통해 최대한의 예외를 처리해야 할 것이고, 한번 쓰고 버릴 너댓 행짜리 스크립트라면 예외 처리를 생략해도 무방할 것이다. 일반적인 프로그램이라면 자주 발생하는 예외를 미리 처리하고, 사용자 보고하는 새 오류를 보완하여 패치해주는 수준 정도도 나쁘지 않다. 하지만 패치를 제공하기 어렵다면 처음부터 완성도를 충분히 높여 공개하는 것이 좋다.

9.3.2 if 문으로 예외 처리하기

예외 처리는 예외를 조건으로 별도의 실행 흐름을 수행하는 것이므로, if 문을 이용해 지시할 수 있다. 사용자로부터 수를 입력받아 나누는 프로그램을 예로 들어 보자.

코드 9-16 예외 처리가 필요한 프로그램

# 데이터 입력
print('0이 아닌 정수를 입력해 주세요:', end=' ')
user_number = int(input())

# 결과 출력
print(1 / user_number)

이 프로그램에 사용자가 '0'을 입력할 경우 실행시간 오류가 일어날 것이다. 이 예외를 if 문으로 처리해 보자.

코드 9-17 if 문으로 예외 처리하기

# 데이터 입력
print('0이 아닌 정수를 입력해 주세요:', end=' ')
user_number = int(input())

# 입력값이 0인 경우 프로그램 종료
if user_number == 0:
    print('0으로 나눌 수 없습니다.')
    exit()   # 프로그램 종료

# 결과 출력
print(1 / user_number)

위 코드는 사용자가 입력한 값을 계산하기 전에 if 문으로 검사한다. 이를 통해 잘못된 값이 계산에 사용되는 것을 방지했다. if 문의 본문에는 예외를 감지한 경우의 대처법이 포함된다. 사용자가 다시 (올바른) 값을 입력하도록 요구할 수도 있고, 문제점을 알려준뒤 프로그램을 종료할 수도 있다. 여기서는 exit() 함수를 호출해 프로그램을 종료시켰다. 따라서 사용자가 0을 입력하지 않았을 때만 나눗셈 연산이 실행된다. 동일한 요령으로 사용자가 정수를 입력했는지도 검사할 수 있다.

코드 9-18 두 가지 예외 처리하기

# 데이터 입력
print('0이 아닌 정수를 입력해 주세요:', end=' ')
user_string = input()

# 입력값이 정수가 아닌 경우 프로그램 종료
if not user_string.isnumeric():
    print(user_string, '은 정수가 아닙니다.')
    exit()   # 프로그램 종료

# 입력값(문자열)을 정수로 변환
user_number = int(user_string)

# 입력값이 0인 경우 프로그램 종료
if user_number == 0:
    print('0으로 나눌 수 없습니다.')
    exit()   # 프로그램 종료

# 결과 출력
print(1 / user_number)

사용자가 잘못된 값을 입력했을 때 입력을 다시 요청하는 것도 좋은 생각이다. 올바른 데이터를 입력할 때까지 계속 입력을 요청하도록 해야 한다면 while 문을 함께 활용하면 좋다.

코드 9-19 올바른 값이 입력될 때까지 반복 입력하기

while(True):   # 무한 반복
    # 데이터 입력
    print('0이 아닌 정수를 입력해 주세요:', end=' ')
    user_string = input()
    
    # 입력값이 정수가 아닌 경우 다시 입력
    if not user_string.isnumeric():
        print(user_string, '은 정수가 아닙니다.')
        continue   # 계속 반복
    
    # 입력값(문자열)을 정수로 변환
    user_number = int(user_string)
    
    # 입력값이 0인 경우 다시 입력
    if user_number == 0:
        print('0으로 나눌 수 없습니다.')
        continue   # 계속 반복
    
    break   # 반복 중지

# 결과 출력
print(1 / user_number)

위 코드는 while 문을 이용한 무한 반복 블록 안에서 입력과 예외 처리를 계속 반복한다. 6장에서 알아본 continue 문과 break 문도 다시 등장했다. 잘못된 값이 입력됐을 때는 continue 문을 통해 입력이 반복 블록이 처음부터 다시 수행되고, 마침내 올바른 값이 입력됐을 때는 반복 블록이 중지된다.

실행 결과:

0이 아닌 정수를 입력해 주세요: 이백
이백 은 정수가 아닙니다.
0이 아닌 정수를 입력해 주세요: 0
0으로 나눌 수 없습니다.
0이 아닌 정수를 입력해 주세요: 200
0.005

입력값 검증

사용자가 입력하는 모든 데이터는 의심스러운 데이터다. 사용자는 실수로 잘못된 데이터를 입력해 오류를 유발하기도 하고, 고의로 잘못된 데이터를 입력해 해킹을 시도하기도 한다. 사용자가 입력하는 모든 데이터를 검증하라!

연습문제

연습문제 9-3 파일 읽기 프로그램 1

김파이 씨는 텍스트 파일의 내용을 읽어 화면에 출력하는 프로그램을 다음과 작성했다.

print('파일 이름을 입력하시오: ', end='')
filename = input()

# filename 파일 열기
with open(filename) as f:
    # 파일의 내용을 읽어 화면에 출력
    print(f.read())

그리고 처음에는 이 프로그램이 잘 실행된다고 생각했다.

파일 이름을 입력하시오: sample.txt
파이썬 프로그래밍
샘플 텍스트 파일입니다

그런데 프로그램을 사용해 본 친구들이, 프로그램이 제대로 동작하지 않는다고 하며 다음과 같은 오류 메시지를 보내주었다.

Traceback (most recent call last):
  File "exercise_9_3.py", line 5, in <module>
    with open(filename) as f:
FileNotFoundError: [Errno 2] No such file or directory: 'some-file.txt'

이 오류가 발생하는 이유는 무엇인지 설명해 보아라. 그리고 이 오류가 일어나지 않도록 하려면 어떤 처리가 필요한지, 그리고 그 처리를 수행할 수 있는지 또는 없는지를 설명해 보아라.

9.3.3 if 문을 이용한 예외 처리의 한계

과거에는 if 문을 이용한 예외 처리가 많이 사용되었으나, 현재는 대부분의 프로그래밍 언어가 별도의 예외 처리 문법을 갖게 되었다. if 문을 이용한 예외 처리에는 다음과 같은 한계가 있기 때문이다.

  1. 예외의 종류를 식별하기 위한 값을 일반화하기 어렵다.
  2. 함수 호출 속에서 발생한 예외를 함수 밖으로 전달하기가 까다롭다.
  3. 예외 상황인지 항상 미리 검사해야 한다.

if 문을 이용한 예외 처리는 특히 함수 안에서 문제가 된다. 함수는 return 문을 이용해 결과를 반환한다. 그런데 함수 호출 도중 예외가 발생한 경우, 예외가 발생한 사실을 함수 밖으로 어떻게 알릴 것인가? 네트워크에 연결된 컴퓨터와 신호를 주고받는 데 걸리는 시간을 측정하여 반환하는 가상의 함수를 예로 들어 보자. 정상적인 경우에는 응답시간(초)을 수로 반환하면 된다. 하지만 서버에 접속할 수 없는 경우에는 어떤 값을 반환해야 할까? 한번 생각해보고, 다음 코드를 보자.

코드 9-20 함수 밖으로 예외 전달하기 (이렇게 하지 마시오)

# 주의: 다음은 예를 위한 가짜 코드이며 실행되지 않는다.
def ping(address):
    """대상 주소(address)의 컴퓨터와 신호를 주고받는 데 걸리는 시간(초)을 측정하여 반환한다."""
    
    # 예외 처리
    if 주소가_잘못된_경우:
        return -1
    
    if 인터넷_연결이_안_된_경우:
        return -2
    
    if 서버에_접속할_수_없는_경우:
        return -3
    
    seconds = ...    # 정상적인 경우: 응답시간 계산하여 반환
    return seconds

위 코드의 ping() 함수는 예외를 함수 밖으로 알리기 위해 음수 -1, -2, -3이라는 특별한 값을 반환한다. 응답시간은 음수가 될 수 없으므로, 함수를 호출한 쪽에서는 양수를 정상적인 결과로, 음수를 예외로 판단할 수 있다.

함수를 호출한 쪽에서는 단순히 예외가 일어났다는 것뿐 아니라, 어떤 예외가 일어났는지도 궁금할 것이다. 따라서 각 예외 상황마다 반환하는 값을 -1, -2, -3으로 각각 차이를 두었다. 숫자로 약속하는 것이 불편하다면, '잘못된 주소' , '인터넷 연결 안 됨', '접속불가' 같은 문자열을 이용할 수도 있을 것이다. 이처럼 특정한 값을 예외를 나타내는 데 사용하기로 약속해 둔 것을 오류 코드(error code)라고 한다.

그러나 if 문과 오류 코드를 이용해 예외를 함수 밖으로 전달하는 방법에는 두 가지 문제가 있다. 첫번째는 오류 코드가 함수마다 제각각 정의된다는 것이다. 오류 코드는 ‘비정상적인 반환값’이어야 하는데, 그런 값은 함수마다 다를 수밖에 없다. 예컨대 정수를 반환하는 함수는 문자열을 오류 코드로 약속할 수 있겠지만, 문자열을 반환하는 함수는 반대로 정수를 이용해야 할 것이다. 통일된 오류 코드가 없으므로 함수마다 정의해야 할 사항이 많아져 불편하다.

두번째 문제는 오류 코드를 이용해 예외 처리한 함수를 호출할 때, 호출 지점에서도 예외 처리를 중복으로 해야 한다는 점이다. 이 점은 함수를 연쇄적으로 호출할 때 문제가 된다. 앞의 ping() 함수를 호출하여 두 서버의 응답시간을 비교하는 함수를 정의한다고 해 보자.

코드 9-21 전달받은 예외 전달하기 (이렇게 하지 마시오)

def compare_two_servers(a, b):
    """두 서버 a, b의 주소를 입력받아, 응답시간을 비교한다.
    a의 응답시간이 더 짧을 경우 True, 그렇지 않을 경우 False를 반환한다.
    """
    # 서버 a의 응답시간 측정
    response_a = ping(a)
    
    # 서버 a의 응답시간에 대한 예외를 확인하고 전달
    if response_a == -1:
        return '-1'
    if response_a == -2:
        return '-2'
    if response_a == -3:
        return '-3'
    
    # 서버 b의 응답시간 측정
    response_b = ping(b)
    
    # 서버 b의 응답시간에 대한 예외를 확인하고 전달
    if response_b == -1:
        return '-4'
    if response_b == -2:
        return '-5'
    if response_b == -3:
        return '-6'
    
    # 예외가 발생하지 않은 경우, 응답시간 비교하여 반환
    return response_a < response_b

compare_two_servers() 함수는 다른 함수에 의해 호출될 수 있으므로, 그 자신도 ping() 함수로부터 전달받은 결과가 예외가 아닌지 확인하여 반환해야 한다. 이미 ping() 함수에서 예외 검사를 마쳤음에도 또다시 예외가 있었는지 확인해야 하니 낭비다. 이 함수가 오류 코드를 반환할 수 있는 또 다른 함수를 호출한다면, 이 함수가 처리해야 할 오류 코드는 점점 더 불어날 것이다. 이 함수를 호출하는 상위 함수에서도 그 모든 오류 코드를 처리해야하니 문제는 끝이 없다.

이 외에도 예외 처리를 언제 어떻게 할 것인지에 관한 문제가 있다. 다음 두 지시를 비교해 보자.

  1. “냄비가 있는지 확인한다. 냄비가 있으면 냄비에 육수 1리터를 담고 센 불로 끓인다. 냄비가 없으면 주전자를 대신 사용한다.”
  2. “냄비에 육수 1리터를 담고 센 불로 끓인다. 냄비가 없을 경우 주전자를 대신 사용한다.”

둘다 냄비가 없는 상황을 상정한 조리법이지만, 접근법에는 차이가 있다. 첫번째는 앞에 지뢰가 있는지 조심스럽게 탐지한 후 다음 걸음을 내딛는 방식이다. 두번째는 지뢰밭이라 하더라도 일단 행군을 개시하고, 지뢰를 밟기 직전에 눈치채고 발걸음을 돌리는 방식이다. 예외가 일어나지 않는다면, 두번째 방법이 더 빠르게 목적지에 도달하는 방법일 것이다. 파이썬 문화에서는 두번째 예외 처리 방식을 권장한다. 하지만 if 문을 사용하는 방식은 첫번째 방식을 고집한다.

이상 if 문과 오류 코드를 사용한 예외 처리 방법과 그 세 가지 문제점을 소개했다. 이어지는 내용에서는 이 방법을 대체하는 파이썬의 예외 처리 전용 기능을 살펴본다.

오류 코드는 지금도 사용된다

오늘날에는 하나의 프로그램 안에서 오류를 전달하는 데는 오류 코드 대신 프로그래밍 언어에서 정의된 특별한 메시지(파이썬의 경우, 예외 객체)가 사용되는 경우가 대부분이다. 하지만 하나의 프로그램을 넘어 여러 프로그램들 사이에서 데이터를 주고 받을 때는 여전히 오류 코드가 활용된다. 수많은 프로그램들을 하나의 프로그래밍 언어 규칙으로 통일할 수 없기 때문이다.

9.3.4 try 문으로 예외 처리하기

파이썬에서 예외 처리를 올바르게 수행하려면 try 문을 사용해야 한다. try 문을 사용하면 다음과 같은 장점이 있다.

  1. 파편화된 오류 코드 대신 일반적인 예외 객체를 사용해 예외를 전달한다.
  2. 함수 호출 속에서 발생한 예외를 함수 밖에서 잡아낼 수 있다.
  3. 예외를 미리 검사하지 않고, 예외가 발생했을 때 처리한다.

이 세 장점은 모두 앞에서 살펴본 if 문의 한계를 해결한 것이다.

try 문과 except 절

try 문에는 except 절이 포함된다. ‘try’는 ‘시도하라’라는 뜻이다. try 문의 본문 블록에 예외 발생 가능성이 있는 코드를 기술하여, 코드를 일단 ‘시도해’ 보도록 지시한다. ‘except’는 ‘~를 제외하고’라는 뜻이다. except 절에는 처리할 예외의 종류와 그 처리 방법을 기술한다. 예외의 종류에 따라 그 대처법도 다를 것이므로, 하나의 try 문은 여러 개의 except 절을 포함할 수 있다. 하지만 하나의 예외도 처리하지 않는다면 try 문 자체가 필요 없을 것이므로, except 절이 최소한 하나는 있어야 한다. try 문의 표기법은 다음과 같다.

try:
    예외가 발생할 수 있는 명령의 코드 블록
    ...
except 예외종류:
    예외종류에 해당하는 예외가 발생했을 때의 대응 명령 코드 블록
    ...
(필요에 따라 except 절을 추가로 작성)

이 양식에서 ‘예외종류’란 예외를 나타내는 클래스다. 파이썬에는 다양한 예외 상황이 클래스로 미리 정의되어 있다. 발생한 예외가 어떤 예외인지 알기 위해서는 오류 메시지를 확인하면 된다. 예를 들어, 0으로 나누는 오류의 메시지를 대화식 셸로 확인해 보면 다음과 같은 오류 메시지를 얻을 수 있다.

코드 9-22 0으로 나누는 오류의 오류 메시지

>>> 1 / 0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero

오류 메시지의 마지막 행이 오류의 종류와 내용을 알려주는 행이다. 여기서 콜론(:) 왼쪽의 이름이 오류의 종류를 나타내는 클래스다. 즉, 0으로 나누는 오류는 파이썬에서 ZeroDivisionError로 정의되어 있다. 이러한 예외의 종류는 매우 다양하다. 9.3.5 예외의 종류에서 좀더 소개하기로 한다.

그러면 try 문을 이용해 0으로 나누는 오류와 사용자가 입력한 값을 정수로 변환할 수 없는 오류를 처리해 보자.

코드 9-23 0으로 나누는 오류의 오류 메시지

# 먼저, try 블록에 예외가 일어날 수 있는 코드를 기술한다.
try:
    print('0이 아닌 정수를 입력해 주세요:', end=' ')
    user_number = int(input())
    print(1 / user_number)
# 그 다음, 처리해야 할 예외의 이름과 처리방법을 except 블록에 기술한다.
except ZeroDivisionError:  # 0으로 나누는 오류 처리
    print('0으로 나눌 수 없습니다.')
except ValueError:         # int 유형이 될 수 없는 문자열의 오류 처리
    print('입력한 값은 정수가 아닙니다.')

if 문을 이용해 예외를 처리했던 코드 9-17에서는 사용자가 입력한 값이 정수로 변환할 수 있는 문자열인지, '0'은 아닌지 등을 직접 검사했다. 하지만 try 문을 활용한 코드 9-23에서는 그런 검사를 수행하지 않는다. try 문에서는 예외를 미리 검사하지 않는다. 일단 코드를 실행하고, 예외가 일어나지 않는다면 문제 없는 것이고, 예외가 일어났다면 해당 예외를 처리하는 except 절의 코드를 실행하는 것이다.

try 문을 사용할 때도, 사용자가 올바른 값을 입력할 때까지 계속 반복 입력받도록 할 수 있다. try 문 전체를 while 문으로 감싸고 정상 실행되었을 때 break 문으로 반복 블록을 빠져나오면 된다.

코드 9-24 올바른 입력을 반복하여 요구하기

while True:
    try:
        print('0이 아닌 정수를 입력해 주세요:', end=' ')
        user_number = int(input())
        print(1 / user_number)
        break   # 예외가 발생하지 않은 경우, 반복을 빠져나간다
    except ZeroDivisionError:
        print('0으로 나눌 수 없습니다.')
    except ValueError:
        print('입력한 값은 정수가 아닙니다.')

try 문의 본문을 실행하는 도중 예외가 발생한 경우에는 본문의 나머지 내용을 실행하지 않고, 그 예외를 처리할 수 있는 except 절을 실행한다. 따라서 코드 9-24에서 ZeroDivisionError, ValueError 예외가 발생한 경우에는 try 문 마지막의 break 문은 실행되지 않고, 계속해서 반복 입력받는다.

0이 아닌 정수를 입력해 주세요: 백
입력한 값은 정수가 아닙니다.
0이 아닌 정수를 입력해 주세요: 0
0으로 나눌 수 없습니다.
0이 아닌 정수를 입력해 주세요: 20
0.05

try 문의 else 절

try 문에는 모든 except 절을 작성한 뒤 그 아래에 추가로 else 절을 작성할 수 있다. else 절에 작성한 코드는 예외가 발생하지 않은 경우에만 try 절의 본문 코드에 이어서 실행된다. try 절의 작성 양식에 else 절을 추가해 두자.

try:
    예외가 발생할 수 있는 명령의 코드 블록
    ...
except 예외종류:
    예외종류에 해당하는 예외가 발생했을 때의 대응 명령 코드 블록
    ...
(필요에 따라 except 절을 추가로 작성)
else:
    예외가 발생하지 않은 경우 이어서 실행할 코드

else 절을 이용하면 코드 9-24를 다음과 같이 수정할 수 있다.

코드 9-25 try 문에서 else 절 사용하기

while True:
    try:
        print('0이 아닌 정수를 입력해 주세요:', end=' ')
        user_number = int(input())
        result = 1 / user_number
    except ZeroDivisionError:
        print('0으로 나눌 수 없습니다.')
    except ValueError:
        print('입력한 값은 정수가 아닙니다.')
    
    # 예외가 발생하지 않은 경우에만 실행
    else:
        print(result)  # 결과를 출력하고
        break          # 반복을 빠져나간다

else 절을 사용하는 것은 선택사항이다. 굳이 try 절의 본문이 아니라 else 절에 코드를 작성하는 이유는 의도하지 않은 예외 처리를 방지하기 위해서다. 예외 처리의 대상이 되는 코드와 이어서 수행될 코드를 명시적으로 구별한다는 장점도 있다.

처리되지 않은 예외의 전파

try 문은 except 절에 처리할 예외를 명시한다. 따라서 발생할 수 있는 모든 예외를 try 문 안에서 다 처리하지 않을 수도 있다. 처리되지 않은 예외가 발생한다면 어떤 일이 일어날까? 직접 확인해보자. 코드 9-25를 실행하고, 수를 입력하는 대신 Ctrl + C 키를 입력해 보자.

실행 결과 (Ctrl + C 입력):

0이 아닌 정수를 입력해 주세요: ^C
Traceback (most recent call last):
  File "example_9_24.py", line 4, in <module>
    user_number = int(input())
KeyboardInterrupt

파이썬 프로그램 실행중에 Ctrl + C 키를 입력하면 KeyboardInterrupt라는 예외가 발생한다. 코드 9-25의 try 문에는 이 예외를 처리하는 except 절이 없기 때문에, 예외는 처리되지 않은 채로 try 문을 빠져나와 버린다. 예외가 try 문을 빠져나왔을 때의 결과는 try 문을 사용하지 않았을 때와 같다. 즉, 처리되지 않은 예외로 인해 실행시간 오류가 발생하고, 오류 메시지가 출력된 후 프로그램 실행이 중단된다.

여기서 흥미로운 점은 try 문을 빠져나온 예외를 그 바깥쪽의 try 문으로 처리하는 것이 가능하다는 것이다.

코드 9-26 try 문을 빠져나온 예외 처리하기

# 블록 1: 바깥쪽 try 문
try:
    
    # 블록 2: while 문
    while True:
        
        # 블록 3: 안쪽 try 문
        try:
            print('0이 아닌 정수를 입력해 주세요:', end=' ')
            user_number = int(input())
            result = 1 / user_number
        except ZeroDivisionError:
            print('0으로 나눌 수 없습니다.')
        except ValueError:
            print('입력한 값은 정수가 아닙니다.')
        else:
            print(result)  # 결과를 출력하고
            break          # 반복을 빠져나간다

# 바깥쪽의 try 문에서 KeyboardInterrupt 예외를 처리한다
except KeyboardInterrupt:
    print('Ctrl + C를 누르셨군요.')

코드 9-26을 실행하고 Ctrl + C 키를 입력해보면 세번째 블록인 안쪽 try 문에서 예외가 발생했지만 try 문 블록과 while 문 블록을 빠져나가 바깥쪽 try 문에서 처리되는 것을 확인할 수 있다.

실행 결과 (Ctrl + C 입력):

0이 아닌 정수를 입력해 주세요: ^C
Ctrl + C를 누르셨군요.

예외가 깊숙한 코드 블록 안에서 발생할 경우, except 절에 의해 처리되거나 또는 끝까지 처리되지 않아 오류가 일어날 때까지 코드 블록을 차례대로 빠져나간다. 이 특징을 이용하면 함수 호출 안에서 일어난 예외를 함수 호출 밖에서 처리할 수 있다.

코드 9-27 함수 연쇄 호출 속의 오류

def a(x):
    return 8 / x      # x가 0인 경우 오류 발생

def b(y):
    return a(y - 1)   # y가 1인 경우 오류 발생

def c(z):
    return b(z - 2)   # z가 3인 경우 오류 발생

def d():
    print(c(int(input())))

d()

위 코드는 사용자가 무엇을 입력하느냐에 따라 정상 실행될 수도, 오류를 일으킬 수도 있다. 그런데 예외 처리를 어디에서 해야 할까? 8 / 0이 실행될 수 있는 a() 함수? a()에 잘못된 값을 전달할 가능성이 있는 b(), c() 두 함수? 사용자로부터 값을 입력받는 d() 함수? 모든 코드가 예외를 일으킬 가능성이 있다. 따라서 다음과 같이 예외 처리할 수 있다.

코드 9-28 함수 연쇄 호출 속의 오류 처리하기 (나쁜 방법)

def a(x):
    try:
        return 8 / x      # x가 0인 경우 오류 발생
    except ZeroDivisionError:
        print('0으로는 나눌 수 없습니다.')

def b(y):
    try:
        return a(y - 1)   # y가 1인 경우 오류 발생
    except ZeroDivisionError:
        print('0으로는 나눌 수 없습니다.')

def c(z):
    try:
        return b(z - 2)   # z가 3인 경우 오류 발생
    except ZeroDivisionError:
        print('0으로는 나눌 수 없습니다.')

def d():
    try:
        print(c(int(input())))
    except ZeroDivisionError:
        print('0으로는 나눌 수 없습니다.')

try:
    d()
except ZeroDivisionError:
    print('0으로는 나눌 수 없습니다.')

위 코드와 같이 모든 함수에서 예외를 처리할 필요는 없다. try 문을 사용하면 안쪽에서 발생한 오류를 바깥에서 처리할 수 있다. 예외가 실행 흐름을 따라 밖으로 빠져나오는 경우, 그 사이의 한 지점에서 한 번만 예외를 처리해주면 된다.

코드 9-29 함수 연쇄 호출 속의 오류 처리하기

def a(x):
    return 8 / x      # x가 0인 경우 오류 발생

def b(y):
    return a(y - 1)   # y가 1인 경우 오류 발생

def c(z):
    return b(z - 2)   # z가 3인 경우 오류 발생

def d():
    try:
        print(c(int(input())))
    except ZeroDivisionError:
        print('0으로는 나눌 수 없습니다.')

d()

위 코드는 d() 함수에서만 0으로 나누는 오류를 처리했다. 하지만 a()에서 오류가 발생하더라도 그 예외가 실행 흐름을 따라 빠져나와 결국 d() 함수에서 처리되므로 문제 없다. 단, a(8)과 같이 a() 함수를 직접 호출한다면 예외 처리가 되지 않을 것이다. 예외 처리를 할 때는 이처럼 다양한 상황과 프로그램 전체의 맥락을 고려할 필요가 있다.

try 문을 사용하는 방식을 if 문을 사용할 때와 비교해 보자. if 문을 사용할 때는 예외가 일어날 상황을 직접 예측해 검사해야 했으며, 그 사실을 함수 밖으로 전달하기 위해 return 문과 오류 코드에 의존해야 했다. try 문을 사용할 때는 예외가 발생할 것을 미리 검사하지 않고, 예외가 발생하여 코드 블록을 빠져나왔을 때 처리한다. 처리되지 않은 예외는 자동으로 바깥쪽 실행 블록으로 전달되므로, 함수 안에서 return 문을 오류 코드를 밖으로 전달하는 작업이 필요 없다.

이상으로 try 문으로 예외 처리를 하는 이유와 방법을 알아보았다. 실행시간 오류가 일어날 가능성이 있거나 일어난 경우에는 try 문으로 예외 처리하는 것을 기억하자.

연습문제

연습문제 9-4 파일 읽기 프로그램 2

연습문제 9-3에서 살펴본 파일 읽기 프로그램을 try 문을 이용해 수정해라. 사용자가 입력한 파일 이름과 일치하는 파일이 없을 때, 오류를 일으키지 말고 다음과 같이 적절한 안내를 하도록 한다.

파일 이름을 입력하시오: some-file.txt
파일이 존재하지 않아 읽을 수 없습니다.