데코레이터와 pdb는 함께 사용하면 디버깅 작업을 크게 효율화할 수 있습니다. 데코레이터를 통해 특정 함수에만 자동으로 pdb를 적용할 수 있어, 코드 수정 없이 필요한 함수에 디버깅 모드를 설정할 수 있습니다.

이번 가이드에서는 pdb와 데코레이터를 함께 활용해 특정 함수에만 자동으로 디버거를 활성화하는 방법과, 실전에서 사용할 수 있는 예제를 소개합니다.


1. pdb를 자동으로 활성화하는 디버깅 데코레이터

아래 데코레이터는 디버깅 모드가 필요한 함수에만 자동으로 pdb를 적용하는 방법을 보여줍니다. 이 데코레이터는 함수가 호출될 때마다 자동으로 중단점을 설정하여 pdb 디버깅 모드로 진입하도록 합니다.

import pdb
from functools import wraps

def debug_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"[DEBUG] Entering function '{func.__name__}' with args={args}, kwargs={kwargs}")
        pdb.set_trace()  # 디버깅 모드 활성화
        result = func(*args, **kwargs)
        print(f"[DEBUG] Exiting function '{func.__name__}' with result={result}")
        return result
    return wrapper

@debug_decorator
def calculate_total(price, tax):
    total = price + (price * tax)
    return total

# 실제 실행
calculate_total(100, 0.1)

설명: @debug_decorator 데코레이터는 calculate_total 함수가 호출될 때마다 pdb를 활성화하여, 함수 내에서 변수를 확인하고 코드 흐름을 추적할 수 있도록 합니다. 함수 입출력 상태를 자동으로 기록하므로, 함수의 시작과 종료 시점에서 변수 상태를 추적하기에 유용합니다.


2. 특정 조건에서만 pdb 디버깅 활성화하기

때로는 특정 조건을 충족할 때만 디버거가 활성화되도록 설정하고 싶을 때가 있습니다. 예를 들어, 특정 인자를 받을 때만 디버깅 모드를 활성화하도록 데코레이터에 조건을 추가할 수 있습니다.

def conditional_debug_decorator(condition):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            if condition(*args, **kwargs):
                print(f"[CONDITIONAL DEBUG] '{func.__name__}' activated for debugging.")
                pdb.set_trace()  # 조건을 만족할 때만 디버거 활성화
            return func(*args, **kwargs)
        return wrapper
    return decorator

# 특정 조건 함수: tax가 0.1일 때만 디버깅 활성화
def debug_condition(price, tax):
    return tax == 0.1

@conditional_debug_decorator(debug_condition)
def calculate_discounted_total(price, tax):
    total = price + (price * tax)
    return total

# 실제 실행
calculate_discounted_total(100, 0.1)  # 조건에 맞아 디버깅 활성화
calculate_discounted_total(100, 0.2)  # 디버깅 비활성화

설명: conditional_debug_decorator는 인자가 특정 조건을 만족할 때만 pdb를 활성화합니다. 이렇게 조건부 디버깅을 설정하면 특정 상황에서만 함수 내부를 집중적으로 추적할 수 있습니다. 위 코드에서는 tax0.1일 때만 디버깅 모드로 진입하도록 설정했습니다.


3. 오류 발생 시 자동 디버깅 모드 활성화 데코레이터

코드가 오류를 발생시킬 때 자동으로 pdb 디버깅 모드로 진입하게 하는 데코레이터입니다. 이 방법은 예기치 않은 예외가 발생할 경우 문제가 발생한 즉시 디버깅을 시작할 수 있어 매우 유용합니다.

def error_debug_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as e:
            print(f"[ERROR DEBUG] Exception in '{func.__name__}': {e}")
            pdb.set_trace()  # 예외 발생 시 디버거 활성화
            raise e  # 예외를 다시 발생시켜 호출자에게 전달
    return wrapper

@error_debug_decorator
def risky_division(a, b):
    return a / b

# 실제 실행
risky_division(10, 2)  # 정상 실행
risky_division(10, 0)  # ZeroDivisionError 발생 시 디버깅 모드 진입

설명: @error_debug_decorator는 함수 실행 중 예외가 발생했을 때 자동으로 pdb를 활성화해, 오류 발생 시점에서 변수 상태와 코드 흐름을 즉시 확인할 수 있습니다. 이 데코레이터는 예외 디버깅에 매우 유용합니다.


4. 함수 호출 흐름을 한눈에 살펴보는 pdb와 로깅 데코레이터 결합

다양한 함수가 서로 호출될 때 함수 호출 순서를 확인하는 것은 매우 중요합니다. 아래는 각 함수 호출을 기록하고, 필요할 때 pdb로 중단해 호출 흐름과 변수 상태를 한꺼번에 추적하는 데코레이터입니다.

def trace_and_debug_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"[TRACE] Entering '{func.__name__}' with args={args}, kwargs={kwargs}")
        pdb.set_trace()  # 함수 호출 시 디버깅 활성화
        result = func(*args, **kwargs)
        print(f"[TRACE] Exiting '{func.__name__}' with result={result}")
        return result
    return wrapper

@trace_and_debug_decorator
def add(a, b):
    return a + b

@trace_and_debug_decorator
def multiply(x, y):
    return x * y

@trace_and_debug_decorator
def main():
    result1 = add(3, 5)
    result2 = multiply(result1, 10)
    return result2

# 실제 실행
main()

설명: trace_and_debug_decorator는 함수가 호출될 때마다 pdb를 활성화하여 각 함수의 입출력을 기록하고, 함수 호출 흐름과 변수를 추적할 수 있도록 합니다. 이 방법은 서로 호출하는 여러 함수에서 코드 흐름을 추적할 때 특히 유용합니다.


결론: pdb와 데코레이터로 디버깅을 자동화하고 효율화하기

이제 pdb와 데코레이터를 결합해 필요한 함수에만 자동으로 디버거를 적용하거나, 조건부로 디버깅을 활성화하는 방법을 알게 되었습니다. 이 방식은 코드 수정 없이 필요한 함수에만 pdb를 적용할 수 있어, 더욱 유연하고 효율적인 디버깅을 가능하게 합니다.

이제 print 대신 pdb와 데코레이터를 통해 디버깅을 체계화하고 자동화해보세요!

파이썬을 사용하다 보면 코드를 재사용하고 기능을 확장하는 데 유용한 데코레이터를 자주 만나게 됩니다. 데코레이터는 기존의 함수를 수정하지 않으면서 기능을 추가할 수 있는 매우 강력한 기능으로, 특히 로그 기록, 캐싱, 인증 등의 작업에 유용합니다.

데코레이터는 기본적으로 함수클래스를 인자로 받아 새로운 함수를 반환하는 함수입니다. 이 방식으로 특정 기능을 함수에 추가하거나 수정 없이 재사용할 수 있습니다. 데코레이터는 파이썬의 고유 기능 중 하나이며, 코드를 효율적으로 관리하고 모듈화하는 데 매우 유리합니다.

이번 글에서는 파이썬 데코레이터의 기초부터 시작해, 실제 개발에서 활용할 수 있는 다양한 데코레이터 예제를 함께 알아보겠습니다.


1. 기본 데코레이터 예제: 접근 전후 알림 추가하기

함수를 호출하기 전후로 알림을 주는 기본적인 데코레이터입니다. API 호출 전후, 파일 작업 전후 등의 상황에서 쉽게 사용할 수 있습니다.

def notify_execution(func):
    def wrapper(*args, **kwargs):
        print(f"[INFO] Starting '{func.__name__}' execution.")
        result = func(*args, **kwargs)
        print(f"[INFO] Finished '{func.__name__}' execution.")
        return result
    return wrapper

@notify_execution
def process_data(data):
    # 데이터 처리 로직
    print("Processing data...")
    return f"Processed {data}"

# 실제 실행
print(process_data("Sample data"))

설명: notify_execution 데코레이터는 함수의 시작과 끝에 알림을 출력합니다. 주로 데이터 처리나 작업 흐름을 추적하는 데 유용합니다.


2. 입력 데이터 유효성 검사 데코레이터

함수에 전달되는 인자의 유효성을 검사하는 데코레이터입니다. 웹 애플리케이션이나 데이터 처리 파이프라인에서 데이터 검증을 간단히 추가할 수 있습니다.

def validate_positive_args(func):
    def wrapper(*args):
        # 모든 인자가 양수인지 확인
        if not all(isinstance(arg, (int, float)) and arg > 0 for arg in args):
            raise ValueError("All arguments must be positive numbers")
        return func(*args)
    return wrapper

@validate_positive_args
def calculate_volume(length, width, height):
    return length * width * height

# 실제 실행
print(calculate_volume(5, 10, 3))  # 정상 실행
# print(calculate_volume(5, -10, 3))  # ValueError 발생

설명: validate_positive_args는 모든 인자가 양수인지 확인하고, 그렇지 않을 경우 예외를 발생시킵니다. 유효성 검증을 위해 여러 함수에 일관된 검사를 적용할 수 있습니다.


3. 재시도 (Retry) 데코레이터

API 요청과 같은 외부 자원을 사용할 때 일시적인 네트워크 문제나 서버 응답 실패가 발생할 수 있습니다. 이를 대비해 재시도 데코레이터를 사용해보겠습니다.

import time

def retry(retries=3, delay=1):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for attempt in range(1, retries + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"Attempt {attempt} failed: {e}")
                    time.sleep(delay)
            raise Exception("All attempts failed")
        return wrapper
    return decorator

@retry(retries=3, delay=2)
def fetch_data():
    # 외부 API 호출을 시뮬레이션
    if time.time() % 2 < 1:
        raise ConnectionError("Failed to fetch data")
    return "Data fetched successfully"

# 실제 실행
print(fetch_data())

설명: retry 데코레이터는 실패할 가능성이 있는 함수 호출을 특정 횟수만큼 재시도합니다. 예기치 않은 일시적 오류에 대비해 유용하게 사용할 수 있습니다.


4. 캐시 데코레이터 (메모이제이션)

복잡한 계산 결과를 저장해두고, 동일한 인자로 호출될 때 캐싱된 결과를 반환하는 데코레이터입니다. CPU 사용량이 높은 작업에서 성능을 개선할 수 있습니다.

def cache(func):
    cached_results = {}
    def wrapper(*args):
        if args in cached_results:
            print(f"[CACHE] Returning cached result for {args}")
            return cached_results[args]

        result = func(*args)
        cached_results[args] = result
        print(f"[CACHE] Caching result for {args}")
        return result
    return wrapper

@cache
def expensive_calculation(x, y):
    # 복잡한 계산 시뮬레이션
    time.sleep(1)
    return x * y

# 실제 실행
print(expensive_calculation(5, 3))  # 캐싱하지 않은 결과 계산
print(expensive_calculation(5, 3))  # 캐싱된 결과 반환

설명: cache 데코레이터는 동일한 인자로 반복 호출되는 함수에서 캐시된 결과를 반환합니다. 계산 속도를 높이고 서버 부하를 줄이는 데 유용합니다.


5. 권한 기반 접근 제어 데코레이터

웹 애플리케이션에서 특정 역할이 있는 사용자만 특정 기능에 접근할 수 있도록 제한할 수 있습니다.

def role_required(role):
    def decorator(func):
        def wrapper(user, *args, **kwargs):
            if user.get("role") != role:
                raise PermissionError(f"Access denied: User needs '{role}' role")
            return func(user, *args, **kwargs)
        return wrapper
    return decorator

@role_required("admin")
def delete_content(user, content_id):
    print(f"{user['name']} deleted content {content_id}")

# 사용자 데이터
admin_user = {"name": "Alice", "role": "admin"}
regular_user = {"name": "Bob", "role": "user"}

# 실제 실행
delete_content(admin_user, 42)  # 정상 접근
# delete_content(regular_user, 42)  # PermissionError 발생

설명: role_required 데코레이터는 사용자가 특정 권한이 있을 때만 함수를 호출할 수 있도록 제한합니다. 보안이 중요한 웹 애플리케이션에 유용합니다.


6. 함수 실행 전후 시간 측정 (성능 프로파일링)

복잡한 계산이나 데이터 처리 함수의 성능을 측정하여, 실행 시간이 오래 걸리는지 확인할 수 있습니다.

import time

def profile_execution(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"[PROFILE] Execution time for '{func.__name__}': {end_time - start_time:.4f} seconds")
        return result
    return wrapper

@profile_execution
def complex_task():
    # 연산 시간이 오래 걸리는 작업을 시뮬레이션
    time.sleep(2)
    return "Task complete"

# 실제 실행
complex_task()

설명: profile_execution 데코레이터는 함수의 실행 시간을 계산해줍니다. 성능 최적화가 필요한 함수에 적용해 느린 작업을 추적할 수 있습니다.


7. 함수의 성공 및 실패 결과 로깅

함수가 성공했는지, 오류가 발생했는지를 로깅하는 데코레이터입니다. 로그를 통해 함수를 모니터링하고 디버깅할 때 유용합니다.

import logging

logging.basicConfig(level=logging.INFO)

def log_result(func):
    def wrapper(*args, **kwargs):
        try:
            result = func(*args, **kwargs)
            logging.info(f"Function '{func.__name__}' executed successfully with result: {result}")
            return result
        except Exception as e:
            logging.error(f"Function '{func.__name__}' failed with error: {e}")
            raise e
    return wrapper

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

# 실제 실행
print(divide(10, 2))  # 정상 실행
# print(divide(10, 0))  # ZeroDivisionError 발생 및 로깅

설명: log_result 데코레이터는 함수가 정상적으로 실행되었는지, 오류가 발생했는지 로깅합니다. 로그 기록을 통해 디버깅하고, 오류를 추적할 때 유용합니다.


이렇게 다양한 기능의 데코레이터를 통해 함수의 실행 전후 알림, 인자 검증, 캐싱, 권한 관리, 성능 프로파일링, 실행 결과 로깅 등의 기능을 손쉽게 추가할 수 있습니다. 각각의 데코레이터는 현실적인 상황에서 코드의 효율성과 보안성을 높이며, 디버깅과 유지보수에도 큰 도움을 줍니다.


+ Recent posts