25. 예외를 우아하게 처리하기
24장에서는 예외(exception)가 발생했을 때 그것을 읽고 이해하는 방법을 배웠습니다. 이제는 예외를 우아하게 처리(handle) 하는 방법을 배워, 프로그램이 크래시 나지 않고 오류에서 복구할 수 있도록 하겠습니다. 이는 예기치 못한 상황을 처리할 수 있는 견고하고 사용자 친화적인 프로그램을 작성하는 데 필수적입니다.
Python에서 예외가 발생하면 프로그램의 정상적인 흐름은 즉시 멈춥니다. 그런데 그 예외가 프로그램을 크래시 내기 전에 잡을 수 있다면 어떨까요? 오류에 대응해서 사용자에게 다시 시도하게 하거나, 기본값을 사용하거나, 문제를 로그로 남기고 계속 진행할 수 있다면 어떨까요? 바로 이것이 예외 처리(exception handling)가 가능하게 해주는 일입니다.
25.1) try와 except 블록 사용하기
25.1.1) try와 except의 기본 구조
try-except 블록은 Python에서 “이걸 시도해 보고, 예외가 발생하면 대신 이걸 하라”고 말하는 방식입니다. 기본 구조는 다음과 같습니다:
try:
# 예외를 발생시킬 수 있는 코드
risky_operation()
except:
# 어떤 예외든 발생하면 실행되는 코드
print("Something went wrong!")try 블록에는 예외를 발생시킬 수 있는 코드가 들어갑니다. try 블록 어디에서든 예외가 발생하면, Python은 즉시 try 블록 실행을 중단하고 except 블록으로 이동합니다. 예외가 발생하지 않으면 except 블록은 완전히 건너뜁니다.
구체적인 예를 봅시다. 24장에서, 올바르지 않은 문자열을 정수로 변환하려 하면 ValueError가 발생한다는 것을 기억하시죠:
# 예외 처리 없이 - 프로그램이 크래시 남
user_input = "hello"
number = int(user_input) # ValueError: invalid literal for int() with base 10: 'hello'
print("This line never executes")이제 이 예외를 우아하게 처리해 봅시다:
# 예외 처리 포함 - 프로그램이 계속 진행됨
user_input = "hello"
try:
number = int(user_input)
print(f"You entered: {number}")
except:
print("That's not a valid number!")
number = 0 # 기본값 사용
print(f"Using number: {number}")Output:
That's not a valid number!
Using number: 0프로그램이 크래시 나지 않았습니다! int(user_input)가 ValueError를 발생시키자, Python은 except 블록으로 이동했고, 오류 메시지를 출력하고, 기본값을 설정한 뒤, 프로그램의 나머지 부분을 계속 실행했습니다.
다음은 단계별로 일어나는 일입니다:
"점프" 이해하기 - 실제로 무슨 일이 일어날까
Python이 except 블록으로 “점프(jump)”한다고 말할 때, 이는 정상적인 순차 실행을 포기(abandon) 한다는 뜻입니다. 이는 if 문 같은 단순한 분기만이 아니라, 프로그램 흐름이 근본적으로 바뀌는 것입니다. 이를 구체적인 예제로 자세히 살펴봅시다:
# 예외로 실행 흐름을 관찰하기
print("1. Starting program")
try:
print("2. Entered try block")
number = int("hello") # 예외가 여기서 발생함
print("3. After conversion") # 이 줄은 절대 실행되지 않음
result = number * 2 # 이 줄은 절대 실행되지 않음
print("4. After calculation") # 이 줄은 절대 실행되지 않음
except ValueError:
print("5. In except block - handling the error")
print("6. After try-except block")Output:
1. Starting program
2. Entered try block
5. In except block - handling the error
6. After try-except block3번 4번 줄이 절대 실행되지 않는다는 점에 주목하세요! int("hello")가 ValueError를 발생시키는 순간, Python은:
- 예외가 발생한 줄에서 즉시 try 블록 실행을 중단합니다
- 이 예외 타입을 처리할 수 있는 매칭되는 except 절을 찾습니다
- try 블록에 남아 있는 코드를 모두 건너뛰고, 해당 except 블록으로 점프합니다
- except 블록 실행이 끝나면 try-except 구조 이후로 계속 진행합니다
이는 정상적인 프로그램 흐름과는 근본적으로 다릅니다. 정상 실행에서는 Python이 각 줄을 순차적으로 실행합니다. 예외가 발생하면, Python은 현재 경로를 포기하고 완전히 다른 경로로 이동합니다. 예외 처리가 없다면, 프로그램은 2번 줄에서 크래시 나고 종료됩니다. 예외 처리가 있으면, 프로그램은 복구하고 계속됩니다.
왜 이것이 중요한가:
이 “점프” 동작을 이해하는 것이 중요한 이유는 다음과 같습니다:
- try 블록에서 예외 이후의 코드는 모두 건너뛰어집니다 — try 블록의 뒤쪽 줄들이 실행됐다고 가정할 수 없습니다
- 예외가 대입 전에 발생하면 변수가 초기화되지 않을 수 있습니다
- except 블록이 실행될 때 프로그램이 어떤 상태인지 계획해야 합니다
25.1.2) 사용자 입력을 안전하게 처리하기
예외 처리(exception handling)의 가장 흔한 사용 사례 중 하나는 사용자 입력을 검증하는 것입니다. 사용자는 무엇이든 입력할 수 있으므로, 우리는 잘못된 입력을 우아하게 처리해야 합니다. 다음은 사용자의 나이를 묻는 프로그램의 실용적인 예입니다:
# 예외 처리를 사용한 안전한 나이 입력
print("Please enter your age:")
user_input = input()
try:
age = int(user_input)
print(f"You are {age} years old.")
# 출생 연도 계산(현재 연도가 2024라고 가정)
birth_year = 2024 - age
print(f"You were born around {birth_year}.")
except:
print("Invalid input! Age must be a number.")
print("Using default age of 0.")
age = 0사용자가 "25"를 입력하면 출력은 다음과 같습니다:
Please enter your age:
25
You are 25 years old.
You were born around 1999.사용자가 "twenty-five"를 입력하면 출력은 다음과 같습니다:
Please enter your age:
twenty-five
Invalid input! Age must be a number.
Using default age of 0.트레이스백(traceback)과 함께 크래시 나는 대신, 프로그램이 오류를 우아하게 처리하는 것을 볼 수 있습니다. 이는 사용자 경험 측면에서 훨씬 더 좋습니다.
25.1.3) try 블록에서 여러 작업 처리하기
하나의 try 블록에 여러 작업을 넣을 수 있습니다. 그중 어느 하나라도 예외를 발생시키면, Python은 즉시 except 블록으로 점프합니다. 간단한 예제로 시작해 봅시다:
# try 블록에 두 개의 작업
print("Enter a number:")
user_input = input()
try:
number = int(user_input) # 첫 번째 작업 - ValueError를 발생시킬 수 있음
result = 100 / number # 두 번째 작업 - ZeroDivisionError를 발생시킬 수 있음
print(f"100 / {number} = {result}")
except:
print("Something went wrong!")사용자가 "hello"를 입력하면 첫 번째 작업(변환)에서 예외가 발생합니다. 사용자가 "0"을 입력하면 두 번째 작업(나눗셈)에서 예외가 발생합니다. 어느 쪽이든, 하나의 except 블록이 이를 잡습니다.
이제 이를 세 가지 작업으로 확장해 봅시다:
# try 블록에 여러 작업
print("Enter two numbers to divide:")
numerator_input = input("Numerator: ")
denominator_input = input("Denominator: ")
try:
numerator = int(numerator_input) # ValueError를 발생시킬 수 있음
denominator = int(denominator_input) # ValueError를 발생시킬 수 있음
result = numerator / denominator # ZeroDivisionError를 발생시킬 수 있음
print(f"{numerator} / {denominator} = {result}")
except:
print("Something went wrong with the calculation!")
print("Make sure you enter valid numbers and don't divide by zero.")사용자가 "10"과 "2"를 입력하면:
Enter two numbers to divide:
Numerator: 10
Denominator: 2
10 / 2 = 5.0사용자가 "10"과 "zero"를 입력하면:
Enter two numbers to divide:
Numerator: 10
Denominator: zero
Something went wrong with the calculation!
Make sure you enter valid numbers and don't divide by zero.사용자가 "10"과 "0"을 입력하면:
Enter two numbers to divide:
Numerator: 10
Denominator: 0
Something went wrong with the calculation!
Make sure you enter valid numbers and don't divide by zero.이 예제에서는 세 가지 서로 다른 문제가 발생할 수 있습니다. 분자 변환이 실패할 수도 있고, 분모 변환이 실패할 수도 있고, 분모가 0이라 나눗셈이 실패할 수도 있습니다. 하나의 except 블록이 이 모든 경우를 처리합니다. 하지만 이 접근에는 한계가 있습니다. 어떤 특정 오류가 발생했는지 알 수 없습니다. 다음 절에서 이를 다루겠습니다.
25.1.4) bare except 절의 문제점
예외 타입을 지정하지 않고 except:를 사용하는 것을 bare except 절(bare except clause) 이라고 합니다. 이는 모든 예외를 잡지만, 범위가 너무 넓어서 예상치 못한 문제를 숨길 수 있습니다. 다음 예제를 보세요:
# bare except는 모든 것을 잡음 - 우리가 예상하지 못한 것까지
numbers = [10, 20, 30]
try:
index = 5 # index가 범위를 벗어나면 IndexError를 기대함
value = numbers[index]
print(f"Value at index {index}: {value}")
except:
print("Could not access the list element.")리스트 요소가 없을 수 있으니 접근을 시도하는 것은 합리적으로 보입니다. 그런데 코드에 오타가 있다면 어떨까요?
# 코드에 오타가 있다면?
numbers = [10, 20, 30]
try:
index = 2
value = numbrs[index] # 오타: 'numbers'가 아니라 'numbrs'
print(f"Value at index {index}: {value}")
except:
print("Could not access the list element.")Output:
Could not access the list element.bare except가 오타로 인한 NameError까지 잡아 “Could not access the list element”를 출력해 버립니다. 즉, 무엇이 잘못됐는지에 대한 잘못된 정보를 주게 됩니다! 우리는 인덱스가 범위를 벗어났다고 생각하지만, 실제로는 변수 이름에 오타가 있습니다.
bare except는 KeyboardInterrupt(사용자가 Ctrl+C를 누를 때)와 SystemExit(프로그램이 종료하려 할 때)도 잡는데, 보통 이런 것들은 잡지 않는 편이 좋습니다. 이런 이유로, 다음에 배울 것처럼 특정 예외를 잡는 것이 더 낫습니다.
25.2) 특정 예외 잡기
25.2.1) 예외 타입 지정하기
bare except로 모든 예외를 잡는 대신, 우리가 처리하고 싶은 예외 타입을 지정할 수 있습니다. 이렇게 하면 코드가 더 정확해지고, 서로 다른 오류에 적절하게 대응할 수 있습니다:
# 특정 예외 타입을 잡기
user_input = "hello"
try:
number = int(user_input)
print(f"You entered: {number}")
except ValueError:
print("That's not a valid number!")
number = 0
print(f"Using number: {number}")Output:
That's not a valid number!
Using number: 0이제 except 절은 ValueError 예외만 잡습니다. 다른 타입의 예외(예: 오타로 인한 NameError)가 발생하면 잡히지 않고 전체 트레이스백이 출력되는데, 이는 디버깅(debugging)에 실제로 도움이 됩니다!
문법은 except ExceptionType:이며, ExceptionType은 잡고 싶은 예외 클래스의 이름입니다(예: ValueError, TypeError, ZeroDivisionError 등).
흔한 실수: 잘못된 예외 타입을 잡기
실제로 발생한 예외와 일치하지 않는 예외 타입을 지정하면 어떻게 될까요? 확인해 봅시다:
# 잘못된 예외 타입을 잡기
user_input = "hello"
try:
number = int(user_input) # 이것은 ValueError를 발생시킴
print(f"You entered: {number}")
except TypeError: # 하지만 TypeError를 잡고 있음!
print("That's not a valid number!")
number = 0
print(f"Using number: {number}")Output:
Traceback (most recent call last):
File "example.py", line 4, in <module>
number = int(user_input)
ValueError: invalid literal for int() with base 10: 'hello'프로그램이 크래시 났습니다! 왜일까요? int("hello")는 ValueError를 발생시키지만, except 절은 TypeError만 잡기 때문입니다. 매칭되는 except 절이 없으므로 예외는 잡히지 않고 프로그램이 종료됩니다.
이는 개발 중에는 오히려 도움이 됩니다. 잘못된 예외 타입을 잡고 있으면 전체 트레이스백이 보이고 실수를 깨닫게 되기 때문입니다. 이것이 bare except보다 특정 예외를 잡는 것이 더 좋은 이유 중 하나입니다.
이 실수를 피하는 방법:
- 트레이스백을 읽어서 실제로 어떤 예외 타입이 발생했는지 확인합니다
- except 절에 그 특정 예외 타입을 사용합니다
- 확신이 없으면 코드를 실행해 크래시 나게 두세요 — 트레이스백이 알려줍니다!
25.2.2) 서로 다른 예외를 서로 다르게 처리하기
여러 개의 except 절을 두어 서로 다른 예외 타입을 서로 다른 방식으로 처리할 수 있습니다. 이는 서로 다른 오류가 서로 다른 대응을 필요로 할 때 매우 유용합니다:
# 예외 종류별로 다르게 처리하기
print("Enter two numbers to divide:")
numerator_input = input("Numerator: ")
denominator_input = input("Denominator: ")
try:
numerator = int(numerator_input)
denominator = int(denominator_input)
result = numerator / denominator
print(f"{numerator} / {denominator} = {result}")
except ValueError:
print("Error: Both inputs must be valid integers.")
print("You entered something that isn't a number.")
except ZeroDivisionError:
print("Error: Cannot divide by zero.")
print("The denominator must be a non-zero number.")사용자가 "10"과 "abc"를 입력하면:
Enter two numbers to divide:
Numerator: 10
Denominator: abc
Error: Both inputs must be valid integers.
You entered something that isn't a number.사용자가 "10"과 "0"을 입력하면:
Enter two numbers to divide:
Numerator: 10
Denominator: 0
Error: Cannot divide by zero.
The denominator must be a non-zero number.Python은 각 except 절을 순서대로 확인합니다. 예외가 발생하면, 예외 타입과 매칭되는 첫 번째 except 절을 찾아 그 블록을 실행합니다. 다른 except 절들은 건너뜁니다.
25.2.3) 하나의 절에서 여러 예외 타입 잡기
여러 예외 타입을 같은 방식으로 처리하고 싶은 경우가 있습니다. 동일한 except 블록을 여러 번 쓰는 대신, 괄호 안에 튜플 형태로 예외 타입을 나열해 하나의 절에서 여러 예외를 잡을 수 있습니다:
# 여러 예외 타입을 함께 잡기
print("Enter a number:")
user_input = input()
try:
number = int(user_input)
result = 100 / number
print(f"100 divided by {number} is {result}")
except (ValueError, ZeroDivisionError):
print("Invalid input or division by zero.")
print("Please enter a non-zero number.")사용자가 "hello"를 입력하면:
Enter a number:
hello
Invalid input or division by zero.
Please enter a non-zero number.사용자가 "0"을 입력하면:
Enter a number:
0
Invalid input or division by zero.
Please enter a non-zero number.잘못된 변환에서 생기는 ValueError와 0으로 나눌 때 생기는 ZeroDivisionError가 같은 except 절에서 처리됩니다. 서로 다른 오류가 동일한 반응을 유발해야 할 때 유용합니다.
25.2.4) 예외 정보 접근하기
때로는 발생한 예외에 대해 더 많은 세부 정보가 필요합니다. as 키워드를 사용해 예외 객체(exception object)를 캡처할 수 있습니다. 하지만 먼저 예외 객체가 무엇인지 이해해 봅시다.
예외 객체란?
Python이 예외를 발생시킬 때, 단지 “뭔가 잘못됐다”는 신호만 보내는 것이 아니라, 오류에 대한 정보를 담은 객체(object) 를 생성합니다. 이 예외 객체는 다음을 포함하는 상세 오류 보고서와 같습니다:
- 오류 메시지: 무엇이 잘못됐는지에 대한 설명
- 예외 타입: 어떤 종류의 오류인지(ValueError, TypeError 등)
- 추가 속성(attributes): 예외 타입에 따라 달라지는 구체적인 정보
예외 객체는 오류에 대한 모든 정보를 담는 컨테이너라고 생각하면 됩니다. 리스트(list) 객체가 항목들을 담고 append() 같은 메서드를 가지는 것처럼, 예외 객체도 오류 정보를 담고 접근 가능한 속성을 가집니다.
except ValueError as error:라고 쓰면 Python에게 “ValueError가 발생하면 error라는 변수를 만들고 그 안에 예외 객체를 넣어서 내가 살펴볼 수 있게 해라”라고 말하는 것입니다.
예외 객체 안에 무엇이 들어 있는지 살펴봅시다:
# 예외 객체 내용 살펴보기
try:
number = int("hello")
except ValueError as error:
print("Exception caught! Let's examine it:")
print(f"Type: {type(error)}")
print(f"String representation: {error}")
print(f"Args tuple: {error.args}")Output:
Exception caught! Let's examine it:
Type: <class 'ValueError'>
String representation: invalid literal for int() with base 10: 'hello'
Args tuple: ("invalid literal for int() with base 10: 'hello'",)예외 객체에는 다음이 있습니다:
- 타입(ValueError 클래스) — 어떤 종류의 오류인지 알려줍니다
- 문자열 표현(오류 메시지) — 트레이스백에서 보게 되는 내용입니다
- args 속성(메시지 및 기타 인자를 담은 튜플) — 오류 세부 정보에 구조적으로 접근할 수 있습니다
왜 이것이 중요한가:
예외 타입마다 특정 정보를 제공하는 서로 다른 속성을 가집니다. 예외 객체의 구조를 이해하면 디버깅이나 사용자 피드백을 위해 유용한 정보를 추출할 수 있습니다:
# 예외 타입마다 서로 다른 속성을 가짐
numbers = [10, 20, 30]
try:
value = numbers[10]
except IndexError as error:
print(f"IndexError message: {error}")
print(f"Exception args: {error.args}")
# 이제 딕셔너리로 시도
grades = {"Alice": 95}
try:
grade = grades["Bob"]
except KeyError as error:
print(f"KeyError message: {error}")
print(f"Missing key: {error.args[0]}")Output:
IndexError message: list index out of range
Exception args: ('list index out of range',)
KeyError message: 'Bob'
Missing key: BobKeyError가 메시지에 실제로 누락된 키를 포함하는 것을 확인할 수 있습니다. 예외 타입마다 서로 다른 유용한 정보가 제공되며, 예외 객체를 통해 접근할 수 있습니다.
25.3) try 블록에서 else와 finally 사용하기
25.3.1) else 절: 성공했을 때만 실행되는 코드
try-except 블록에서 else 절은 try 블록에서 예외가 발생하지 않았을 때만 실행됩니다. 이는 위험한 작업이 성공했을 때만 실행되어야 하는 코드에 유용합니다:
# 성공 시에만 실행되는 코드를 else로 처리하기
print("Enter a number:")
user_input = input()
try:
number = int(user_input)
except ValueError:
print("That's not a valid number!")
else:
# int(user_input)가 성공했을 때만 실행됨
print(f"Successfully converted: {number}")
squared = number ** 2
print(f"The square of {number} is {squared}")사용자가 "5"를 입력하면:
Enter a number:
5
Successfully converted: 5
The square of 5 is 25사용자가 "hello"를 입력하면:
Enter a number:
hello
That's not a valid number!왜 else를 쓰고, 그냥 try 블록 끝에 코드를 두지 않을까요? 중요한 이유가 두 가지 있습니다:
- 명확성(clarity):
else절은 이 코드가 성공 시에만 실행된다는 점을 명시적으로 보여줍니다 - 예외 범위(exception scope):
else절에서 발생한 예외는 앞의except절들에 의해 잡히지 않습니다
두 번째 포인트가 왜 중요한지 보여주는 예제입니다:
# else가 예외 범위 측면에서 유용한 이유 시연
try:
number_1 = int(input("Enter a number_1: "))
except ValueError:
print("Invalid input!")
else:
# 여기서 오류가 발생하면 위의 except에서 잡히지 않음
# 이는 입력 오류와 처리 오류를 구분하는 데 도움이 됨
number_2 = int(input("Enter a number_2: ")) # ValueError를 발생시킬 수 있음number_2 = int(input(...))를 number_1과 함께 try 블록에 넣으면, 두 입력 중 어느 것에서 발생한 ValueError든 동일한 except ValueError 절에서 잡히게 됩니다. 이렇게 하면 어떤 입력이 문제를 일으켰는지 알 수 없습니다.
number_2 = int(input(...))를 else 블록에 넣으면, 오류 처리를 분리할 수 있습니다. except 절은 number_1의 오류만 잡고, number_2의 오류는 전체 traceback과 함께 잡히지 않은 예외로 발생합니다 - 이를 통해 첫 번째가 아닌 두 번째 입력이 실패했음을 명확히 알 수 있습니다.
25.3.2) finally 절: 항상 실행되는 코드
finally 절에는 어떤 경우에도 실행되는 코드가 들어갑니다. 예외가 발생했는지 여부와 관계없고, 예외가 잡혔는지 여부와도 관계가 없습니다. 이는 반드시 수행되어야 하는 정리(cleanup) 작업에 필수입니다:
# 정리를 위해 finally 사용하기
print("Enter a number:")
user_input = input()
try:
number = int(user_input)
result = 100 / number
print(f"Result: {result}")
except ValueError:
print("Invalid number!")
except ZeroDivisionError:
print("Cannot divide by zero!")
finally:
print("Calculation attempt completed.")사용자가 "5"를 입력하면:
Enter a number:
5
Result: 20.0
Calculation attempt completed.사용자가 "hello"를 입력하면:
Enter a number:
hello
Invalid number!
Calculation attempt completed.사용자가 "0"을 입력하면:
Enter a number:
0
Cannot divide by zero!
Calculation attempt completed.세 경우 모두 finally 블록이 실행됩니다! 이것이 finally의 핵심 동작입니다. 어떤 일이 있었든 항상 실행됩니다.
25.3.3) try, except, else, finally 조합하기
네 가지 절을 모두 함께 사용하면 포괄적인 예외 처리를 만들 수 있습니다:
# 완전한 예외 처리 구조
print("Enter a number to calculate its reciprocal:")
user_input = input()
try:
# 위험한 작업들
number = int(user_input)
reciprocal = 1 / number
except ValueError:
# 변환 오류 처리
print("Error: Input must be a valid integer.")
except ZeroDivisionError:
# 0으로 나누기 처리
print("Error: Cannot calculate reciprocal of zero.")
else:
# 성공했을 때만 실행되는 코드
print(f"The reciprocal of {number} is {reciprocal}")
print(f"Verification: {number} × {reciprocal} = {number * reciprocal}")
finally:
# 항상 실행되는 정리 코드
print("Reciprocal calculation completed.")사용자가 "4"를 입력하면:
Enter a number to calculate its reciprocal:
4
The reciprocal of 4 is 0.25
Verification: 4 × 0.25 = 1.0
Reciprocal calculation completed.사용자가 "hello"를 입력하면:
Enter a number to calculate its reciprocal:
hello
Error: Input must be a valid integer.
Reciprocal calculation completed.사용자가 "0"을 입력하면:
Enter a number to calculate its reciprocal:
0
Error: Cannot calculate reciprocal of zero.
Reciprocal calculation completed.실행 흐름은 다음과 같습니다:
try블록은 항상 먼저 실행됩니다- 예외가 발생하면 매칭되는
except블록이 실행됩니다 - 예외가 발생하지 않으면(있다면)
else블록이 실행됩니다 finally블록은 무슨 일이 있었든 항상 마지막에 실행됩니다
25.4) raise로 예외를 의도적으로 발생시키기
25.4.1) 왜 예외를 발생시킬까?
지금까지는 Python이 자동으로 발생시키는 예외를 잡았습니다. 하지만 때로는 여러분이 직접 코드에서 의도적으로 예외를 발생시켜야 합니다. 이는 다음과 같은 경우에 유용합니다:
- 코드가 처리할 수 없는 잘못된 상황을 감지했을 때
- 규칙이나 제약 조건을 강제하고 싶을 때
- 함수를 호출한 코드에 오류를 알리고 싶을 때
예외를 발생시키는 것은 Python에서 “나는 계속할 수 없어 — 뭔가 잘못됐고, 나를 호출한 쪽이 이를 처리해야 한다”라고 말하는 방식입니다.
문법은 간단합니다: raise ExceptionType("error message")
기본 예제를 보겠습니다:
# 의도적으로 예외를 발생시키기
age = -5
if age < 0:
raise ValueError("Age cannot be negative!")
print(f"Age: {age}") # 이 줄은 절대 실행되지 않음Output:
Traceback (most recent call last):
File "example.py", line 5, in <module>
raise ValueError("Age cannot be negative!")
ValueError: Age cannot be negative!Python이 raise를 만나면 즉시 예외를 생성하고 이를 처리할 except 블록을 찾기 시작합니다. 없다면, 프로그램은 트레이스백과 함께 종료됩니다.
25.4.2) 함수에서 예외 발생시키기
예외를 발생시키는 것은 입력을 검증하고 제약을 강제하기 위해 함수(function)에서 특히 유용합니다:
# 예외를 발생시켜 입력을 검증하는 함수
def calculate_discount(price, discount_percent):
"""Calculate discounted price.
Args:
price: Original price (must be positive)
discount_percent: Discount percentage (must be 0-100)
Returns:
Discounted price
Raises:
ValueError: If inputs are invalid
"""
if price < 0:
raise ValueError("Price cannot be negative!")
if discount_percent < 0 or discount_percent > 100:
raise ValueError("Discount must be between 0 and 100!")
discount_amount = price * (discount_percent / 100)
return price - discount_amount
# Using the function
try:
final_price = calculate_discount(100, 20)
print(f"Final price: ${final_price}")
except ValueError as error:
print(f"Error: {error}")Output:
Final price: $80.0이제 잘못된 입력으로 시도해 봅시다:
# Invalid price
try:
final_price = calculate_discount(-50, 20)
print(f"Final price: ${final_price}")
except ValueError as error:
print(f"Error: {error}")Output:
Error: Price cannot be negative!# Invalid discount
try:
final_price = calculate_discount(100, 150)
print(f"Final price: ${final_price}")
except ValueError as error:
print(f"Error: {error}")Output:
Error: Discount must be between 0 and 100!예외를 발생시키면, 함수는 무엇이 잘못됐는지 명확하게 전달합니다. 그러면 호출하는 코드는 그 오류를 어떻게 처리할지 결정할 수 있습니다. 예를 들어 사용자에게 새 입력을 요청하거나, 기본값을 사용하거나, 오류를 로그로 남길 수 있습니다.
25.4.3) 올바른 예외 타입 선택하기
Python에는 많은 내장 예외 타입이 있으며, 올바른 타입을 선택하면 코드가 더 명확해집니다. 검증(validation)에 가장 자주 사용되는 예외는 다음과 같습니다:
- ValueError: 타입은 맞지만 값이 부적절할 때 사용합니다(예: 음수 나이, 잘못된 퍼센트)
- TypeError: 타입 자체가 완전히 잘못됐을 때 사용합니다(예: 숫자 대신 문자열)
- KeyError: 딕셔너리 키가 존재하지 않을 때 사용합니다
- IndexError: 시퀀스 인덱스가 범위를 벗어났을 때 사용합니다
서로 다른 예외 타입을 보여주는 예제입니다:
# 적절한 예외 타입 사용하기
def get_student_grade(grades, student_name):
"""Get a student's grade from the grades dictionary.
Args:
grades: Dictionary mapping student names to grades
student_name: Name of the student
Returns:
The student's grade
Raises:
TypeError: If grades is not a dictionary
KeyError: If student_name is not in grades
ValueError: If the grade is invalid
"""
if not isinstance(grades, dict):
raise TypeError("Grades must be a dictionary!")
if student_name not in grades:
raise KeyError(f"Student '{student_name}' not found!")
grade = grades[student_name]
if not (0 <= grade <= 100):
raise ValueError(f"Invalid grade: {grade} (must be 0-100)")
return grade
# Test with valid data
grades = {"Alice": 95, "Bob": 87, "Carol": 92}
try:
grade = get_student_grade(grades, "Alice")
print(f"Alice's grade: {grade}")
except (TypeError, KeyError, ValueError) as error:
print(f"Error: {error}")Output:
Alice's grade: 95# Test with missing student
try:
grade = get_student_grade(grades, "David")
print(f"David's grade: {grade}")
except (TypeError, KeyError, ValueError) as error:
print(f"Error: {error}")Output:
Error: Student 'David' not found!# Test with wrong type
try:
grade = get_student_grade("not a dict", "Alice")
print(f"Alice's grade: {grade}")
except (TypeError, KeyError, ValueError) as error:
print(f"Error: {error}")Output:
Error: Grades must be a dictionary!적절한 예외 타입을 사용하면 다른 프로그래머(그리고 미래의 여러분)가 어떤 종류의 오류가 발생했는지 이해하는 데 도움이 됩니다.
25.4.4) 예외를 다시 발생시키기
때로는 예외를 잡아서 무언가를(예: 로깅) 한 뒤, 예외가 계속 전파되도록 하고 싶을 수 있습니다. except 블록 안에서 인자 없이 raise를 사용하면 됩니다:
# 로깅 후 예외를 다시 발생시키기
def divide_numbers(a, b):
"""Divide two numbers with error logging."""
try:
result = a / b
return result
except ZeroDivisionError:
print("ERROR LOG: Division by zero attempted")
print(f" Numerator: {a}, Denominator: {b}")
raise # 같은 예외를 다시 발생시킴
# Using the function
try:
result = divide_numbers(10, 0)
print(f"Result: {result}")
except ZeroDivisionError:
print("Cannot divide by zero!")Output:
ERROR LOG: Division by zero attempted
Numerator: 10, Denominator: 0
Cannot divide by zero!인자 없는 raise 문은 방금 잡힌 예외를 다시 발생시킵니다. 이는 다음과 같은 경우에 유용합니다:
- 오류를 로그로 남기거나 기록하기
- 정리 작업 수행하기
- 호출자(caller)에게 오류를 전파하기
25.4.5) 예외에서 예외를 발생시키기
때로는 하나의 예외를 처리하는 중에 새로운 예외를 발생시키되, 원래 오류의 맥락(context)을 보존하고 싶을 수 있습니다. Python 3는 이를 위해 raise ... from ... 문법을 제공합니다:
# 기존 예외로부터 새 예외를 발생시키기
def load_config(config_dict, key):
"""Load configuration value from dictionary."""
try:
config_value = config_dict[key]
# 정수로 파싱 시도
parsed_value = int(config_value)
return parsed_value
except KeyError as error:
raise RuntimeError(f"Configuration key missing: {key}") from error
except ValueError as error:
raise RuntimeError(f"Invalid configuration format for {key}") from error
# Using the function
config = {"timeout": "30", "retries": "5"}
try:
value = load_config(config, "timeout")
print(f"Config value: {value}")
except RuntimeError as error:
print(f"Configuration error: {error}")
print(f"Original cause: {error.__cause__}")Output:
Config value: 30키가 존재하지 않으면:
try:
value = load_config(config, "missing_key")
print(f"Config value: {value}")
except RuntimeError as error:
print(f"Configuration error: {error}")
print(f"Original cause: {error.__cause__}")Output:
Configuration error: Configuration key missing: missing_key
Original cause: 'missing_key'from 키워드는 새 예외를 원래 예외와 연결합니다. 이를 통해 예외 체인(chain)이 만들어져 디버깅에 도움이 됩니다. 즉, 높은 수준에서는 무엇이 잘못됐는지(설정 오류)와 근본 원인이 무엇인지(키를 찾지 못함)를 모두 볼 수 있습니다.
예외 처리(exception handling)는 신뢰할 수 있는 프로그램을 작성하는 데 가장 중요한 도구 중 하나입니다. try-except 블록을 사용하면 문제를 예상하고, 우아하게 처리하며, 사용자에게 더 나은 경험을 제공할 수 있습니다. 다음을 기억하세요:
try-except로 예상 가능한 오류를 우아하게 처리하세요- bare
except를 쓰기보다 특정 예외 타입을 잡으세요 - 성공했을 때만 실행되어야 하는 코드는
else를 사용하세요 - 반드시 실행되어야 하는 정리(cleanup) 코드는
finally를 사용하세요 - 문제를 알리기 위해 여러분의 코드에서 예외를 발생시키세요
- 오류를 명확히 하기 위해 적절한 예외 타입을 선택하세요
- 무엇이 잘못됐는지 설명하는 도움이 되는 오류 메시지를 제공하세요
다음 장에서는 예외 처리와 입력 검증 및 기타 전략을 결합해 프로그램을 더 견고하게 만드는 방어적 프로그래밍(defensive programming) 기법을 배우겠습니다.