본문 바로가기
👩🏻‍💻 Computer Science/clean-code(ft.python)

Chapter5. 데코레이터 활용: 흔한 실수 피하기(래핑된 원본 객체의 데이터 보존, 데코레이터 부작용 처리)

by devhertz 2026. 10. 18.

래핑된 원본 객체의 데이터 보존

문제점: 원본 메타데이터 손실

  • 데코레이터를 함수에 적용하면 원본 함수가 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를 사용해서 원본 함수의 정보를 보존하고, 부작용이 있는 코드는 반드시 래퍼 함수 안쪽에 작성하라.

 

댓글