여기서는 @ 문법 자체보다 여러 함수에 반복되는 전처리·후처리를 감싸는가, 아니면 그냥 함수를 한 번 더 호출하면 되는가를 먼저 봅니다.
핵심 로직은 그대로 두고 주변 관심사만 반복된다면 데코레이터가 맞고, 흐름을 숨기는 비용이 더 크면 일반 함수 호출이 낫습니다.
핵심 정리
from functools import wraps
def log_calls(func):
@wraps(func)
def wrapper(*args, kwargs):
print(f"calling {func.__name__}")
return func(*args, kwargs)
return wrapper
@log_calls
def greet(name: str) -> str:
return f"Hello, {name}"문법
기본형은 단순 데코레이터, 인자를 받는 decorator factory, 클래스 메서드 대상까지 같이 보면 됩니다.
def deco(func):
def wrapper(*args, kwargs):
return func(*args, kwargs)
return wrapper
@deco
def work():
...
def repeat(times):
def deco(func):
def wrapper(*args, **kwargs):
...
return wrapper
return deco
class Service:
@deco
def run(self) -> None:
...@deco는 결국 work = deco(work)를 읽기 좋게 쓴 형태입니다.
문법이 특별해 보여도 본질은 함수를 입력으로 받아 다른 함수로 감싸는 패턴입니다.
감싸기 모델
데코레이터는 함수를 받아 다른 함수로 감싼다
함수 하나를 받아 그 앞뒤에 공통 동작을 끼운 새 함수를 반환하는 패턴입니다.
def deco(func):
def wrapper(*args, kwargs):
print("before")
return func(*args, kwargs)
return wrapper범용 데코레이터라면 원래 함수 시그니처가 제각각일 수 있으므로 wrapper(*args, **kwargs)가 기본 패턴으로 자주 나옵니다.
적용 기준
공통 부가 기능이 여러 함수에 반복되면 데코레이터가 잘 맞는다
로그, 인증, 캐싱, 재시도 같은 코드는 함수마다 본문 맨 위나 아래에 반복되기 쉽습니다. 이럴 때 데코레이터로 감싸면 핵심 로직을 덜 흐리게 만들 수 있습니다.
@log_calls
def fetch_user(user_id: int):
...
@log_calls
def save_user(data: dict):
...반대로 한두 군데만 쓰고 흐름이 단순하면 일반 함수 호출이 더 직관적일 수 있습니다.
def timed(func):
@wraps(func)
def wrapper(*args, kwargs):
return func(*args, kwargs)
return wrapper원래 함수 호출 방식을 최대한 그대로 통과시키는 쪽이 보통 안전합니다.
메타데이터
wraps를 빼면 원래 함수 정보가 흐려진다
데코레이터를 씌우면 함수 이름, docstring, 디버깅 정보가 wrapper 쪽으로 바뀔 수 있습니다.
functools.wraps는 이 문제를 줄여 줍니다.
from functools import wraps
def deco(func):
@wraps(func)
def wrapper(*args, kwargs):
return func(*args, kwargs)
return wrapper테스트, 문서화, 로그 분석에서 차이가 크게 드러나므로 범용 데코레이터라면 거의 습관처럼 붙이는 편이 좋습니다.
주의할 점
wrapper에서 원래 함수의 반환값을 그대로 돌려주지 않으면 데코레이터를 붙인 함수가 None을 반환할 수 있습니다.
# ❌ 결과를 잃어버림
def log_calls(func):
def wrapper(*args, kwargs):
print("call")
func(*args, kwargs)
return wrapper
# ✅ 원래 함수 결과를 반환
def log_calls(func):
def wrapper(*args, kwargs):
print("call")
return func(*args, kwargs)
return wrapper# ❌ 너무 많은 부작용을 숨기면 실행 흐름을 추적하기 어려워진다
@auth_required
@retry
@log_calls
def fetch_user():
...참고 링크
2 sources