镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

KR_Exception

somaz edited this page Sep 22, 2026 · 9 revisions

Python 예외 처리(Exception Handling) 개념 정리


1️⃣ 예외 처리(Exception Handling)

예외 처리는 프로그램 실행 중 발생할 수 있는 오류를 처리하는 방법이다.

기본 구조

try:
    # 실행할 코드
    result = 10 / 0
except ZeroDivisionError as e:
    # 예외 발생시 실행할 코드
    print(f"에러 발생: {e}")
else:
    # 예외가 발생하지 않았을 때 실행할 코드
    print("성공!")
finally:
    # 예외 발생 여부와 관계없이 실행할 코드
    print("종료")

try-except의 동작 원리

# 기본 예외 처리
try:
    num = int(input("숫자를 입력하세요: "))
    result = 100 / num
    print(f"결과: {result}")
except ValueError:
    print("올바른 숫자를 입력하세요.")
except ZeroDivisionError:
    print("0으로 나눌 수 없습니다.")

예외 처리 흐름:

  1. try 블록의 코드가 실행된다.
  2. 예외가 발생하면 해당 예외와 일치하는 except 블록이 실행된다.
  3. 일치하는 except가 없으면 예외는 상위 호출자로 전파된다.
  4. 예외가 발생하지 않으면 else 블록이 실행된다.
  5. 마지막으로 finally 블록이 항상 실행된다.

예외 처리의 응용

def safe_division(a, b):
    try:
        result = a / b
    except ZeroDivisionError:
        print("0으로 나눌 수 없습니다.")
        return None
    except TypeError:
        print("숫자만 나눌 수 있습니다.")
        return None
    else:
        print("나눗셈 성공!")
        return result
    finally:
        print("나눗셈 연산 종료")

# 테스트
print(safe_division(10, 2))    # 정상 케이스
print(safe_division(10, 0))    # 0으로 나누기
print(safe_division(10, "2"))  # 타입 에러


2️⃣ 주요 예외 유형

Python은 다양한 내장 예외 클래스를 제공한다. 모든 예외는 BaseException 클래스에서 파생된다.

# 기본적인 내장 예외들
try:
    # IndexError
    list1 = [1, 2, 3]
    print(list1[4])
except IndexError:
    print("리스트 인덱스 범위 초과")

try:
    # KeyError
    dict1 = {"a": 1}
    print(dict1["b"])
except KeyError:
    print("존재하지 않는 키")

try:
    # ValueError
    int("abc")
except ValueError:
    print("잘못된 값 변환")

주요 내장 예외 클래스

  • Exception: 대부분의 내장 예외의 기본 클래스
  • ArithmeticError: 모든 산술 연산 관련 오류의 기본 클래스
    • ZeroDivisionError: 0으로 나누기 시도
    • OverflowError: 연산 결과가 너무 큼
  • LookupError: 매핑 또는 시퀀스 관련 오류의 기본 클래스
    • IndexError: 시퀀스에서 범위를 벗어난 인덱스
    • KeyError: 매핑(딕셔너리)에서 존재하지 않는 키
  • ValueError: 값이 적절하지 않음 (올바른 타입이지만 부적절한 값)
  • TypeError: 부적절한 타입의 객체에 연산 적용
  • NameError: 존재하지 않는 변수 참조
  • AttributeError: 객체에 존재하지 않는 속성 참조
  • SyntaxError: 파이썬 문법 오류 (컴파일 시간 예외)
  • RuntimeError: 프로그램 실행 중 발생한 일반적인 오류
  • FileNotFoundError: 파일이 존재하지 않음
  • PermissionError: 파일 접근 권한 부족
  • ImportError/ModuleNotFoundError: 모듈 가져오기 실패
  • MemoryError: 메모리 부족
  • RecursionError: 최대 재귀 깊이 초과

예외 계층 구조 이해하기

# 예외 타입 계층 구조 확인
def print_exception_hierarchy(exception_class, indent=0):
    print(' ' * indent + exception_class.__name__)
    for subclass in exception_class.__subclasses__():
        print_exception_hierarchy(subclass, indent + 4)

# 일부 예외 계층 구조 출력
print_exception_hierarchy(BaseException)

# 상속 관계를 이용한 예외 처리
try:
    # 다양한 산술 예외 가능성이 있는 코드
    result = 1 / 0
except ArithmeticError as e:
    print(f"수학 연산 오류: {type(e).__name__} - {e}")


3️⃣ 사용자 정의 예외

표준 라이브러리의 내장 예외만으로 모든 오류 상황을 표현하기 어려울 때 사용자 정의 예외를 만들 수 있다.

class CustomError(Exception):
    def __init__(self, message):
        self.message = message
    
    def __str__(self):
        return self.message

def validate_age(age):
    if age < 0:
        raise CustomError("나이는 음수일 수 없습니다")
    return age

사용자 정의 예외 확장

# 기본 예외 클래스
class AppError(Exception):
    """애플리케이션 기본 예외"""
    def __init__(self, message="애플리케이션 오류가 발생했습니다", code=None):
        self.message = message
        self.code = code
        super().__init__(self.message)

# 특정 기능별 예외
class ValidationError(AppError):
    """입력값 검증 예외"""
    def __init__(self, message="입력값이 유효하지 않습니다", field=None):
        self.field = field
        message_with_field = f"{message} (필드: {field})" if field else message
        super().__init__(message_with_field, code="VALIDATION_ERROR")

class DatabaseError(AppError):
    """데이터베이스 작업 예외"""
    def __init__(self, message="데이터베이스 오류", operation=None):
        self.operation = operation
        message_with_op = f"{message} (작업: {operation})" if operation else message
        super().__init__(message_with_op, code="DB_ERROR")

# 예외 사용
def register_user(username, age):
    if not username:
        raise ValidationError("사용자 이름은 필수입니다", "username")
    
    if not isinstance(age, int) or age < 0 or age > 150:
        raise ValidationError("나이는 0-150 사이의 정수여야 합니다", "age")
    
    # 데이터베이스 저장 시뮬레이션
    if username == "admin":
        raise DatabaseError("사용자 저장 실패", "INSERT")
    
    return {"username": username, "age": age}

# 예외 처리
try:
    user = register_user("", 25)
except ValidationError as e:
    print(f"검증 오류: {e.message}, 코드: {e.code}, 필드: {e.field}")
except DatabaseError as e:
    print(f"DB 오류: {e.message}, 코드: {e.code}, 작업: {e.operation}")
except AppError as e:
    print(f"일반 오류: {e.message}, 코드: {e.code}")


4️⃣ 예외 처리 패턴

여러 예외 처리

# 여러 예외 처리
try:
    value = input("값을 입력하세요: ")
    num = int(value)
    result = 100 / num
    print(f"결과: {result}")
except (TypeError, ValueError) as e:
    # 여러 예외를 한번에 처리
    print(f"타입 또는 값 에러: {e}")
except ZeroDivisionError as e:
    print(f"0으로 나눌 수 없습니다: {e}")
except Exception as e:
    # 모든 예외 처리 (마지막 예외 처리자로만 사용)
    print(f"기타 에러: {e}")

예외 회피 및 다시 발생

# 예외 회피 (일부러 무시)
def ignore_exception():
    try:
        # 실패해도 괜찮은 작업
        risky_operation()
    except Exception:
        pass  # 예외 무시

# 예외 다시 발생 (예외 체인)
def process_data(data):
    try:
        return process_value(data)
    except ValueError as e:
        # 추가 정보와 함께 예외 다시 발생
        raise ValueError(f"데이터 처리 실패: {data}") from e

# 현재 예외 정보 접근
import sys

def log_exception():
    try:
        1 / 0
    except:
        # 현재 예외 정보 확인
        exc_type, exc_value, exc_traceback = sys.exc_info()
        print(f"예외 유형: {exc_type}")
        print(f"예외 값: {exc_value}")
        
        # 예외 추적 정보 출력
        import traceback
        traceback.print_exc()  # 간단한 스택 추적 정보 출력

assert 문

def calculate_average(numbers):
    # 조건 확인 및 실패 시 AssertionError 발생
    assert len(numbers) > 0, "리스트가 비어있습니다"
    return sum(numbers) / len(numbers)

# assert 사용 예
try:
    avg = calculate_average([])
except AssertionError as e:
    print(f"어설션 오류: {e}")

EAFP vs LBYL 패턴

Python에서는 보통 두 가지 코딩 스타일을 사용한다:

  • EAFP (Easier to Ask for Forgiveness than Permission): 허락보다 용서가 쉽다
  • LBYL (Look Before You Leap): 도약하기 전에 살펴본다
# LBYL 방식 (사전 검사)
def lbyl_style(data, key):
    if key in data:  # 먼저 확인
        return data[key]
    else:
        return None

# EAFP 방식 (예외 처리)
def eafp_style(data, key):
    try:
        return data[key]  # 일단 시도
    except KeyError:
        return None

# Python은 일반적으로 EAFP 스타일을 선호한다


5️⃣ 컨텍스트 관리자

컨텍스트 관리자는 with 문과 함께 사용되며, 자원 관리를 자동화한다.

# with 문을 사용한 자원 관리
with open('file.txt', 'r') as f:
    content = f.read()
    # 파일이 자동으로 닫힘 (예외 발생해도 닫힘)

자체 컨텍스트 관리자 작성

# 클래스 기반 컨텍스트 관리자
class MyContextManager:
    def __init__(self, name):
        self.name = name
    
    def __enter__(self):
        print(f"{self.name} 컨텍스트 시작")
        return self  # with 문의 as 변수에 할당됨
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        print(f"{self.name} 컨텍스트 종료")
        # 예외 정보 출력
        if exc_type is not None:
            print(f"예외 발생: {exc_type.__name__} - {exc_val}")
        # True 반환 시 예외를 처리한 것으로 간주
        # False 또는 None 반환 시 예외를 호출자에게 전파
        return False  # 예외 전파

# 사용 예시
try:
    with MyContextManager("테스트") as cm:
        print("작업 수행 중...")
        raise ValueError("테스트 예외")
    print("이 줄은 예외가 발생하면 실행되지 않음")
except ValueError:
    print("예외가 전파되었습니다.")

contextlib을 사용한 컨텍스트 관리자

from contextlib import contextmanager

@contextmanager
def my_context(name):
    print(f"{name} 컨텍스트 시작")
    try:
        # yield 이전: __enter__에 해당
        yield name  # 컨텍스트 내부로 값 전달
        # yield 이후: 정상 종료 시 __exit__에 해당
        print(f"{name} 컨텍스트 정상 종료")
    except Exception as e:
        # 예외 발생 시 __exit__에 해당
        print(f"{name} 컨텍스트 예외 종료: {e}")
        # 다시 발생시키면 호출자에게 예외 전파
        raise

# 사용 예시
with my_context("데코레이터") as value:
    print(f"컨텍스트 내부, 값: {value}")
    # 예외 발생 시 컨텍스트 관리자가 처리
    # raise ValueError("테스트 예외")

컨텍스트 관리자 응용

# 임시 디렉터리 컨텍스트 관리자
import tempfile
import shutil
import os

@contextmanager
def temporary_directory():
    temp_dir = tempfile.mkdtemp()
    print(f"임시 디렉터리 생성: {temp_dir}")
    try:
        yield temp_dir
    finally:
        print(f"임시 디렉터리 삭제: {temp_dir}")
        shutil.rmtree(temp_dir)

# 데이터베이스 트랜잭션 컨텍스트 관리자 (가상)
@contextmanager
def transaction():
    print("트랜잭션 시작")
    try:
        yield
        print("트랜잭션 커밋")
    except:
        print("트랜잭션 롤백")
        raise

# 사용 예시
with temporary_directory() as temp_dir:
    # 임시 파일 생성
    file_path = os.path.join(temp_dir, "test.txt")
    with open(file_path, "w") as f:
        f.write("테스트 데이터")
    print(f"파일 생성됨: {file_path}")
    # 컨텍스트 종료 시 임시 디렉터리와 파일 삭제


6️⃣ 효과적인 예외 처리 전략

올바른 예외 처리 수준 선택

# 세분화된 예외 처리
def process_user_input():
    try:
        # 작은 try 블록으로 특정 작업만 보호
        age = int(input("나이를 입력하세요: "))
    except ValueError:
        print("나이는 숫자로 입력해야 합니다.")
        return None
    
    try:
        # 다른 작업은 별도의 try 블록으로 보호
        name = input("이름을 입력하세요: ")
        if not name:
            raise ValueError("이름은 필수입니다.")
    except ValueError as e:
        print(f"입력 오류: {e}")
        return None
    
    # 성공 시 결과 반환
    return {"name": name, "age": age}

# 대비 - 너무 큰 try 블록 (안티 패턴)
def process_user_input_bad():
    try:
        # 너무 많은 로직을 하나의 try에 포함
        age = int(input("나이를 입력하세요: "))
        name = input("이름을 입력하세요: ")
        if not name:
            raise ValueError("이름은 필수입니다.")
        return {"name": name, "age": age}
    except Exception as e:
        # 너무 일반적인 예외 처리
        print(f"오류 발생: {e}")
        return None

에러 로깅과 처리

import logging

# 로깅 설정
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

def divide_numbers(a, b):
    try:
        result = a / b
        logger.info(f"나눗셈 성공: {a} / {b} = {result}")
        return result
    except ZeroDivisionError:
        # 예외 로깅 (적절한 레벨 선택)
        logger.error(f"0으로 나누기 시도: {a} / {b}", exc_info=True)
        # 사용자에게 친숙한 메시지 반환
        return "0으로 나눌 수 없습니다"
    except TypeError as e:
        logger.exception(f"타입 오류: {a} / {b}")  # 전체 스택 추적 로깅
        return f"숫자만 나눌 수 있습니다: {e}"

예외 처리와 클린코드

# 집중화된 오류 처리
def get_user_by_id(user_id):
    try:
        # 데이터베이스 액세스 코드
        user = database.find_user(user_id)
        return user
    except DatabaseConnectionError:
        log_error("데이터베이스 연결 실패")
        raise ServiceUnavailableError("서비스가 일시적으로 불가능합니다")
    except UserNotFoundError:
        return None  # 사용자가 없으면 None 반환
    except Exception as e:
        log_error(f"사용자 조회 중 알 수 없는 오류: {e}")
        raise InternalServerError("내부 서버 오류가 발생했습니다")

# 조기 반환과 가드 조항 (Guard Clause)
def process_payment(payment):
    # 전제 조건 검사 - 빠른 실패(Fail Fast) 전략
    if not payment:
        raise ValueError("결제 정보가 없습니다")
    
    if not payment.get('amount'):
        raise ValueError("결제 금액이 없습니다")
    
    if payment.get('amount') <= 0:
        raise ValueError("결제 금액은 양수여야 합니다")
    
    # 전제 조건이 충족되면 주요 로직 수행
    return process_valid_payment(payment)

주요 팁

✅ 예외 처리 모범 사례:

  • 구체적인 예외를 먼저 처리하고, 일반적인 예외를 나중에 처리한다
  • 예외 계층 구조를 활용하여 관련 예외를 그룹화한다
  • 최소한의 코드만 try 블록에 포함시켜 오류 원인을 명확히 한다
  • 매우 큰 try-except 블록은 피하고 적절한 크기로 나눈다
  • 비어 있는 except 블록은 특별한 이유가 없다면 사용하지 않는다
  • 표준 라이브러리 예외를 재사용할 수 있다면 사용자 정의 예외 대신 사용한다
  • 의미 있는 오류 메시지를 제공하고 가능하면 해결 방법도 제시한다
  • 디버깅에 도움되는 정보를 로그에 기록한다
  • 사용자 정의 예외는 의미 있는 계층 구조로 설계한다
  • 자원 관리를 위해 항상 컨텍스트 관리자(with 문)를 사용한다


Clone this wiki locally