모든 강의를 무료로 볼 수 있어요. 회원가입 없이도 학습 가능합니다.

38강 / 전체 40강

예외 처리 — 오류를 다루는 설계 철학

11분 읽기 조회 0

파이썬 예외 계층을 이해하고 try/except/else/finally, 예외 연쇄, 사용자 정의 예외, EAFP 철학을 완전히 마스터한다.

🎯 이 강의에서 배우는 것

예외(Exception)는 프로그램 실행 중 발생하는 비정상적 상황입니다. 파이썬에서 예외는 객체이며, 예외가 발생하면 호출 스택을 거슬러 올라가며 처리자(handler)를 탐색합니다. 올바른 예외 처리는 오류를 숨기는 것이 아니라 명확히 전달하는 것입니다. 어디서 잡고, 어떻게 복구하고, 어떤 예외를 새로 만들지 설계하는 방법을 배웁니다.

🌳 파이썬 예외 계층 구조

# 파이썬 예외 계층:
# BaseException
#   ├── SystemExit           (sys.exit() 호출 시)
#   ├── KeyboardInterrupt    (Ctrl+C)
#   ├── GeneratorExit
#   └── Exception            ← 대부분의 예외가 여기서 상속
#         ├── ValueError     (잘못된 값)
#         ├── TypeError      (잘못된 타입)
#         ├── KeyError       (딕셔너리 키 없음)
#         ├── IndexError     (인덱스 범위 초과)
#         ├── AttributeError (속성 없음)
#         ├── NameError      (변수명 없음)
#         ├── ZeroDivisionError
#         ├── StopIteration
#         ├── RuntimeError
#         └── OSError
#               ├── FileNotFoundError
#               ├── PermissionError
#               └── TimeoutError

# 예외 정보 확인
try:
    result = 10 / 0
except ZeroDivisionError as e:
    print(type(e))           # <class 'ZeroDivisionError'>
    print(e)                 # division by zero
    print(e.__class__.__name__)
    print(e.args)            # ('division by zero',)

# isinstance로 계층 확인
try:
    int("hello")
except ValueError as e:
    print(isinstance(e, ValueError))    # True
    print(isinstance(e, Exception))     # True (부모)
    print(isinstance(e, BaseException)) # True (더 위 부모)

🔁 try/except/else/finally 완전 분석

def read_and_parse(filename: str) -> dict:
    try:
        # 예외가 발생할 수 있는 코드
        f = open(filename, encoding="utf-8")
    except FileNotFoundError:
        # try에서 FileNotFoundError 발생 시
        print(f"파일 없음: {filename}")
        return {}
    except PermissionError as e:
        # 여러 except 절 — 순서대로 확인 (위에서 아래)
        print(f"권한 없음: {e}")
        return {}
    except (TypeError, ValueError) as e:
        # 여러 예외를 하나로 묶기
        print(f"타입/값 오류: {e}")
        return {}
    else:
        # try가 예외 없이 성공했을 때만 실행
        # (finally와 달리 예외 없을 때만!)
        content = f.read()
        f.close()
        return {"content": content, "lines": content.count("\n")}
    finally:
        # 예외 여부와 무관하게 항상 실행
        # 파일 닫기, 네트워크 연결 해제 등 정리 코드
        print("read_and_parse 완료")

# 실행 흐름:
# 예외 발생 → try 블록 중단 → except 확인 → finally 실행
# 예외 없음  → try 블록 완료 → else 실행 → finally 실행

# 여러 예외를 동시에 잡기
import json

def parse_json(text: str):
    try:
        return json.loads(text)
    except json.JSONDecodeError as e:
        print(f"JSON 파싱 오류 (줄 {e.lineno}): {e.msg}")
        return None
    except UnicodeDecodeError as e:
        print(f"인코딩 오류: {e}")
        return None

print(parse_json('{"key": "value"}'))    # {'key': 'value'}
print(parse_json('invalid json'))         # 오류 출력 후 None

🔗 raise와 예외 연쇄

class DatabaseError(Exception):
    pass

# raise: 새 예외 발생
def get_user(user_id: int):
    if user_id <= 0:
        raise ValueError(f"유효하지 않은 user_id: {user_id}")

# raise ... from ...: 원인 예외 보존 (예외 연쇄)
def query_db(sql: str):
    try:
        # 실제로는 DB 쿼리
        raise ConnectionError("DB 연결 실패")   # 시뮬레이션
    except ConnectionError as e:
        raise DatabaseError("사용자 조회 실패") from e
        # "raise X from Y": X.__cause__ = Y 설정
        # 트레이스백에 원인 예외도 표시됨

try:
    query_db("SELECT * FROM users")
except DatabaseError as e:
    print(f"에러: {e}")
    print(f"원인: {e.__cause__}")   # ConnectionError: DB 연결 실패

# raise: 현재 예외 재발생 (컨텍스트 잃지 않고)
def process():
    try:
        raise ValueError("처리 오류")
    except ValueError:
        print("로그 기록...")
        raise   # 잡았다가 다시 던지기

🏛️ 사용자 정의 예외 설계

# 도메인 예외 계층 설계
class AppError(Exception):
    """애플리케이션 기본 예외"""
    pass

class ValidationError(AppError):
    """입력값 검증 실패"""
    def __init__(self, field: str, message: str):
        self.field = field
        self.message = message
        super().__init__(f"{field}: {message}")

class NotFoundError(AppError):
    """리소스 없음"""
    def __init__(self, resource: str, resource_id):
        self.resource = resource
        self.resource_id = resource_id
        super().__init__(f"{resource}(id={resource_id})를 찾을 수 없습니다")

class AuthorizationError(AppError):
    """권한 없음"""
    pass

# 활용
def create_user(name: str, age: int) -> dict:
    if not name:
        raise ValidationError("name", "이름은 비워둘 수 없습니다")
    if age < 0 or age > 150:
        raise ValidationError("age", f"나이가 범위를 벗어났습니다: {age}")
    return {"name": name, "age": age}

def get_user(user_id: int) -> dict:
    users = {1: {"name": "홍길동"}}
    if user_id not in users:
        raise NotFoundError("User", user_id)
    return users[user_id]

# 계층적 처리
try:
    create_user("", 25)
except ValidationError as e:
    print(f"검증 실패 — {e.field}: {e.message}")
except AppError as e:
    print(f"앱 오류: {e}")

try:
    get_user(999)
except NotFoundError as e:
    print(f"없음: {e.resource} #{e.resource_id}")

🦅 EAFP vs LBYL — 파이썬의 철학

# LBYL: Look Before You Leap (확인 후 실행)
def get_value_lbyl(d: dict, key: str):
    if key in d:         # 먼저 확인
        return d[key]
    return None

# EAFP: Easier to Ask Forgiveness than Permission (먼저 실행, 실패 처리)
def get_value_eafp(d: dict, key: str):
    try:
        return d[key]    # 먼저 시도
    except KeyError:
        return None

# 파이썬은 EAFP 권장 (특히 dict 접근, 파일 처리, 변환)
# LBYL의 문제: TOCTOU(Time-Of-Check-Time-Of-Use) 경쟁 조건 가능
# 멀티스레드에서 확인과 사용 사이에 상태가 바뀔 수 있음

# 파일 처리: EAFP가 더 안전
def read_file_eafp(path: str) -> str:
    try:
        with open(path) as f:
            return f.read()
    except FileNotFoundError:
        return ""
    except PermissionError:
        raise   # 권한 오류는 호출자에게 전달

# 타입 변환: EAFP
def to_int(value) -> int | None:
    try:
        return int(value)
    except (ValueError, TypeError):
        return None

print(to_int("42"))    # 42
print(to_int("abc"))   # None
print(to_int(None))    # None

⚠️ 자주 하는 실수

  • 예외 삼키기 (빈 except 블록): except: pass는 모든 예외를 무시해 버그를 숨깁니다. 최소한 logging.exception(e)로 기록하거나, 어떤 예외를 처리할지 명시하세요.
  • 너무 넓게 잡기 (except Exception): except Exception은 메모리 오류를 제외한 모든 예외를 잡습니다. 처리 가능한 특정 예외만 잡고, 나머지는 호출 스택 위로 전파하세요.
  • finally에서 return 사용: finally 블록에서 return을 사용하면 try나 except의 return을 덮어쓰고, 진행 중인 예외도 삼켜버립니다. finally에서는 return을 절대 쓰지 마세요.

📝 정리 및 다음 강의 예고

  • 예외 계층: BaseException → Exception → 구체적 예외. 좁은 예외부터 잡아야 순서가 의미 있음.
  • try/except/else/finally: else는 예외 없을 때만, finally는 항상 실행.
  • raise X from Y: 예외 연쇄로 원인 보존. raise: 현재 예외 재발생.
  • 도메인 예외 설계: AppError → 구체적 오류. 필드와 메시지를 담은 풍부한 예외 클래스.
  • EAFP(try-except) vs LBYL(if-else): 파이썬은 EAFP 선호.

다음 강의: 파일 I/O와 컨텍스트 매니저 — with 문의 내부 원리, 텍스트·바이너리 파일 처리, pathlib로 파일 시스템 탐색, 커스텀 컨텍스트 매니저를 배웁니다.

관련 주제

  • 예외 계층 구조
  • try/except/else/finally
  • raise from 예외연쇄
  • 사용자 정의 예외
  • EAFP vs LBYL
  • 예외 처리 원칙
  • 개발·프로그래밍
  • 개발·프로그래밍 강의
  • 파이썬 기초 40강 — 처음 배우는 프로그래밍
  • 무료강의
  • 무료 온라인 강의
  • NUGUNA
  • 누구나

📚 시리즈 전체 공유

파이썬 기초 40강 — 처음 배우는 프로그래밍

이 강의가 속한 시리즈는 총 40강, 모두 무료입니다. 처음부터 배우려는 동료에게 시리즈 전체를 알려 주세요.

댓글

0/1000

불러오는 중...