24. 오류와 트레이스백 이해하기
오류는 프로그래밍에서 피할 수 없는 부분입니다. 초보자부터 전문가까지 모든 프로그래머는 정기적으로 오류를 마주합니다. 오류 때문에 헤매는 것과 오류로부터 배우는 것의 차이는, 무언가 잘못됐을 때 Python이 무엇을 말하려는지 이해하는 데 있습니다.
Python이 코드에서 문제를 만나면 조용히 멈추기만 하는 것이 아니라, 무엇이 잘못됐는지, 어디서 발생했는지, 그리고 종종 왜 그런지에 대한 힌트까지 자세한 정보를 제공합니다. 이런 오류 메시지를 읽고 해석하는 법을 배우는 것은 프로그래머로서 키울 수 있는 가장 가치 있는 기술 중 하나입니다.
이 장에서는 여러분이 만나게 될 두 가지 주요 오류 범주, 즉 문법 오류(syntax errors)(코드를 작성한 방식의 문제)와 런타임 예외(runtime exceptions)(코드가 실행되는 동안 발생하는 문제)를 살펴보겠습니다. 또한 Python의 자세한 오류 보고서인 트레이스백(tracebacks)을 읽는 법을 배우고, 예외가 프로그램의 정상적인 흐름을 어떻게 바꾸는지 이해하겠습니다. 무엇보다도, 오류를 실패가 아니라 더 좋은 코드를 쓰도록 도와주는 가치 있는 정보로 받아들이는 디버깅(debugging) 사고방식을 기르겠습니다.
24.1) 문법 오류 vs 런타임 예외
Python은 코드에서 발생하는 문제를 크게 두 가지로 구분합니다. 문법 오류와 런타임 예외입니다. 이 차이를 이해하면 문제를 더 빠르게 진단하고, 해결책을 어디서 찾아야 하는지 알 수 있습니다.
24.1.1) 문법 오류란 무엇인가
문법 오류(syntax error)는 Python이 언어의 문법 규칙을 위반한 코드 때문에 코드를 이해할 수 없을 때 발생합니다. "The cat sat on the"가 불완전한 영어 문장인 것처럼, 문법 오류가 있는 코드는 인터프리터가 파싱할 수 없는 불완전하거나 잘못된 형태의 Python 코드입니다.
문법 오류는 프로그램이 실행되기 전에 감지됩니다. Python은 먼저 전체 스크립트를 읽으면서 언어 규칙을 따르는지 확인합니다. 문법 오류를 발견하면, 올바른 부분이 있더라도 어떤 코드도 실행하지 않습니다.
간단한 예시는 다음과 같습니다:
# 경고: 문법 오류 - 시연용입니다
# 실수: if 문 뒤에 콜론 누락
age = 25
if age >= 18
print("You are an adult")이 코드를 실행하려고 하면 Python은 즉시 다음과 같이 보고합니다:
File "example.py", line 3
if age >= 18
^
SyntaxError: expected ':'이 오류 메시지의 몇 가지 핵심 특징을 확인해 보세요:
- 파일과 줄 번호: Python은 문제가 발견된 정확한 위치를 알려줍니다(
line 3) - 시각적 표시: 캐럿(
^)이 Python이 혼란스러워한 지점을 가리킵니다 - 오류 유형:
SyntaxError는 문법 문제임을 명확히 식별합니다 - 도움이 되는 힌트:
expected ':'는 무엇이 빠졌는지 알려줍니다
이 코드는 문법이 유효하지 않기 때문에 Python이 실행을 시작조차 할 수 없어서 실행되지 않습니다.
또 다른 흔한 문법 오류를 살펴보겠습니다:
# 경고: 문법 오류 - 시연용입니다
# 실수: 괄호 짝이 맞지 않음
numbers = [1, 2, 3, 4, 5]
total = sum(numbers
print(f"Total: {total}")Python은 다음과 같이 보고합니다:
File "example.py", line 2
total = sum(numbers
^
SyntaxError: '(' was never closed여기서 Python은 2번째 줄에서 여는 괄호를 썼지만 닫는 괄호를 쓰지 않았다는 것을 감지했습니다. 오류는 2번째 줄(닫히지 않은 괄호가 있는 곳)에서 보고되고, 캐럿은 Python이 닫는 괄호를 기대했던 지점을 가리킵니다.
문법 오류의 핵심 특징:
- 어떤 코드도 실행되기 전에 감지됨
- 전체 프로그램 실행을 막음
- 보통 오타, 빠진 구두점, 또는 잘못된 들여쓰기를 의미함
- 오류 위치는 실제 실수 지점보다 약간 뒤일 수 있음
24.1.2) 런타임 예외란 무엇인가
런타임 예외(runtime exception)(또는 간단히 "예외(exception)")는 문법적으로는 올바른 코드가 실행 중에 문제를 만나면 발생합니다. 문법적으로는 유효한 Python 코드지만, 프로그램이 실제로 실행될 때 무언가가 잘못됩니다.
문법 오류와 달리 예외는 프로그램이 실행되는 동안 발생합니다. Python은 코드를 성공적으로 파싱하고 실행을 시작했지만, 처리할 수 없는 상황을 만납니다.
간단한 예시는 다음과 같습니다:
# 이 코드는 문법은 올바르지만 예외를 발생시킵니다
numbers = [10, 20, 30]
print(numbers[0]) # Output: 10
print(numbers[5]) # 이 줄에서 IndexError가 발생합니다
print("This line never executes")Output:
10
Traceback (most recent call last):
File "example.py", line 3, in <module>
print(numbers[5])
~~~~~~~^^^
IndexError: list index out of range무슨 일이 일어났는지 확인해 봅시다:
- 첫 번째
print문은 성공적으로 실행되었습니다(10이 출력됨) - 두 번째
print는 존재하지 않는 인덱스 5에 접근하려고 했습니다 - Python이
IndexError예외를 발생시켰습니다 - 프로그램이 중단되어 세 번째
print는 실행되지 않았습니다
코드는 문법적으로 올바랐습니다. Python은 우리가 하려는 일을 이해하는 데 문제가 없었습니다. 문제는 존재하지 않는 리스트 요소에 접근하려고 했을 때 실행 중에 발생했습니다.
다른 종류의 런타임 예외를 보여주는 또 다른 예시는 다음과 같습니다:
# 문법은 올바르지만 실행 시 0으로 나누기
def calculate_average(total, count):
return total / count
# 이것들은 문제없이 동작합니다
print(calculate_average(100, 4)) # Output: 25.0
print(calculate_average(75, 3)) # Output: 25.0
# 이것은 예외를 발생시킵니다
print(calculate_average(50, 0)) # ZeroDivisionErrorOutput:
25.0
25.0
Traceback (most recent call last):
File "example.py", line 8, in <module>
print(calculate_average(50, 0))
^^^^^^^^^^^^^^^^^^^^^^^^
File "example.py", line 2, in calculate_average
return total / count
~~~~~~^~~~~~~
ZeroDivisionError: division by zero이 함수는 두 번은 완벽하게 작동했지만 세 번째 호출에서는 count로 0을 전달해 0으로 나누기가 발생했습니다. Python은 해당 값들로 코드가 실제로 실행되기 전에는 이 문제를 감지할 수 없습니다.
런타임 예외의 핵심 특징:
- 프로그램 실행 중에 발생함
- 코드는 문법적으로 유효함
- 특정 데이터나 조건에 의존하는 경우가 많음
- 예외가 발생한 지점까지는 프로그램이 실행됨
- 입력이 다르면 다른 예외가 발생할 수도(또는 전혀 발생하지 않을 수도) 있음
24.1.3) 문법 오류와 런타임 예외 비교하기
두 가지 오류 유형을 나란히 보고 차이를 이해해 보겠습니다:
# 예제 1: 문법 오류
# 실수: 닫는 따옴표 누락
print("Program started!")
message = "Hello, world
print(message)이 코드는 즉시 문법 오류를 발생시킵니다:
File "example.py", line 4
message = "Hello, world
^
SyntaxError: unterminated string literal (detected at line 4)중요: 출력에 "Program started!"가 표시되지 않는 것을 주목하세요. Python은 코드를 실행하기 전에 구문 오류를 발견했습니다.
이제 런타임 예외와 비교해 보겠습니다:
# 예제 2: 런타임 예외
# 문법은 올바르지만 변수가 존재하지 않습니다
print("Program started!")
message = "Hello, world"
print(mesage) # 오타: 'message'가 아니라 'mesage'Output:
Program started!
Traceback (most recent call last):
File "example.py", line 5, in <module>
print(mesage)
^^^^^^
NameError: name 'mesage' is not defined중요: 이번에는 출력에 "Program started!"가 표시됩니다. Python은 첫 두 문장인 print와 대입문(3-4번째 줄)을 성공적으로 실행했지만, 5번째 줄에서 mesage를 찾으려 할 때 문제가 발생했습니다.
핵심 차이점: 첫 번째 예제에서는 Python이 코드를 실행조차 시도하지 않았습니다. 파싱 단계에서 구문 오류를 발견했기 때문입니다. 두 번째 예제에서는 Python이 프로그램 실행을 성공적으로 시작하여 여러 줄을 실행한 후에 런타임 오류가 발생했습니다.
24.2) 자주 나오는 내장 예외 유형
Python에는 다양한 내장 예외 유형이 있으며, 각각은 특정 종류의 문제를 나타냅니다. 이런 흔한 예외를 알아두면 무엇이 잘못됐는지, 어떻게 고쳐야 할지 빠르게 이해할 수 있습니다. 각 예외 유형은 문제를 암시하는 설명적인 이름을 가지고 있습니다.
24.2.1) NameError: 정의되지 않은 이름 사용
NameError는 Python이 인식하지 못하는 변수, 함수 또는 다른 이름을 사용하려고 할 때 발생합니다. 보통 무언가를 정의하는 것을 잊었거나, 이름을 잘못 썼거나, 생성되기 전에 사용하려는 경우입니다.
# 예제 1: 변수를 정의하는 것을 잊음
print(greeting) # NameError: name 'greeting' is not definedOutput:
Traceback (most recent call last):
File "example.py", line 2, in <module>
print(greeting)
^^^^^^^^
NameError: name 'greeting' is not definedPython은 greeting이 무엇인지 모른다고 말하고 있습니다. 먼저 만들어야 합니다:
# 올바른 버전
greeting = "Hello, Python!"
print(greeting) # Output: Hello, Python!오타가 있는 더 미묘한 예시도 보겠습니다:
# 예제 2: 변수 이름의 오타
user_name = "Alice"
age = 30
print(f"{username} is {age} years old") # NameError: name 'username' is not defineduser_name(언더스코어 포함)을 정의했지만, username(언더스코어 없음)을 사용하려고 했습니다. Python은 이를 완전히 다른 이름으로 봅니다.
24.2.2) TypeError: 연산에 맞지 않는 타입
TypeError는 잘못된 타입의 값에 대해 연산을 수행하려고 할 때 발생합니다. 예를 들어 문자열과 정수를 더할 수 없고, 함수가 아닌 것을 호출할 수도 없습니다.
# 예제 1: 호환되지 않는 타입 섞기
age = 25
message = "You are " + age + " years old" # TypeErrorOutput:
Traceback (most recent call last):
File "example.py", line 2, in <module>
message = "You are " + age + " years old"
~~~~~~~~~~~^~~~~
TypeError: can only concatenate str (not "int") to strPython은 + 연산자가 문자열과 문자열을 연결할 수는 있지만, 문자열과 정수를 연결할 수는 없다고 말하고 있습니다. 정수를 문자열로 변환해야 합니다:
# 올바른 버전
age = 25
message = "You are " + str(age) + " years old"
print(message) # Output: You are 25 years oldTypeError는 함수에 잘못된 개수의 인자를 전달할 때도 발생합니다:
# 예제 3: 인자 개수가 잘못됨
def calculate_area(length, width):
return length * width
area = calculate_area(5) # TypeError: missing 1 required positional argumentOutput:
Traceback (most recent call last):
File "example.py", line 4, in <module>
area = calculate_area(5)
TypeError: calculate_area() missing 1 required positional argument: 'width'이 함수는 두 개의 인자를 기대하지만, 우리는 하나만 제공했습니다.
24.2.3) ValueError: 타입은 맞지만 값이 잘못됨
ValueError는 올바른 타입의 값을 전달했지만, 그 값 자체가 해당 연산에 적절하지 않을 때 발생합니다. 타입은 맞지만, 특정 값이 그 맥락에서는 말이 되지 않습니다.
# 예제 1: 유효하지 않은 문자열을 정수로 변환
user_input = "twenty-five"
age = int(user_input) # ValueError: invalid literal for int()Output:
Traceback (most recent call last):
File "example.py", line 2, in <module>
age = int(user_input)
ValueError: invalid literal for int() with base 10: 'twenty-five'int() 함수는 문자열을 기대하고, 우리는 문자열을 줬습니다. 그래서 타입은 맞습니다. 하지만 "twenty-five"는 문자를 포함하고 있어서 정수로 변환할 수 없습니다. "25"라면 문제없이 동작합니다:
# 올바른 버전
user_input = "25"
age = int(user_input)
print(age) # Output: 25ValueError는 리스트 메서드에서도 발생합니다:
# 예제 3: 존재하지 않는 항목 삭제
fruits = ["apple", "banana", "orange"]
fruits.remove("grape") # ValueError: 'grape' is not in listOutput:
Traceback (most recent call last):
File "example.py", line 2, in <module>
fruits.remove("grape")
~~~~~~~~~~~~~^^^^^^^^^
ValueError: list.remove(x): x not in listremove() 메서드는 리스트에 존재하는 값을 기대합니다. 먼저 확인해야 합니다:
# 올바른 버전
fruits = ["apple", "banana", "orange"]
if "grape" in fruits:
fruits.remove("grape")
else:
print("Grape not found in list") # Output: Grape not found in list24.2.4) IndexError: 잘못된 시퀀스 인덱스
IndexError는 존재하지 않는 인덱스로 시퀀스(리스트, 튜플, 문자열)에 접근하려 할 때 발생합니다. Python은 0부터 시작하는 인덱싱을 사용하며, 유효한 인덱스 범위는 0부터 len(sequence) - 1까지라는 점을 기억하세요.
# 예제 1: 인덱스가 너무 큼
colors = ["red", "green", "blue"]
print(colors[0]) # Output: red
print(colors[3]) # IndexError: list index out of rangeOutput:
red
Traceback (most recent call last):
File "example.py", line 3, in <module>
print(colors[3])
~~~~~~^^^
IndexError: list index out of range이 리스트에는 인덱스 0, 1, 2에 세 개의 요소가 있습니다. 인덱스 3은 존재하지 않습니다. 이는 인덱싱이 0부터 시작한다는 것을 잊었을 때 매우 흔히 발생하는 실수입니다:
# 올바른 버전
colors = ["red", "green", "blue"]
print(colors[2]) # Output: blue (세 번째 요소)24.2.5) KeyError: 딕셔너리 키 누락
KeyError는 존재하지 않는 딕셔너리 키에 접근하려 할 때 발생합니다. 리스트는 길이를 확인할 수 있지만 딕셔너리는 어떤 키든 가질 수 있으므로, 접근하기 전에 키가 존재하는지 확인해야 합니다.
# 예제 1: 존재하지 않는 키 접근
student = {
"name": "Alice",
"age": 20,
"major": "Computer Science"
}
print(student["name"]) # Output: Alice
print(student["grade"]) # KeyError: 'grade'Output:
Alice
Traceback (most recent call last):
File "example.py", line 7, in <module>
print(student["grade"])
~~~~~~~^^^^^^^^^
KeyError: 'grade'이 딕셔너리에는 "grade" 키가 없습니다. 먼저 키가 존재하는지 확인할 수 있습니다:
# 'in'을 사용한 올바른 버전
student = {
"name": "Alice",
"age": 20,
"major": "Computer Science"
}
if "grade" in student:
print(student["grade"])
else:
print("Grade not available") # Output: Grade not available또는 get() 메서드를 사용할 수 있는데, 이는 오류를 발생시키는 대신 None(또는 기본값)을 반환합니다:
# get()을 사용한 대안
grade = student.get("grade")
if grade is not None:
print(f"Grade: {grade}")
else:
print("Grade not available") # Output: Grade not availableKeyError는 구조가 일관되지 않은 데이터를 처리할 때 흔히 발생합니다:
# 예제 2: 여러 레코드 처리
students = [
{"name": "Alice", "age": 20, "grade": "A"},
{"name": "Bob", "age": 21}, # 'grade' 키가 누락됨
{"name": "Carol", "age": 19, "grade": "B"}
]
for student in students:
print(f"{student['name']}: {student['grade']}") # Bob에서 KeyError 발생Output:
Alice: A
Traceback (most recent call last):
File "example.py", line 7, in <module>
print(f"{student['name']}: {student['grade']}")
~~~~~~~^^^^^^^^^
KeyError: 'grade'누락된 키를 우아하게 처리하려면 기본값과 함께 get()을 사용하세요:
# 올바른 버전
students = [
{"name": "Alice", "age": 20, "grade": "A"},
{"name": "Bob", "age": 21},
{"name": "Carol", "age": 19, "grade": "B"}
]
for student in students:
grade = student.get("grade", "Not assigned")
print(f"{student['name']}: {grade}")Output:
Alice: A
Bob: Not assigned
Carol: B24.2.6) AttributeError: 잘못된 속성 접근
AttributeError는 객체에 존재하지 않는 속성(attribute)이나 메서드에 접근하려 할 때 발생합니다. 이는 서로 다른 타입의 메서드를 혼동하거나 속성 이름을 잘못 쓸 때 자주 발생합니다.
# 예제 1: 타입에 맞지 않는 메서드
numbers = [1, 2, 3, 4, 5]
numbers.append(6) # 이건 동작합니다 - 리스트에는 append()가 있습니다
print(numbers) # Output: [1, 2, 3, 4, 5, 6]
text = "Hello"
text.append("!") # AttributeError: 'str' object has no attribute 'append'Output:
[1, 2, 3, 4, 5, 6]
Traceback (most recent call last):
File "example.py", line 6, in <module>
text.append("!")
^^^^^^^^^^^
AttributeError: 'str' object has no attribute 'append'문자열은 불변(immutable)이기 때문에 append() 메서드가 없습니다. 대신 연결(concatenation) 또는 다른 문자열 메서드를 사용해야 합니다:
# 올바른 버전
text = "Hello"
text = text + "!" # 연결
print(text) # Output: Hello!AttributeError는 오타에서도 자주 발생합니다:
# 예제 2: 메서드 이름 오타
message = "Python Programming"
result = message.uppper() # AttributeError: 'str' object has no attribute 'uppper'Output:
Traceback (most recent call last):
File "example.py", line 2, in <module>
result = message.uppper()
^^^^^^^^^^^^^^
AttributeError: 'str' object has no attribute 'uppper'. Did you mean: 'upper'?Python 3.10+에서는 종종 올바른 철자를 제안해 준다는 점에 주목하세요! 올바른 메서드는 upper()입니다:
# 올바른 버전
message = "Python Programming"
result = message.upper()
print(result) # Output: PYTHON PROGRAMMING24.2.7) ZeroDivisionError: 0으로 나누기
ZeroDivisionError는 0으로 나누려고 할 때 발생하며, 이는 수학적으로 정의되지 않습니다. 이는 사용자 입력이나 계산 결과가 0일 것이라고 예상하지 못했을 때 자주 발생합니다.
# 예제 1: 직접 0으로 나누기
result = 10 / 0 # ZeroDivisionError: division by zeroOutput:
Traceback (most recent call last):
File "example.py", line 1, in <module>
result = 10 / 0
~~~^~~
ZeroDivisionError: division by zero이는 몫 나눗셈과 나머지 연산에도 적용됩니다:
# 예제 2: 다른 나눗셈 연산
a = 10 // 0 # ZeroDivisionError
b = 10 % 0 # ZeroDivisionError더 현실적인 예는 계산에서 나옵니다:
# 예제 3: 평균 계산
def calculate_average(numbers):
total = sum(numbers)
count = len(numbers)
return total / count
scores = [85, 90, 78, 92]
print(calculate_average(scores)) # Output: 86.25
empty_scores = []
print(calculate_average(empty_scores)) # ZeroDivisionErrorOutput:
86.25
Traceback (most recent call last):
File "example.py", line 9, in <module>
print(calculate_average(empty_scores))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "example.py", line 4, in calculate_average
return total / count
~~~~~~^~~~~~~
ZeroDivisionError: division by zero빈 리스트의 길이는 0이라서 0으로 나누게 됩니다. 항상 이 조건을 확인하세요:
# 올바른 버전
def calculate_average(numbers):
if len(numbers) == 0:
return 0 # 또는 None을 반환하거나, 더 설명적인 오류를 발생시킬 수도 있습니다
total = sum(numbers)
count = len(numbers)
return total / count
scores = [85, 90, 78, 92]
print(calculate_average(scores)) # Output: 86.25
empty_scores = []
print(calculate_average(empty_scores)) # Output: 0이런 흔한 예외 유형을 이해하면 문제를 빠르게 진단할 수 있습니다. 예외를 보면 유형 이름이 즉시 어떤 범주의 문제가 발생했는지 알려주고, 오류 메시지는 무엇이 구체적으로 잘못됐는지에 대한 세부 정보를 제공합니다.
24.3) 트레이스백 자세히 읽고 해석하기
런타임 예외가 발생하면 Python은 무엇이 잘못됐는지만 말하는 것이 아니라, 프로그램이 그 지점에 도달하기까지의 과정을 보여주는 자세한 트레이스백(traceback)을 제공합니다. 트레이스백을 읽는 법을 배우는 것은 효과적인 디버깅에 필수입니다. 트레이스백은 오류를 만나기 전에 프로그램이 어떤 경로를 거쳤는지 보여주는 빵부스러기(breadcrumb) 같은 흔적입니다.
24.3.1) 트레이스백의 구조
간단한 예제부터 시작해 트레이스백의 모든 부분을 살펴보겠습니다:
# 오류가 있는 간단한 프로그램
def calculate_discount(price, discount_percent):
discount_amount = price * (discount_percent / 100)
final_price = price - discount_amount
return final_price
def process_order(item_price, discount):
discounted_price = calculate_discount(item_price, discount)
tax = discounted_price * 0.08
total = discounted_price + tax
return total
# 메인 프로그램
original_price = "50" # 이런! 숫자여야 합니다
discount_rate = 10
final_cost = process_order(original_price, discount_rate)
print(f"Final cost: ${final_cost:.2f}")Output:
Traceback (most recent call last):
File "example.py", line 16, in <module>
final_cost = process_order(original_price, discount_rate)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "example.py", line 8, in process_order
discounted_price = calculate_discount(item_price, discount)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "example.py", line 2, in calculate_discount
discount_amount = price * (discount_percent / 100)
~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~
TypeError: can't multiply sequence by non-int of type 'float'이 트레이스백의 각 구성 요소를 분해해 보겠습니다:
1. 헤더: "Traceback (most recent call last):"
이 줄은 이어지는 내용이 트레이스백, 즉 함수 호출 기록이라는 것을 알려줍니다. "most recent call last"라는 문구는 트레이스백이 시간 순서대로 표시된다는 뜻입니다. 즉, 처음 호출된 함수가 먼저 나오고, 실제로 오류가 발생한 위치가 마지막에 나옵니다.
2. 호출 스택(위에서 아래로 읽기):
File "example.py", line 16, in <module>
final_cost = process_order(original_price, discount_rate)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^이는 체인의 첫 번째 함수 호출입니다. 여기에는 다음이 표시됩니다:
- 파일 이름:
"example.py"- 코드가 있는 위치 - 줄 번호:
line 16- 이 호출을 만든 정확한 줄 - 컨텍스트:
in <module>- 이 코드는 최상위 레벨(함수 내부가 아님)에 있음 - 코드: 실제로 실행된 줄
- 하이라이트:
^문자는 해당 줄에서 관련된 특정 부분을 가리킴
<module> 컨텍스트는 이것이 함수 내부가 아닌 모듈 레벨(스크립트의 메인 부분)에서 실행되는 코드라는 뜻입니다.
File "example.py", line 8, in process_order
discounted_price = calculate_discount(item_price, discount)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^이는 두 번째 함수 호출입니다. process_order 함수가 16번째 줄에서 호출되었고, 이제 이 함수 내부의 8번째 줄에서 calculate_discount를 호출하고 있습니다.
File "example.py", line 2, in calculate_discount
discount_amount = price * (discount_percent / 100)
~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~여기가 오류가 실제로 발생한 지점입니다. 이제 calculate_discount 함수 내부 2번째 줄이며, 문제가 된 연산이 있는 줄입니다.
3. 오류 메시지:
TypeError: can't multiply sequence by non-int of type 'float'이것이 실제로 발생한 오류입니다:
- 예외 유형:
TypeError- 오류 범주를 알려줌 - 설명: 뒤의 내용은 무엇이 구체적으로 잘못됐는지 설명함
이 경우 Python은 시퀀스(여기서는 문자열)를 float로 곱하려 했는데, 이는 허용되지 않는다고 말합니다.
24.3.2) 트레이스백을 아래에서 위로 읽기
트레이스백은 시간 순서대로(위에서 아래로) 출력되지만, 숙련된 프로그래머들은 실제 오류가 아래에 있기 때문에 종종 아래에서 위로 읽습니다. 위쪽 줄들은 그 지점에 어떻게 도달했는지를 보여줍니다.
이전 트레이스백을 아래에서 위로 읽어보겠습니다:
1단계: 오류 메시지부터 시작
TypeError: can't multiply sequence by non-int of type 'float'"좋아, 시퀀스를 float로 곱하려고 했는데 그건 허용되지 않네."
2단계: 오류가 발생한 위치 확인
File "example.py", line 2, in calculate_discount
discount_amount = price * (discount_percent / 100)
~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~"오류는 calculate_discount 함수의 2번째 줄에서 발생했어. price에 뭔가를 곱하려 하고 있네."
3단계: 어떻게 거기에 도달했는지 추적
File "example.py", line 8, in process_order
discounted_price = calculate_discount(item_price, discount)"calculate_discount는 process_order 8번째 줄에서 호출됐고, item_price를 price 파라미터로 전달했네."
4단계: 계속 추적
File "example.py", line 16, in <module>
final_cost = process_order(original_price, discount_rate)"그리고 process_order는 메인 프로그램의 16번째 줄에서 호출됐고, original_price를 item_price로 전달했네."
5단계: 근본 원인 찾기
이제 문제를 추적할 수 있습니다. original_price가 "50"(문자열)이고, 이것이 process_order에 item_price로 전달되고, 다시 calculate_discount에 price로 전달되어 float와 곱하려다 오류가 납니다. 해결책은 original_price를 숫자로 만드는 것입니다:
# 수정된 버전
def calculate_discount(price, discount_percent):
discount_amount = price * (discount_percent / 100)
final_price = price - discount_amount
return final_price
def process_order(item_price, discount):
discounted_price = calculate_discount(item_price, discount)
tax = discounted_price * 0.08
total = discounted_price + tax
return total
# 메인 프로그램 - 타입을 수정했습니다
original_price = 50 # 이제 문자열이 아니라 숫자입니다
discount_rate = 10
final_cost = process_order(original_price, discount_rate)
print(f"Final cost: ${final_cost:.2f}") # Output: Final cost: $48.60트레이스백을 읽는 법을 이해하면, 트레이스백은 위협적인 텍스트 덩어리에서 유용한 디버깅 도구로 바뀝니다. 각 줄은 프로그램의 실행 경로에 대한 가치 있는 정보를 제공하며, 연습을 통해 트레이스백의 안내를 따라 문제를 빠르게 찾아 고칠 수 있게 됩니다.
24.4) 예외가 프로그램의 정상 흐름을 어떻게 바꾸는가
예외가 발생하면 프로그램은 단순히 멈추는 것이 아니라, 프로그램 실행 방식 자체가 근본적으로 바뀝니다. 이 동작을 이해하는 것은 견고한 코드를 작성하는 데 중요하며, 오류가 발생했을 때 무엇이 일어나는지 이해하는 데도 필수입니다.
24.4.1) 정상 프로그램 흐름 vs 예외 흐름
정상 실행에서는 Python이 코드를 위에서 아래로 한 줄씩 실행합니다:
# 정상 프로그램 흐름
print("Step 1: Starting calculation")
result = 10 + 5
print(f"Step 2: Result is {result}")
final = result * 2
print(f"Step 3: Final value is {final}")
print("Step 4: Program complete")Output:
Step 1: Starting calculation
Step 2: Result is 15
Step 3: Final value is 30
Step 4: Program complete모든 줄이 순서대로 실행됩니다. 이제 예외가 발생하면 어떤 일이 일어나는지 보겠습니다:
# 예외가 있는 프로그램 흐름
print("Step 1: Starting calculation")
result = 10 / 0 # 여기서 ZeroDivisionError가 발생합니다
print(f"Step 2: Result is {result}") # 실행되지 않습니다
final = result * 2 # 실행되지 않습니다
print(f"Step 3: Final value is {final}") # 실행되지 않습니다
print("Step 4: Program complete") # 실행되지 않습니다Output:
Step 1: Starting calculation
Traceback (most recent call last):
File "example.py", line 2, in <module>
result = 10 / 0
~~~^~~
ZeroDivisionError: division by zero첫 번째 print만 실행된 것을 확인할 수 있습니다. 2번째 줄에서 예외가 발생하자마자 Python은 나머지 코드를 실행하지 않았습니다. 예외가 정상 흐름을 중단시켰습니다.
24.4.2) 예외는 호출 스택을 따라 전파된다
함수 내부에서 예외가 발생하면 Python은 그 함수에서만 멈추지 않고, 누군가 처리하거나 프로그램이 종료될 때까지 호출 스택을 통해 위로 전파(propagate)됩니다.
# 예제 1: 함수들 사이에서 전파되는 예외
def calculate_average(numbers):
total = sum(numbers)
count = len(numbers)
return total / count # ZeroDivisionError가 발생할 수 있음
def process_scores(score_list):
print("Processing scores...")
avg = calculate_average(score_list)
print(f"Average calculated: {avg}")
return avg
def main():
print("Program starting")
scores = [] # 빈 리스트
result = process_scores(scores)
print(f"Final result: {result}")
print("Program ending")
main()Output:
Program starting
Processing scores...
Traceback (most recent call last):
File "example.py", line 18, in <module>
main()
File "example.py", line 14, in main
result = process_scores(scores)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "example.py", line 9, in process_scores
avg = calculate_average(score_list)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "example.py", line 4, in calculate_average
return total / count
~~~~~~^~~~~~~
ZeroDivisionError: division by zero무슨 일이 있었는지 추적해 봅시다:
main()이 실행을 시작하고 "Program starting"을 출력했습니다main()이process_scores()를 호출했습니다process_scores()가 "Processing scores..."를 출력했습니다process_scores()가calculate_average()를 호출했습니다calculate_average()가 0으로 나누려고 했습니다- 예외가 발생하고 위로 전파되었습니다:
calculate_average()는 즉시 멈췄습니다(값을 반환하지 못함)- 제어가
process_scores()로 돌아갔지만 정상적으로가 아니라, 예외가 계속 전파됨 process_scores()도 즉시 멈췄습니다(calculate_average()뒤의 print는 실행되지 않음)- 제어가
main()으로 돌아갔지만 예외는 계속 전파됨 main()도 즉시 멈췄습니다(process_scores()뒤의 print는 실행되지 않음)
- 프로그램이 트레이스백과 함께 종료되었습니다
어떤 함수에서도 예외 이후의 코드는 실행되지 않았습니다. 예외는 모든 함수 호출을 뚫고 위로 "거품처럼" 올라가 최상위 레벨에 도달하면서 프로그램을 종료시켰습니다.
24.5) 디버깅 사고방식: 오류를 실패가 아닌 정보로 받아들이기
프로그래밍에서 가장 중요한 기술 중 하나는 완벽한 코드를 쓰는 것이 아니라, 불완전한 코드와 효과적으로 함께 일하는 법을 배우는 것입니다. 경험 수준과 관계없이 모든 프로그래머는 오류를 만드는 코드를 작성합니다. 어려움을 겪는 프로그래머와 효과적으로 일하는 프로그래머의 차이는 오류를 피하는 데 있지 않고, 오류에 어떻게 반응하느냐에 있습니다.
24.5.1) 오류는 실패가 아니다
프로그래밍을 배우는 동안 오류를 만났을 때 좌절감을 느끼는 것은 자연스러운 일입니다. 자신이 뭔가 잘못했다고 느끼거나, "이해를 못 하고 있다"고 생각할 수도 있습니다. 이런 사고방식은 생산적이지 않을 뿐 아니라, 더 중요하게는 정확하지도 않습니다.
오류는 실패가 아니라 피드백입니다.
오류를 GPS가 경로를 재탐색하는 것과 같다고 생각해 보세요. 길을 잘못 들었을 때 GPS는 "실패했다!"고 말하지 않습니다. "경로를 재탐색합니다"라고 말하고 새로운 길을 안내합니다. Python의 오류 메시지도 똑같이 동작합니다. 지금 선택한 경로가 작동하지 않았다는 것을 알려주고, 작동하는 경로를 찾도록 돕는 정보를 제공합니다.
간단한 예를 살펴보겠습니다:
# 평균 계산 첫 시도
def calculate_average(numbers):
total = sum(numbers)
average = total / len(numbers)
return average
scores = []
result = calculate_average(scores)
print(f"Average: {result}")Output:
Traceback (most recent call last):
File "example.py", line 8, in <module>
result = calculate_average(scores)
^^^^^^^^^^^^^^^^^^^^^^^^^
File "example.py", line 4, in calculate_average
average = total / len(numbers)
~~~~~~^~~~~~~~~~~~~~
ZeroDivisionError: division by zero이 오류는 여러분이 나쁜 프로그래머라고 말하는 것이 아닙니다. 구체적이고 유용한 정보를 알려줍니다. "0으로 나누려고 했는데, 리스트가 비어 있을 때 이런 일이 생겨. 그 경우를 처리해야 해."
이 정보를 바탕으로 코드를 개선할 수 있습니다:
# 오류 피드백을 바탕으로 개선한 버전
def calculate_average(numbers):
if len(numbers) == 0:
return 0 # 또는 None을 반환하거나, 더 설명적인 오류를 발생시킬 수도 있습니다
total = sum(numbers)
average = total / len(numbers)
return average
scores = []
result = calculate_average(scores)
print(f"Average: {result}") # Output: Average: 0오류가 더 나은 코드를 쓰게 도와준 것입니다. 오류가 없었다면 함수가 빈 리스트를 처리하지 못한다는 것을 알아차리지 못했을 수도 있습니다.
24.5.2) 모든 오류는 무언가를 가르쳐 준다
여러분이 만나는 각 오류는 Python, 여러분의 코드, 또는 일반적인 프로그래밍에 대해 무언가를 가르쳐 줍니다. 서로 다른 오류가 무엇을 알려주는지 예시를 보겠습니다:
예제 1: 타입에 대해 배우기
# 호환되지 않는 타입을 더하려고 시도
age = 25
message = "You are " + age + " years old"Output:
TypeError: can only concatenate str (not "int") to str이것이 가르쳐 주는 것: Python은 엄격한 타입 규칙을 가지고 있습니다. 문자열 연결에서 문자열과 숫자를 섞을 수 없습니다. 이 오류는 타입 호환성과 타입 변환 개념을 알려줍니다.
예제 2: 자료구조에 대해 배우기
# 딕셔너리를 리스트처럼 접근하려고 시도
student = {"name": "Alice", "age": 20}
first_value = student[0]Output:
KeyError: 0이것이 가르쳐 주는 것: 딕셔너리는 숫자 인덱스가 아니라 키를 사용합니다. 이 오류는 딕셔너리와 리스트의 차이, 그리고 딕셔너리 값을 올바르게 접근하는 방법을 알려줍니다.
예제 3: 스코프에 대해 배우기
# 변수를 정의하기 전에 사용하려고 시도
def greet():
print(f"Hello, {name}!")
greet()
name = "Alice"Output:
NameError: name 'name' is not defined이것이 가르쳐 주는 것: 변수는 사용하기 전에 정의되어야 하며, 실행 순서가 중요합니다. 이 오류는 변수 스코프와 초기화의 중요성을 알려줍니다.
이 오류들은 각각 Python을 더 잘 이해하도록 돕는 구체적이고 실행 가능한 정보를 제공합니다. 장애물로 보기보다는 학습 기회로 바라보세요.
24.5.3) 디버깅 사고방식 받아들이기
전문 프로그래머들은 상당한 시간을 디버깅에 사용합니다. 이는 약점의 신호가 아니라 업무의 핵심 부분입니다. 최고의 프로그래머는 실수를 절대 하지 않는 사람이 아니라, 다음을 하는 사람입니다:
- 오류를 예상함: 오류는 일어날 수밖에 없음을 알고 놀라거나 낙담하지 않습니다
- 오류를 꼼꼼히 읽음: 오류 메시지에서 최대한의 정보를 뽑아냅니다
- 체계적으로 디버깅함: 무작정 바꾸는 대신 논리적 과정을 따릅니다
- 오류로부터 배움: 각 오류를 Python을 더 잘 이해하는 기회로 삼습니다
- 호기심을 유지함: "어떻게 고치지?"만이 아니라 "왜 이런 일이 일어났지?"를 묻습니다
기억하세요. 모든 오류는 Python, 프로그래밍, 또는 문제 해결에 대해 새로운 것을 배울 기회입니다. 오류를 가치 있는 피드백으로 받아들이고, 체계적으로 접근하며, 디버깅에 성공했을 때 이를 축하하세요. 이런 사고방식은 여러분의 프로그래밍 여정 내내 큰 도움이 될 것입니다.
오류와 트레이스백을 이해하는 것은 효과적인 Python 프로그래머가 되기 위한 기본입니다. 이 장에서는 문법 오류(코드 구조의 문제)와 런타임 예외(실행 중 문제)를 구분하는 법, 흔한 예외 유형과 그것들이 의미하는 바를 알아보는 법, 자세한 트레이스백을 읽고 해석해 문제의 근본 원인을 찾는 법, 예외가 호출 스택을 따라 위로 전파되면서 프로그램 흐름을 어떻게 바꾸는지 이해하는 법, 그리고 오류를 실패가 아닌 가치 있는 정보로 받아들이는 디버깅 사고방식을 기르는 법을 배웠습니다.
이 기술들은 다음 장의 기반이 됩니다. 다음 장에서는 try와 except 블록을 사용해 예외를 우아하게 처리하는 법을 배우며, 이를 통해 프로그램이 오류에서 회복하고 계속 실행되도록 만들 것입니다. 하지만 예외를 효과적으로 다루기 전에, 먼저 예외를 철저히 이해해야 합니다. 그리고 바로 그것이 우리가 이 장에서 해낸 일입니다.