본문 바로가기
프로그래밍/파이썬

파이썬 - 데코레이터(Decorator)의 이해와 구현

by ennak 2024. 12. 1.
반응형

데코레이터의 본질

파이썬 데코레이터는 코드를 간결하고 재사용 가능하게 만드는 강력한 도구다.
데코레이터의 핵심 아이디어는 기존 함수나 클래스를 수정하지 않고도 그 동작을 확장하거나 변경할 수 있다는 점이다.
이는 개방-폐쇄 원칙(Open-Closed Principle)을 따르는 우아한 방법으로, 코드의 유지보수성과 확장성을 크게 향상시킨다.


데코레이터는 '@' 기호를 사용하여 함수나 클래스 위에 선언된다. 이 간단한 구문은 복잡한 로직을 숨기고, 코드의 가독성을 높이는 역할을 한다.
데코레이터의 작동 원리를 이해하기 위해서는 파이썬의 일급 객체(First-Class Objects) 개념을 알아야 한다.


일급 객체로서의 함수

파이썬에서 함수는 일급 객체다. 이는 함수를 변수에 할당하거나, 다른 함수의 인자로 전달하거나, 함수에서 반환할 수 있음을 의미한다. 이러한 특성이 데코레이터의 기반이 된다.

def greet(name):
    return f"안녕하세요, {name}님!"

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

# 함수를 인자로 전달
def apply_function(func, value):
    return func(value)

result = apply_function(greet, "철수")
print(result)  # 출력: 안녕하세요, 철수님!

이 예제에서 greet 함수는 변수에 할당되고 다른 함수의 인자로 전달된다. 이는 함수가 일급 객체임을 보여준다.


데코레이터의 기본 구조

데코레이터의 가장 기본적인 형태는 다음과 같다.

def my_decorator(func):
    def wrapper():
        print("함수 실행 전")
        func()
        print("함수 실행 후")
    return wrapper

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

say_hello()

이 코드를 실행하면 다음과 같은 출력이 나온다.

함수 실행 전
안녕하세요!
함수 실행 후

@my_decorator 구문은 사실 다음 코드의 축약형이다.

say_hello = my_decorator(say_hello)

데코레이터는 원래 함수를 받아 새로운 함수를 반환한다. 이 새로운 함수는 원래 함수를 감싸고 있으며, 추가적인 기능을 제공한다.


인자를 가진 함수에 데코레이터 적용하기

실제 상황에서는 대부분의 함수가 인자를 받는다. 이런 경우에도 데코레이터를 사용할 수 있다.

def log_function_call(func):
    def wrapper(*args, **kwargs):
        print(f"{func.__name__} 함수 호출. 인자: {args}, {kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} 함수 반환. 결과: {result}")
        return result
    return wrapper

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

result = add(3, 5)
print(f"최종 결과: {result}")

이 코드의 출력은 다음과 같다.

add 함수 호출. 인자: (3, 5), {}
add 함수 반환. 결과: 8
최종 결과: 8

여기서 *args**kwargs를 사용하여 어떤 인자라도 받을 수 있는 범용적인 래퍼 함수를 만들었다.


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

때로는 데코레이터 자체에 인자를 전달하고 싶을 때가 있다. 이를 위해서는 추가적인 레벨의 중첩 함수가 필요하다.

def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

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

greet("영희")

이 코드는 "안녕하세요, 영희님!"을 세 번 출력한다.


클래스 데코레이터

함수뿐만 아니라 클래스에도 데코레이터를 적용할 수 있다.

def singleton(cls):
    instances = {}
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

@singleton
class DatabaseConnection:
    def __init__(self):
        print("데이터베이스 연결 생성")

# 여러 번 인스턴스를 생성해도 항상 같은 인스턴스가 반환된다
conn1 = DatabaseConnection()
conn2 = DatabaseConnection()
print(conn1 is conn2)  # 출력: True

이 예제에서 singleton 데코레이터는 클래스의 인스턴스가 하나만 생성되도록 보장한다.


데코레이터의 실제 사용 사례

데코레이터는 다양한 실제 상황에서 유용하게 사용된다. 몇 가지 예를 살펴보자.

1. 실행 시간 측정

함수의 실행 시간을 측정하는 데코레이터를 만들 수 있다.

import time

def measure_time(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:.5f}초")
        return result
    return wrapper

@measure_time
def slow_function():
    time.sleep(2)
    print("함수 실행 완료")

slow_function()

이 코드는 함수의 실행 시간을 측정하고 출력한다.

2. 캐싱 (메모이제이션)

계산 비용이 큰 함수의 결과를 캐싱하는 데코레이터를 만들 수 있다.

def memoize(func):
    cache = {}
    def wrapper(*args):
        if args in cache:
            return cache[args]
        result = func(*args)
        cache[args] = result
        return result
    return wrapper

@memoize
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(100))  # 매우 빠르게 계산된다

이 데코레이터는 함수의 결과를 캐시에 저장하여 동일한 인자로 함수가 다시 호출될 때 계산을 반복하지 않고 저장된 결과를 반환한다.


3. 권한 검사

웹 애플리케이션에서 특정 뷰 함수에 대한 접근 권한을 검사하는 데코레이터를 만들 수 있다.

def require_auth(func):
    def wrapper(request, *args, **kwargs):
        if not request.user.is_authenticated:
            return redirect('login')
        return func(request, *args, **kwargs)
    return wrapper

@require_auth
def protected_view(request):
    return render(request, 'protected_page.html')

이 데코레이터는 사용자가 인증되지 않은 경우 로그인 페이지로 리다이렉트한다.


데코레이터의 고급 기능

1. functools.wraps 사용하기

데코레이터를 사용할 때 주의해야 할 점 중 하나는 원래 함수의 메타데이터(이름, 문서열 등)가 손실될 수 있다는 것이다. 이를 방지하기 위해 functools.wraps를 사용할 수 있다.

from functools import wraps

def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """래퍼 함수"""
        print("함수 실행 전")
        result = func(*args, **kwargs)
        print("함수 실행 후")
        return result
    return wrapper

@my_decorator
def greet(name):
    """인사를 하는 함수"""
    print(f"안녕하세요, {name}님!")

print(greet.__name__)  # 출력: greet
print(greet.__doc__)   # 출력: 인사를 하는 함수

@wraps(func)를 사용함으로써 원래 함수의 메타데이터가 보존된다.


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

함수 대신 클래스를 사용하여 데코레이터를 만들 수 있다. 이 방법은 데코레이터가 상태를 유지해야 할 때 유용하다.

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("안녕하세요!")

say_hello()
say_hello()
say_hello()

이 예제에서 CountCalls 클래스는 데코레이터로 사용되며, 함수가 호출된 횟수를 추적한다.


데코레이터의 장단점

장점

1. 코드 재사용성

공통 기능을 데코레이터로 분리하여 여러 함수에 쉽게 적용할 수 있다.

2. 관심사의 분리

핵심 로직과 부가 기능을 분리하여 코드의 구조를 개선한다.

3. 가독성 향상

복잡한 로직을 간단한 '@' 구문으로 표현할 수 있다.

4. 유지보수성

공통 기능의 변경이 필요할 때 데코레이터만 수정하면 되므로 유지보수가 용이하다.

단점

1. 디버깅의 어려움

데코레이터가 여러 겹 중첩되면 디버깅이 복잡해질 수 있다.

2. 성능 오버헤드

데코레이터는 추가적인 함수 호출을 발생시키므로 미세한 성능 저하가 있을 수 있다.

3. 이해의 어려움

데코레이터의 동작 원리를 이해하지 못하면 코드를 해석하기 어려울 수 있다.

반응형