파이썬 교재 · 17편 / 27편

파이썬 데코레이터 — 함수를 감싸는 함수

원본을 안 건드리고 능력만 추가. @flask.route·@dataclass 의 비밀.

중급읽는 시간 7분2026-05-13
timer 데코레이터를 정의하고 @로 적용하는 코드 화면

웹 프레임워크 코드를 보면 @app.route("/"), 14편의 @dataclass, 디버깅용 @functools.lru_cache 같은 골뱅이 표기가 함수 위에 자주 붙어있습니다. 이게 데코레이터(decorator). 본래 함수에 손대지 않고 "이 함수에 능력 X 를 추가하라" 라는 선언입니다. 17편부터 중급 묶음 — 입문·기초가 끝나고 라이브러리 소스 코드가 읽히기 시작하는 지점.

17편을 마치면 ① 함수가 일급 객체라는 의미 ② @decorator 문법의 정체 ③ *args/**kwargs 로 어떤 함수든 감싸기 ④ functools.wraps 의 이유 ⑤ 표준 라이브러리 단골 데코레이터 — 5가지가 손에 익습니다.

함수가 일급 객체라는 말

파이썬에서 함수도 값입니다. 변수에 담을 수 있고, 다른 함수의 인자로 넘길 수 있고, 함수가 함수를 돌려줄 수도 있어요.

def square(n):
    return n * n

f = square            # 함수를 변수에 — 호출 아님(괄호 없음)
f(5)                  # 25 — f 가 square 와 같은 함수
type(f)               # <class 'function'>

# 함수를 인자로
def apply(fn, x):
    return fn(x)

apply(square, 7)      # 49

# 함수가 함수를 반환
def make_multiplier(factor):
    def inner(x):
        return x * factor
    return inner

double = make_multiplier(2)
triple = make_multiplier(3)
double(5), triple(5)  # (10, 15)

마지막 패턴이 데코레이터의 뼈대. make_multiplier 가 자기 안에 inner 함수를 만들고 그걸 돌려줍니다. innerfactor 를 기억해요 — 이걸 클로저(closure) 라 부르고, 18편 타입힌트와도 연결됩니다.

@decorator — 함수를 감싸는 함수

import time

def timer(fn):
    def wrapped(*args, **kwargs):
        t0 = time.time()
        result = fn(*args, **kwargs)
        elapsed = time.time() - t0
        print(f"{fn.__name__}: {elapsed:.3f} 초")
        return result
    return wrapped

@timer
def slow_sum(n):
    return sum(range(n))

slow_sum(10_000_000)
# slow_sum: 0.187 초
# 49999995000000

@timer 한 줄이 이 코드와 같습니다:

def slow_sum(n):
    return sum(range(n))

slow_sum = timer(slow_sum)    # 원본 함수를 timer 로 감싸 다시 할당

핵심 패턴 세 가지.

  1. 데코레이터는 함수를 받아 함수를 돌려준다 — timer 가 fn 을 받고 wrapped 를 반환
  2. wrapped 안에서 원본 호출result = fn(*args, **kwargs). 앞뒤로 부가 동작
  3. *args, **kwargs 로 모든 시그니처 수용 — 어떤 인자 조합이든 받아 그대로 전달

*args 와 **kwargs — 가변 인자

def f(*args, **kwargs):
    print(args)      # 위치 인자들이 튜플로
    print(kwargs)    # 키워드 인자들이 딕셔너리로

f(1, 2, 3, name="준성", age=30)
# (1, 2, 3)
# {'name': '준성', 'age': 30}

# 펼쳐서 전달
nums = [1, 2, 3]
print(*nums)         # 1 2 3 — 리스트를 위치 인자로 펼침
config = {"sep": "-", "end": "!\n"}
print("a", "b", "c", **config)   # a-b-c!

데코레이터에서 *args, **kwargs 가 거의 표준 — "원본 함수가 어떤 시그니처든 그대로 전달" 한다는 선언이에요.

functools.wraps — 자기 정체성을 잃지 않기

위의 @timer 에는 미묘한 버그가 있습니다. 감싼 뒤 함수의 이름·docstring 이 사라져요.

@timer
def slow_sum(n):
    """0부터 n-1 까지의 합."""
    return sum(range(n))

slow_sum.__name__     # 'wrapped'  ❌ 'slow_sum' 이어야
slow_sum.__doc__      # None       ❌ docstring 사라짐

해결은 functools.wraps 데코레이터(데코레이터를 만드는 데코레이터). 한 줄만 추가.

from functools import wraps

def timer(fn):
    @wraps(fn)                # ← 이 한 줄로 메타데이터 보존
    def wrapped(*args, **kwargs):
        ...
    return wrapped

slow_sum.__name__     # 'slow_sum'   ✓
slow_sum.__doc__      # '0부터 n-1 까지의 합.'   ✓

데코레이터를 만들 땐 거의 항상 @wraps 를 같이. 디버깅·문서 자동화 때 본인을 구합니다.

표준 라이브러리 단골 데코레이터 + 마무리

📌 자주 만나는 데코레이터 5종

· @functools.lru_cache — 인자가 같으면 결과를 캐싱. 재귀 피보나치 100배 빠르게

· @functools.cache — 무제한 캐시 (3.9+)

· @dataclass — 14편의 자동 클래스 생성

· @property — 메서드를 속성처럼 호출하게

· @staticmethod·@classmethod — self 안 받는 메서드

from functools import lru_cache

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

fib(100)    # 354224848179261915075 — 캐시 없으면 우주가 끝남
⚠️ 데코레이터를 직접 자주 만들지는 마세요. 입문~중급 단계에서는 표준 라이브러리·프레임워크가 제공하는 것을 쓰는 게 90%. 본인 코드 5천 줄 안에서 같은 부가 동작이 8~10곳에 반복돼야 직접 만들 가치가 생깁니다. 단순 함수로 충분한 걸 데코레이터로 만들면 가독성이 떨어져요.

다음 미션: ① @logger 데코레이터 — 호출 시작·끝·인자·반환값 출력 ② @lru_cache 로 피보나치 속도 비교 ③ @retry(times=3) 처럼 인자 받는 데코레이터 도전 (힌트: 한 단계 더 감싸기).

다음 편 미리보기

18편 — "파이썬 타입 힌트": def f(x: int) -> str. 동적 타입의 부드러운 안전망. mypy·VS Code 활용.

📚 27편 파이썬 교재 시리즈 — 17/27편
← 16편 "venv" · 다음: 18편 "타입 힌트"

© 2026 주나이테크(주) @JUNAITECH