오늘은 이전 포스트에서 소개해 드린 WSGI 유틸리티인 Werkzeug와 이를 사용하는 프레임워크인 Flask가 HTTP 요청을 어떻게 추상화하는지를 구체적으로 살펴보도록 하겠습니다.
이번 포스트에 첨부된 코드는 Github 저장소(Flask 0.8, Werkzeug 0.8.3)에서 인용하겠습니다. 혹시 좀 더 궁금한 점이 있으시다면, 직접 코드를 내려받아 확인해보시는 것도 좋습니다.
Down the Rabbit-Hole
우선 HTTP로 요청받은 쿼리스트링(querystring)을 그대로 출력하는 간단한 애플리케이션부터 시작해봅시다.
@app.route()
와 같은 코드는 이제 익숙하실 겁니다. 등록된 핸들러가 문자열을 반환하면 이를 응답으로 돌려주는 것 역시 이해하기 크게 어렵지 않습니다.
그런데 request
는 과연 어떨까요? import
문을 통해 부르는 것으로 보아 외부 모듈에 정의된 변수 같기는 한데, 매번 쿼리를 바꿀 때마다 그에 맞춰서 자동으로 그 값이 바뀝니다. 특별히 app
나 handler
로부터 값을 전달받거나 하는 것 같지도 않습니다.
한번 type()
을 통해 확인해볼까요?
불행하게도 브라우저에는 아무것도 출력되지 않습니다. 이번엔 pdb를 통해 디버그해 봅시다. (단 Flask의 debug
옵션은 False
로 놓아야 합니다.)
dir()
을 통해 대강 무엇을 하는 객체인지 짐작을 할 순 있지만, type()
으로 확인한 타입은 Request
가 아닌 werkzeug.local.LocalProxy
라는 알쏭달쏭한 타입입니다. 과연 이 request
의 진짜 정체는 뭘까요?
Back to basics
잠깐 여기서 처음으로 돌아가 보죠. WSGI 애플리케이션은 HTTP 요청을 처리하기 위한 함수(혹은 부를 수 있는 객체)로 environ
과 start_response
를 컨테이너로부터 받아 요청을 처리합니다. Flask는 Flask.wsgi_app()
을 통해 이 부분을 처리합니다.
여기서 실제로 environ
에 담긴 HTTP 요청에 대한 상세를 Request
형태로 바꾸는 역할을 하는 것이 바로 request_context()
입니다. request_context()
는 현재 클라이언트로부터 받은 environ
을 토대로 문자 그대로 요청(Request)에 대한 문맥(Context)을 만들어서 이를 핸들러에서 사용할 수 있게끔 합니다.
여기서 만들어 내는 RequestContext
에 대해서 조금만 더 자세히 봅시다.
RequestContext
는 넘겨받는 app
의 request_class
(기본은 뒤에 살펴볼 werkzeug.wrappers.Request
입니다.)를 통해 문맥 안에서 사용할 request
를 초기화합니다. 또한 with
구문을 통해 실행되는 __enter__()
를 통해 자기 자신을 _request_ctx_stack
에 넣습니다.
여기서 주목해야 할 것이 _request_ctx_stack
입니다. 이 스택은 .global
에 정의되어있습니다.
_request_ctx
는 Werkzeug의 LocalStack
의 인스턴스입니다. 또 우리가 여태까지 찾아 헤매던 request
역시 여기에 정의되어있는데, 이는 _request_ctx
중 가장 위의 .request
를 가져오게 되어있는 LocalProxy
임을 알 수 있습니다.
Globalocal
그렇다면 과연 LocalStack
과 LocalProxy
는 무엇이며, 왜 필요한 것일까요? 백문이 불여일견이란 말도 있듯이 코드를 봅시다. 이 두 클래스는 werkzeug
에 정의되어 있습니다.
코드가 약간 복잡하지만, 기본적으로는 get_ident
를 이용해서 데이터를 저장하고 가져옵니다. get_ident
는 현재 문맥(스레드(Thread)나 코루틴(Coroutine))을 나타내는 식별자로, 2개의 클래스는 모두 개개의 문맥에서 전역적으로 사용할 수 있는 값으로 저장될 수 있습니다.(Java의 java.lang.ThreadLocal나 Clojure의 binding을 떠올리시면 이해가 쉽습니다.) 스레드별로 다른 요청을 처리하는 경우 각각의 요청 문맥이 분리되어야 하기 때문이죠.
이제 정리해봅시다. environ
을 통해 넘어온 요청 내용은 request_context()
에 의해 Request
객체로 만들어지고 이 객체는 현재 문맥에서 전역적으로 사용할 수 있게끔 _request_ctx_stack
에 저장됩니다. 우리는 핸들러에서 이를 Flask.request
로 접근하여 사용합니다.
How it works?
이제 request
객체가 어디서 만들어지는지를 살펴보았으니 어떻게 만들어지는지에 초점을 맞춰보죠. request
객체는 아까 잠깐 언급한 request_class
, 즉 별다른 설정이 없다면 werkzeug.wrappers.Request
를 구현합니다.
werk.zeug.wrappers.Request
는 werkzeug.wrappers.BaseRequest
에 HTTP 요청에 대한 명세를 독립적으로 구현하는 몇 개의 다른 믹스인(MixIn)으로 구성되어있습니다. 기본적인 구현 전략은 environ
을 객체의 멤버 변수로 가지고 있다가 조회하는 메소드나 프로퍼티가(ex. .values
, forms
)가 처음으로 불릴 때 명세에 맞게 이를 불러옵니다. (._load_from_data
)
Prologue
지금까지 살펴본 과정은 사실 Flask가 HTTP 요청을 어떻게 처리하는지에 대한 시작점에 지나지 않습니다. 이러한 Request
가 처리되고 적절한 HTTP 응답으로 변환되는 과정에도 살펴볼 점이 많은데, 이는 기회가 되면 다음 시간에 다뤄보도록 하겠습니다.
스포카에서는 프론트엔드 프로그래머, 시니어 풀스택 프로그래머를 채용 중입니다! 웹 프론트엔드에 관심을 가지고 공부하는 디자이너, 뛰어난 서버 개발자 등 각자의 분야에서 전문적인 사람들이 능력있는 분들과 함께 일하기를 기대하고 있습니다. 채용 정보 페이지를 확인해주세요!