모든 강의를 무료로 볼 수 있어요. 회원가입 없이도 학습 가능합니다.

23강 / 전체 32강

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

11분 읽기 조회 5

데코레이터의 내부 동작 원리를 완전히 이해하고 타이머·캐시·재시도 데코레이터를 직접 구현한다.

🎯 이 강의에서 배우는 것

데코레이터(@decorator)는 파이썬의 가장 우아한 기능 중 하나입니다. 함수에 기능을 추가하면서도 함수 자체를 수정하지 않습니다. 로깅, 캐싱, 재시도, 인증 검사 등 "횡단 관심사(cross-cutting concerns)"를 깔끔하게 분리합니다. 이 강의에서 클로저와 고차 함수로 데코레이터의 원리를 완전히 이해합니다.

🔧 데코레이터의 정체 — 문법 설탕 제거

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("함수 실행 전")
        result = func(*args, **kwargs)   # 원본 함수 호출
        print("함수 실행 후")
        return result
    return wrapper

def greet(name):
    print(f"안녕하세요, {name}님!")

# @ 없이 수동으로 데코레이터 적용
greet = my_decorator(greet)   # greet는 이제 wrapper를 가리킴
greet("홍길동")
# 함수 실행 전
# 안녕하세요, 홍길동님!
# 함수 실행 후

# @ 문법 (위와 완전히 동일)
@my_decorator
def greet2(name):
    print(f"안녕하세요, {name}님!")

greet2("이순신")
# 동일한 출력

# @ 는 단순한 문법 설탕:
# @decorator
# def func(): ...
# 는 func = decorator(func)와 동일

🏷️ functools.wraps — 메타데이터 보존

import functools

# wraps 없는 데코레이터의 문제
def bad_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@bad_decorator
def add(a, b):
    """두 수를 더한다."""
    return a + b

print(add.__name__)   # 'wrapper' ← 원본 이름 잃음!
print(add.__doc__)    # None ← 원본 docstring 잃음!

# functools.wraps 사용 (권장)
def good_decorator(func):
    @functools.wraps(func)   # func의 메타데이터를 wrapper에 복사
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@good_decorator
def subtract(a, b):
    """두 수의 차를 반환한다."""
    return a - b

print(subtract.__name__)   # 'subtract' ← 원본 이름 유지
print(subtract.__doc__)    # '두 수의 차를 반환한다.' ← 원본 doc 유지
print(subtract.__wrapped__)  # <function subtract at 0x...> ← 원본 함수 접근 가능

⏱️ 실용 데코레이터 구현 — 타이머, 캐시, 재시도

import functools
import time

# 실행 시간 측정 데코레이터
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:.6f}초")
        return result
    return wrapper

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

slow_function()   # slow_function 실행 시간: 0.100xxx초

# 캐싱 데코레이터 (lru_cache 원리 직접 구현)
def simple_cache(func):
    cache = {}   # 자유 변수 — 클로저로 캡처됨
    @functools.wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    wrapper.cache = cache   # 외부에서 캐시 확인 가능
    return wrapper

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

print(fibonacci(30))   # 832040 (빠름!)
print(fibonacci.cache)  # 캐시 내용 확인

# 재시도 데코레이터 (인자 있는 데코레이터 — 다음 섹션에서 설명)
def retry(times=3, delay=1.0):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_exception = e
                    print(f"시도 {attempt}/{times} 실패: {e}")
                    if attempt < times:
                        time.sleep(delay)
            raise last_exception
        return wrapper
    return decorator

import random

@retry(times=3, delay=0.1)
def unstable_api():
    if random.random() < 0.7:   # 70% 확률로 실패
        raise ConnectionError("연결 실패")
    return "성공!"

# unstable_api()

🎛️ 인자 있는 데코레이터 — 3중 중첩

import functools

# @retry(times=3)를 풀어쓰면:
# 1. retry(times=3)  → decorator 함수 반환
# 2. decorator(func) → wrapper 함수 반환
# 3. wrapper(*args, **kwargs) → 실제 실행

# 권한 검사 데코레이터 (인자 있음)
def require_role(role):          # 레이어 1: 팩토리
    def decorator(func):         # 레이어 2: 데코레이터
        @functools.wraps(func)
        def wrapper(*args, **kwargs):  # 레이어 3: 래퍼
            current_user = kwargs.get("current_user", {})
            if current_user.get("role") != role:
                raise PermissionError(f"{role} 권한이 필요합니다")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@require_role("admin")
def delete_user(user_id, current_user=None):
    return f"사용자 {user_id} 삭제 완료"

admin = {"name": "관리자", "role": "admin"}
user = {"name": "일반", "role": "user"}

print(delete_user(42, current_user=admin))   # 사용자 42 삭제 완료
try:
    delete_user(42, current_user=user)   # PermissionError!
except PermissionError as e:
    print(e)   # admin 권한이 필요합니다

🏛️ 클래스 기반 데코레이터

import functools

# __call__을 구현한 클래스를 데코레이터로 사용
class CallCounter:
    def __init__(self, func):
        functools.update_wrapper(self, func)   # wraps의 클래스 버전
        self.func = func
        self.call_count = 0

    def __call__(self, *args, **kwargs):
        self.call_count += 1
        print(f"{self.func.__name__} {self.call_count}번째 호출")
        return self.func(*args, **kwargs)

@CallCounter
def greet(name):
    return f"안녕, {name}!"

greet("홍길동")   # greet 1번째 호출
greet("이순신")   # greet 2번째 호출
print(greet.call_count)   # 2 ← 클래스 속성으로 상태 접근

# 다중 데코레이터: 적용 순서 주의
def bold(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return f"<b>{func(*args, **kwargs)}</b>"
    return wrapper

def italic(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return f"<i>{func(*args, **kwargs)}</i>"
    return wrapper

@bold          # 바깥
@italic        # 안쪽
def text(s):
    return s

print(text("파이썬"))   # <b><i>파이썬</i></b>
# @bold @italic def text(): 는 text = bold(italic(text))
# 아래부터 위로 적용됨

⚠️ 자주 하는 실수

  • functools.wraps 빠뜨림: wraps 없이 데코레이터를 만들면 함수 이름·docstring이 wrapper로 교체됩니다. 디버깅과 help()가 어려워집니다. 모든 데코레이터에 @functools.wraps를 습관화하세요.
  • 인자 있는 데코레이터와 없는 데코레이터 혼동: @decorator는 decorator(func)이고, @decorator(arg)는 decorator(arg)(func)입니다. 인자 유무에 따라 중첩 수준이 다릅니다.
  • 다중 데코레이터 적용 순서 착각: @a @b는 위에서 아래로 읽히지만, 적용은 아래에서 위로 됩니다(b가 먼저, a가 나중). 순서가 중요한 데코레이터(인증→로깅 vs 로깅→인증)는 순서를 명확히 하세요.

📝 정리 및 다음 강의 예고

  • 데코레이터 = 고차 함수 + 클로저. @decorator는 func = decorator(func)의 문법 설탕입니다.
  • 반드시 @functools.wraps(func)를 사용해 원본 함수의 메타데이터를 보존하세요.
  • 인자 있는 데코레이터는 3중 중첩 함수(팩토리→데코레이터→래퍼)입니다.
  • 클래스 기반 데코레이터는 __call__로 상태를 가진 데코레이터를 만들 때 유용합니다.

다음 강의: 람다와 고차 함수 — map(), filter(), sorted(), reduce()의 완전한 이해와 람다의 적절한 사용 기준을 배웁니다.

관련 주제

  • 데코레이터 정의와 문법설탕
  • functools.wraps
  • 인자를 받는 데코레이터
  • 타이머·캐시·재시도 데코레이터
  • 클래스 기반 데코레이터
  • 다중 데코레이터 적용순서
  • 개발·프로그래밍
  • 개발·프로그래밍 강의
  • 파이썬 기초 40강 — 처음 배우는 프로그래밍
  • 무료강의
  • 무료 온라인 강의
  • NUGUNA
  • 누구나

📚 시리즈 전체 공유

파이썬 기초 40강 — 처음 배우는 프로그래밍

이 강의가 속한 시리즈는 총 32강, 모두 무료입니다. 처음부터 배우려는 동료에게 시리즈 전체를 알려 주세요.

댓글

0/1000

불러오는 중...