데코레이터 (Decorator)

By | 9월 8, 2025
데코레이터 (Decorator)

파이썬 데코레이터 심층 분석

파이썬의 데코레이터는 기존 함수의 소스 코드를 건드리지 않고도 그 기능을 확장하거나 변경할 수 있도록 돕는 유연하고 강력한 기능입니다. ‘장식하다’라는 뜻처럼, 함수에 추가적인 기능을 덧붙이는 역할을 수행합니다.

데코레이터의 핵심 원리

데코레이터는 본질적으로 다음 세 가지 특성을 가집니다.

  • 함수를 인수로 받는 함수: 다른 함수를 입력으로 받아들입니다.
  • 새로운 함수를 반환하는 함수: 입력받은 함수를 “감싸서” 추가 기능이 적용된 새로운 함수를 만들어 반환합니다.
  • 구문 간편화: @ 기호를 사용하여 함수 = 데코레이터(함수)와 같은 복잡한 코드를 간결하게 표현할 수 있게 해주는 기능입니다.

데코레이터가 가능한 이유: 일급 객체로서의 함수

데코레이터가 파이썬에서 구현 가능한 이유는 함수가 ‘일급 객체’이기 때문입니다. 일급 객체란 다른 데이터 타입처럼 자유롭게 다룰 수 있는 대상을 의미하며, 함수가 다음 조건을 만족한다는 뜻입니다.

  • 변수에 할당할 수 있습니다.
  • 다른 함수의 인수로 전달할 수 있습니다.
  • 다른 함수의 반환 값으로 사용할 수 있습니다.
  • 리스트나 딕셔너리 같은 자료 구조에 저장할 수 있습니다.

데코레이터의 기본 구조와 동작 방식

가장 기본적인 데코레이터는 함수를 인수로 받아 새로운 함수를 반환하는 중첩 함수 구조를 가집니다.

def my_decorator(func):  # 1. 함수(func)를 인수로 받음
    def wrapper(*args, **kwargs):  # 2. func를 감싸는 내부 함수(wrapper) 정의
        print("함수 실행 전에 할 작업")  # 3. 추가 기능 (함수 실행 전)
        result = func(*args, **kwargs)  # 4. 원본 함수(func) 실행
        print("함수 실행 후에 할 작업")  # 5. 추가 기능 (함수 실행 후)
        return result  # 6. 원본 함수의 결과 반환
    return wrapper  # 7. wrapper 함수를 반환

이 데코레이터를 사용하는 방법은 두 가지가 있습니다.

1. @ 구문 활용 (일반적으로 사용)

이 방식은 코드를 더 직관적으로 만듭니다.

@my_decorator
def say_hello():
    print("안녕하세요!")

say_hello()

위 코드는 내부적으로 say_hello = my_decorator(say_hello)와 동일하게 작동합니다. 즉, say_hello 변수에 my_decorator가 반환한 wrapper 함수가 할당됩니다. 따라서 say_hello()를 호출하면 실제로는 wrapper 함수가 실행됩니다.

2. 직접 할당 방식

위 @ 구문이 어떻게 동작하는지 이해하는 데 도움이 됩니다.

def say_hello():
    print("안녕하세요!")

decorated_say_hello = my_decorator(say_hello)
decorated_say_hello()

데코레이터의 다양한 활용 사례

데코레이터는 여러 상황에서 유용하게 사용됩니다.

  • 로그 기록: 함수 호출 전후에 로그를 남기거나 함수의 입력 및 출력 값을 기록할 때 사용합니다.
  • 성능 분석: 함수의 실행 시간을 측정하여 성능을 분석할 때 활용합니다.
  • 사용자 인증 및 권한 확인: 특정 함수를 실행하기 전에 사용자 로그인 상태나 권한을 확인해야 할 때 사용합니다.
  • 데이터 캐싱: 함수의 결과를 저장해두고, 동일한 입력으로 다시 호출될 때 연산을 다시 수행하지 않고 저장된 값을 반환합니다.
  • 프레임워크: Django나 Flask 같은 웹 프레임워크에서 URL 라우팅 (@app.route('/hello')) 등에 널리 쓰입니다.

예제: 함수 실행 시간 측정

다음은 함수의 실행 시간을 측정하는 데코레이터 예시입니다.

import time

def timer_decorator(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 long_running_task():
    time.sleep(2)  # 2초간 대기
    print("작업 완료!")

@timer_decorator
def calculate_sum(a, b):
    time.sleep(0.5) # 0.5초 대기
    return a + b

long_running_task()
print(calculate_sum(10, 20))

인수를 받는 데코레이터

데코레이터도 인수를 받을 수 있습니다. 이 경우, 세 개의 중첩된 함수 구조를 가집니다. 가장 바깥 함수는 데코레이터의 인수를 받고, 중간 함수는 실제 데코레이터 역할을 하며, 가장 안쪽 함수는 원본 함수를 감싸는 역할을 합니다.

def repeat(num_times):
    def decorator_repeat(func):
        def wrapper(*args, **kwargs):
            for _ in range(num_times):
                func(*args, **kwargs)
        return wrapper
    return decorator_repeat

@repeat(num_times=3)
def greet(name):
    print(f"안녕하세요, {name}!")

greet("앨리스")

위 예제에서 @repeat(num_times=3)는 먼저 repeat(3)을 호출하고, 이 호출이 반환하는 decorator_repeat 함수가 greet 함수에 적용됩니다.


클래스를 이용한 데코레이터

데코레이터는 함수 외에 클래스로도 만들 수 있습니다. 클래스 데코레이터는 상태(state)를 유지할 수 있다는 장점이 있습니다. 클래스 데코레이터를 만들 때는 __init__ 메서드에서 데코레이트될 함수를 받고, __call__ 메서드에서 데코레이션 로직을 정의합니다.

class CountCalls:
    def __init__(self, func):
        self.func = func
        self.num_calls = 0

    def __call__(self, *args, **kwargs):
        self.num_calls += 1
        print(f"'{self.func.__name__}' 함수 호출 횟수: {self.num_calls}회")
        return self.func(*args, **kwargs)

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

say_hello()
say_hello()
say_hello()

클래스 메서드에 데코레이터 적용하기

클래스 내부의 메서드에 데코레이터를 적용하는 방법은 일반 함수에 적용하는 것과 동일합니다. 단, 메서드는 첫 번째 인수로 self를 받으므로, 데코레이터의 래퍼(wrapper) 함수 또한 self를 반드시 포함해야 합니다.

def my_method_decorator(func):
    def wrapper(self, *args, **kwargs):
        print(f"'{self.__class__.__name__}.{func.__name__}' 메서드 실행 전")
        result = func(self, *args, **kwargs)
        print(f"'{self.__class__.__name__}.{func.__name__}' 메서드 실행 후")
        return result
    return wrapper

class MyClass:
    def __init__(self, name):
        self.name = name

    @my_method_decorator
    def greet(self):
        print(f"안녕하세요, {self.name}!")

obj = MyClass("김민준")
obj.greet()

클래스 데코레이터의 기본 개념

클래스 데코레이터는 인수로 클래스 자체를 받아 새로운 클래스를 반환합니다. class 키워드 바로 위에 @ 기호와 함께 사용됩니다.

def my_class_decorator(cls):
    print(f"클래스 '{cls.__name__}'를 데코레이트합니다.")
    cls.new_method = lambda self: print("데코레이터가 추가한 새로운 메서드입니다.")
    return cls

@my_class_decorator
class MyClass:
    def __init__(self, name):
        self.name = name

    def greet(self):
        print(f"Hello, {self.name}!")

obj = MyClass("Charlie")
obj.greet()
obj.new_method()

functools.wraps의 중요성

데코레이터를 사용하면 원본 함수의 이름이나 문서 문자열(__doc__) 같은 메타데이터가 손실될 수 있습니다. 이를 방지하기 위해 파이썬 표준 라이브러리의 functools.wraps를 사용합니다.

import functools

def simple_decorator(func):
    @functools.wraps(func)  # 이 줄이 핵심
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@simple_decorator
def my_function():
    """이 함수는 중요한 작업을 수행합니다."""
    pass

print(my_function.__name__)
print(my_function.__doc__)

@functools.wraps(func)를 사용하면 wrapper 함수가 원본 함수 func의 메타데이터를 그대로 유지하게 되어 디버깅 및 문서화에 큰 도움이 됩니다.

마무리

파이썬의 데코레이터는 코드의 재사용성을 높이고, 기능을 모듈화하여 여러 함수에 쉽게 적용할 수 있는 강력한 패턴입니다. 함수의 동작을 직접 수정하지 않고도 확장할 수 있어 많은 파이썬 프레임워크와 라이브러리에서 핵심적인 역할을 수행합니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다