포스트

[Python] 데코레이터를 활용한 효과적인 에러 핸들링 패턴


1. 에러 핸들링의 중요성 ** 소프트웨어 개발에서 에러 핸들링은 필수적인 요소입니다.

하지만 많은 경우,

같은 방식의 에러 핸들링 코드가 반복되면서 코드 중복이 발생하고 유지보수가 어려워집니다.

파이썬에서는 데코레이터(Decorator) 를 활용하여 이러한 문제를 해결할 수 있습니다.

데코레이터를 사용하면 에러 핸들링을 중앙화하여 가독성과 유지보수성을 향상시킬 수 있습니다.

이 글에서는 데코레이터를 활용한 에러 핸들링 패턴을 설명하고,

실전에서 활용할 수 있는 다양한 예제와 모범 사례를 소개하겠습니다.


2. 데코레이터란? ** 데코레이터는 함수나 메서드의 기능을 확장하는 파이썬의 강력한 기능입니다.

특정 함수에 추가적인 기능(로깅, 성능 측정, 에러 핸들링 등)을 쉽게 부여할 수 있습니다.

기본적인 데코레이터 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import functools

def my_decorator(func):
    @functools.wraps(func)  # 원본 함수의 메타데이터 유지
    def wrapper(*args, **kwargs):
        print("함수가 실행되기 전입니다.")
        result = func(*args, **kwargs)
        print("함수가 실행된 후입니다.")
        return result
    return wrapper

@my_decorator
def my_function():
    print("실제 함수 실행")

my_function()


3. 에러 핸들링을 위한 데코레이터 패턴 에러 핸들링은 데코레이터를 적용하기 좋은 대표적인 영역 중 하나입니다.

여러 함수에서 반복적으로 사용되는 try-except 블록을 데코레이터로 감싸면,

코드의 일관성과 유지보수성이 향상됩니다.

기본적인 에러 핸들링 데코레이터

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import functools
import logging

# 로깅 설정
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def handle_exceptions(func):
    """예외를 처리하는 데코레이터"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as e:
            logger.error(f"{func.__name__} 실행 중 오류 발생: {str(e)}")
            return None  # 기본값 반환
    return wrapper

@handle_exceptions
def divide(a, b):
    return a / b

# 사용 예시
print(divide(10, 2))  # 정상 실행
print(divide(10, 0))  # 오류 발생 → None 반환


4. 고급 에러 핸들링 데코레이터 실제 프로젝트에서는 보다 복잡한 예외 처리가 필요합니다.

예를 들어,

재시도(자동 재시도),

특정 예외 구분,

사용자 정의 예외 처리 기능이 포함될 수 있습니다.

고급 에러 핸들링 데코레이터

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import functools
import logging
import time

# 로깅 설정
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class ErrorHandler:
    def __init__(self, retry_count=0, retry_delay=1.0, critical_exceptions=None, default_value=None):
        self.retry_count = retry_count
        self.retry_delay = retry_delay
        self.critical_exceptions = critical_exceptions or []
        self.default_value = default_value

    def __call__(self, func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            attempt = 0
            while attempt  self.retry_count:
                        logger.error(f"{func.__name__} 최대 재시도 횟수 초과. 오류: {e}")
                        return self.default_value
                    logger.warning(f"{func.__name__} 실행 중 오류 발생: {e}. {attempt}번째 재시도 중...")
                    time.sleep(self.retry_delay)
        return wrapper

# 사용 예시
@ErrorHandler(retry_count=3, retry_delay=2.0, default_value="오류 발생")
def unstable_function():
    import random
    if random.random() < 0.7:
        raise ValueError("랜덤 오류 발생!")
    return "성공"

print(unstable_function())  # 실패할 경우 3번 재시도 후 "오류 발생" 반환

5. 실전 활용: HTTP 요청에 적용 HTTP 요청 시 네트워크 오류가 발생할 수 있습니다.

이를 해결하기 위해,

지수 백오프(exponential backoff) 전략을 사용한 에러 핸들링 데코레이터를 적용할 수 있습니다.

HTTP 요청용 에러 핸들링 데코레이터

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import functools
import requests
import logging
import time

logging.basicConfig(level&#x3D;logging.INFO)
logger &#x3D; logging.getLogger(__name__)

def http_error_handler(retry_count&#x3D;3, retry_delay&#x3D;1.0):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(retry_count + 1):
                try:
                    response &#x3D; func(*args, **kwargs)
                    if response.status_code &#x3D;&#x3D; 200:
                        return response
                    if response.status_code &#x3D;&#x3D; 429:
                        logger.warning("API 할당량 초과, 대기 후 재시도")
                        time.sleep(retry_delay * (2 ** attempt))
                    elif attempt < retry_count:
                        logger.warning(f"요청 실패: {response.status_code}, {attempt+1}번째 재시도 중...")
                        time.sleep(retry_delay)
                    else:
                        logger.error(f"최대 재시도 횟수 초과. 마지막 상태 코드: {response.status_code}")
                        return response
                except requests.RequestException as e:
                    if attempt < retry_count:
                        time.sleep(retry_delay * (2 ** attempt))
                    else:
                        logger.error(f"네트워크 오류 발생: {e}")
                        raise
        return wrapper
    return decorator

@http_error_handler(retry_count&#x3D;3, retry_delay&#x3D;2)
def fetch_data(url):
    return requests.get(url, timeout&#x3D;5.0)

# 사용 예시
try:
    response &#x3D; fetch_data("https://api.example.com/data")
    print(response.json())
except Exception as e:
    print(f"데이터 요청 실패: {e}")

6. 데코레이터를 활용한 에러 핸들링의 장점

  • 코드 중복 감소: 동일한 에러 처리 로직을 여러 곳에 반복하지 않아도 됨

  • 가독성 향상: 핵심 로직이 try-except 블록으로 가려지지 않음

  • 유지보수성 증가: 에러 처리 로직을 변경할 때 한 곳만 수정하면 됨

  • 일관성 유지: 모든 함수가 동일한 방식으로 예외를 처리


7. 결론

데코레이터를 활용한 에러 핸들링은 코드를 깔끔하게 정리하고 유지보수성을 높이는 효과적인 방법입니다.

실전에서 활용할 수 있도록,

기본적인 예외 처리부터 재시도 및 HTTP 요청까지 다양한 데코레이터 패턴을 소개했습니다.

sticker

file_1

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.