Do you know flask-restful?이라는 시리즈는 Flask에서 API를 클래스 기반으로 정의하는 데에 자주 사용되는 라이브러리인 flask-restful의 재밌는 요소들을 다룹니다. 공식 문서 + 필자가 코드 리딩을 하며 알아낸 것들로 내용을 채우려고 하며, real world에서 flask-restful을 사용하며 부딪혔던 상황들을 최대한 많이 공유하고자 합니다.

flask-restful은 Flask 자체에서 제공하는 Pluggable ViewMethodView 개념을 확장하여, 할당된 URI에 대한 HTTP 메소드별 핸들러를 클래스의 메소드 형태로 작성하는 것에 큰 도움을 준다. 따라서 flask-restful을 잘 이해하기 위해선 Flask의 Pluggable View 개념을 알고 있는 것이 좋다.

Pluggable View

공식 문서에선 Pluggable Views라는 챕터에서 확인할 수 있는 내용인데, Django의 generic view(django.views.generic)에 영감을 얻어 만들어진, 함수가 아니라 클래스로서 정의되는 뷰를 의미한다. 여기서 이야기하는 뷰는 그냥 하나 이상의 엔드포인트에 대한 요청을 처리하는 핸들러라고 생각하면 된다.

View

Pluggable View의 기본 클래스는 View로, 단지 @app.route함수로서 전달되던 요청 핸들러를 클래스에 정의된 메소드로 옮긴 것 뿐이다. View 클래스를 상속받고 dispatch_request 메소드를 정의하여 로직을 구현하면 된다. 이후 View 클래스에 정의된 as_view 메소드를 호출하며 엔드포인트 이름을 전달하면, 클래스에 정의된 dispatch_request 메소드를 view function처럼 다룰 수 있게 된다.

from flask import Flask
from flask.views import View

app = Flask(__name__)


class SayHello(View):
    def dispatch_request(self):
        return 'hello'

app.add_url_rule('/hello', view_func=SayHello.as_view('say_hello'))

dispatch_request가 지원하는 HTTP method를 명시하려면, methods라는 이름의 문자열로 이뤄진 연속열(list, tuple 등)을 class level attribute로 정의해 두면 된다.

...


class SayHello(View):
    methods = ['GET']

    def dispatch_request(self):
        return 'hello'

...

@app.routemethods 인자를 전달했던 것과 동일하게, 명시해 둔 메소드 외의 요청에 대해서 405 METHOD NOT ALLOWED를 반환한다. 단지 view function이 정의되는 계층이 한 단계 늘어났을 뿐이라 쓸모 없어 보일 수도 있겠지만, view들이 클래스 기반으로 정의되기에 DRY한 코드를 작성하는 등 여러 면에 대해 상속 구조라는 좋은 조력자가 생긴 것이다. 별도로 예제를 작성하진 않겠다.

Method Based Dispatching

View를 써서 클래스를 정의하면, HTTP method에 대응되는 핸들러들을 메소드 형태로 각각 정의해 두고, dispatch_request에선 요청에 따라 이들을 호출해주는 식으로 만들어볼 수 있다.

from flask import request, abort


class Base(View):
    def dispatch_request(self, *args, **kwargs):
        method = getattr(self, request.method.lower(), None)

        if method is None:
            abort(405)

        method(*args, **kwargs)


class Dummy(Base):
    def get(self):
        ...

    def post(self):
        ...

    def delete(self):
        ...

이렇게 직접 만들어줄 필요 없이, flask.views 패키지에는 MethodView라는 클래스가 이미 존재하며, 실제로 위에서 작성한 예와 비슷하게 dispatch_request가 미리 구현되어 있다.

from flask.views import MethodView


class Dummy(MethodView):
    def get(self):
        ...

    def post(self):
        ...

    def delete(self):
        ...

Decorating Views

Flask 0.8부턴 MethodView가 decorators라는 class level attribute를 인식하게 되었는데, 이는 decorators가 명시해 둔 데코레이터들을 해당 MethodView 하위의 모든 view function에 대해 적용하게 만든다.

from functools import wraps

from flask.views import MethodView


def print_hello_before_process(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        print('hello')

        return fn(*args, **kwargs)
    return wrapper


class Dummy(MethodView):
      decorators = [print_hello_before_process]

    def get(self):
        ...

    def post(self):
        ...