래핑된 원본 객체의 데이터 보존
문제점: 원본 메타데이터 손실
- 데코레이터를 함수에 적용하면 원본 함수가 wrapped라는 새로운 함수로 감싸지게 됨.
- 이로 인해 원본 함수 이름, 문서(docstring), 어노테이션(annotations) 등 고유의 메타데이터가 손실되고 래퍼 함수의 정보로 덮어씌워짐.
- 결과: 디버깅이 어려워지고, doctest 같은 단위 테스트 도구가 정상적으로 동작하지 않는 부작용이 발생.
def trace_decorator(function):
def wrapped(*args, **kwargs):
logger.info("%s 실행", function.__qualname__)
return function(*args, **kwargs)
return wrapped
@trace_decorator
def process_account(account_id: str):
"""id별 계정 처리"""
logger.info("%s 계정 처리", account_id)
이 상태에서 확인해보면:
>>> help(process_account)
# wrapped(*args, **kwargs) 로 나옴
>>> process_account.__qualname__
'trace_decorator.<locals>.wrapped'
>>> process_account.__annotations__
{}
사라지는 것들:
- 함수 이름 (
__name__,__qualname__) - docstring
- 타입 힌트 (
__annotations__) - 기타 원본 함수의 속성
→ 디버깅이 어려워지고,doctest도 제대로 동작하지 않음
해결 방법: @functools.wraps 사용
래퍼 함수 위에 @wraps(function)을 붙여주면 원본 함수의 메타데이터가 래퍼 함수로 복사됨.
- 장점:
help()나__qualname__을 조회했을 때 원본 함수의 이름과 설명이 정확하게 출력됨.__wrapped__속성을 통해 원본 함수에 직접 접근할 수 있어 단위 테스트 시 매우 유용함.
from functools import wraps
def trace_decorator(function):
@wraps(function)
def wrapped(*args, **kwargs):
logger.info("running %s", function.__qualname__)
return function(*args, **kwargs)
return wrapped
>>> help(process_account)
# process_account(account_id) 로 정상 출력
# docstring도 그대로 유지
>>> process_account.__qualname__
'process_account'
- 추가로
__wrapped__속성이 생겨서 원본 함수에 접근할 수도 있음.
: 데코레이터를 만들 때는 항상 래핑된 함수 위에 functools.wraps를 사용해라.
데코레이터 부작용 처리
래퍼 함수 바깥에 로직을 둘 때의 문제점
- 데코레이터 함수 내부에서
start_time = time.time()같은 초기화 로직을 래퍼 함수 바깥(wrapped함수 정의 상단)에 두면 심각한 문제가 발생함. - 원인: 데코레이터는 모듈이 임포트(import)되는 시점에 단 한 번 실행됨. 즉, 함수가 호출될 때가 아니라 코드가 처음 로드될 때 시간이 측정됨.
- 결과: 함수를 여러 번 호출할 때마다 실행 시간이 누적되어 점점 더 오래 걸리는 것처럼 잘못된 결과가 출력되는 치명적인 버그가 발생함.
부작용이 발생하는 코드는 반드시 래핑된 함수(wrapped) 안쪽에 작성해야 한다.
def traced_function_wrong(function):
logger.info("%s 함수 실행", function)
start_time = time.time()
@wraps(function)
def wrapped(*args, **kwargs):
result = function(*args, **kwargs)
logger.info(
"함수 %s의 실행시간: %.2fs",
function, time.time() - start_time
)
return result
return wrapped
확인해보면:
- 모듈을
import만 해도 로그가 출력됨 (함수를 호출하지 않았는데 실행됨) start_time이import시점에 고정됨 → 함수를 여러 번 호출할수록 실행 시간이 계속 늘어남 (잘못된 측정)
>>> from decorator_side_effects_1 import process_with_delay
# 이미 로그가 출력됨 (잘못됨)
>>> main()
# 실행시간: 8.67s
>>> main()
# 실행시간: 13.39s ← 계속 증가
해결방법: 실행 지연 (Delaying Side Effects)
- 확실하지 않거나 호출 시점에 영향을 받는 로직은 반드시 래퍼(wrapped) 함수 내부로 이동시켜야 함.
- 즉, 부작용(시간 측정, 외부 설정 파일 파싱, 네트워크 연결 등)은 모듈이 임포트될 때가 아니라 실제로 원본 함수가 호출되는 순간 발생하도록 지연시켜야 한다.
def traced_function(function):
@wraps(function)
def wrapped(*args, **kwargs):
logger.info("%s 함수 실행", function.__qualname__)
start_time = time.time() # ← 안쪽으로 이동
result = function(*args, **kwargs)
logger.info(
"함수 %s의 실행시간: %.2fs",
function.__qualname__,
time.time() - start_time
)
return result
return wrapped
핵심
: 항상 @wraps를 사용해서 원본 함수의 정보를 보존하고, 부작용이 있는 코드는 반드시 래퍼 함수 안쪽에 작성하라.
댓글