Python Decorator (데코레이터)

Python Decorator (데코레이터)

데코레이터가 해결하는 문제

같은 코드를 여러 함수에 반복해서 작성하고 있다면, 데코레이터로 해결

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 로깅이 필요한 여러 함수들
def transfer_money(from_account, to_account, amount):
    print(f"transfer_money 실행 시작")
    # 송금 로직
    result = "송금 완료"
    print(f"transfer_money 실행 완료: {result}")
    return result

def update_profile(user_id, data):
    print(f"update_profile 실행 시작")
    # 프로필 업데이트 로직
    result = "프로필 업데이트 완료"
    print(f"update_profile 실행 완료: {result}")
    return result


데코레이터의 기본 구조

원본 함수에서 wrapper 함수로 교체

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def log_execution(func):                  # 1. 원본 함수를 받는다
    def wrapper(*args, **kwargs):         # 2. 새로운 함수를 정의한다
        print(f"{func.__name__} 실행 시작")
        result = func(*args, **kwargs)    # 3. 원본 함수를 호출한다
        print(f"{func.__name__} 실행 완료: {result}")
        return result                     # 4. 결과를 반환한다
    return wrapper                        # 5. 새로운 함수를 반환한다

@log_execution
def transfer_money(from_account, to_account, amount):
    return "송금 완료"

@log_execution
def update_profile(user_id, data):
    return "프로필 업데이트 완료"

functools.wraps를 사용해야 하는 이유

데코레이터 사용 시 원본 함수의 정보가 손실되는 것을 방지하기 위해 functools.wraps 사용

문제

1
2
3
4
5
6
7
8
9
10
11
12
def simple_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@simple_decorator
def calculate_sum(a, b):
    """두 수의 합을 계산합니다."""
    return a + b

print(calculate_sum.__name__)  # wrapper (원본 함수명 손실!)
print(calculate_sum.__doc__)   # None (독스트링 손실!)

해결

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

def better_decorator(func):
    @functools.wraps(func)  # 원본 함수의 메타데이터를 보존
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@better_decorator
def calculate_sum(a, b):
    """두 수의 합을 계산합니다."""
    return a + b

print(calculate_sum.__name__)  # calculate_sum (원본 함수명 보존!)
print(calculate_sum.__doc__)   # 두 수의 합을 계산합니다. (독스트링 보존!)

클로저가 데코레이터에서 중요한 이유

데코레이터가 상태를 기억할 수 있는 이유 = Python의 클로저 메커니즘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def call_counter(func):
    count = 0  # 이 변수는 call_counter 함수의 로컬 변수
    
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        nonlocal count
        count += 1  # wrapper가 외부 함수의 변수에 접근
        print(f"{func.__name__} 호출 횟수: {count}")
        return func(*args, **kwargs)
    
    return wrapper
    # call_counter 함수가 끝나도 count 변수는 메모리에 살아있음 (클로저)

@call_counter
def say_hello():
    return "Hello"

say_hello()  # say_hello 호출 횟수: 1
say_hello()  # say_hello 호출 횟수: 2

매개변수가 있는 데코레이터

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def retry(max_attempts=3, delay=1):       # 1단계: 설정 받기
    def decorator(func):                  # 2단계: 원본 함수 받기
        @functools.wraps(func)
        def wrapper(*args, **kwargs):     # 3단계: 실제 실행
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts - 1:
                        raise e
                    print(f"시도 {attempt + 1} 실패. {delay}초 후 재시도...")
                    time.sleep(delay)
        return wrapper
    return decorator

# 사용
@retry(max_attempts=5, delay=2)
def unstable_api_call():
    import random
    if random.random() < 0.7:
        raise Exception("API 오류")
    return "성공"

클래스 기반 데코레이터

복잡한 상태 관리추가 메서드가 필요할 때는 클래스 데코레이터

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
class CallTracker:
    def __init__(self, func):
        self.func = func
        self.call_count = 0
        functools.update_wrapper(self, func)  # 클래스 기반 데코레이터에서는 functools.update_wrapper 사용
    
    def __call__(self, *args, **kwargs):
        self.call_count += 1
        print(f"{self.func.__name__} 호출 횟수: {self.call_count}")
        return self.func(*args, **kwargs)
    
    def reset_count(self):  # 추가 메서드 제공
        self.call_count = 0
    
    def get_stats(self):
        return {
            'function_name': self.func.__name__,
            'call_count': self.call_count
        }

@CallTracker
def greet(name):
    return f"Hello, {name}!"

greet("Alice")  # greet 호출 횟수: 1
greet("Bob")    # greet 호출 횟수: 2

print(greet.get_stats())  # {'function_name': 'greet', 'call_count': 2}
greet.reset_count()       # 함수 기반 데코레이터로는 불가능한 기능

유의 사항

# 1: functools.wraps 누락

1
2
3
4
5
6
7
8
9
10
11
12
# 잘못된 예시
def bad_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

# 올바른 예시
def good_decorator(func):
    @functools.wraps(func)  # 반드시 포함
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

# 2: 매개변수 있는 데코레이터에서 괄호 누락

1
2
3
4
5
6
7
8
9
# 잘못된 사용
@retry  # 에러! @retry() 이어야 함
def api_call():
    pass

# 올바른 사용
@retry()  # 괄호 필요
def api_call():
    pass

# 3: 데코레이터 순서

1
2
3
4
5
6
7
8
9
@cache_result
@measure_time
@require_login
def expensive_view(request):
    pass

# 실행 순서: require_login → measure_time → cache_result
# 로그인 체크 후 시간 측정하고 결과를 캐시함
# 순서가 바뀌면 의도와 다를 수 있음
This post is licensed under CC BY 4.0 by the author.