본문 바로가기
python

파이썬 기초 - Decorator

by kyeongseo.oh 2025. 3. 20.

데코레이터의 기본 개념

데코레이터(Decorator)는 함수 또는 클래스를 감싸서 추가적인 기능을 부여하는 기능이다. 이를 통해 코드를 수정하지 않고도 부가적인 동작(로깅, 인증, 캐싱 등)을 쉽게 추가할 수 있다.

데코레이터가 필요한 이유

  • 코드 재사용: 여러 함수에 동일한 기능을 추가할 때 코드 중복을 피할 수 있다.
  • 관심사 분리: 핵심 로직과 부가 기능(로깅, 타이밍, 검증 등)을 분리할 수 있다.
  • 가독성 향상: 함수의 주요 로직을 깔끔하게 유지하면서 부가 기능을 추가할 수 있다.

데코레이터의 원리

Python의 함수는 함수 자체를 변수에 할당하거나, 다른 함수의 인자로 전달할 수 있다. 데코레이터는 이를 활용하여 함수를 인자로 받아 새로운 동작을 추가하는 방식으로 동작한다. 

def greeting():
    return "Hello, World!"

# 함수를 변수에 할당
say_hello = greeting

# 변수를 통해 함수 호출
print(say_hello())  # 출력: Hello, World!

데코레이터의 기본 형태

데코레이터는 이러한 개념들을 활용하여 다음과 같이 구현된다.

def my_decorator(func):
    def wrapper():
        print("코드 실행 전")
        func()
        print("코드 실행 후")
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")

# 이제 함수 호출
say_hello()

출력 결과:

코드 실행 전
Hello!
코드 실행 후

 

@my_decorator 구문은 아래 코드와 동일한 의미이다.

def say_hello():
    print("Hello!")

say_hello = my_decorator(say_hello)

인자를 처리하는 데코레이터

함수 인자 전달하기

함수가 인자를 받는 경우, 데코레이터도 이를 처리할 수 있어야 한다.

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("코드 실행 전")
        result = func(*args, **kwargs)
        print("코드 실행 후")
        return result # return 값이 필요한 경우 사용한다.
    return wrapper

@my_decorator
def greet(name):
    print(f"Hello, {name}!")

greet("John Doe")

출력 결과:

코드 실행 전
Hello, John Doe!
코드 실행 후

 

위 구문은 아래 코드와 동일하다.

def say_hello(name):
    print(f"Hello, {name}!")

say_hello = my_decorator(say_hello)
say_hello("John Doe")

 

데코레이터는 함수를 인자로 받아야 하는 데, 아래와 같이 사용하면 my_decorator(None)이 되어, 에러가 발생하게 된다.

def say_hello(name):
    print(f"Hello, {name}!")

say_hello = my_decorator(say_hello("John Doe"))
say_hello()

데코레이터에 인자 전달하기

데코레이터 자체에 인자를 전달하려면 추가적인 중첩 함수가 필요하다.

def add_prefix(prefix):              # 외부 함수: 데코레이터에 전달할 인자 받음
    def decorator(func):            # 실제 데코레이터: 함수를 인자로 받음
        def wrapper(name):         # 내부 래퍼: 원본 함수 대체 (내부 래퍼가 필요없는 경우 생략 가능)
            return f"{prefix} {func(name)}"
        return wrapper
    return decorator

@add_prefix("Mr.")
def get_name(name):
    return name

print(get_name("John Doe"))  # 출력: Mr. John Doe

 

이 경우 @add_prefix("Mr.")는 다음 코드와 동일하다.

def get_name(name):
    return name

who_am_i = add_prefix("Mr.")(get_name)("John Doe")

 

클래스 데코레이터나 직접 함수를 반환하는 패턴에서는 래퍼없이 데코레이터를 선언할 수 있다.

routes = {}

def route(path):
    def decorator(func):
        # 경로와 함수를 매핑
        routes[path] = func
        return func  # 원본 함수 반환
    return decorator

@route('/home')
def home():
    return "홈페이지"

# 라우트 확인
print(routes)

다중 데코레이터 적용

하나의 함수에 여러 데코레이터를 적용할 수 있다.

def uppercase_decorator(func):
    def wrapper():
        result = func()
        return result.upper()
    return wrapper

def exclamation_decorator(func):
    def wrapper():
        result = func()
        return result + "!!!"
    return wrapper

@uppercase_decorator
@exclamation_decorator
def greet():
    return "hello world"

print(greet())  # 출력: HELLO WORLD!!!

 

여기서 데코레이터는 아래에서 위로 적용된다. 이 예제에서는 @exclamation_decorator이 먼저 적용되고 그 다음 @uppercase_decorator가 적용된다.

functools 모듈의 유용한 데코레이터

@functools.wraps - 메타데이터 보존

데코레이터를 사용할 때 한 가지 문제점은 원래 함수의 메타데이터(이름, 독스트링, 인자 목록 등)가 래퍼 함수에 의해 가려진다는 것이다. 이렇게 되면 디버깅이나 문서화에 문제가 생길 수 있다.

def my_decorator(func):
    def wrapper(*args, **kwargs):
        """Wrapper function"""
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def hello():
    """원본 함수의 독스트링"""
    pass

print(hello.__name__)        # 출력: wrapper
print(hello.__doc__)         # 출력: Wrapper function

 

functools.wraps 데코레이터를 사용하면 이 문제를 해결할 수 있다. 그러니 왠만하면 @wraps를 사용하는 게 좋다.

from functools import wraps

def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """Wrapper function"""
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def hello():
    """원본 함수의 독스트링"""
    pass

print(hello.__name__)        # 출력: hello
print(hello.__doc__)         # 출력: 원본 함수의 독스트링

 

@functools.lru_cache - 메모이제이션과 성능 최적화

lru_cache 데코레이터는 함수 호출 결과를 캐시하여, 동일한 인자로 함수가 다시 호출될 때 계산을 반복하지 않고 캐시된 결과를 반환한다. LRU(Least Recently Used) 캐시 방식으로 가장 오랫동안 사용되지 않은 항목을 우선 삭제하며, 이를 통해 중복 계산을 방지하고 속도를 향상시킨다. 재귀 함수 등에 활용할 수 있다.

from functools import lru_cache
import time

@lru_cache(maxsize=None)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# 측정 시작
start = time.time()
print(fibonacci(100))
print(f"시간 소요: {time.time() - start:.10f}초")

# 두 번째 호출은 캐시된 결과를 사용하므로 매우 빠름
start = time.time()
print(fibonacci(100))
print(f"시간 소요: {time.time() - start:.10f}초")

maxsize 매개변수는 캐시할 최대 호출 수를 지정한다. None은 크기 제한이 없음을 의미한다.

클래스 데코레이터

클래스 데코레이터를 사용해 클래스에 속성/메서드를 추가하거나,메서드의 동작 수정할 수 있다.

 

def add_features(cls):
    # 속성 추가
    cls.version = "1.0"
    
    # 메서드 추가
    def get_info(self):
        return f"{self.__class__.__name__} 객체, 버전: {self.__class__.version}"
    
    cls.get_info = get_info

    # 기존 add() 메서드의 반환값 수정
    original_add = cls.add  # 기존 메서드 저장

    def new_add(self, a, b):
        result = original_add(self, a, b)  # 원래 add() 실행
        return f"{result} 입니다!"  # 반환값 변경

    cls.add = new_add  # 수정된 메서드 적용
    
    return cls

@add_features
class Calculator:
    def __init__(self, name):
        self.name = name
    
    def add(self, a, b):
        return a + b
    
    def multiply(self, a, b):
        return a * b

# 실행 테스트
calc = Calculator("calc")
print(calc.version)         # 1.0
print(calc.get_info())      # Calculator 객체, 버전: 1.0
print(calc.add(5, 3))       # 출력: 8입니다!

예제 : 실행 시간 측정 데코레이터

import time
from functools import wraps

def timer_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"{func.__name__} 실행 시간: {end_time - start_time:.4f}초")
        return result
    return wrapper

@timer_decorator
def sleep_function():
    time.sleep(1)
    return "Function completed"

print(sleep_function())



 

 

댓글