본문 바로가기

Python/Flask

플라스크 서버 동작 원리

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 요청을 처리하기 위한 함수로 environstart_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 초기화

  1. 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 를 반환함

  2. 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

'Python > Flask' 카테고리의 다른 글

Flask Application Context  (1) 2020.06.08