인자를 받는 모양의 데코레이터 작성법

flask로 간단한 서버를 만드는 예제를 보면 좀 특이한 형태의 데코레이터 패턴을 발견할 수 있다. 바로 @app.route('/path')와 같은 모양의 인자를 받아 함수를 실행한 모양의 데코레이터가 그것이다. 흔하게 쓰이는 데코레이터 패턴이라면 @app.route 와 같이 함수나 클래스의 이름만 써서 사용하는데, 이러한 함수를 실행하는 꼴로 사용할 수 있을까? 물론 그렇게 만들 수 있으니까 쓰고 있을 것이다. 그래서 이런 모양의 데코레이터를 어떻게 만들 수 있을지에 대해 생각해 보았다.

타입으로 추정해보는 데코레이터 패턴의 기본 모양

기본적으로 데코레이터 함수는 “함수를 인자로 받아서 함수를 리턴하는” 타입의 함수이다. 어떤 임의 함수가 A 라는 타입의 인자를 받아 B 라는 타입의 값을 리턴하는 것을 (A) -> B 라고 쓴다고 하면, 함수를 F 타입이라 가정할 때, 데코레이터 함수의 타입은 (F) -> F 라 할 수 있다. 이를 각 함수의 인자를 사용해서 표시한다면 다시 ((A) -> B) -> (A) -> B 라고 쓸 수 있을 것이다.

 

인자를 받을 수 있는 데코레이터 함수의 타입

그런데 인자를 받아서 실행한 모양의 데코레이터를 생각해보자. 데코레이터 함수의 최종형태가 ((A) -> B) -> (A) -> B 타입이 된다. 즉 (A) -> B 타입의 함수를 받아서 같은 타입의 함수를 리턴하는 함수라는 의미이다. 그렇다면 flask의 app.route와 같은 함수는 것으로 볼 때 “문자열 타입의 인자를 받아서 데코레이터 함수를 리턴한다”면 말이된다.

따라서 T 라는 타입을 받아서 데코레이터 함수를 리턴하는 함수의 타입은 (T) -> ((A) -> B) -> (A) -> B 타입이 된다.

인자를 받는 데코레이터 생성 함수를 사용할 수 있는 예

데코레이터 함수는 특정한 함수를 받아서 그 함수의 앞이나 뒤로 특정한 동작을 수행하는, 즉 함수를 장식해주는 함수라고 했다. 예를 들어서 어떤 텍스트를 출력하는 함수를 HTML의 h1 태그를 출력하도록 바꿔주는 함수를 작성한다면, 다음과 같이 데코레이터 함수를 쓸 수 있을 것이다.

def html_heading_1(f):
  def inner(*args, **kwds):
    print("<h1>")
    f()
    print("</h1>")
  return inner

이 함수는 특정한 문구를 출력할 것이라 예상되는 함수를 받아서 그 출력의 앞 뒤에 <h1>, </h1>을 출력해주는 역할을 수행한다. 즉 어떤 메시지를 받아서 이를 h1 요소의 html 태그로 출력해주는 것이다. 그러면 h1 태그로 메시지를 출력하고자 하는 함수에 붙여서 장식할 수 있다.

@html_heading_1
def print_title(title):
   print("title:", title)

print_title('hello world')
# <h1>
# title: hello world
# </h1>

그런데, 여기서 태그 종류마다 이런 데코레이터 함수를 생성하는 것은 좀 번거롭다. 따라서 처음에 만든 함수가 별도의 태그 이름을 받아서 해당 태그를 적용해주는 형태로 추상화해보면 좀 더 널리 이용할 수 있지 않을까?

인자를 받는 데코레이터 생성함수를 디자인해보자

데코레이터 함수 자체가 함수를 조작하는 형태로 한 차례 추상화된 간접적인 코드이기 때문에 조금 어려울 수 있는데, 앞에서 데코레이터 함수의 타입 패턴을 살펴보았으니 이를 사용해서 어떤 식으로 작성해야 할지 생각해보자.

  1. 이 함수는 우선 문자열 타입을 인자로 받는다.
  2. 그리고 데코레이터 함수를 리턴해야 한다.

따라서 (str) -> ((A) -> B) -> (A) -> B 타입이 된다. 그리고 대략적인 구성은 다음과 같을 것이다.

def surround_tag(tag_name):
  def decorator(f):
    ...
  return decorator

우리가 여기서 리턴할 함수는 데코레이터 함수이며, surround_tag 함수는 결국 데코레이터를 생성하는 함수라 볼 수 있다. 이제 데코레이터의 내부는 앞에서 살펴본 간단한 예제와 거의 동일하다. 다음과 같이 쓸 수 있을 것이다.

def surround_tag(tag_name):
  def decorator(f):
    def inner(*args, **kwd):
      print("<%s>" % tag_name)
      f()
      print("</%s>" % tag_name)
    return inner
   return decorator

여기서 surround_tag 함수가 받는 인자는 문자열이어야 하므로, surround_tag 함수 자체가 단독으로 데코레이터 표기를 적용할 수는 없다. 대신에 이 함수가 리턴하는 함수는 데코레이터 함수이다. 따라서, h1 태그로 감싸는 함수를 정의하면, 다음과 같이 surround_tag 함수의 리턴값으로 함수를 얻어서 데코레이터로 적용할 수 있다.

html_h1 = surround_tag('h1')

@html_h1
def print_title(title):
  print("title:", title)

이렇게 하면 어떤 순서로 동작하는지 알 수 있겠지? 이를 다시 중간과정을 축약해서 쓴 표기가 아래의 표기가 된다.

@surround_tag('h1')
def print_title(title):
  print("title:", title)

print_title('hello world')
# <h1>
# title: hello world
# </h1>

정리해보자면 데코레이터 함수가 어떤 함수를 장식할 때, 별도의 조건에 따라서 장식의 내용을 변경하고 싶은 경우가 있을 수 있다. 이 때 그러한 조건별로 일일이 데코레이터 함수를 정의하기보다는 이를 다시 한 번 더 래핑해서 데코레이터 함수를 리턴해주는 데코레이터 생성함수를 만드는 것을 생각해 볼 수 있다는 것이다. 이 과정은 2번의 추상화와 두 단계의 네스팅을 거쳐서 “함수를 받아서 함수를 리턴하는 함수를 리턴해주는 함수”라는 괴상한 타입의 함수를 작성해야 하지만, 그 의미를 타입에 따라 추적해보면 그리 어렵지 않게 구현할 수 있다는 점이다.

정리

웹 프레임워크등에서 어렵지 않게 접할 수 있는 @deco_func(somevar)형태의 데코레이터에서 deco_func는 어떤 특정 타입의 값을 인자로 받은 다음, 해당 인자값을 사용하여 데코레이팅하는 데코레이터 함수를 생성해주는 함수이다. 이런 함수는 내부에 데코레이터 함수를 정의하고 그 속에 다시 데코레이터 리턴 함수를 정의하는 3중의 구조를 가지고 있어서 코드상으로는 무척 난해해 보이지만,  사용하기에 따라서는 충분히 쓸모 있는 테크닉이라 할 수 있겠다.