플라스크 서버 동작 원리

플라스크 서버 동작 원리

from flask import Flask # Flask 모듈을 import하고 Flask 웹 서버를 생성한다. app = Flask(__name__) # __name__은 현재 파일을 의미함. main.py가 된다. app이라고 하는 Flask 인스턴스를 생성한다. 새로운 웹 앱이 생성된다. @app.route("/") # default 페이지 def home(): # 사용자가 default 페이지로 접속하면 home()이 실행됨. return "Hello, World!" if __name__ == "__main__": # 파이썬 스크립트가 실행될 때 파이썬은 __main__을 스크립트에 할당한다. app.run(debug=True) # 앱 실행

FLASK_APP=main.py flask run

flask run 명령어는 FLASK_APP 환경 변수가 가리키는 설정으로 앱이 어디에 위치해 있는지 알게 된다.

Werkzeug, request

request의 타입은 werkzeug.local.LocalProxy

WSGI 애플리케이션은 HTTP 요청을 처리하기 위한 함수로 environ 과 start_response 를 컨테이너로부터 받아 요청을 처리한다. Flask는 Flask.wsgi_app() 을 통해 이 부분을 처리한다.

def wsgi_app(self, environ, start_response): with self.request_context(environ): try: response = self.full_dispatch_request() except Exception, e: response = self.make_response(self.handle_exception(e)) return response(environ, start_response)

environ 에 담긴 HTTP 요청에 대한 명세를 Request 형태로 바꾸는 역할을 하는 것이 request_context() . request_context() 는 클라이언트로부터 받은 environ 을 토대로 request에 대한 context를 만들어서 이를 핸들러에서 사용할 수 있게 한다.

def request_context(self, environ): return RequestContext(self, environ)

class RequestContext(object): def __init__(self, app, environ): self.app = app self.request = app.request_class(environ) self.url_adapter = app.create_url_adapter(self.request) self.g = app.request_globals_class() self.flashes = None self.session = None self.preserved = False self._pushed_application_context = None self.match_request() # XXX: Support for deprecated functionality. This is going away with # Flask 1.0 blueprint = self.request.blueprint if blueprint is not None: # better safe than sorry, we don't want to break code that # already worked bp = app.blueprints.get(blueprint) if bp is not None and blueprint_is_module(bp): self.request._is_old_module = True def push(self): top = _request_ctx_stack.top if top is not None and top.preserved: top.pop() self._pushed_application_context = _push_app_if_necessary(self.app) _request_ctx_stack.push(self) self.session = self.app.open_session(self.request) if self.session is None: self.session = self.app.make_null_session() def pop(self, exc=None): self.preserved = False if exc is None: exc = sys.exc_info()[1] self.app.do_teardown_request(exc) rv = _request_ctx_stack.pop() assert rv is self, 'Popped wrong request context. (%r instead of %r)' \ % (rv, self) rv.request.environ['werkzeug.request'] = None if self._pushed_application_context: self._pushed_application_context.pop(exc) self._pushed_application_context = None def __enter__(self): self.push() return self def __exit__(self, exc_type, exc_value, tb): if self.request.environ.get('flask._preserve_context') or \ (tb is not None and self.app.preserve_context_on_exception): self.preserved = True else: self.pop(exc_value) self._pushed_application_context = _push_app_if_necessary(self.app) _request_ctx_stack.push(self) self.session = self.app.open_session(self.request) if self.session is None: self.session = self.app.make_null_session()

app의 request_class 를 통해 문맥 안에서 사용할 request 를 초기화 한다. __enter__ 를 통해 자기 자신을 _request_ctx_stack 에 넣는다.

_request_ctx_stack

이 스택은 .global 에 정의되어 있다. _request_ctx 는 werkzeug의 LocalStack 의 인스턴스다. request 는 여기에 정의되어 있는데, _request_ctx 중 top .request 를 가져오는 LocalProxy 다.

def _lookup_object(name): top = _request_ctx_stack.top if top is None: raise RuntimeError('working outside of request context') return getattr(top, name) def _find_app(): top = _app_ctx_stack.top if top is None: raise RuntimeError('working outside of application context') return top.app # context locals _request_ctx_stack = LocalStack() _app_ctx_stack = LocalStack() current_app = LocalProxy(_find_app) request = LocalProxy(partial(_lookup_object, 'request')) session = LocalProxy(partial(_lookup_object, 'session')) g = LocalProxy(partial(_lookup_object, 'g'))

LocalStack, LocalProxy

두 클래스는 werkzeug에 정의 돼 있다.

class Local(object): __slots__ = ('__storage__', '__ident_func__') def __init__(self): object.__setattr__(self, '__storage__', {}) object.__setattr__(self, '__ident_func__', get_ident) def __iter__(self): return iter(self.__storage__.items()) def __call__(self, proxy): """Create a proxy for a name.""" return LocalProxy(self, proxy) def __release_local__(self): self.__storage__.pop(self.__ident_func__(), None) def __getattr__(self, name): try: return self.__storage__[self.__ident_func__()][name] except KeyError: raise AttributeError(name) def __setattr__(self, name, value): ident = self.__ident_func__() storage = self.__storage__ try: storage[ident][name] = value except KeyError: storage[ident] = {name: value} def __delattr__(self, name): try: del self.__storage__[self.__ident_func__()][name] except KeyError: raise AttributeError(name) class LocalStack(object): def __init__(self): self._local = Local() def __release_local__(self): self._local.__release_local__() def _get__ident_func__(self): return self._local.__ident_func__ def _set__ident_func__(self, value): object.__setattr__(self._local, '__ident_func__', value) __ident_func__ = property(_get__ident_func__, _set__ident_func__) del _get__ident_func__, _set__ident_func__ def __call__(self): def _lookup(): rv = self.top if rv is None: raise RuntimeError('object unbound') return rv return LocalProxy(_lookup) def push(self, obj): """Pushes a new item to the stack""" rv = getattr(self._local, 'stack', None) if rv is None: self._local.stack = rv = [] rv.append(obj) return rv def pop(self): """Removes the topmost item from the stack, will return the old value or `None` if the stack was already empty. """ stack = getattr(self._local, 'stack', None) if stack is None: return None elif len(stack) == 1: release_local(self._local) return stack[-1] else: return stack.pop() @property def top(self): """The topmost item on the stack. If the stack is empty, `None` is returned. """ try: return self._local.stack[-1] except (AttributeError, IndexError): return None class LocalProxy(object): __slots__ = ('__local', '__dict__', '__name__') def __init__(self, local, name=None): object.__setattr__(self, '_LocalProxy__local', local) object.__setattr__(self, '__name__', name) def _get_current_object(self): """Return the current object. This is useful if you want the real object behind the proxy at a time for performance reasons or because you want to pass the object into a different context. """ if not hasattr(self.__local, '__release_local__'): return self.__local() try: return getattr(self.__local, self.__name__) except AttributeError: raise RuntimeError('no object bound to %s' % self.__name__) @property def __dict__(self): try: return self._get_current_object().__dict__ except RuntimeError: raise AttributeError('__dict__') def __repr__(self): try: obj = self._get_current_object() except RuntimeError: return '<%s unbound>' % self.__class__.__name__ return repr(obj) def __nonzero__(self): try: return bool(self._get_current_object()) except RuntimeError: return False def __unicode__(self): try: return unicode(self._get_current_object()) except RuntimeError: return repr(self) def __dir__(self): try: return dir(self._get_current_object()) except RuntimeError: return [] def __getattr__(self, name): if name == '__members__': return dir(self._get_current_object()) return getattr(self._get_current_object(), name) def __setitem__(self, key, value): self._get_current_object()[key] = value def __delitem__(self, key): del self._get_current_object()[key] def __setslice__(self, i, j, seq): self._get_current_object()[i:j] = seq def __delslice__(self, i, j): del self._get_current_object()[i:j] __setattr__ = lambda x, n, v: setattr(x._get_current_object(), n, v) __delattr__ = lambda x, n: delattr(x._get_current_object(), n) __str__ = lambda x: str(x._get_current_object()) __lt__ = lambda x, o: x._get_current_object() < o __le__ = lambda x, o: x._get_current_object() <= o __eq__ = lambda x, o: x._get_current_object() == o __ne__ = lambda x, o: x._get_current_object() != o __gt__ = lambda x, o: x._get_current_object() > o __ge__ = lambda x, o: x._get_current_object() >= o __cmp__ = lambda x, o: cmp(x._get_current_object(), o) __hash__ = lambda x: hash(x._get_current_object()) __call__ = lambda x, *a, **kw: x._get_current_object()(*a, **kw) __len__ = lambda x: len(x._get_current_object()) __getitem__ = lambda x, i: x._get_current_object()[i] __iter__ = lambda x: iter(x._get_current_object()) __contains__ = lambda x, i: i in x._get_current_object() __getslice__ = lambda x, i, j: x._get_current_object()[i:j] __add__ = lambda x, o: x._get_current_object() + o __sub__ = lambda x, o: x._get_current_object() - o __mul__ = lambda x, o: x._get_current_object() * o __floordiv__ = lambda x, o: x._get_current_object() // o __mod__ = lambda x, o: x._get_current_object() % o __divmod__ = lambda x, o: x._get_current_object().__divmod__(o) __pow__ = lambda x, o: x._get_current_object() ** o __lshift__ = lambda x, o: x._get_current_object() << o __rshift__ = lambda x, o: x._get_current_object() >> o __and__ = lambda x, o: x._get_current_object() & o __xor__ = lambda x, o: x._get_current_object() ^ o __or__ = lambda x, o: x._get_current_object() | o __div__ = lambda x, o: x._get_current_object().__div__(o) __truediv__ = lambda x, o: x._get_current_object().__truediv__(o) __neg__ = lambda x: -(x._get_current_object()) __pos__ = lambda x: +(x._get_current_object()) __abs__ = lambda x: abs(x._get_current_object()) __invert__ = lambda x: ~(x._get_current_object()) __complex__ = lambda x: complex(x._get_current_object()) __int__ = lambda x: int(x._get_current_object()) __long__ = lambda x: long(x._get_current_object()) __float__ = lambda x: float(x._get_current_object()) __oct__ = lambda x: oct(x._get_current_object()) __hex__ = lambda x: hex(x._get_current_object()) __index__ = lambda x: x._get_current_object().__index__() __coerce__ = lambda x, o: x._get_current_object().__coerce__(x, o) __enter__ = lambda x: x._get_current_object().__enter__() __exit__ = lambda x, *a, **kw: x._get_current_object().__exit__(*a, **kw)

get_ident 를 이용해서 데이터를 저장하고 가져온다. get_ident 는 현재 문맥을 나타내는 식별자로 2개의 클래스는 모두 개개의 문맥에서 전역적으로 사용할 수 있는 값으로 저장될 수 있다. 스레드별로 다른 요청을 처리하는 경우 각각 요청 문맥이 분리돼야 하기 때문.

environ 을 통해 넘어온 요청 내용은 request_context() 에 의해 Request 객체로 만들어지고 이 객체는 현재 문맥에서 전역적으로 사용할 수 있도록 _request_ctx_stack 에 저장된다. 이를 핸들러에서 Flask.request 로 접근하여 사용한다.

local 설명 작성

https://hustyichi.github.io/2018/08/22/LocalProxy-in-flask/

Local

local이 필요한 이유

thread local 에 저장된 데이터는 이 스레드에서만 데이터를 볼 수 있지만..

서로 다른 coroutine이 동일 스레드에 있는 경우 coroutine간 데이터 분리가 안될 수 있다.

스레드를 사용하는 경우에도 http 요청이 thread local 을 사용해 데이터가 남아 있을 수도 있으므로 WSGI 앱은 각 http 요청에 대해 다른 스레드가 사용되도록 보장할 수 없다.

Werkzeug 는 자체 로컬 객체를 개발했다.

LocalStack

글로벌 스토리지로 사용할 수 있는 스토리지 공간, Local 객체와 유사하나 자료구조는 스택

LocalProxy

Local, LocalStack 객체를 프록시하는 데 사용됨.

LocalProxy 초기화

Local or LocalStack 객체를 통한 __call__ 메소드 from werkzeug.local import Local l = Local() # these are proxies request = l('request') user = l('user') from werkzeug.local import LocalStack _response_local = LocalStack() # this is a proxy response = _response_local() Local, LocalStack 모두 __call__ 메서드가 있고 객체를 함수로 호출할 때 __call__ 메서드가 호출됨. __call__ 메서드는 LocalProxy 를 반환함 LocalProxy 클래스 l = Local() request = LocalProxy(l, 'request')

LocalProxy 사용 이유

from werkzeug.local import LocalStack, LocalProxy user_stack = LocalStack() user_stack.push({'name': 'Bob'}) user_stack.push({'name': 'John'}) # Local 객체를 바로 사용하는 경우, name이라는 key의 값이 덮어씌워짐 def get_user(): print(user_stack()) return user_stack.pop() user = get_user() print(user['name']) print(user['name']) # {'name': 'John'} # John # John user_stack.push({'name': 'Bob'}) user_stack.push({'name': 'John'}) # LocalProxy를 사용하는 경우, 스레드가 분리되어 Bob, John이 존재하는 듯? def get_user(): print(user_stack()) return user_stack.pop() user = LocalProxy(get_user) print(user['name']) print(user['name']) # {'name': 'John'} # John # {'name': 'Bob'} # Bob

from http://oneshottenkill.tistory.com/641 by ccl(A) rewrite - 2021-04-07 11:00:06