본문으로 건너뛰기
Advertisement

내장 예외 계층 구조 — Exception 트리와 올바른 예외 선택

Python은 수십 개의 내장 예외 클래스를 제공합니다. 이 예외들은 계층 구조로 조직되어 있으며, 올바른 예외를 선택하는 것이 견고한 코드의 첫걸음입니다.


BaseException 계층 구조 트리

BaseException
├── SystemExit # sys.exit() 호출 시
├── KeyboardInterrupt # Ctrl+C 입력 시
├── GeneratorExit # 제너레이터 .close() 호출 시
└── Exception # 프로그램 오류의 기반 클래스
├── ArithmeticError
│ ├── ZeroDivisionError
│ ├── OverflowError
│ └── FloatingPointError
├── AttributeError # 존재하지 않는 속성 접근
├── EOFError # input() EOF 도달
├── ImportError
│ └── ModuleNotFoundError
├── LookupError
│ ├── IndexError # 리스트 범위 초과
│ └── KeyError # 딕셔너리 키 없음
├── MemoryError
├── NameError
│ └── UnboundLocalError
├── OSError # 파일/IO 오류의 기반
│ ├── FileNotFoundError
│ ├── FileExistsError
│ ├── PermissionError
│ ├── TimeoutError
│ └── IsADirectoryError
├── RuntimeError
│ ├── RecursionError
│ └── NotImplementedError
├── StopIteration # 이터레이터 종료
├── TypeError # 잘못된 타입
├── ValueError # 올바른 타입이지만 잘못된 값
│ └── UnicodeError
│ ├── UnicodeDecodeError
│ └── UnicodeEncodeError
└── Warning # 경고의 기반 클래스
├── DeprecationWarning
├── RuntimeWarning
└── UserWarning

Exception vs BaseException 차이

절대로 BaseException을 직접 잡지 마십시오.

# 잘못된 예: SystemExit, KeyboardInterrupt까지 잡아버림
try:
do_something()
except BaseException:
pass # Ctrl+C 로도 프로그램이 종료되지 않음!

# 올바른 예: 프로그램 오류만 처리
try:
do_something()
except Exception:
pass

# Ctrl+C, sys.exit()는 의도적으로 잡을 때만
try:
long_running_task()
except KeyboardInterrupt:
print("사용자가 중단했습니다. 정리 중...")
cleanup()

SystemExitKeyboardInterrupt는 프로그램 제어에 관한 신호이므로 일반 예외 처리에서 제외해야 합니다.


주요 내장 예외 상세

ValueError — 올바른 타입이지만 잘못된 값

# 발생 시나리오
int("abc") # ValueError: invalid literal
int("3.14") # ValueError: invalid literal (소수점 문자열)
[].pop() # ValueError: pop from empty list (IndexError 아님!)
"hello".index("z") # ValueError: substring not found

# 올바른 처리
def parse_age(value: str) -> int:
try:
age = int(value)
if age < 0 or age > 150:
raise ValueError(f"나이 범위 초과: {age}")
return age
except ValueError as e:
print(f"유효하지 않은 나이: {e}")
return 0

TypeError — 잘못된 타입 사용

# 발생 시나리오
"hello" + 5 # TypeError: can only concatenate str
len(42) # TypeError: object of type 'int' has no len()
None() # TypeError: 'NoneType' is not callable

# 타입 검사로 예방
def add_numbers(a, b):
if not isinstance(a, (int, float)):
raise TypeError(f"숫자 타입 필요, 받음: {type(a).__name__}")
return a + b

KeyError — 딕셔너리 키 없음

# 발생 시나리오
data = {"name": "Alice"}
data["age"] # KeyError: 'age'

# 올바른 처리 방법들
# 방법 1: get() 메서드
age = data.get("age", 0) # 기본값 반환

# 방법 2: in 연산자
if "age" in data:
age = data["age"]

# 방법 3: try-except
try:
age = data["age"]
except KeyError:
age = 0

# 방법 4: defaultdict
from collections import defaultdict
counts = defaultdict(int)
counts["a"] += 1 # KeyError 없음

IndexError — 시퀀스 범위 초과

# 발생 시나리오
items = [1, 2, 3]
items[5] # IndexError: list index out of range
items[-10] # IndexError: list index out of range

# 안전한 접근 패턴
def safe_get(lst: list, index: int, default=None):
try:
return lst[index]
except IndexError:
return default

# 또는 조건 검사
if 0 <= index < len(items):
value = items[index]

AttributeError — 존재하지 않는 속성

# 발생 시나리오
x = None
x.name # AttributeError: 'NoneType' has no attribute 'name'
"hello".upper() # 이건 정상 — upper 메서드 존재
(1, 2).append(3) # AttributeError: 'tuple' has no attribute 'append'

# 안전한 속성 접근
name = getattr(obj, "name", "unknown") # 기본값 사용
hasattr(obj, "name") # 존재 여부 확인

OSError / FileNotFoundError — 파일 및 I/O 오류

# OSError 계층: FileNotFoundError, PermissionError, etc.
import os

def safe_read(path: str) -> str:
try:
with open(path, "r", encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
print(f"파일 없음: {path}")
except PermissionError:
print(f"권한 없음: {path}")
except IsADirectoryError:
print(f"디렉터리입니다: {path}")
except OSError as e:
# 위에서 잡히지 않은 OS 관련 오류
print(f"IO 오류 ({e.errno}): {e.strerror}")
return ""

errno 속성으로 OS 수준 오류 코드를 확인할 수 있습니다.

import errno

try:
open("/nonexistent/path/file.txt")
except OSError as e:
if e.errno == errno.ENOENT:
print("파일 없음")
elif e.errno == errno.EACCES:
print("접근 거부")

여러 예외를 한 번에 잡기 — except (A, B)

괄호 안에 예외 타입을 튜플로 나열하면 여러 예외를 하나의 except 절로 처리할 수 있습니다.

def parse_input(value: str) -> int:
try:
return int(value)
except (ValueError, TypeError):
# ValueError: 숫자가 아닌 문자열
# TypeError: None 등 int() 불가 타입
return 0

# 여러 OS 오류를 하나로
def read_data(path: str) -> bytes:
try:
with open(path, "rb") as f:
return f.read()
except (FileNotFoundError, IsADirectoryError):
return b""
except (PermissionError, OSError) as e:
print(f"읽기 실패: {e}")
return b""

주의: except (A, B) as e에서 e는 발생한 예외 인스턴스입니다. 두 예외 타입을 모두 처리하지만, 어떤 타입인지 구분하려면 isinstance(e, A)로 확인합니다.


올바른 예외 선택 가이드

상황올바른 예외
잘못된 인수 값ValueError
잘못된 인수 타입TypeError
존재하지 않는 딕셔너리 키KeyError
리스트/튜플 인덱스 범위 초과IndexError
파일 없음FileNotFoundError
권한 없음PermissionError
구현되지 않은 기능NotImplementedError
추상 기반 클래스 직접 인스턴스화TypeError
재귀 깊이 초과RecursionError
모듈 없음ModuleNotFoundError
일반 런타임 오류RuntimeError

except Exception vs bare except

# bare except — 절대 사용 금지
try:
risky()
except: # SystemExit, KeyboardInterrupt 포함!
pass

# except Exception — 허용. 프로그램 오류만 포착
try:
risky()
except Exception:
pass

# 특정 예외 명시 — 가장 권장
try:
risky()
except (ValueError, RuntimeError) as e:
handle(e)

bare exceptexcept BaseException과 동일하게 동작합니다. Ctrl+C(KeyboardInterrupt)나 sys.exit()(SystemExit)도 잡아버리기 때문에 프로그램이 의도치 않게 종료되지 않는 버그를 만들 수 있습니다.


실전 예제 — 예외 타입 기반 분기 처리

from typing import Any
import json
import os

def load_json_file(path: str) -> dict[str, Any]:
"""
JSON 파일을 읽고 적절한 예외 메시지를 제공합니다.
"""
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
raise FileNotFoundError(
f"설정 파일을 찾을 수 없습니다: {path}\n"
f"현재 디렉터리: {os.getcwd()}"
)
except PermissionError:
raise PermissionError(
f"파일 읽기 권한이 없습니다: {path}"
)
except json.JSONDecodeError as e:
raise ValueError(
f"JSON 파싱 실패 ({path}:{e.lineno}:{e.colno}): {e.msg}"
) from e
except UnicodeDecodeError as e:
raise ValueError(
f"파일 인코딩 오류 ({path}): UTF-8이 아닌 파일입니다"
) from e

고수 팁

팁 1 — LookupError를 활용한 통합 처리

IndexErrorKeyError는 모두 LookupError의 서브클래스입니다. 컨테이너의 종류가 불확실할 때 활용합니다.

def safe_lookup(container, key):
try:
return container[key]
except LookupError:
return None

팁 2 — ArithmeticError로 수학 오류 통합

def safe_math(a, b, op):
try:
if op == "/":
return a / b
elif op == "**":
return a ** b
except ArithmeticError as e:
# ZeroDivisionError, OverflowError 모두 처리
print(f"수학 오류: {type(e).__name__}: {e}")
return None

팁 3 — 예외 계층을 직접 탐색하기

# 예외의 MRO(Method Resolution Order) 확인
print(FileNotFoundError.__mro__)
# (<class 'FileNotFoundError'>, <class 'OSError'>,
# <class 'Exception'>, <class 'BaseException'>, <class 'object'>)

# 특정 예외가 다른 예외의 서브클래스인지 확인
issubclass(FileNotFoundError, OSError) # True
issubclass(KeyError, LookupError) # True

팁 4 — Python 3.12 Exception Notes

Python 3.11+에서는 add_note() 메서드로 예외에 추가 메모를 붙일 수 있습니다.

try:
process_data(raw_data)
except ValueError as e:
e.add_note(f"원본 데이터: {raw_data!r}")
e.add_note("데이터 형식 문서를 확인하세요.")
raise
Advertisement