프로그래밍

예외 처리

잘 작성한 코드도 예상 밖의 값을 만나면 멈춥니다.
그대로 멈추게 둘지, 오류를 잡아 실행을 이어 갈지 미리 정해 둡니다.

학습 목표

  1. 문법 오류 · 실행 오류 · 논리 오류를 구별할 수 있다.
  2. try·except로 오류를 잡아 실행을 이어 갈 수 있다.
  3. 오류의 종류마다 다르게 처리할 수 있다.
  4. else와 finally가 언제 실행되는지 설명하고, raise로 예외를 직접 발생시킬 수 있다.
  5. 잡을 오류를 좁게 지정해야 하는 이유를 말하고, 잡을 오류와 그대로 둘 오류를 구별할 수 있다.

멈추는 오류

잘 만든 프로그램도 사용자가 입력한 값 하나 때문에 멈춥니다.

나이를 입력받는 프로그램입니다. 사용자가 「스물」이라고 입력하면 어떻게 될까요?

print("시작")

age = int(input("나이: "))      # 「스물」을 입력하면 여기서 멈춘다
print("입력받은 나이:", age)

print("끝")                     # 멈추면 이 줄은 아예 실행되지 않는다

# 입력이 「스물」일 때
# 시작
# 나이: 스물
# ValueError: invalid literal for int() with base 10: '스물'
잡지 않으면 「거기서」 끝난다 print("시작") age = int(input()) print("끝") 여기서 멈춘다 아래는 실행되지 않는다 잡으면 「건너뛰고」 이어 간다 try: age = int(input()) except ValueError: print("숫자로") print("끝") 여기까지 온다

"스물"은 정수로 변환할 수 없는 문자열입니다. 파이썬은 그 줄에서 실행을 멈추고 오류 메시지를 출력합니다. 그 뒤의 줄은 아예 실행되지 않습니다.

코드가 틀린 것이 아닙니다

int(input())은 잘못 쓴 코드가 아닙니다. 사용자가 입력한 값이 예상 밖이었을 뿐입니다. 이런 상황은 코드를 아무리 잘 작성해도 없앨 수 없습니다. 그러므로 이런 상황에서 무엇을 할지 미리 정해 두는 방법을 이번 시간에 배웁니다.

프로그램을 멈추게 하는 오류는 두 종류이고, 이번에 다루는 것은 그중 하나입니다. 멈추지 않는 오류까지 더하면 오류는 모두 세 종류입니다.

실행 「전에」 발견되는가, 실행 중에 발생하는가 문법 오류 실행 전 SyntaxError 한 줄도 실행되지 않는다 실행 오류 실행 중 ValueError 그 줄까지는 실행된 뒤 멈춘다 try로 잡을 수 있는 것은 「실행 오류」뿐이다
# 1) 문법 오류 — 실행되기 「전에」 발견된다
# if x > 3
#     print("큼")          # SyntaxError. 콜론이 없다

# 2) 실행 오류 — 실행되다가 「그 줄에서」 멈춘다
nums = [1, 2, 3]
print(nums[5])              # IndexError: list index out of range

예외(exception)

프로그램이 실행되는 중에 발생하는 오류로, 처리하지 않으면 프로그램이 그 줄에서 멈춥니다. 콜론을 빠뜨린 것 같은 문법 오류는 try로 잡을 수 없습니다. 실행이 시작되기도 전에 발견되므로 try가 실행될 기회조차 없기 때문입니다.

멈추지 않는 세 번째 오류

프로그램이 끝까지 멈추지 않고 실행되었는데 결과만 틀린 경우가 있습니다. 평균을 구하면서 개수로 나누지 않고 합만 반환한 코드가 그렇습니다. 이것을 논리 오류라고 합니다. 파이썬은 코드에 쓴 대로 실행했을 뿐이므로 아무것도 알려 주지 않고, 예외도 발생하지 않으므로 try로 잡을 수도 없습니다. 정답을 미리 알고 있는 값을 넣어 실행 결과와 비교해 보아야 찾을 수 있습니다.

스스로 확인

print(nums[5])는 문법이 틀린 곳이 없는데도 멈춥니다. 콜론을 빠뜨린 코드와 이 코드는 멈추는 시점이 어떻게 다를까요?

잡아서 이어 가기

멈추는 대신 무엇을 할지 코드로 써 둡니다.

오류가 날 수 있는 코드를 try와 except로 감싸면 예외가 발생해도 프로그램이 멈추지 않고 except 안의 코드를 실행합니다. 이처럼 예외가 발생했을 때 할 일을 미리 정해 두는 것을 예외 처리(exception handling)라고 합니다.

try:
    age = int(input("나이: "))
    print("입력받은 나이:", age)
except ValueError:
    print("숫자로 입력해 주세요")

print("끝")                 # 예외를 잡아 처리했으므로 이 줄도 실행된다

# 입력이 「스물」일 때
# 나이: 스물
# 숫자로 입력해 주세요
# 끝
오류가 났을 때와 나지 않았을 때 try 안의 줄 ? 났다 나지 않았다 except 안으로 else 안으로 잡아서 처리한다 except는 건너뛴다 그 뒤 줄로 이어 간다
위치 언제 실행되나
try: 안 언제나 실행을 시작한다. 오류가 난 줄에서 실행을 중단하고 남은 줄은 건너뛴다
except X: 안 try 안에서 X 오류가 났을 때만
else: 안 try 안이 오류 없이 끝났을 때만
그 뒤의 줄 오류를 잡았든 오류가 없었든 이어서

try 안은 짧게 씁니다

try 안에 열 줄을 넣으면 그중 어느 줄에서 오류가 났는지 알기 어렵습니다. 오류가 날 수 있는 줄만 감싸는 것이 좋습니다.

오류의 종류마다 다르게 처리할 수 있습니다.

nums = [10, 0, 30]

try:
    i = int(input("인덱스: "))
    print(100 / nums[i])
except ValueError:
    print("숫자로 입력해 주세요")
except IndexError:
    print("없는 인덱스입니다. 0부터 2까지 입력해 주세요")
except ZeroDivisionError:
    print("그 인덱스의 값이 0이라 나눌 수 없습니다")

셋을 입력하면 ValueError, 5를 입력하면 IndexError를 처리하는 except로 갑니다. 1을 입력하면 인덱스 1의 값이 0이므로 ZeroDivisionError를 처리하는 except로 갑니다. 0을 입력하면 10.0이 출력됩니다. -1처럼 음수를 입력하면 뒤에서부터 세는 인덱스로 해석되어 오류 없이 3.3333333333333335가 출력되고, -4처럼 그 범위까지 벗어나야 IndexError가 납니다.

예외가 발생하면 파이썬은 except를 위에서부터 차례로 살펴 예외의 종류와 비교합니다. 종류가 맞는 except를 만나면 그 하나만 실행하고 나머지는 건너뜁니다. 맞는 것이 하나도 없으면 예외를 처리하지 않은 것과 같아서 그 줄에서 멈춥니다.

예외 이름 언제 발생하나
ValueError 자료형은 맞지만 값이 알맞지 않을 때 — int("스물")
IndexError 리스트에 없는 인덱스를 꺼낼 때
KeyError 딕셔너리에 없는 키를 꺼낼 때
ZeroDivisionError 0으로 나눌 때
FileNotFoundError 없는 파일을 열려고 할 때
TypeError 그 자료형에 쓸 수 없는 연산을 할 때

반복문과 함께 쓰면 올바른 값을 입력할 때까지 다시 입력받는 프로그램이 됩니다.

while True:
    try:
        age = int(input("나이: "))
        break               # 여기까지 실행됐다면 숫자가 제대로 입력된 것이다
    except ValueError:
        print("숫자로 입력해 주세요")

print("나이는", age)

break는 오류가 나지 않았을 때만 실행됩니다. 오류가 나면 바로 except로 넘어가므로 break는 실행되지 않고 반복을 한 번 더 합니다.

스스로 확인

try 안에서 break를 except 뒤로 옮기면 어떻게 될까요?

좁게 잡기

어떤 오류를 잡을지 쓰지 않으면 모든 오류가 잡힙니다.

except:라고만 쓰면 모든 오류가 잡힙니다. 편해 보이지만 위험합니다.

그물을 넓게 치면 고칠 곳을 못 찾는다 좁게 잡기 except ValueError: 나머지 둘은 그대로 멈춘다 — 고칠 곳이 보인다 Value 잡힘 Index 멈춤 Name 멈춤 넓게 잡기 except: 오타로 난 NameError까지 삼킨다 Value 잡힘 Index 잡힘 Name 잡힘
nums = [10, 20, 30]

# 넓게 잡으면 「무엇이 잘못됐는지」가 사라진다
try:
    i = int(input("인덱스: "))
    print(100 / nums[i])
except:
    print("오류가 났습니다")       # 어느 오류인지 알 수 없다

# 오타로 난 NameError까지 여기서 잡힌다. 고칠 곳을 찾을 수 없게 된다

고칠 곳을 감춥니다

nums를 nusm으로 잘못 쓰면 NameError가 납니다. 이것은 사용자의 잘못이 아니라 코드를 작성한 내 잘못이고, 프로그램이 멈춰서 알려 주어야 고칠 수 있습니다. 모든 오류를 잡으면 「오류가 났습니다」만 출력되어 어디를 고쳐야 할지 알 수 없게 됩니다.

잡을 오류를 고르는 기준

「미리 예상할 수 있고, 발생했을 때 무엇을 할지 정해 둔 오류」만 잡습니다. 그 밖의 오류는 멈추게 두는 편이 낫습니다. 프로그램이 멈추는 것은 실패가 아니라 고칠 곳을 알려 주는 신호입니다.

파일을 다룰 때가 예외 처리를 쓰는 대표적인 경우입니다. 파일이 있는지 없는지는 프로그램이 정할 수 없기 때문입니다.

try:
    with open("성적.csv", "r", encoding="utf-8") as f:
        print(f.read())
except FileNotFoundError:
    print("성적.csv를 찾을 수 없습니다")
def read_scores(path):
    """파일에서 이름과 점수를 읽어 딕셔너리로 반환한다."""
    table = {}
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            values = line.strip().split(",")
            table[values[0]] = int(values[1])
    return table


try:
    scores = read_scores("성적.csv")
except FileNotFoundError:
    print("성적.csv가 없습니다. 파일을 먼저 만들어 주세요")
except ValueError:
    print("점수 열에 숫자가 아닌 값이 있습니다")
else:
    for name, score in scores.items():
        print(f"{name}: {score}")
# 학생 A: 90
# 학생 B: 85

파일이 없을 때와 점수가 숫자가 아닐 때를 따로 잡아 각각 무엇이 잘못됐는지 알려 줍니다. 오류가 나지 않았을 때만 할 일은 else에 두어 try 안을 짧게 유지했습니다.

스스로 확인

파일을 여는 첫 번째 코드에서 except FileNotFoundError를 except:로 바꾸면, f.read()를 f.raed()로 잘못 쓴 오타도 「성적.csv를 찾을 수 없습니다」를 출력합니다. 왜 그것이 문제일까요?

finally와 raise

오류가 나든 나지 않든 할 일은 어디에 두고, 파이썬이 알아채지 못하는 오류는 어떻게 알릴까요?

try 문의 끝에 finally:를 붙이면 그 안의 코드는 try 안에서 예외가 발생했든 발생하지 않았든 마지막에 반드시 실행됩니다. 결과와 상관없이 해야 하는 마무리를 여기에 둡니다.

def divide(a, b):
    try:
        result = a / b
    except ZeroDivisionError:
        print("0으로 나눌 수 없습니다")
        return None
    else:
        return result
    finally:
        print("나누기를 마칩니다")    # return보다 먼저, 어느 경우에나 실행된다


print(divide(10, 4))
print(divide(10, 0))

# 나누기를 마칩니다
# 2.5
# 0으로 나눌 수 없습니다
# 나누기를 마칩니다
# None

divide(10, 4)는 예외 없이 else 안의 return result에 이릅니다. 그런데 2.5보다 「나누기를 마칩니다」가 먼저 출력됩니다. 함수가 값을 반환하고 빠져나가기 직전에 finally 안이 실행되기 때문입니다. divide(10, 0)은 except 안에서 안내 문구를 출력한 뒤 return None에 이르고, 이때도 반환하기 전에 finally 안이 실행됩니다.

경우 실행 순서
예외가 나지 않았을 때 try 안 → else 안 → finally 안 → 그 뒤의 줄
except에 쓴 종류의 예외가 났을 때 try 안(오류가 난 줄까지) → 그 except 안 → finally 안 → 그 뒤의 줄
except에 쓰지 않은 종류의 예외가 났을 때 try 안(오류가 난 줄까지) → finally 안 → 멈춘다. 그 뒤의 줄은 실행되지 않는다
return으로 함수를 빠져나갈 때 반환하기 직전에 finally 안

else와 finally는 다릅니다

else 안은 try 안이 오류 없이 끝났을 때만 실행되고, finally 안은 오류가 났을 때도 실행됩니다. 오류가 없을 때만 할 일은 else에, 어느 경우에나 할 일은 finally에 둡니다.

지금까지 본 예외는 모두 파이썬이 발생시켰습니다. raise 뒤에 예외의 종류와 메시지를 쓰면 프로그래머가 직접 예외를 발생시킬 수 있습니다.

def check_age(age):
    if age < 0:
        raise ValueError("나이는 0 이상이어야 합니다")
    return age                  # raise가 실행되면 이 줄은 실행되지 않는다


for text in ["17", "-3", "스물"]:
    try:
        age = check_age(int(text))
    except ValueError as e:
        print(f"{text}: {e}")
    else:
        print(f"{text}: 나이 {age}")

# 17: 나이 17
# -3: 나이는 0 이상이어야 합니다
# 스물: invalid literal for int() with base 10: '스물'

"-3"은 정수로 변환할 수 있으므로 int("-3")은 오류를 내지 않습니다. 하지만 나이로는 알맞지 않은 값이고, 이것은 파이썬이 아니라 이 프로그램만 판단할 수 있습니다. 그래서 check_age가 raise ValueError(...)로 직접 예외를 발생시킵니다. 자료형은 맞지만 값이 알맞지 않은 경우이므로 ValueError를 골랐습니다.

raise가 실행되면 함수는 그 줄에서 실행을 멈추고 return age는 실행되지 않습니다. 발생한 예외는 함수를 호출한 곳으로 전달되어, 파이썬이 발생시킨 예외와 똑같이 except에서 처리됩니다. 「스물」에서 int가 발생시킨 ValueError와 -3에서 check_age가 발생시킨 ValueError를 except 하나가 함께 처리하는 것도 이 때문입니다. 처리하는 곳이 없으면 프로그램이 멈춥니다.

except ValueError as e:처럼 as 뒤에 변수 이름을 쓰면 발생한 예외가 그 변수에 저장됩니다. 그 변수를 출력하면 raise에 쓴 메시지가 나오고, 파이썬이 발생시킨 예외라면 파이썬이 정한 영어 메시지가 나옵니다.

함수 안에서 출력하지 않고 예외를 발생시키는 이유

check_age가 안내 문구만 출력하고 끝나면, 호출한 곳에서는 값이 잘못됐다는 것을 알 수 없어 다음 줄을 그대로 실행합니다. 예외를 발생시키면 그 상황에서 무엇을 할지를 호출한 곳에서 정할 수 있습니다. 다시 입력받을 수도 있고, 처리하지 않고 멈추게 둘 수도 있습니다.

스스로 확인

while True: 안의 try 문에 finally: print("확인")을 붙였습니다. 올바른 값이 입력되어 try 안의 break로 반복을 빠져나갈 때도 「확인」이 출력될까요?

핵심 정리

멈추는 것은 코드가 틀려서만이 아니다

int("스물")은 잘 작성한 코드가 예상 밖의 값을 만난 것이다. 이런 상황은 없앨 수 없으므로 발생했을 때 무엇을 할지 정해 둔다. 음수 나이처럼 파이썬은 알아채지 못하지만 알맞지 않은 값은 raise로 직접 예외를 발생시킨다.

잡을 수 있는 것은 실행 오류뿐

콜론을 빠뜨린 문법 오류는 실행 전에 발견되므로 try로 잡을 수 없다. 멈추지 않고 결과만 틀린 논리 오류도 예외가 발생하지 않으므로 잡히지 않는다.

갈림길이 생긴다

try 안에서 오류가 나면 except로, 나지 않으면 else로 간다. 어느 쪽으로 갔든 그 뒤의 줄로 이어진다. except에 쓰지 않은 종류의 오류가 나면 그대로 멈춘다. finally 안은 이 세 경우 모두, return으로 빠져나갈 때도 try 문을 벗어나기 직전에 실행된다.

try 안은 짧게

열 줄을 넣으면 어느 줄에서 오류가 났는지 알기 어렵다. 오류가 날 수 있는 줄만 감싼다.

좁게 잡는다

except:는 내 오타로 난 오류까지 삼켜 고칠 곳을 감춘다. 발생했을 때 할 일을 정해 둔 오류만 잡고 나머지는 멈추게 둔다.

확인 퀴즈

배운 내용을 확인해 봅시다.

1. try로 잡을 수 없는 것은?

2. try 안에서 오류가 나지 않았을 때만 실행되는 부분은?

3. except:라고만 쓰는 것이 위험한 이유는?