Skip to main content

데코레이터 (Decorator)

학습 목표

  • 데코레이터의 동작 원리를 이해한다
  • @wraps를 사용하여 메타데이터를 보존할 수 있다
  • 매개변수가 있는 데코레이터를 작성할 수 있다
  • 실무에서 자주 사용하는 데코레이터 패턴을 활용할 수 있다

왜 중요한가

데코레이터는 기존 함수의 동작을 수정하지 않고 확장하는 패턴입니다. Python 생태계 전반에서 사용됩니다: @property, @staticmethod, @torch.no_grad(), @app.route(), @pytest.fixture 등. 데코레이터를 이해하면 프레임워크 코드를 읽고, 자신만의 확장을 작성할 수 있습니다.

데코레이터 원리

데코레이터는 함수를 인자로 받아 새 함수를 반환하는 함수입니다.
# 데코레이터 정의
def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("함수 실행 전")
        result = func(*args, **kwargs)
        print("함수 실행 후")
        return result
    return wrapper

# @ 문법으로 적용
@my_decorator
def greet(name):
    print(f"안녕하세요, {name}!")

greet("김철수")
# 출력:
# 함수 실행 전
# 안녕하세요, 김철수!
# 함수 실행 후

# 위의 @my_decorator는 아래와 동일
# greet = my_decorator(greet)

@wraps로 메타데이터 보존

from functools import wraps

def my_decorator(func):
    @wraps(func)  # 원본 함수의 이름, docstring 보존
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def greet(name):
    """인사 메시지를 출력합니다."""
    print(f"안녕하세요, {name}!")

# @wraps 없으면
print(greet.__name__)     # "greet" (@wraps 없으면 "wrapper")
print(greet.__doc__)      # "인사 메시지를 출력합니다."
@wraps(func)를 항상 추가하세요. 없으면 디버깅, 문서화, 테스트에서 문제가 발생합니다.

실용 데코레이터 패턴

실행 시간 측정

import time
from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__}: {elapsed:.4f}초")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)
    return "완료"

slow_function()  # slow_function: 1.0012초

재시도 (Retry)

def retry(max_attempts=3, delay=1):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts:
                        raise
                    print(f"시도 {attempt} 실패: {e}. {delay}초 후 재시도...")
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(max_attempts=3, delay=2)
def fetch_data(url):
    """데이터를 가져옵니다."""
    # 네트워크 요청...
    pass

캐싱

def cache(func):
    """결과를 캐싱하는 데코레이터"""
    memo = {}

    @wraps(func)
    def wrapper(*args):
        if args not in memo:
            memo[args] = func(*args)
        return memo[args]
    return wrapper

@cache
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(100))  # 즉시 계산 (캐싱 덕분)
Python 내장 functools.lru_cache가 더 강력한 캐싱을 제공합니다.
from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

매개변수 있는 데코레이터

# 3단 구조: 팩토리 -> 데코레이터 -> 래퍼
def repeat(n):
    """함수를 n번 반복 실행합니다."""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            results = []
            for _ in range(n):
                results.append(func(*args, **kwargs))
            return results
        return wrapper
    return decorator

@repeat(3)
def say_hello():
    print("Hello!")
    return "done"

say_hello()
# Hello!
# Hello!
# Hello!

데코레이터 스택

여러 데코레이터를 중첩할 수 있습니다. 아래에서 위로 적용됩니다.
@timer
@retry(max_attempts=3)
def process_data():
    pass

# 실행 순서: timer(retry(process_data))
# timer가 가장 바깥, retry가 안쪽

AI/ML에서의 활용

import torch
from functools import wraps

# PyTorch @torch.no_grad() 유사 패턴
def no_grad(func):
    """그래디언트 계산을 비활성화합니다."""
    @wraps(func)
    def wrapper(*args, **kwargs):
        with torch.no_grad():
            return func(*args, **kwargs)
    return wrapper

@no_grad
def evaluate(model, data):
    return model(data)

# 로깅 데코레이터
def log_experiment(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"실험 시작: {func.__name__}")
        print(f"매개변수: args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"실험 완료: {result}")
        return result
    return wrapper

@log_experiment
def train_model(model_name, epochs=10, lr=0.001):
    # 학습 로직...
    return {"accuracy": 0.95}

# 입력 검증 데코레이터
def validate_inputs(**validators):
    def decorator(func):
        @wraps(func)
        def wrapper(**kwargs):
            for param, check in validators.items():
                if param in kwargs and not check(kwargs[param]):
                    raise ValueError(f"유효하지 않은 {param}: {kwargs[param]}")
            return func(**kwargs)
        return wrapper
    return decorator

@validate_inputs(
    lr=lambda x: 0 < x < 1,
    epochs=lambda x: x > 0,
    batch_size=lambda x: x > 0 and (x & (x-1) == 0)  # 2의 거듭제곱
)
def train(lr=0.001, epochs=10, batch_size=32):
    pass
네. __call__ 메서드를 구현한 클래스는 데코레이터로 사용할 수 있습니다. 상태가 복잡할 때 클래스 기반 데코레이터가 유용합니다.
네. @property는 메서드를 속성처럼 접근할 수 있게 만드는 내장 데코레이터입니다. OOP 문서에서 자세히 다룹니다.

체크리스트

  • 데코레이터의 동작 원리를 설명할 수 있다
  • @wraps를 사용하여 메타데이터를 보존할 수 있다
  • 매개변수 있는 데코레이터(3단 구조)를 작성할 수 있다
  • timer, retry, cache 등 실용 데코레이터를 구현할 수 있다

다음 문서