프로그래밍/Python

파이썬 예외 처리: 기본부터 고급 기법까지

shimdh 2025. 2. 21. 10:52
728x90

1. 예외 처리의 기본: try-except 블록

예외 처리는 프로그램 실행 중 발생할 수 있는 오류를 관리하는 방법입니다. 파이썬에서는 try-except 블록을 사용하여 이러한 오류를 우아하게 처리할 수 있습니다.

1.1 try-except 블록의 기본 구조

try 블록 안에는 오류가 발생할 가능성이 있는 코드를 작성하고, except 블록에서는 오류가 발생했을 때 실행될 코드를 작성합니다.

try:
    result = 10 / 0  # 오류가 발생할 가능성이 있는 코드
except ZeroDivisionError as e:
    print(f"오류 발생: {e}")  # 오류가 발생했을 때 실행될 코드
  • try: 이 블록 안에 작성된 코드는 정상적으로 실행되기를 기대하는 부분입니다.
  • except: 이 블록은 try에서 예외가 발생했을 때 실행됩니다. 여기서 특정 예외 타입(예: ZeroDivisionError)을 지정하여 그에 대한 처리를 할 수 있습니다.

1.2 여러 개의 except 절 사용하기

하나의 try 블록 내에서 여러 종류의 예외를 처리하고 싶다면, 각각의 예외에 대해 별도의 except 절을 사용할 수 있습니다.

try:
    num = int(input("숫자를 입력하세요: "))
    result = 10 / num
except ValueError:
    print("유효하지 않은 숫자입니다.")
except ZeroDivisionError:
    print("0으로 나눌 수 없습니다.")

위 코드에서는 두 가지 상황(유효하지 않은 숫자 입력 및 0으로 나누기)을 고려하여 각 경우에 맞는 메시지를 출력합니다.

1.3 모든 예외 처리하기

모든 종류의 예외를 한 번에 처리하려면 일반적인 Exception 클래스를 사용할 수 있습니다.

try:
    data = [1, 2, 3]
    print(data[5])  # 인덱스 에러 발생
except Exception as e:
    print(f"오류가 발생했습니다: {e}")

이 방법은 특정한 예외 타입이 아닌 모든 에러를 포괄적으로 다룰 때 유용하지만, 지나치게 일반화하면 문제 해결이 어려워질 수도 있으므로 주의해야 합니다.

1.4 finally 절 사용하기

finally 절은 예외 발생 여부와 상관없이 항상 실행되어야 하는 코드를 작성할 때 사용됩니다. 주로 리소스 해제와 같은 작업에 활용됩니다.

try:
    file = open('example.txt', 'r')
    content = file.read()
except FileNotFoundError as e:
    print(f"파일이 존재하지 않습니다: {e}")
finally:
    if 'file' in locals():
        file.close()  # 파일 닫기

finally 내부 코드는 파일 열기에 성공하든 실패하든 항상 실행됩니다.


2. 사용자 정의 예외 만들기

파이썬에서는 기본적으로 제공되는 예외 외에도, 사용자 정의 예외를 만들어 특정 상황에 맞는 오류를 처리할 수 있습니다. 사용자 정의 예외는 Exception 클래스를 상속받아 만들 수 있습니다.

2.1 사용자 정의 예외의 필요성

  • 명확성: 특정 조건에서 발생하는 오류를 명확하게 구분하여 이해하기 쉽게 만듭니다.
  • 유지보수성: 코드의 유지보수가 용이해집니다. 나중에 어떤 종류의 오류인지 파악하기 쉬워집니다.
  • 디버깅: 디버깅 과정에서 보다 유용한 정보와 피드백을 제공합니다.

2.2 사용자 정의 예외 만들기

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

def check_value(value):
    if value < 0:
        raise CustomError("값은 음수일 수 없습니다.")

try:
    check_value(-10)
except CustomError as e:
    print(e)

위 코드에서는 값이 음수일 경우 사용자 정의 에러인 CustomError를 발행하고 이를 잡아서 메시지를 출력합니다.

2.3 여러 개의 사용자 정의 예외 사용하기

때때로 여러 종류의 에러를 다룰 필요가 있을 때도 있습니다. 이럴 경우 각기 다른 이름으로 여러 개의 사용자 정의 에러 클래스를 만들어 사용할 수 있습니다.

class DivisionByZeroError(Exception):
    pass

class NegativeNumberError(Exception):
    pass

def calculate_square_root(x):
    if x < 0:
        raise NegativeNumberError("음수의 제곱근은 계산할 수 없습니다.")
    return x ** 0.5

try:
    print(calculate_square_root(-4))
except NegativeNumberError as e:
    print(f"오류 발생: {e}")

이 코드에서는 음수를 입력하면 NegativeNumberError라는 별도의 사용자 정의 예외를 통해 에러를 처리하고 있습니다.


3. 예외 전파와 고급 기법

예외 전파는 발생한 예외가 현재 블록에서 처리되지 않을 때, 그 예외가 호출된 함수나 메서드로 전달되는 과정을 의미합니다. 이를 통해 최종적으로 해당 예외를 적절하게 처리할 수 있는 위치까지 도달하게 됩니다.

3.1 기본적인 예외 전파

def divide(x, y):
    return x / y

def calculate():
    try:
        result = divide(10, 0)
        print("결과:", result)
    except ZeroDivisionError as e:
        print("오류 발생:", e)

calculate()

위 코드는 divide 함수에서 ZeroDivisionError가 발생하지만, 이 오류는 calculate 함수 내의 try-except 블록에 의해 포착되어 출력됩니다.

3.2 중첩된 예외 처리

때로는 예외 처리를 중첩하여 사용해야 할 경우가 있습니다. 예를 들어, 외부 함수에서 발생한 예외를 내부 함수에서 처리하고, 다시 외부 함수에서 추가적인 처리를 할 수 있습니다.

def outer_function():
    try:
        inner_function()
    except ValueError as e:
        print(f"외부 함수에서 ValueError 처리: {e}")

def inner_function():
    try:
        num = int("abc")  # ValueError 발생
    except ValueError as e:
        print(f"내부 함수에서 ValueError 처리: {e}")
        raise  # 예외를 다시 발생시켜 외부 함수로 전파

outer_function()

3.3 예외 체인

예외 체인은 하나의 예외가 다른 예외를 발생시킬 때 사용됩니다. 이는 디버깅 시 매우 유용하며, 예외의 원인을 추적하는 데 도움을 줍니다.

def function_a():
    try:
        function_b()
    except Exception as e:
        raise RuntimeError("function_a에서 오류 발생") from e

def function_b():
    raise ValueError("function_b에서 오류 발생")

try:
    function_a()
except RuntimeError as e:
    print(f"최종 오류: {e}")
    print(f"원인: {e.__cause__}")

4. 예외 처리의 모범 사례

4.1 예외 처리의 원칙

  1. 명확성: 예외 처리는 명확하고 구체적이어야 합니다. 너무 일반적인 예외 처리는 디버깅을 어렵게 만듭니다.
  2. 필요한 경우에만 예외 처리: 모든 코드에 예외 처리를 적용하는 것은 좋지 않습니다. 예외 처리는 예상치 못한 상황에 대비하는 것이 목적입니다.
  3. 예외 로깅: 예외가 발생했을 때 로그를 남기는 것은 디버깅에 매우 유용합니다. logging 모듈을 사용하여 예외 정보를 기록할 수 있습니다.
import logging

logging.basicConfig(level=logging.ERROR)

try:
    result = 10 / 0
except ZeroDivisionError as e:
    logging.error(f"오류 발생: {e}", exc_info=True)  # 스택 트레이스 포함

4.2 리소스 관리와 예외 처리

예외 처리를 할 때 리소스 관리도 중요합니다. 파일이나 네트워크 연결과 같은 리소스를 사용할 때는 finally 절이나 with 문을 사용하여 리소스를 안전하게 해제해야 합니다.

try:
    with open('example.txt', 'r') as file:
        content = file.read()
except FileNotFoundError as e:
    print(f"파일이 존재하지 않습니다: {e}")

5. 예외 처리의 실제 사례

5.1 파일 처리에서의 예외 처리

파일을 처리할 때는 파일이 존재하지 않거나, 파일을 읽는 도중에 오류가 발생할 수 있습니다. 이러한 상황에서 예외 처리를 통해 안정적으로 파일을 처리할 수 있습니다.

def read_file(filename):
    try:
        with open(filename, 'r') as file:
            content = file.read()
            print(content)
    except FileNotFoundError:
        print(f"{filename} 파일이 존재하지 않습니다.")
    except IOError as e:
        print(f"파일 읽기 중 오류 발생: {e}")

read_file('example.txt')

5.2 네트워크 요청에서의 예외 처리

네트워크 요청을 할 때는 연결 오류, 시간 초과 등 다양한 예외가 발생할 수 있습니다. 이러한 예외를 처리하여 사용자에게 적절한 피드백을 제공할 수 있습니다.

import requests

def fetch_data(url):
    try:
        response = requests.get(url, timeout=5)
        response.raise_for_status()  # HTTP 오류가 발생하면 예외를 발생시킴
        return response.json()
    except requests.exceptions.RequestException as e:
        print(f"네트워크 요청 중 오류 발생: {e}")
        return None

data = fetch_data('https://api.example.com/data')
if data:
    print(data)

5.3 데이터베이스 작업에서의 예외 처리

데이터베이스 작업을 할 때는 연결 오류, 쿼리 오류 등 다양한 예외가 발생할 수 있습니다. 이러한 예외를 처리하여 데이터베이스 작업을 안정적으로 수행할 수 있습니다.

import sqlite3

def execute_query(db_path, query):
    try:
        conn = sqlite3.connect(db_path)
        cursor = conn.cursor()
        cursor.execute(query)
        result = cursor.fetchall()
        return result
    except sqlite3.Error as e:
        print(f"데이터베이스 오류 발생: {e}")
        return None
    finally:
        if 'conn' in locals():
            conn.close()

result = execute_query('example.db', 'SELECT * FROM users')
if result:
    print(result)

6. 결론

예외 처리는 안정적이고 신뢰성 높은 프로그램 개발에 필수적입니다. 기본적인 try-except 블록부터 사용자 정의 예외, 예외 전파, 그리고 고급 기법까지 다양한 방법으로 오류를 관리함으로써 사용자의 경험을 향상시킬 뿐만 아니라 디버깅 과정을 단순화할 수 있습니다. 이러한 기술들을 활용해 더 복잡한 로직에서도 안전하게 작업할 수 있도록 하세요!

728x90