데코레이터의 내부 동작 원리를 완전히 이해하고 타이머·캐시·재시도 데코레이터를 직접 구현한다.
🎯 이 강의에서 배우는 것
데코레이터(@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
불러오는 중...
