파이썬165번5 MIN
중첩 데코레이터와 적용 순서
@A @B @C로 여러 데코레이터를 쌓을 때 적용 순서와 실행 순서가 어떻게 다른지, 순서가 결과에 미치는 영향을 코드 예제로 설명합니다.
이 글은 PALDYN Tech Blog의 글입니다.원문 보기
지난 글에서 클래스 기반 데코레이터를 살펴봤다. 실무에서는 하나의 함수에 여러 데코레이터를 동시에 적용하는 경우가 많다. 이때 적용 순서와 실행 순서가 직관과 반대여서 혼란을 일으킨다. 이 글에서 그 규칙을 명확히 정리한다.
적용(wrapping) 순서: 아래에서 위로
@A
@B
@C
def func():
pass
Python은 이 코드를 다음과 동일하게 처리한다.
func = A(B(C(func)))
함수에서 가장 가까운 @C가 먼저 적용되고, 그 다음 @B, 마지막으로 @A가 적용된다. 위에서 아래가 아니라 아래에서 위로 wrap된다.
실행 순서: 위에서 아래로
wrap 순서와 실행 순서는 반대다. func()를 호출하면 가장 바깥인 A wrapper가 먼저 실행된다.
# 실행 흐름
A_wrapper 전처리
B_wrapper 전처리
C_wrapper 전처리
원본 func 실행
C_wrapper 후처리
B_wrapper 후처리
A_wrapper 후처리
쉽게 기억하는 방법: 겹겹이 쌓인 양파 껍질처럼, 벗길 때는 바깥 껍질부터(실행 순서), 입을 때는 안쪽부터(적용 순서).
실험으로 확인하기
from functools import wraps
def make_tag(name):
def decorator(func):
@wraps(func)
def wrapper(*a, **k):
return f"<{name}>" + func(*a, **k) + f"</{name}>"
return wrapper
return decorator
@make_tag("b") # 마지막 적용 (바깥)
@make_tag("i") # 첫 번째 적용 (안쪽)
def greet():
return "Hello"
print(greet()) # <b><i>Hello</i></b>
greet = make_tag("b")(make_tag("i")(greet))와 동일하다. 실행 시 <b> wrapper가 먼저 실행되고, 그 안에서 <i> wrapper, 마지막으로 원본 greet가 실행된다.
순서가 결과를 바꾸는 경우
데코레이터 순서는 기능적 차이를 만들 수 있다.
import time
from functools import wraps
def log(func):
@wraps(func)
def wrapper(*a, **k):
print(f"[LOG] {func.__name__} 호출")
return func(*a, **k)
return wrapper
def timer(func):
@wraps(func)
def wrapper(*a, **k):
start = time.perf_counter()
result = func(*a, **k)
print(f"[TIME] {time.perf_counter()-start:.4f}s")
return result
return wrapper
# 순서 A: log가 바깥, timer가 안쪽
@log
@timer
def process_a():
time.sleep(0.01)
# 순서 B: timer가 바깥, log가 안쪽
@timer
@log
def process_b():
time.sleep(0.01)
process_a()
# [LOG] process_a 호출 ← log가 먼저
# [TIME] 0.0101s ← timer가 func 실행 시간만 측정
process_b()
# [LOG] process_b 호출 ← log 실행 시간도 포함됨
# [TIME] 0.0102s ← timer가 log + func 시간 측정
timer가 바깥(@timer @log)이면 log 실행 시간까지 포함하여 측정한다. timer가 안쪽(@log @timer)이면 원본 함수 실행 시간만 측정한다. 어느 쪽이 맞는지는 의도에 따라 다르다.
__name__과 중첩 데코레이터
각 데코레이터에 @wraps를 올바르게 적용했다면, 아무리 많이 쌓아도 __name__은 원본을 가리킨다.
@log
@timer
def process():
"""처리 함수"""
pass
print(process.__name__) # process
print(process.__doc__) # 처리 함수
print(process.__wrapped__) # timer의 wrapper
# inspect.unwrap(process)로 원본까지 도달 가능
__wrapped__는 바로 안쪽 함수를 가리킨다. inspect.unwrap으로 체인 전체를 따라가면 원본 process에 도달할 수 있다.
실무 권장 순서
여러 데코레이터를 함께 쓸 때 보편적으로 권장하는 순서가 있다.
@login_required # 가장 바깥: 진입 전 인증 체크
@cache(ttl=60) # 그 다음: 캐시 hit이면 내부 실행 불필요
@log_call # 마지막 바깥: 실제 처리 전 로깅
def get_user(uid):
...
외부 관심사(인증, 캐시)를 바깥에, 내부 관심사(로깅, 타이밍)를 안쪽에 두는 것이 일반적인 패턴이다. 팀마다 컨벤션이 다를 수 있으므로 일관성이 중요하다.
지난 글: 클래스로 만드는 데코레이터
다음 글: 상태를 가진 데코레이터
읽어주셔서 감사합니다. 😊

