본문 바로가기

프로그래밍/Python

[Python] decorator에서 wrapper 자세히 보기

728x90
Python 데코레이터의 핵심, wrapper 실습 가이드
Python · Decorator / Wrapper

Python 데코레이터의 핵심, wrapper 실습 가이드

이 글은 데코레이터 안에서 항상 등장하는 wrapper 함수에만 초점을 맞춰, 실습 중심으로 동작 원리를 이해하도록 돕는 가이드입니다.

1. wrapper의 정체는 무엇인가?

데코레이터의 기본 형태는 다음과 같습니다.

def deco(func):
    def wrapper(*args, **kwargs):
        # (1) 실행 전
        result = func(*args, **kwargs)  # 원래 함수 호출
        # (2) 실행 후
        return result
    return wrapper

@deco
def hello():
    print("Hello")

핵심은 다음 두 줄입니다.

  • @decohello = deco(hello) 로 치환
  • deco가 반환하는 것은 wrapper 이므로, 이제 hello()는 곧 wrapper() 호출
즉, wrapper는 “원래 함수를 감싼 새로운 함수”이고,
그 안에서 원하는 전/후 처리 로직을 넣을 수 있습니다.

2. wrapper 동작 원리 – 인자 없는 함수 예제

def deco(func):
    def wrapper():
        print("[wrapper] 함수 호출 전")
        func()
        print("[wrapper] 함수 호출 후")
    return wrapper

@deco
def hello():
    print("hello 함수 본문")

hello()

실행 흐름:

  1. @decohello = deco(hello)
  2. hello() → 내부적으로 wrapper() 실행
  3. wrapper 안에서:
    • “함수 호출 전” 출력
    • func() 로 원래 hello 실행
    • “함수 호출 후” 출력

3. 인자가 있는 함수에 wrapper 적용하기

두 숫자를 더하는 함수에 로그를 찍는 예제입니다.

def log_deco(func):
    def wrapper(a, b):
        print(f"{func.__name__} 호출: a={a}, b={b}")
        result = func(a, b)
        print(f"{func.__name__} 결과: {result}")
        return result
    return wrapper

@log_deco
def add(a, b):
    return a + b

add(3, 5)

이 방식은 add에는 잘 동작하지만, 인자 개수가 다른 함수에는 적용할 수 없다는 한계가 있습니다.

4. 범용 wrapper를 위한 *args, **kwargs

여러 종류의 함수에 모두 쓰려면 다음과 같이 정의하는 것이 일반적입니다.

def log_deco(func):
    def wrapper(*args, **kwargs):
        print(f"{func.__name__} 호출, args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} 결과: {result}")
        return result
    return wrapper

@log_deco
def add(a, b):
    return a + b

@log_deco
def greet(name, msg="안녕"):
    return f"{msg}, {name}"

add(1, 2)
greet("홍길동", msg="반가워")

이렇게 하면 인자 형태와 상관없이, 모든 함수에 같은 데코레이터를 적용할 수 있습니다.

5. wrapper 안에 “실행 전/후 훅” 넣기

5-1. 실행 시간 측정 데코레이터

import time

def timeit(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} 실행 시간: {end - start:.4f}초")
        return result
    return wrapper

@timeit
def slow_add(a, b):
    time.sleep(1)
    return a + b

slow_add(3, 4)

5-2. 조건/권한 체크

def require_positive(func):
    def wrapper(x):
        if x <= 0:
            print("양수만 허용됩니다.")
            return None
        return func(x)
    return wrapper

@require_positive
def sqrt(x):
    return x ** 0.5

sqrt(9)    # 정상
sqrt(-1)   # wrapper에서 걸러짐

6. @wraps와 wrapper – 메타데이터 보존

6-1. @wraps 없이

def deco(func):
    def wrapper(*args, **kwargs):
        print("실행 중")
        return func(*args, **kwargs)
    return wrapper

@deco
def hello():
    """인사하는 함수입니다."""
    print("Hello")

print(hello.__name__)  # wrapper
print(hello.__doc__)   # None

6-2. @wraps 사용

from functools import wraps

def deco(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("실행 중")
        return func(*args, **kwargs)
    return wrapper

@deco
def hello():
    """인사하는 함수입니다."""
    print("Hello")

print(hello.__name__)  # hello
print(hello.__doc__)   # 인사하는 함수입니다.
@wraps(func)는 wrapper에게 원래 함수의 이름, docstring 등 메타데이터를 복사해 주어, 디버깅과 문서화 시에 함수 정보가 깨지지 않도록 해줍니다.

7. wrapper와 클로저(closure)

wrapper는 바깥 스코프의 변수를 기억하는 클로저이기도 합니다.

7-1. 단순 클로저 예제

def multiplier(n):
    def wrapper(x):
        return n * x
    return wrapper

times2 = multiplier(2)
times3 = multiplier(3)

print(times2(10))  # 20
print(times3(10))  # 30

7-2. 데코레이터 + 클로저

def log_prefix(prefix):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(prefix, "시작")
            result = func(*args, **kwargs)
            print(prefix, "끝")
            return result
        return wrapper
    return decorator

@log_prefix("[TEST]")
def run():
    print("실행!")

run()

여기서 wrapper는 외부 스코프의 prefix 값을 기억하고 있습니다. 이처럼 데코레이터는 클로저와 함께 사용되는 경우가 매우 많습니다.

8. 연습 문제

연습 1: 함수 호출 횟수 세기

요구 사항:

  • 어떤 함수에 붙이면 그 함수가 몇 번 호출됐는지 출력
  • 예: 첫 호출 → “호출 횟수: 1”, 두 번째 → “호출 횟수: 2”

힌트:

  • 데코레이터 안에 count = 0 두고, wrapper 안에서 nonlocal count 사용

연습 2: 간단 캐시 데코레이터

요구 사항:

  • 동일한 인자로 호출된 함수는 이전 결과를 재사용
  • 두 번째 호출부터는 계산 없이 바로 반환

힌트:

  • 데코레이터 안에 cache = {} 딕셔너리 두고, args를 키로 사용
연습 문제를 직접 구현해 보면,
“아, wrapper가 결국 함수 호출 전후에 공통 로직을 넣는 자리구나”라는 감각이 확실하게 자리 잡습니다.

9. 한줄 정리

  • wrapper는 데코레이터가 반환하는 “새로운 함수”이다.
  • 원래 함수는 사실상 wrapper로 대체되고, wrapper 안에서 원래 함수를 원하는 타이밍에 호출한다.
  • 범용성을 위해 거의 항상 *args, **kwargs를 사용한다.
  • 공통 전/후 처리, 조건 체크, 로깅, 캐싱 등의 로직을 wrapper 안에 넣으면 깔끔하게 재사용할 수 있다.