본문 바로가기
IT/파이썬 기초 완전 정복

파이썬 데코레이터 패턴 익히기 (19)

by 지식 발전소 2024. 4. 20.
728x90
반응형

 

데코레이터란 무엇인가요?

안녕하세요, 여러분! 오늘 우리가 알아볼 주제는 파이썬에서 매우 중요하고 유용한 개념 중 하나인 '데코레이터(Decorator)'입니다. 데코레이터가 무엇일까요?

 

데코레이터란 함수를 받아 기능을 추가하거나 수정한 새로운 함수를 반환하는 기법을 말합니다. 쉽게 말해서 함수의 앞뒤에 원하는 기능을 구현해주는 역할을 한다고 볼 수 있죠. 데코레이터는 기존 함수의 소스코드를 수정하지 않고도 함수의 동작을 확장시킬 수 있다는 점에서 매우 유용합니다.

 

데코레이터 개념이 아직 잘 와닿지 않는다면 걱정 마세요. 우리는 이번 장에서 데코레이터가 무엇이고 왜 필요한지, 그리고 어떻게 활용하는지 자세하게 알아볼 것입니다. 이해가 부족하다고 느껴지면 언제든 말씀해 주시기 바랍니다. 데코레이터 개념을 충분히 익히면 여러분의 코딩 능력이 한층 더 발전할 것입니다.

 

 

데코레이터의 기본 구조와 동작 원리

그렇다면 데코레이터는 정확히 어떤 구조를 가지고 있으며, 어떤 방식으로 동작할까요? 먼저 기본적인 데코레이터의 구조와 동작 원리부터 이해해 봅시다.

파이썬에서 데코레이터는 다음과 같은 구조를 가집니다.

def decorator(func):
    def wrapper(*args, **kwargs):
        # 데코레이터가 추가할 코드
        result = func(*args, **kwargs)
        # 데코레이터가 추가할 코드
        return result
    return wrapper

여기서 decorator는 함수를 인자로 받아 wrapper 함수에 기능을 추가한 뒤, 이를 반환합니다. wrapper 함수는 원래 함수인 func를 호출하면서 앞뒤로 추가 기능을 삽입할 수 있습니다.

이렇게 만든 데코레이터를 함수에 적용하는 방법은 다음과 같습니다.

@decorator
def my_func(a, b):
    return a + b

@decorator 구문은 my_func 함수에 decorator 함수를 적용한다는 의미입니다. 이는 다음과 같은 과정을 거치게 됩니다.

  1. decorator 함수가 호출되어 my_func를 인자로 받습니다.
  2. decorator 함수 내부에서 wrapper 함수가 정의됩니다.
  3. my_func에 wrapper 함수를 덧씌운 새로운 함수 객체를 반환합니다.
  4. 반환된 새 함수 객체가 my_func에 재할당되어 데코레이터가 적용됩니다.

이제 my_func는 원래의 기능에 데코레이터가 추가한 기능까지 가지게 되었습니다. 데코레이터로 인해 함수의 동작이 확장된 것이죠.

만약 여러 개의 데코레이터를 함수에 중첩해서 적용한다면 가장 밑에 있는 데코레이터부터 안쪽으로 적용되는 구조가 됩니다.

 

@decorator1
@decorator2
def my_func():
    pass

이렇게 되면 my_func는 decorator2에 의해 한 번 래핑된 뒤, 다시 decorator1에 의해 한 번 더 래핑됩니다.

이처럼 데코레이터는 실제 함수 코드를 수정하지 않고 기능을 연장할 수 있는 간단하면서도 강력한 기법입니다. 그럼 이번에는 실제로 데코레이터를 어떻게 활용하는지 예시를 통해 자세히 알아보겠습니다.

데코레이터의 활용 예시

앞서 알아본 대로 데코레이터는 함수에 특정 기능을 추가하는 데 활용됩니다. 여기서는 데코레이터가 어떤 식으로 사용될 수 있는지 구체적인 예시를 보겠습니다.

1. 함수 실행 시간 측정

프로그래밍을 하다 보면 특정 함수의 실행 시간을 알아야 할 때가 있습니다. 이럴 때 데코레이터를 활용하면 매우 편리합니다.

import time

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

@measure_time
def factorial(n):
    result = 1
    for i in range(1, n+1):
        result *= i
    return result

factorial(10)
# factorial 실행 시간: 0.000005 초
# 출력 결과: 3628800

여기서 measure_time 데코레이터는 함수 실행 전후의 시간을 측정하여 실행 시간을 출력합니다. factorial 함수에 이 데코레이터를 적용하면 함수 실행 시간을 손쉽게 확인할 수 있습니다.

2. HTTP 요청 로깅

데코레이터를 활용하면 HTTP 요청에 대한 로그도 쉽게 기록할 수 있습니다.

import requests

def log_http(func):
    def wrapper(*args, **kwargs):
        url = args[0]
        response = func(*args, **kwargs)
        print(f"Request: {url}")
        print(f"Status Code: {response.status_code}")
        return response
    return wrapper

@log_http        
def get_website(url):
    return requests.get(url)

response = get_website("https://www.python.org")
# Request: https://www.python.org
# Status Code: 200

log_http 데코레이터는 URL과 HTTP 상태 코드를 기록하고 있습니다. 이렇게 하면 함수 호출 시마다 자동으로 로깅이 수행되므로 디버깅에 큰 도움이 됩니다.

3. 캐싱 구현

또한 데코레이터로 캐싱 기능도 쉽게 구현할 수 있습니다.

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

@cache
def factorial(n):
    if n == 0:
        return 1
    else:
        return n * factorial(n-1)

print(factorial(5))  # 120 출력
print(factorial(5))  # 120 출력 (캐싱된 결과 사용)

 

여기서 cache 데코레이터는 함수의 인자와 반환값을 딕셔너리로 저장하고 있습니다. 함수가 처음 호출되면 실제 계산을 수행하지만, 그 이후에는 캐싱된 결과를 바로 반환하게 됩니다. 이렇게 데코레이터로 캐싱을 구현하면 중복 계산을 피할 수 있어 프로그램의 성능이 크게 향상될 수 있습니다.

 

이렇게 데코레이터는 실제로 다양한 상황에서 활용될 수 있습니다. 비단 위의 예시에 그치지 않고 로깅, 인증, 트랜잭션 관리 등 여러 곳에서 데코레이터 기법을 응용할 수 있습니다. 데코레이터를 잘 활용하면 소스코드를 수정하지 않고도 효과적으로 코드를 확장시킬 수 있습니다.

파이썬 데코레이터의 주의 사항

데코레이터는 매우 유용한 기법이지만 주의해야 할 점도 있습니다. 여기서는 데코레이터 활용 시 염두에 두어야 할 사항들을 알아보겠습니다.

첫째, 데코레이터로 인해 가독성이 나빠질 수 있습니다. 데코레이터가 많아지면 코드가 복잡해지기 때문입니다.

@decorator1
@decorator2
@decorator3
def my_func():
    pass

위와 같이 여러 개의 데코레이터가 중첩되면 코드를 이해하기 어려워질 수 있습니다. 따라서 최소한의 데코레이터만 사용하는 것이 좋습니다.

 

둘째, 함수의 메타데이터에 영향을 줄 수 있습니다. 데코레이터를 거치면서 함수 이름, 주석, 함수 시그니처 등의 메타데이터가 바뀔 수 있습니다. 예를 들어 다음과 같은 상황이 발생할 수 있습니다.

@decorator
def my_func(a, b):
    """이것은 주석입니다"""
    return a + b

print(my_func.__name__)    # wrapper 출력
print(my_func.__doc__)     # None 출력

 

그렇기 때문에 필요한 경우 데코레이터 내에서 메타데이터를 복사해 주는 방식으로 메타데이터 유지 작업을 해주어야 합니다.

 

셋째, 매개변수를 받는 데코레이터를 만드는 것이 어려울 수 있습니다. 위에서 살펴본 예시는 비교적 간단한 데코레이터였지만, 실제로는 매개변수를 받는 데코레이터를 만들 때가 많습니다. 이때는 다음과 같은 *args, **kwargs 문법과 함께 내부 함수 등을 활용해야 합니다.

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

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

say_hello("John")
# Hello, John! 
# Hello, John!
# Hello, John!

이처럼 매개변수를 받는 데코레이터는 구현이 복잡해지므로 주의가 필요합니다.

따라서 데코레이터는 효과적이지만 너무 남발하면 안 됩니다. 적절한 상황에서 적절한 수준으로 활용해야 하며, 가독성과 메타데이터 관리, 구현의 난이도 등을 종합적으로 고려해야 합니다. 이러한 주의 사항들을 인지하고 데코레이터를 사용한다면 더욱 효과적인 코딩이 가능해집니다.

데코레이터와 클로저, 함수 객체

데코레이터에 대해 깊이 있게 이해하려면 클로저와 함수 객체에 대해서도 알아야 합니다. 왜냐하면 데코레이터는 이 두 가지 개념을 활용하고 있기 때문입니다.

 

먼저 클로저(Closure) 개념부터 알아보겠습니다. 클로저란 내부 함수가 외부 함수의 지역 변수에 접근할 수 있는 함수를 말합니다.

def outer_func(x):
    y = 4  # 외부 함수의 지역 변수
    def inner_func(z):
        print(x + y + z)  # 클로저
    return inner_func

add_numbers = outer_func(2)
add_numbers(3)  # 9 출력

 

여기서 inner_func는 outer_func의 지역 변수 x, y에 접근할 수 있습니다. 이렇게 내부 함수에서 외부 함수 변수를 활용할 수 있게 하는 기법이 바로 클로저입니다.

 

데코레이터에서의 wrapper 함수도 클로저의 일종입니다. wrapper 함수 내에서 데코레이터 함수의 지역변수인 func에 접근하여 원래 함수를 호출할 수 있습니다.

def decorator(func):
    def wrapper(*args, **kwargs):
        # 데코레이터 코드
        result = func(*args, **kwargs)  # 원래 함수에 접근
        # 데코레이터 코드  
        return result
    return wrapper

 

이처럼 클로저 기법을 활용하면 데코레이터가 원래 함수의 정보를 유지할 수 있게 됩니다.

한편 함수 객체도 데코레이터 이해에 중요한 개념입니다. 파이썬에서 함수 자체도 객체로 취급되기 때문입니다. 따라서 함수를 변수에 할당하거나, 함수를 다른 함수의 인자로 전달하거나, 함수에서 함수를 반환할 수도 있습니다.

def my_func(a, b):
    return a + b

func_obj = my_func  # 함수를 변수에 할당

print(func_obj(3, 4))  # 7 출력

def operator(op, x, y):
    funcs = {
        '+': lambda a, b: a + b,
        '-': lambda a, b: a - b
    }
    return funcs[op](x, y)

print(operator('+', 2, 3))  # 5 출력

 

위 예시에서 볼 수 있듯이 파이썬에서는 함수 자체를 변수에 할당하거나 다른 함수의 인자로 전달할 수 있습니다. 이런 특성이 바로 데코레이터가 동작할 수 있는 기반이 됩니다.

@decorator
def my_func():
    pass

여기서 @decorator는 decorator 함수를 호출하여 my_func를 인자로 전달하는 과정입니다. decorator 함수는 my_func 함수 객체 자체를 받아서 wrapper 함수에 기능을 추가한 후 새로운 함수 객체로 반환합니다. 반환된 새 함수 객체가 my_func에 할당되는 것이죠.

 

이렇게 클로저와 함수 객체 개념이 데코레이터의 핵심 동작 원리가 됩니다. 즉, 데코레이터 함수는 클로저인 wrapper 함수를 생성하고, 함수 객체를 인자로 받아 wrap 합니다. 그리고 새로운 함수 객체를 반환하는 방식으로 동작하는 것입니다.

물론 이 개념들이 처음에는 다소 생소하고 어려울 수 있습니다. 하지만 이런 개념적 기반이 탄탄해야 데코레이터의 동작 원리를 제대로 이해할 수 있습니다. 데코레이터를 잘 활용하려면 반드시 클로저와 함수 객체에 대한 이해가 선행되어야 합니다.

제너레이터 기반 데코레이터

앞서 살펴본 데코레이터 예시는 모두 함수 기반의 데코레이터였습니다. 하지만 파이썬에서는 제너레이터를 활용하여 보다 간결하고 유연한 데코레이터를 구현할 수도 있습니다.

 

먼저 함수 기반 데코레이터와 제너레이터 기반 데코레이터의 차이점을 보겠습니다.

# 함수 기반 데코레이터
def decorator(func):
    def wrapper(*args, **kwargs):
        # 기능 추가
        result = func(*args, **kwargs)
        # 기능 추가
        return result
    return wrapper

# 제너레이터 기반 데코레이터  
def decorator(func):
    def wrapper(*args, **kwargs):
        # 기능 추가
        result = yield func(*args, **kwargs)
        # 기능 추가
    return wrapper(func)

 

제너레이터 기반 데코레이터에서는 함수 대신 제너레이터 함수를 사용합니다. 그리고 기존에 반환되던 부분에서 yield를 사용하여 실행 흐름을 잠시 중단합니다. 이후 데코레이터가 적용된 함수가 호출되면 yield 이후 부분이 실행됩니다.

 

이렇게 제너레이터 기반 데코레이터를 구현하면 간결하고 읽기 쉬운 코드가 됩니다. 예를 들어 factorial 함수에 대한 캐싱 데코레이터는 다음과 같이 작성할 수 있습니다.

cache = {}

def memoize(func):
    def wrapper(*args):
        if args not in cache:
            cache[args] = yield func(*args)
        return cache[args]
    return wrapper(func)

@memoize
def factorial(n):
    if n == 0:
        return 1
    else:
        return n * factorial(n-1)
        
print(factorial(5))  # 120 출력
print(factorial(5))  # 120 출력 (캐싱된 값 사용)

 

함수 기반 데코레이터와 비교하면 코드가 간결해졌음을 알 수 있습니다. 단, 제너레이터를 사용하면 코드가 실행되는 순서가 다소 복잡해져 가독성이 떨어질 수 있다는 단점도 있습니다.

 

하지만 파이썬에서 제너레이터는 매우 유용한 기능입니다. 특히 메모리 효율성과 실행 제어 측면에서 제너레이터는 장점을 가지고 있습니다. 따라서 상황에 따라 적절히 제너레이터 기반 데코레이터를 활용하면 유연하고 효율적인 코드를 작성할 수 있습니다.

요약

  • 데코레이터는 함수를 받아 기능을 추가하거나 수정한 새로운 함수를 반환하는 기법입니다.
  • 데코레이터는 기존 함수 코드를 수정하지 않고도 동작을 확장할 수 있습니다.
  • 데코레이터는 다양한 상황에서 유용하게 활용될 수 있습니다. (로깅, 인증, 캐싱, 시간 측정 등)
  • 데코레이터는 클로저와 함수 객체 개념을 활용하여 동작합니다.
  • 매개변수를 받는 데코레이터 구현은 다소 복잡할 수 있습니다.
  • 데코레이터 과용 시 가독성 저하, 메타데이터 변화의 단점이 있습니다.
  • 제너레이터를 활용하면 보다 간결하고 유연한 데코레이터를 구현할 수 있습니다.

참고 자료

[1] Lutz, M. (2013). Learning Python (5th ed.). O'Reilly Media.

[2] Summerfield, M. (2009). "Aside: Decorators and Descriptors", Programming in Python 3 (2nd ed.). Addison-Wesley Professional.

[3] Pilgrim, M. (2004). "Decorators", Dive Into Python. Apress.

[4] Python 공식 문서 - 데코레이터: https://docs.python.org/3/glossary.html#term-decorator

[5] Reuven, M. L. (2018). Python's Decorator Syntax Breakdown. Real Python. https://realpython.com/primer-on-python-decorators/

 

 

한 고대 문서 이야기

여기 한 고대 문서가 있습니다. 이 문서는 B.C. 1,500년 부터 A.D 100년까지 약 1,600 여 년 동안 기록되었습니다. 이 문서의 저자는 약 40 명입니다. 이 문서의 고대 사본은 25,000 개가 넘으나, 사본간 오

gospel79.tistory.com

 

유튜브 프리미엄 월 1만원 할인받고 월 4000원에 이용하는 방법

올해 5월부터 월 8000원 정도이던 유튜브 프리미엄 요금이 15000원 정도로 인상됩니다. 각종 OTT 서비스, ChatGPT 같은 서비스들이 늘어나다보니 이런 거 몇 개만 이용하더라도 월 이용요금이 5만원을

stock79.tistory.com

 

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."

728x90
반응형

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

댓글