데보션앱 소개페이지 바로가기
로그인 선택

신고하기

CLOSE
신고사유 (대표 사유 1개)
상세내용 (선택)
0/200
  • 신고한 게시글은 더 이상 보이지 않습니다.
  • 이용약관과 운영정책에 따라 신고사유에 해당하는지 검토 후 조치됩니다.
  • 허위 신고인 경우, 신고자의 서비스 이용이 제한될 수 있으니 유의하시어 신중하게 신고해 주세요.
(이 회원이 작성한 모든 댓글과 커뮤니티 게시물이 보이지 않고, 알림도 오지 않습니다.)

미리보기

커뮤니티

      1,234

      badge 23.06.15

      글 등록

      카테고리를 선택해주세요.

      DEVOTEE를 활성화 시키면
      지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.

      버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.

      임시저장함에 저장되었습니다. 저장일시 : 2022.5.17 14:29:08

      임시저장함

      제목을 선택하시면 이어서 작성이 가능하며,
      최대 20건까지 저장합니다.
      컨텐츠 유형, 제목, 저장일시, 삭제로 이뤄진 임시저장 목록
      컨텐츠 유형 제목 저장일 삭제

      데보션 블로그 게재 요청

      CLOSE
      • *
      • *

      본인인증

      효율적인 데보션 서비스 이용 및
      고객님의 소중한 개인정보보호를 위해
      본인인증을 진행해주세요. 본인인증 미 진행 시 로그인이 제한됩니다.
      본인인증 실패

      본인인증 로그인에 실패하였습니다.
      회원이 아니시거나 본인인증 등록이
      완료되지 않은 사용자입니다.

      회원정보 연결

      [네트워크] Flask는 어떻게 동작하는가 2편(with WSGI)

      Teus 24.01.17
      3,281 12 2
      DEVOTEE 요약
      이 포스트는 Python의 웹 프레임워크인 Flask에 대해 자세히 설명하고 있습니다. 포스트는 Flask가 어떻게 다양한 EndPoint를 처리하는지 차근차근 살펴보고, 데코레이터 함수 등 각 요소들이 어떻게 작동하는지 복잡하게 구성되어 있는 부분들을 코드와 함께 보여주며 설명합니다. 마지막에는 Flask가 동작하는 기본적인 순서를 요약적으로 보여줍니다.
      DEVOTEE 추천 블로그

      안녕하세요. Teus입니다.


      그동안 계속해서 네트워크 관련 Low Level을 알아 보았습니다.

      이번에는 Python에서 많이 사용되는 WebFrameWork인 Flask에 대해서 알아보는 두번째 시간입니다!


      0. 지난포스팅에서...

      지난 포스팅에서

      Flask는 werkzeug서버를 이용하고, werkzeug는 wsgiref를 이용한다고 했습니다.

      하지만 이걸로만은 부족하죠?

      분명 Flask는 다양한 url_rule, EndPoint를 설정하고, 하나의 서버에서 이 EndPoint를 모두 다룰 수가 있습니다.

      그러면 어떻게 다양한 EndPoint를 다룰 수 있는지 이 부분에 대해서 알아보겠습니다.


      1. Flask Tutorial

      #https://github.com/pallets/flask#a-simple-example
      from flask import Flask
      
      app = Flask(__name__)
      
      @app.route("/")
      def hello():
          return "Hello, World!"
          
      #이부분을 추가할 경우 flask.exe를 사용하지 않고 웹서버를 띄울 수 있음
      app.run()

      tutorial을 보면 Flask Class의 self.route()를 Decorator로 사용해서

      EndPoint를 추가하는 것을 볼 수 있습니다.

      그럼 Flask Class의 self.route()를 찾아가 볼까요?

      #https://github.com/pallets/flask/blob/main/src/flask/sansio/scaffold.py#L49C1-L49C16
      class Scaffold:
          ...
          @setupmethod
          def route(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
              def decorator(f: T_route) -> T_route:
                  endpoint = options.pop("endpoint", None)
                  #toturial처럼 app.route에 "/my_endpoint"
                  #처럼 사용할 경우, 여기서 endpoint는 None이 됩니다.
                  self.add_url_rule(rule, endpoint, f, **options)
                  return f
      
              return decorator

      위 패턴은 Decorator 함수 이면서 매개변수를 받는 경우입니다.

      그렇기 때문에 rule = endpoint를 String으로받음 일것 같지만

      self.route()가 @setupmethod Decorator로 한번더 감싸진것을 볼 수 있습니다.

      이때 setupmethod()는 같은 파일에서 정의 되어 있습니다.

      #https://github.com/pallets/flask/blob/main/src/flask/sansio/scaffold.py#L39
      F = t.TypeVar("F", bound=t.Callable[..., t.Any])
      def setupmethod(f: F) -> F:
          f_name = f.__name__
      
          def wrapper_func(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
              self._check_setup_finished(f_name)
              return f(self, *args, **kwargs)
      
          return t.cast(F, update_wrapper(wrapper_func, f))

      그렇기 때문에

      @setupmethod : app.route()를 통해서 받아들인 매개변수를 *args, **kwargs로 받아들임

      self.route() : @seupmethod의 매개변수 f로 들어가서 wrapper_func에서 사용됨


      Decorator를 풀어서 보면, 아래처럼 실행 될겁니다.

      setupmethod(route) -> seupmethod.wrapper_func
      setupmethod(route)() -> seupmethod.wrapper_func(*args, **kwargs) -> route.decorator
      setupmethod(route)()(hello) -> route.decorator(hello) -> hello

      조금 복잡하게 구성되어있지만, 여튼 위와같은 순서로 작동하게 됩니다.

      결국 중간에 self.add_url_rule을 통과하면서 url 리스트와 상호작용 후

      함수 자기자신이 다시 나오게 되는 구조입니다.


      이제 self.add_url_rule()을 볼 차례입니다.

      #https://github.com/pallets/flask/blob/main/src/flask/sansio/app.py#L608
      from werkzeug.routing import Map
      from werkzeug.routing import Rule
      class App(Scaffold):
          url_map_class = Map
          url_rule_class = Rule
          def __init__():
              #url을 담을 수 있는 werkzeug.routing.Map Object를 생성함
              self.url_map = self.url_map_class(host_matching=host_matching)
          ...
          @setupmethod
          def add_url_rule(
              self,
              rule: str,
              endpoint: str | None = None,
              view_func: ft.RouteCallable | None = None,
              provide_automatic_options: bool | None = None,
              **options: t.Any,
          ) -> None:
              if endpoint is None:
                  #endpoint를 별도로 설정하지 않을 경우
                  #view_func의 이름을 사용해서 endpoint를 생성합니다.
                  endpoint = _endpoint_from_view_func(view_func)
              options["endpoint"] = endpoint
              methods = options.pop("methods", None)
              # method가 설정되지 않을 경우 GET을 Default로 가지고감
              if methods is None:
                  methods = getattr(view_func, "methods", None) or ("GET",)
              ...
              methods = {item.upper() for item in methods}
              ...
              #매개변수rule : app.route에 집어넣은 주소를 String으로 표현한것.
              #werkzeug.routing.Rule Object 생성
              rule = self.url_rule_class(rule, methods=methods, **options)
              ...
              
              self.url_map.add(rule)
              if view_func is not None:            
                  ...
                  #endpoint와 함께 등록된 view_func(예제에서는 hello 함수)를 dict에 매칭 후 저장
                  self.view_functions[endpoint] = view_func

      결국 여기까지보면

      1. Decorator를 달아도 함수 자체는 변하지 않는다.

      2. 이 과정에서 self.add_url_rule()을 통과한다

      3. werkzeug.routing.Rule Object를 만들어서 werkzeug.routing.Map Object에 추가한다.

      4. Flask는 자체적으로 self.view_functions라는 dict에 hello()를 저장한다

      #https://github.com/pallets/flask/blob/main/src/flask/sansio/scaffold.py#L110
      class Scaffold:
          ...
          self.view_functions: dict[str, t.Callable] = {}

      인것을 볼 수 있습니다.


      결국 돌아가서 werkzeug.routing.Map객체가 werkzeug Server에서 어떤 역할을 하는지를 알아야합니다,

      그래서 다시 werkzeug로 돌아가 보겠습니다.


      2. werkzeug

      이전 포스팅에서 werkzeug 서버는 Requests를 처리할 때 BaseHTTPRequestHandler를 상속받은 WSGIRequestHandler를 사용한다고 했습니다.

      #https://github.com/pallets/werkzeug/blob/main/src/werkzeug/serving.py#L150
      class WSGIRequestHandler(BaseHTTPRequestHandler):
          ...
          def handle(self) -> None:
              """Handles a request ignoring dropped connections."""
              try:
                  super().handle()
              except (ConnectionError, socket.timeout) as e:
                  self.connection_dropped(e)
              except Exception as e:
                  if self.server.ssl_context is not None and is_ssl_error(e):
                      self.log_error("SSL error occurred: %s", e)
                  else:
                      raise

      그리고 이때 super().handle()에서 do_POST, do_GET을 호출하면서 Requests 처리가 시작됩니다.(이 내용 역시 Python의 CGI에서 다뤘었죠)

      #https://github.com/python/cpython/blob/7dead6a33ac786465c3d927d5c2025caf0c1a71b/Lib/http/server.py#L432
      class BaseHTTPRequestHandler(socketserver.StreamRequestHandler):
          ...
          def handle(self):
              """Handle multiple requests if necessary."""
              self.close_connection = True
      
              self.handle_one_request()
              while not self.close_connection:
                  self.handle_one_request()
          def handle_one_request(self):
              try:
                  ...
                  mname = 'do_' + self.command
                  ...
                  #여기서 하부의 do_GET, do_POST와 같은 method를 가지고옴
                  method = getattr(self, mname)
                  method()
                  ...

      이때 지난 포스팅에서 넘어갔던 부분을 다시 살펴보겠습니다.

      #https://github.com/pallets/werkzeug/blob/main/src/werkzeug/serving.py#L150C1-L150C50
      class WSGIRequestHandler(BaseHTTPRequestHandler):
          ...
          #https://github.com/pallets/werkzeug/blob/main/src/werkzeug/serving.py#L406
          def __getattr__(self, name: str) -> t.Any:
              # wsgiref와 달리 werkzeug Server는 모든 req_method를 하나의 Method로 처리합니다.
              if name.startswith("do_"):
                  return self.run_wsgi
                  
              # All other attributes are forwarded to the base class.
              return getattr(super(), name)

      BaseHTTPRequestHandler에서 method를 호출하고, 호출된 함수를 실행시키면서 Response를 보내게 되어 있습니다.


      그럼 이 self.run_wsgi()를 좀더 세밀하게 살펴볼 시간 입니다.

      #https://github.com/pallets/werkzeug/blob/main/src/werkzeug/serving.py#L150C1-L150C50
      class WSGIRequestHandler(BaseHTTPRequestHandler):
          ...
          #https://github.com/pallets/werkzeug/blob/main/src/werkzeug/serving.py#L240
          def run_wsgi(self) -> None:
              if self.headers.get("Expect", "").lower().strip() == "100-continue":
                  self.wfile.write(b"HTTP/1.1 100 Continue\r\n\r\n")
      
              self.environ = environ = self.make_environ()        
              ...
              try:
                  execute(self.server.app)
              ...

      일단 먼저 함수 내부에서 정의한 execute()함수를 통해서 requests 처리가 시작됩니다.

      이때 self.server.app이라는 것을 넣어주게 되는데

      self.server는 BaseWSGIServer Object입니다.

      이 self.server는 BaseRequestHandler의 생성자에서 설정이 됩니다.

      그러면 최초로 RequestHandler라 Call된 시점으로 가 보겠습니다.

      #https://github.com/python/cpython/blob/7dead6a33ac786465c3d927d5c2025caf0c1a71b/Lib/socketserver.py#L342
      class BaseServer:
          ...
          def process_request(self, request, client_address):
              self.finish_request(request, client_address)
              self.shutdown_request(request)
          ...
              
          def finish_request(self, request, client_address):
              """Finish one request by instantiating RequestHandlerClass."""
              self.RequestHandlerClass(request, client_address, self)

      여기서 self.finish_requst()를 통해서 시작이 되는데

      이때 self.server = BaseServer를 상속받은 Object인것을 알 수 있습니다.

      BaseServer

      ⬆

      TCPServer

      ⬆

      HTTPServer

      ⬆

      BaseWSGIServer

      그래서 self.server에는 BaseWSGIServer가 들어가게 될겁니다.

      그리고 self.server.app의 정체가 바로 WSGIApplication=Flask가 되게 되죠.


      이제 좀더 나가볼까요?

      #https://github.com/pallets/werkzeug/blob/main/src/werkzeug/serving.py#L150C1-L150C50
      class WSGIRequestHandler(BaseHTTPRequestHandler):
          ...
          #https://github.com/pallets/werkzeug/blob/main/src/werkzeug/serving.py#L240
          def run_wsgi(self) -> None:
              if self.headers.get("Expect", "").lower().strip() == "100-continue":
                  self.wfile.write(b"HTTP/1.1 100 Continue\r\n\r\n")
      
              self.environ = environ = self.make_environ()
              ...
              try:
                  execute(self.server.app)
              ...
              def execute(app: WSGIApplication) -> None:
                  application_iter = app(environ, start_response)
                  try:
                      for data in application_iter:
                          write(data)

      이때 application_iter를 통해서 작성해야할 HTTP Msg를 받아오게 됩니다.

      이제 다시 Flask.__call__()로 가보겠습니다.

      #https://github.com/pallets/flask/blob/main/src/flask/app.py#L1476
      class Flask(App)
          ...
          def request_context(self, environ: dict) -> RequestContext:
              return RequestContext(self, environ)
          ...
          def wsgi_app(self, environ: dict, start_response: t.Callable) -> t.Any:
              ctx = self.request_context(environ)
              error: BaseException | None = None
              try:
                  try:
                      ctx.push()
                      response = self.full_dispatch_request()
                  ...
                  return response(environ, start_response)
              ...        
          ...
          def __call__(self, environ: dict, start_response: t.Callable) -> t.Any:       
              return self.wsgi_app(environ, start_response)        

      위에서 보면 environ(Dict로된 환경변수 모음)이 self.wsgi_app()의 매개변수로 들어가고


      environ을 활용해서RequstContext Object를 생성합니다.

      (이때 내부에서 werkzeug.local.LocalProxy생성자를 사용하는데, 이부분은 생략하겠습니다.)


      여기서 self.full_dispatch_request()를 통해서 단서가 잡히게 됩니다.

      class Flask(App):
          ...
          #https://github.com/pallets/flask/blob/main/src/flask/app.py#L854
          def full_dispatch_request(self) -> Response:        
              try:
                  request_started.send(self, _async_wrapper=self.ensure_sync)
                  #Flask의 blueprint를 사용한 경우
                  rv = self.preprocess_request()
                  #normal Case
                  if rv is None:
                      rv = self.dispatch_request()
              ...
              return self.finalize_request(rv)
      from .globals import request_ctx
      class Flask(App):
          ...
          #https://github.com/pallets/flask/blob/main/src/flask/app.py#L829
          def dispatch_request(self) -> ft.ResponseReturnValue:        
              req = request_ctx.request
              ...
              rule: Rule = req.url_rule  # type: ignore[assignment]
              ...
              # otherwise dispatch to the handler for that endpoint
              view_args: dict[str, t.Any] = req.view_args
              return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)

      근데 위에서 ctx.push()를 했던거 기억 나시나요?


      이부분을 다시 살펴보겠습니다.

      class RequestContext:
          def __init__(
              self,
              app: Flask,
              environ: dict,
              request: Request | None = None,
              session: SessionMixin | None = None,
          ) -> None:
              self.app = app
              if request is None:
                  request = app.request_class(environ)
                  request.json_module = app.json
              self.request: Request = request
              self.url_adapter = None
              try:
                  self.url_adapter = app.create_url_adapter(self.request)
              except HTTPException as e:
                  self.request.routing_exception = e
              ...
              self._after_request_functions: list[ft.AfterRequestCallable] = []
              ...
          ...
          def push(self) -> None:
              ...
              if self.url_adapter is not None:
                  self.match_request()
                  
          def match_request(self) -> None:
              try:
                  result = self.url_adapter.match(return_rule=True)
                  self.request.url_rule, self.request.view_args = result
              ...

      Flask.dispatch()에서 사용하던 req.url_rule이 만들어지는 부분을 확인할 수 있습니다.


      self.url_adapther를 잠시 보겠습니다.

      class Flask(App):
          ...
          #https://github.com/pallets/flask/blob/main/src/flask/app.py#L388
          def create_url_adapter(self, request: Request | None) -> MapAdapter | None:
              if request is not None:
                  if not self.subdomain_matching:
                      subdomain = self.url_map.default_subdomain or None
                  else:
                      subdomain = None
      
                  return self.url_map.bind_to_environ(
                      request.environ,
                      server_name=self.config["SERVER_NAME"],
                      subdomain=subdomain,
                  )

      self.url_map==Class Map Object이기 때문에

      Map.bind_to_environ()을 살펴보겠습니다.

      #https://github.com/pallets/werkzeug/blob/main/src/werkzeug/routing/map.py#L40
      class Map:
          ...
          #https://github.com/pallets/werkzeug/blob/main/src/werkzeug/routing/map.py#L244
          def bind_to_environ(
              self,
              environ: WSGIEnvironment | Request,
              server_name: str | None = None,
              subdomain: str | None = None,
          ) -> MapAdapter:
              env = _get_environ(environ)
              wsgi_server_name = get_host(env).lower()
              scheme = env["wsgi.url_scheme"]
              ...
              def _get_wsgi_string(name: str) -> str | None:
                  val = env.get(name)
                  if val is not None:
                      return val.encode("latin1").decode(errors="replace")
                  return None
              script_name = _get_wsgi_string("SCRIPT_NAME")
              path_info = _get_wsgi_string("PATH_INFO")
              query_args = _get_wsgi_string("QUERY_STRING")
              #매개변수 전처리 후 MapAdapter Object를 만들어줌
              return Map.bind(
                  self,
                  server_name,
                  script_name,
                  subdomain,
                  scheme,
                  env["REQUEST_METHOD"],
                  path_info,
                  query_args=query_args,
              )
          def bind(
              self,
              server_name: str,
              script_name: str | None = None,
              subdomain: str | None = None,
              url_scheme: str = "http",
              default_method: str = "GET",
              path_info: str | None = None,
              query_args: t.Mapping[str, t.Any] | str | None = None,
          ) -> MapAdapter:
              
              server_name = server_name.lower()
              ...
              elif subdomain is None:
                  subdomain = self.default_subdomain
              if script_name is None:
                  script_name = "/"
              if path_info is None:
                  path_info = "/"
              
              server_name, port_sep, port = server_name.partition(":")
      
              try:
                  server_name = server_name.encode("idna").decode("ascii")
              except UnicodeError as e:
                  raise BadHost() from e
      
              return MapAdapter(
                  self,
                  f"{server_name}{port_sep}{port}",
                  script_name,
                  subdomain,
                  url_scheme,
                  path_info,
                  default_method,
                  query_args,
              )

      결국 self.url_adapter == MapAdapter Object가 됩니다.

      그러면 MapAdapter.match()를 통해서 url정보를 가지고 오는것을 예상해 볼 수 있습니다.

      #https://github.com/pallets/werkzeug/blob/main/src/werkzeug/routing/map.py#L374
      class MapAdapter:
          '''
          example
          >>> m = Map([
              ...     Rule('/', endpoint='index'),
              ...     Rule('/downloads/', endpoint='downloads/index'),
              ...     Rule('/downloads/<int:id>', endpoint='downloads/show')
              ... ])
              >>> urls = m.bind("example.com", "/")
              >>> urls.match("/", "GET")
              ('index', {})
              >>> urls.match("/downloads/42")
              ('downloads/show', {'id': 42})
          '''
          ...
          def match(
              self,
              path_info: str | None = None,
              method: str | None = None,
              return_rule: bool = False,
              query_args: t.Mapping[str, t.Any] | str | None = None,
              websocket: bool | None = None,
          ) -> tuple[str | Rule, t.Mapping[str, t.Any]]:
              ...
              try:
                  result = self.map._matcher.match(domain_part, path_part, method, websocket)
              else:
                  rule, rv = result
                  ...
                  if return_rule:
                      return rule, rv
                  else:
                      return rule.endpoint, rv

      이제야 App.add_url_rule()에서 어떤행동을 했는지 알 수 있습니다.

      마지막으로 view_func를 실행하고, 그 결과값을 바탕으로 response를 보내는것을 확인할 수 있습니다.

      class Flask(App):
          ...
          #https://github.com/pallets/flask/blob/main/src/flask/app.py#L829
          def dispatch_request(self) -> ft.ResponseReturnValue:
              ...
              rule: Rule = req.url_rule  # type: ignore[assignment]
              ...
              view_args: dict[str, t.Any] = req.view_args
              #async함수를 sync로 바꿔줌
              #일반적으로는 self.view_functions[rule.endpoint](**view_args)와 동일
              return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
      class Flask(App):
          #https://github.com/pallets/flask/blob/main/src/flask/app.py#L872
          def finalize_request(
              self,
              rv: ft.ResponseReturnValue | HTTPException,
              from_error_handler: bool = False,
          ) -> Response:
              #view_func의 결과물을 가지고
              #response를 보낸 수 있는 Response Object로 변환해줍니다.
              response = self.make_response(rv)
              try:
                  response = self.process_response(response)
              ...
              #이 return값이 WSGIRequestHandler.run_wsgi에서 반복문을 통해서 clinet한태 송출됨
              return response

      자 복잡했지만


      결국 아래와 같은 순서로 Flask는 동작하게 됩니다.👀

      1. 유저가 입력한 rule을 werkzeug.routing.RuleObject로 만듦

      2. werkzeug.routing.Map에다가 rule을 저장시킴

      3. request가 도착하면 해당 request에 대응되는 requestHandler를 실행시킴

      4. WSGIRequestHandler의 self.run_wsgi()를 실행시킴

      5. request정보를 바탕으로 werkzeug.routing.Map에서 대응되는 Rule을 빼옴

      6. Rule.endpoint에 대응되는 view_func를 실행시킴

      7. view_func의 결과값을 Response Object로 만들어서 client한태 보내줌.

      Flask를 이해하는데 도움되셨길 바랍니다!


      감사합니다!

      댓글 0

      DEVOTEE를 활성화 시키면
      지금 작성한 댓글에 AI가 댓글을 달아줍니다.

      Teus 님의 최신 블로그

      더보기

      DEVOTEE 추천 블로그

      동영상 기고하기