C
Python/중급/Lesson 12

데코레이터 (Decorator)

1시간·theory
이 챕터
4/8
Python

데코레이터 (Decorator)

🎯 이 lesson 을 읽고 나면

이 lesson 을 다 읽고 나면 아래 3가지를 자신 있게 할 수 있습니다.

  • ✅ 함수가 함수를 반환하는 고차 함수 원리
  • ✅ @decorator 문법 + functools.wraps 의 이유
  • ✅ @lru_cache · @property · @staticmethod 활용

학습 목표를 체크리스트로 두고 다 답할 수 있게 되면 lesson 을 닫으세요.

데코레이터 — 코드 + 실행 결과

@decorator = 함수를 감싸서 기능 추가. 로깅·캐싱·인증 같은 공통 처리 분리.


1. 가장 단순한 데코레이터

python
def 로그(원래_함수):
    def 감싼것(*args, **kwargs):
        print(f"호출: {원래_함수.__name__}({args})")
        결과 = 원래_함수(*args, **kwargs)
        print(f"결과: {결과}")
        return 결과
    return 감싼것

@로그
def 더하기(a, b):
    return a + b

더하기(3, 5)
# 출력:
# 호출: 더하기((3, 5))
# 결과: 8

@로그 = 더하기 = 로그(더하기) 와 동일.


2. 실행 시간 측정

python
import time

def 시간측정(f):
    def 감싼(*args, **kwargs):
        시작 = time.time()
        결과 = f(*args, **kwargs)
        걸린 = time.time() - 시작
        print(f"{f.__name__}: {걸린:.3f}초")
        return 결과
    return 감싼

@시간측정
def 무거운_작업():
    time.sleep(1)
    return "완료"

무거운_작업()      # 무거운_작업: 1.001초

3. 인자 받는 데코레이터

python
def 반복(횟수):
    def 데코(f):
        def 감싼(*args, **kwargs):
            for _ in range(횟수):
                결과 = f(*args, **kwargs)
            return 결과
        return 감싼
    return 데코

@반복(3)
def 인사():
    print("안녕!")

인사()
# 안녕!
# 안녕!
# 안녕!

4. 자주 쓰는 빌트인 데코레이터

python
from functools import lru_cache

@lru_cache(maxsize=100)         # 결과 캐시 — 같은 인자면 즉시 반환
def 피보나치(n):
    return n if n < 2 else 피보나치(n-1) + 피보나치(n-2)

피보나치(50)                    # 캐시 덕에 즉시 (없으면 천억 회 호출)

@property (getter), @staticmethod, @classmethod 도 클래스에서 자주.


한 줄 요약

패턴코드
단순@데코
인자@데코(인자)
캐시@lru_cache
접근@property
💻 나쁜 예시 — functools.wraps 없이 구현
# wraps 없이 만든 데코레이터 — 디버깅이 어려워짐
def timer(func):
    def wrapper(*args, **kwargs):
        import time
        start = time.time()
        result = func(*args, **kwargs)
        print(f"실행 시간: {time.time() - start:.4f}s")
        return result
    return wrapper  # wrapper 함수 이름이 원본을 덮어씀

@timer
def calculate(n):
    """n의 제곱을 계산합니다"""
    return n ** 2

print(calculate.__name__)  # 'wrapper' — 원본 이름 손실!
print(calculate.__doc__)   # None — 독스트링 손실!
💻 좋은 예시 — functools.wraps + 인자 있는 데코레이터
import functools
import time
import logging

# 기본 데코레이터 — functools.wraps 필수
def timer(func):
    @functools.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}s")
        return result
    return wrapper

# 인자 있는 데코레이터 — 3중 중첩
def retry(max_attempts=3, exceptions=(Exception,)):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    if attempt == max_attempts:
                        raise
                    print(f"재시도 {attempt}/{max_attempts}: {e}")
        return wrapper
    return decorator

# 실전: FastAPI 스타일 인증 데코레이터
def require_auth(func):
    @functools.wraps(func)
    def wrapper(request, *args, **kwargs):
        if not request.get('user'):
            raise PermissionError("로그인이 필요합니다")
        return func(request, *args, **kwargs)
    return wrapper

@timer
@retry(max_attempts=3, exceptions=(ConnectionError,))
def fetch_data(url: str) -> dict:
    """외부 API 데이터 조회"""
    import urllib.request
    with urllib.request.urlopen(url) as response:
        import json
        return json.loads(response.read())

print(fetch_data.__name__)  # 'fetch_data' — 원본 이름 유지
print(fetch_data.__doc__)   # '외부 API 데이터 조회' — 독스트링 유지
💻 실전 예시 — 클래스 기반 데코레이터 (캐시)
import functools
from typing import Callable, Any

# 클래스 기반 데코레이터 — 상태 관리에 유리
class Cache:
    """간단한 메모이제이션 캐시 데코레이터"""
    
    def __init__(self, max_size: int = 128):
        self.max_size = max_size
        self._cache: dict = {}
    
    def __call__(self, func: Callable) -> Callable:
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            key = (args, tuple(sorted(kwargs.items())))
            if key not in self._cache:
                if len(self._cache) >= self.max_size:
                    # LRU 간략 구현: 가장 오래된 항목 제거
                    oldest = next(iter(self._cache))
                    del self._cache[oldest]
                self._cache[key] = func(*args, **kwargs)
            return self._cache[key]
        wrapper.cache_clear = lambda: self._cache.clear()
        wrapper.cache_info = lambda: {'size': len(self._cache), 'max': self.max_size}
        return wrapper

@Cache(max_size=64)
def fibonacci(n: int) -> int:
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(50))              # 빠르게 계산
print(fibonacci.cache_info())     # {'size': 51, 'max': 64}
fibonacci.cache_clear()           # 캐시 초기화

# 참고: 표준 라이브러리의 functools.lru_cache도 동일 역할
from functools import lru_cache

@lru_cache(maxsize=128)
def factorial(n: int) -> int:
    return 1 if n == 0 else n * factorial(n - 1)

🐍 실행해보기 — 데코레이터 (Decorator)

위 개념을 실제로 코드로 실행해보세요. 값을 바꿔가며 어떻게 동작하는지 직접 확인하는 게 가장 빠른 학습.
✏️ Python 코드
📟 콘솔 출력
▶ 실행 버튼을 눌러보세요
🐍 Pyodide로 실제 Python 실행 — 첫 실행 시 로딩 3~5초 소요

🤖 AI 에게 이렇게 요청해보세요

이 lesson 의 개념을 알면 AI 에게 구체적으로 지시할 수 있습니다. 막연한 "고쳐줘" 가 아니라 어휘를 가진 요청 — 그게 토큰 절약의 출발점입니다.

  • "이 함수에 functools.lru_cache 데코레이터 적용해줘"
  • "타이밍·로깅 데코레이터를 functools.wraps 사용해서 만들어줘"

왜 이게 토큰을 줄이나

개념을 모를 땐 AI 답변을 받고도 "그게 뭐예요?" 를 다시 물어야 합니다. 그 "다시 물음" 이 토큰을 잡아먹습니다. 개념 한 번 익혀두면 대화가 한 번에 끝납니다.

먼저 읽으면 좋은 개념: 제너레이터
데코레이터 - Python