Python 데코레이터 및 wraps 이해

데코레이터는 Python에서 자주 사용되는 기능으로, 이를 적절히 활용하면 코드의 생산성과 가독성을 크게 향상시킬 수 있습니다.

데코레이터 소개

초기 문제 설정

어떤 회사에 A, B, C 세 개의 비즈니스 부서와 S라는 기반 서비스 부서가 있다고 가정해보겠습니다. 현재 S 부서는 다른 부서들이 호출할 수 있는 두 개의 함수를 제공하고 있습니다.

def f1():
    print('f1 호출됨')

def f2():
    print('f2 호출됨')

초기에는 이 함수들을 호출하는 데 문제가 없었지만, 회사의 사업이 성장하면서 S 부서는 함수 호출 전 권한 검증이 필요하게 되었습니다. 즉, 권한이 있는 경우에만 함수를 호출할 수 있도록 하고자 합니다. 이러한 요구사항을 어떻게 해결할 수 있을까요?

가능한 솔루션

  1. ABC 부서들이 함수를 호출하기 전에 직접 권한을 검증하도록 요청합니다.
  2. S 부서가 제공하는 모든 함수 내부에서 권한 검증을 먼저 수행한 후 실제 작업을 실행합니다.

문제점

  • 첫 번째 방법은 권한 검증 로직을 호출 측에 노출시키며, 여러 부서가 존재할 경우 각 부서마다 일관된 구현을 보장하기 어렵습니다.
  • 두 번째 방법은 새로운 함수가 추가될 때마다 권한 검증 로직을 반복적으로 삽입해야 하므로 코드의 유지보수성이 저하됩니다.

이러한 문제를 해결하기 위해 데코레이터를 사용할 수 있습니다.

데코레이터 예시

아래는 데코레이터를 이용한 간단한 예입니다.

def 권한_검증(func):
    def wrapper():
        print("...권한 검증 중...")
        func()
    return wrapper

@권한_검증
def f1():
    print('f1 호출됨')

@권한_검증
def f2():
    print('f2 호출됨')

f1()
f2()

위 코드의 출력 결과는 다음과 같습니다.

...권한 검증 중...
f1 호출됨
...권한 검증 중...
f2 호출됨

코드 분석:

  • 권한_검증 함수는 클로저 형태로 정의되어 있으며, 내부 함수 wrapper가 원본 함수(func)를 호출하기 전에 권한 검증을 수행합니다.
  • @권한_검증 문법은 f1 = 권한_검증(f1)과 같은 효과를 가지며, f1 함수가 호출될 때마다 권한 검증이 자동으로 이루어집니다.

데코레이터 작동 원리

데코레이터는 기본적으로 클로저와 유사한 개념으로 작동합니다. 아래는 데코레이터의 실행 시점을 확인하는 예제입니다.

def 데코레이터(func):
    print("...데코레이터 적용 시작...")
    def wrapper():
        print("...권한 검증 중...")
        func()
    return wrapper

@데코레이터
def test():
    print('test')

test()

출력 결과:

...데코레이터 적용 시작...
...권한 검증 중...
test

위 코드에서 볼 수 있듯이, @데코레이터 문이 해석될 때 해당 함수가 즉시 데코레이터에 의해 감싸집니다.

여러 데코레이터 적용

두 개 이상의 데코레이터를 하나의 함수에 적용할 수도 있습니다.

def 굵게(func):
    print("----a----")
    def wrapper():
        print("----1----")
        return "<b>" + func() + "</b>"
    return wrapper

def 기울게(func):
    print("----b----")
    def wrapper():
        print("----2----")
        return "<i>" + func() + "</i>"
    return wrapper

@굵게
@기울게
def test():
    print("----c----")
    print("----3----")
    return '안녕하세요'

print(test())

출력 결과:

----b----
----a----
----1----
----2----
----c----
----3----
<b><i>안녕하세요</i></b>

여기서는 데코레이터가 가장 안쪽부터 바깥쪽 순서로 적용되며, 호출 시에는 외곽부터 내부 순서로 실행됩니다.

파라미터를 갖는 함수 장식

파라미터를 받는 함수에도 데코레이터를 적용할 수 있습니다.

def 인사_데코레이터(func):
    def wrapper(name):
        print("인사말 데코레이터 호출됨")
        func(name)
    return wrapper

@인사_데코레이터
def hello(name):
    print(f'안녕하세요 {name}')

hello('홍길동')

결과:

인사말 데코레이터 호출됨
안녕하세요 홍길동

여러 파라미터나 가변 파라미터를 처리하는 경우도 가능합니다.

def 덧셈_데코레이터(func):
    def wrapper(*args, **kwargs):
        print("덧셈 데코레이터 호출됨")
        return func(*args, **kwargs)
    return wrapper

@덧셈_데코레이터
def add(a, b):
    return a + b

print(add(3, 5))

결과:

덧셈 데코레이터 호출됨
8

리턴 값을 처리하는 데코레이터

리턴 값을 갖는 함수에도 데코레이터를 적용할 수 있습니다.

def 리턴값_처리(func):
    def wrapper():
        result = func()
        return f"결과: {result}"
    return wrapper

@리턴값_처리
def test():
    return "테스트"

print(test())

결과:

결과: 테스트

파라미터를 갖는 데코레이터

데코레이터 자체도 파라미터를 받을 수 있습니다.

def 로그_레벨(level="INFO"):
    def decorator(func):
        def wrapper():
            print(f"[{level}] 로그: 함수 실행")
            func()
        return wrapper
    return decorator

@로그_레벨(level="DEBUG")
def test_log():
    print("테스트 로그")

test_log()

결과:

[DEBUG] 로그: 함수 실행
테스트 로그

클래스 기반 데코레이터

함수뿐만 아니라 클래스도 데코레이터로 사용할 수 있습니다.

class TestDecorator:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print("데코레이터 내부 기능 실행")
        return self.func(*args, **kwargs)

@TestDecorator
def test():
    print("테스트 함수 실행")

test()

결과:

데코레이터 내부 기능 실행
테스트 함수 실행

wraps 사용

functools.wraps는 데코레이터를 사용했을 때 원본 함수의 메타데이터를 보존하는 데 유용합니다.

from functools import wraps

def 데코레이터(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("데코레이터 실행")
        return func(*args, **kwargs)
    return wrapper

@데코레이터
def test():
    """테스트 함수 설명"""
    pass

print(test.__doc__)

결과:

테스트 함수 설명

wraps는 원본 함수의 이름, 문서 문자열 등을 보존하여 더 깔끔한 코드를 작성할 수 있게 해줍니다.

태그: python decorator functools

6월 27일 21:00에 게시됨