7-4 다양한 예외 유형과 처리 방법

7-4 다양한 예외 유형과 처리 방법

← 목차로 돌아가기

썸네일

코드 실행 중 예상치 못한 문제가 발생했을 때, 프로그램이 갑자기 멈춰버린다면 사용자 경험은 물론 개발자에게도 큰 어려움이 됩니다. 파이썬은 이러한 예외(Exception) 상황을 체계적으로 관리할 수 있는 강력한 메커니즘을 제공합니다. 이번 장에서는 다양한 예외 유형들을 살펴보고, 각 상황에 맞는 현명한 처리 방법을 배워 견고한 프로그램을 만드는 방법을 알아보겠습니다.

핵심 요약

파이썬 예외 처리는 프로그램의 안정성을 높이는 핵심 기술입니다. 다양한 예외 유형(NameError, TypeError, ValueError, IndexError 등)을 이해하고, try-except 블록을 활용해 상황에 맞춰 유연하게 대처하는 것이 중요합니다. 예외를 의도적으로 발생시키는 raise 키워드와 함께, 지나친 예외 처리보다는 프로그램의 핵심 로직을 보호하는 데 집중해야 합니다.

1. 예외(Exception)란 무엇이며 왜 중요한가요?

예외는 프로그램 실행 중 발생하는 오류 중 하나로, 개발자가 의도한 정상적인 흐름을 벗어나는 상황을 의미합니다.
이전 장에서 예외 처리의 기본 개념을 다루었지만, 여기서 더 깊이 들어가 보겠습니다. 저는 취미로 10년, 현업에서 3년간 파이썬을 다루면서 정말 다양한 예외를 마주했습니다. 파일이 없을 수도 있고, 숫자로 나눠야 할 것을 갑자기 문자로 나눌 수도 있죠. 이런 예외 상황을 제대로 처리하지 않으면 프로그램은 즉시 종료(크래시)되어 버립니다. 이는 사용자에게 매우 불쾌한 경험을 주고, 자동화 스크립트라면 치명적인 문제를 일으킬 수 있습니다. 따라서 예외 처리는 단순히 오류를 막는 것을 넘어, 사용자에게 더 나은 경험을 제공하고 프로그램의 신뢰성을 확보하는 필수적인 과정입니다.

2. 파이썬의 대표적인 예외 유형들을 살펴봅니다.

파이썬에는 코드의 다양한 문제 상황을 나타내는 수많은 내장 예외 클래스가 존재하며, 각 예외는 특정 유형의 오류를 나타냅니다.
이번 섹션에서는 제가 현업에서 가장 자주 마주쳤던 예외들을 중심으로 살펴보겠습니다. 각 예외가 왜 발생하는지 이해하고, 간단한 try-except 구문으로 어떻게 처리하는지 예제를 통해 익혀보시죠.

2.1. NameError: 이름이 정의되지 않았을 때

NameError는 정의되지 않은 변수나 함수를 호출하려 할 때 발생합니다.

파이썬은 변수나 함수를 사용하기 전에 반드시 정의해야 합니다. 만약 오타를 내거나 스코프 밖의 이름을 사용하면 이 예외를 만나게 됩니다.

# NameError 발생 예제
x = 10
print(y) # y는 정의되지 않았습니다.

이 코드가 하는 일: 정의되지 않은 변수 ‘y’를 출력하려 하여 NameError를 발생시킵니다.

# NameError 처리 예제
try:
    x = 10
    print(z)
except NameError:
    print("오류: 정의되지 않은 변수를 사용하려 했습니다.")

이 코드가 하는 일: NameError 발생 시 사용자에게 메시지를 출력하여 프로그램 종료를 방지합니다.

2.2. TypeError: 부적절한 타입 연산

TypeError는 예상치 못한 데이터 타입 간의 연산이나 함수 호출이 발생할 때 나타납니다.

예를 들어, 숫자와 문자열을 직접 더하거나, 함수가 요구하는 타입과 다른 인자를 넘겼을 때 발생합니다. 저도 데이터 분석 코드를 짜다가 숫자 컬럼과 문자열 컬럼을 실수로 더하는 바람에 이 에러를 자주 보곤 했습니다.

# TypeError 발생 예제
"안녕" + 10 # 문자열과 숫자를 직접 더할 수 없습니다.

이 코드가 하는 일: 문자열과 정수를 덧셈 연산하려 하여 TypeError를 발생시킵니다.

# TypeError 처리 예제
try:
    "안녕" + 10
except TypeError:
    print("오류: 서로 다른 데이터 타입끼리 연산할 수 없습니다.")

이 코드가 하는 일: TypeError 발생 시 사용자에게 오류를 알립니다.

2.3. ValueError: 값은 맞지만 적절하지 않을 때

ValueError는 함수의 인자로 유효하지 않은 값이 전달되었을 때 발생합니다.

int() 함수에 숫자로 변환할 수 없는 문자열을 전달하는 것이 대표적인 예입니다. 데이터 타입 변환 시 특히 주의해야 합니다.

# ValueError 발생 예제
int("hello") # 'hello'는 정수로 변환될 수 없습니다.

이 코드가 하는 일: ‘hello’ 문자열을 정수로 변환하려 시도하여 ValueError를 발생시킵니다.

# ValueError 처리 예제
try:
    num_str = "hello"
    num = int(num_str)
    print(num)
except ValueError:
    print(f"오류: '{num_str}'는 유효한 숫자가 아닙니다.")

이 코드가 하는 일: ValueError 발생 시, 입력된 문자열이 숫자가 아님을 사용자에게 알립니다.

2.4. ZeroDivisionError: 0으로 나누려 할 때

ZeroDivisionError는 어떤 수를 0으로 나누려 할 때 발생하는 수학적 오류입니다.

컴퓨터 공학뿐만 아니라 수학의 기본 규칙에서도 0으로 나누는 것은 정의되지 않은 연산입니다. 예측 불가능한 사용자 입력이 있을 때 이 예외가 발생할 가능성이 높습니다.

# ZeroDivisionError 발생 예제
10 / 0 # 어떤 숫자도 0으로 나눌 수 없습니다.

이 코드가 하는 일: 10을 0으로 나누는 연산을 시도하여 ZeroDivisionError를 발생시킵니다.

# ZeroDivisionError 처리 예제
try:
    numerator = 10
    denominator = 0
    result = numerator / denominator
    print(result)
except ZeroDivisionError:
    print("오류: 숫자를 0으로 나눌 수 없습니다.")

이 코드가 하는 일: ZeroDivisionError 발생 시 0으로 나눌 수 없다는 메시지를 출력합니다.

2.5. IndexError & KeyError: 없는 요소에 접근할 때

IndexError는 시퀀스(리스트, 튜플 등)의 범위를 벗어난 인덱스에 접근하려 할 때 발생하며, KeyError는 딕셔너리에 존재하지 않는 키로 접근하려 할 때 발생합니다.

리스트나 딕셔너리 같은 데이터 구조를 다룰 때 리스트, 딕셔너리의 범위나 키 존재 여부를 확인하는 것은 매우 중요합니다.

# IndexError 발생 예제
my_list = [1, 2, 3]
print(my_list[3]) # 인덱스 3은 존재하지 않습니다.

이 코드가 하는 일: 리스트의 범위를 벗어난 인덱스로 접근하여 IndexError를 발생시킵니다.

# KeyError 발생 예제
my_dict = {"name": "Alice", "age": 30}
print(my_dict["city"]) # 'city' 키는 딕셔너리에 없습니다.

이 코드가 하는 일: 딕셔너리에 없는 키로 접근하여 KeyError를 발생시킵니다.

# IndexError & KeyError 처리 예제
try:
    my_list = [1, 2, 3]
    print(my_list[3])
except IndexError:
    print("오류: 리스트 인덱스가 범위를 벗어났습니다.")

try:
    my_dict = {"name": "Alice"}
    print(my_dict["city"])
except KeyError:
    print("오류: 딕셔너리에 해당 키가 존재하지 않습니다.")

이 코드가 하는 일: 각각 IndexErrorKeyError 발생 시 적절한 메시지를 출력합니다.

2.6. FileNotFoundError: 파일이 없을 때

FileNotFoundError는 존재하지 않는 파일을 열거나 접근하려 할 때 발생합니다.

파일 입출력은 예상치 못한 변수가 많은 작업입니다. 사용자가 경로를 잘못 입력하거나, 파일이 다른 곳으로 옮겨지거나 삭제될 수 있습니다. 파일 열고 닫기를 배울 때 이 예외는 특히 중요합니다.

# FileNotFoundError 발생 예제
with open("non_existent_file.txt", "r") as f:
    content = f.read()

이 코드가 하는 일: 존재하지 않는 파일을 읽으려 시도하여 FileNotFoundError를 발생시킵니다.

# FileNotFoundError 처리 예제
try:
    with open("non_existent_file.txt", "r") as f:
        content = f.read()
except FileNotFoundError:
    print("오류: 파일을 찾을 수 없습니다. 경로를 확인해주세요.")
except Exception as e:
    print(f"알 수 없는 파일 오류가 발생했습니다: {e}")

이 코드가 하는 일: FileNotFoundError 발생 시 사용자에게 파일 경로 확인을 요청합니다. 다른 종류의 예외도 함께 처리합니다.

3. 여러 예외를 한 번에 처리하는 방법

파이썬은 하나의 try 블록에서 여러 종류의 예외를 처리할 수 있도록 다양한 except 구문을 제공합니다.
특정 예외에 따라 다른 처리가 필요할 때 여러 except 블록을 사용할 수 있으며, 예상치 못한 모든 예외를 포괄적으로 처리할 수도 있습니다. 저는 자동화 스크립트에서 사용자 입력 처리 시 ValueErrorTypeError를 동시에 처리하는 경우가 많았습니다.

# 여러 예외 처리 예제
def safe_divide():
    try:
        num = int(input("나눠질 숫자를 입력하세요: "))
        den = int(input("나눌 숫자를 입력하세요: "))
        result = num / den
        print(f"결과: {result}")
    except ValueError:
        print("오류: 유효한 숫자를 입력해야 합니다.")
    except ZeroDivisionError:
        print("오류: 0으로 나눌 수 없습니다.")
    except Exception as e: # 그 외 모든 예외 처리
        print(f"알 수 없는 오류가 발생했습니다: {e}")

safe_divide()

이 코드가 하는 일: 사용자 입력에 따른 숫자 나눗셈 중 ValueError, ZeroDivisionError, 그리고 그 외의 모든 예외를 각각 처리합니다.

4. 예외 발생시키기: raise 키워드

raise 키워드를 사용하면 개발자가 특정 조건에서 의도적으로 예외를 발생시킬 수 있습니다.
이는 함수의 입력 값이 유효한지 검사하거나, 특정 상황에서 프로그램이 더 이상 진행될 수 없음을 명확히 알릴 때 유용합니다. 저도 FastAPI로 API를 만들 때 사용자 입력 값 검증이 실패하면 HTTPExceptionraise하여 클라이언트에게 오류를 명확히 전달했습니다.

# raise 키워드 사용 예제
def validate_age(age):
    if not isinstance(age, int):
        raise TypeError("나이는 정수여야 합니다.")
    if not (0 < age < 150):
        raise ValueError("나이는 0보다 크고 150보다 작아야 합니다.")
    print(f"유효한 나이: {age}")

try:
    validate_age("스무살")
except (TypeError, ValueError) as e:
    print(f"검증 오류: {e}")

try:
    validate_age(200)
except (TypeError, ValueError) as e:
    print(f"검증 오류: {e}")

try:
    validate_age(25)
except (TypeError, ValueError) as e:
    print(f"검증 오류: {e}")

이 코드가 하는 일: validate_age 함수는 입력된 나이가 유효한 정수 범위에 있는지 검사하며, 유효하지 않으면 TypeError 또는 ValueError를 발생시킵니다. 이후 try-except 블록에서 이 예외들을 처리합니다.

5. [실전 팁] 예외 처리, 언제 얼마나 해야 할까요?

예외 처리는 코드의 안정성을 높이지만, 과도하거나 잘못된 사용은 오히려 코드 가독성을 해치고 문제를 숨길 수 있습니다.
저의 3년 현업 경험에 따르면, 모든 라인마다 try-except를 남발하는 것은 좋지 않습니다. 중요한 것은 ‘이 예외가 발생했을 때 프로그램이 어떻게 동작해야 하는가?’를 명확히 아는 것입니다. 불필요하게 넓은 범위의 except Exception: 사용은 지양하고, 가능한 한 구체적인 예외를 잡는 것이 좋습니다. 또한, 예외를 잡았을 때 단순히 ‘오류 발생’만 출력하기보다는, 사용자에게 의미 있는 피드백을 주거나 다른 대안을 제시하는 것이 진정한 예외 처리의 목표입니다.

FAQ: 예외 처리에 대해 궁금한 점들

Q1: try-except 없이 그냥 실행하면 안 되나요?
A1: 네, 간단한 스크립트나 확실히 오류가 발생하지 않는 코드라면 try-except 없이 실행할 수 있습니다. 하지만 사용자 입력, 파일 접근, 네트워크 통신 등 외부 요인이 개입될 가능성이 있는 코드에서는 try-except를 사용하지 않으면 예상치 못한 상황에서 프로그램이 강제 종료될 수 있습니다.

Q2: 모든 예외를 except Exception as e:로 처리하는 것이 좋은가요?
A2: 일반적으로 권장되지 않습니다. except Exception as e:는 파이썬의 모든 내장 예외를 한 번에 잡기 때문에, 예상치 못한 심각한 오류까지도 ‘정상적으로’ 처리된 것처럼 보일 수 있습니다. 이로 인해 진짜 문제의 원인을 파악하기 어려워질 수 있으므로, 가능한 한 구체적인 예외 유형을 명시하여 처리하는 것이 좋습니다.

Q3: 특정 예외만 잡고 싶을 땐 어떻게 하나요?
A3: except 키워드 뒤에 처리하고 싶은 예외 클래스 이름을 명시하면 됩니다. 예를 들어, except ValueError:except FileNotFoundError:처럼 사용합니다. 여러 종류의 예외를 한 번에 잡고 싶다면, except (ValueError, TypeError):와 같이 튜플 형태로 묶어줄 수도 있습니다.

참고 자료

  1. [1] Python 공식 튜토리얼 — 오류와 예외 — https://docs.python.org/ko/3/tutorial/errors.html

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다