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

신고하기

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

미리보기

커뮤니티

      1,234

      badge 23.06.15

      글 등록

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

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

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

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

      임시저장함

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

      데보션 블로그 게재 요청

      CLOSE
      • *
      • *

      본인인증

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

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

      회원정보 연결

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

      Teus 23.12.26
      6,265 20 0
      DEVOTEE 요약
      본 블로그는 웹 프레임워크인 플라스크(Flask)에 대해 설명한다. 플라스크는 각 URL 규칙, 엔드포인트마다 동작할 함수를 미리 기술하고 이에 따라 요청을 처리하는 파이썬 패키지다. 웹 서버 동작을 위한 튜토리얼부터 자세히 설명하고, Flask의 작동 원리와 이를 지원하는 패키지인 werkzeug를 살펴본다.
      DEVOTEE 추천 블로그

      안녕하세요. Teus입니다.


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

      이번에는 Python에서 많이 사용되는 Web FrameWork인 Flask에 대해서 알아보겠습니다.

      대신, 어렵지않게 튜토리얼과 함께 말이죠!


      1. Flask

      Flask는 Python의 대표적인 WebFrameWork입니다.

      각 url_rule, endpoint마다 동작할 함수를 미리 기술해두고

      이 기술된 함수에 맞춰 requests를 처리하는 패키지 입니다.

      이러한 방식은 Python fastapi나 JAVA springboot도 동일한 구조를 갖고있죠.

      이러한 Flask가 어떻게 동작하는지 튜토리얼과 함께 알아 볼겁니다.


      2. Flask의 Tutorial

      그럼 Flask 공식 github에서 제공하는 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()

      최초로 Flask Object를 만들고

      여기에 decorator를 사용해서 routing을 추가합니다.

      그리고 마지막에 Flask.run()을 사용해서 웹서버가 동작하게 됩니다.


      그럼 Flask Class부터 살펴볼까요?

      일단 Flask Class는 아래처럼 상속구조를 가지고 있습니다.

      Scaffold

      ⬆

      App

      ⬆

      Flask


      이때 다행히도(?) Flask Class에서 self.run()을 바로 찾을 수 있습니다.

      #https://github.com/pallets/flask/blob/main/src/flask/app.py#L76C1-L76C18
      class Flask(App):
          ...
          def __init__(
              self,
              import_name: str,
              static_url_path: str | None = None,
              static_folder: str | os.PathLike | None = "static",
              static_host: str | None = None,
              host_matching: bool = False,
              subdomain_matching: bool = False,
              template_folder: str | os.PathLike | None = "templates",
              instance_path: str | None = None,
              instance_relative_config: bool = False,
              root_path: str | None = None,
          ):
              super().__init__(
              ...
              )
              if self.has_static_folder:
                  ...
          ...
          def run(
              self,
              host: str | None = None,
              port: int | None = None,
              debug: bool | None = None,
              load_dotenv: bool = True,
              **options: t.Any,
          ) -> None:
              ...
              '''
              예외 처리 및 매개변수 사전 점검부분
              '''
              from werkzeug.serving import run_simple
      
              try:
                  run_simple(t.cast(str, host), port, self, **options)
              ...

      self.run()을 통해서

      Flask는 자체적으로 werkzeug의 run_simple()을 사용하는 것을 볼 수 있습니다.

      그럼 이제 잠깐 werkzeug를 알아보러 가보겠습니다.


      3. werkzeug

      werkzeug는 WSGI를 wrapping한 패키지 입니다.

      Flask에 사용되는것으로 유명하지만, werkzeug 자체적으로 webserver를 띄울 수가 있습니다.

      from werkzeug.wrappers import Request, Response
      
      @Request.application
      def application(request):
          return Response('Hello, World!')
      
      if __name__ == '__main__':
          from werkzeug.serving import run_simple
          run_simple('localhost', 4000, application)

      사용패턴을 보면 알 수 있지만, server에 application을 집어넣어주는 방식이

      wsgiref와 굉장히 유사한게 보이시죠?

      requests와 urllib3의 관계와 비슷하게, Flask는 하부에 werkzeug를 사용해서 구현되고 있습니다.

      Flask는 werkzeug를 이용하고, werkzeug는 WSGI를 이용하기 때문에, Flask는 WSGI를 사용해서 구현된다고 볼 수 있죠.

      여턴 werkzeug에서 사용되는 run_simple()을 한번 봐보겠습니다.


      3_1. werkzeug 서버

      #https://github.com/pallets/werkzeug/blob/main/src/werkzeug/serving.py#L947C1-L964C11
      def run_simple(
          hostname: str,
          port: int,
          #wsgiref처럼 application을 받으며
          #이때 WSGIApplication이라는 type으로 받습니다.
          application: WSGIApplication,
          use_reloader: bool = False,
          use_debugger: bool = False,
          use_evalex: bool = True,
          extra_files: t.Iterable[str] | None = None,
          exclude_patterns: t.Iterable[str] | None = None,
          reloader_interval: int = 1,
          reloader_type: str = "auto",
          threaded: bool = False,
          processes: int = 1,
          request_handler: type[WSGIRequestHandler] | None = None,
          static_files: dict[str, str | tuple[str, str]] | None = None,
          passthrough_errors: bool = False,
          ssl_context: _TSSLContextArg | None = None,
      ) -> None:
          ...
          if static_files:
              from .middleware.shared_data import SharedDataMiddleware
              #static_files을 사용할 경우
              #Flask Object를 static_files과 혼합하여 Update해줍니다.
              application = SharedDataMiddleware(application, static_files)
          ...
          if not is_running_from_reloader():
              fd = None
          else:
              fd = int(os.environ["WERKZEUG_SERVER_FD"])
      
          srv = make_server(
              hostname,
              port,
              application,
              threaded,
              processes,
              request_handler,
              passthrough_errors,
              ssl_context,
              fd=fd,
          )
          srv.socket.set_inheritable(True)
          os.environ["WERKZEUG_SERVER_FD"] = str(srv.fileno())
          
          ...    
          if use_reloader:
              #reload case정의
              ...
          else:
              srv.serve_forever()   

      이때 WSGIApplication이 정확이 어떤 type인지 알수가 없는 상태 입니다.

      코드상으로 보면 Flask Object 역시 WSGIApplication에 포함됩니다.

      #https://github.com/pallets/werkzeug/blob/main/src/werkzeug/serving.py#L79
      if t.TYPE_CHECKING:
          from _typeshed.wsgi import WSGIApplication
          from _typeshed.wsgi import WSGIEnvironment

      이때 소스코드에서 _typeshed라는것이 등장합니다.

      _typeshed는 Python의 Standard Library로, Python의 common type을 가지고 올 수 있게 해줍니다.

      출처 : https://github.com/python/typeshed/tree/main/stdlib/_typeshed

      #https://github.com/python/typeshed/blob/main/stdlib/_typeshed/wsgi.pyi
      class StartResponse(Protocol):
          def __call__(
              self, __status: str, __headers: list[tuple[str, str]], __exc_info: OptExcInfo | None = ...
          ) -> Callable[[bytes], object]: ...
      
      WSGIEnvironment: TypeAlias = dict[str, Any]  # stable
      WSGIApplication: TypeAlias = Callable[[WSGIEnvironment, StartResponse], Iterable[bytes]]
      #(dict[str, Any], Response)를 받고, 이에따라 Iterable[bytes]를 return해줌
      #추가적으로 Call할 수 있어야됨

      그래서 Flask Class에서 self.__call__()이 정의되어 있고, type과 일치하는 것을 볼 수 있습니다.

      #https://github.com/pallets/flask/blob/main/src/flask/app.py#L1476C5-L1481C54
      class Flask(app):
          ...
          def __call__(self, environ: dict, start_response: t.Callable) -> t.Any:
              return self.wsgi_app(environ, start_response)

      잠깐 딴길로 샜지만

      다시 돌아가보면, run_simple() 내부에서 make_server()를 사용하는 상황 입니다.

      그럼 make_server()를 한번 볼까요?

      #https://github.com/pallets/werkzeug/blob/main/src/werkzeug/serving.py#L893
      def make_server(
          host: str,
          port: int,
          app: WSGIApplication,
          threaded: bool = False,
          processes: int = 1,
          request_handler: type[WSGIRequestHandler] | None = None,
          passthrough_errors: bool = False,
          ssl_context: _TSSLContextArg | None = None,
          fd: int | None = None,
      ) -> BaseWSGIServer:
          #processes와 threaded의 기본값으로 작동합니다
          #따라서 기본값으로 동작할 BaseWSGIServer만 알아봅니다.
          ...
          return BaseWSGIServer(
              host, port, app, request_handler, passthrough_errors, ssl_context, fd=fd
          )

      make_server()는 thread와 process정보에 따라 다른 ServerClass를 제공해 줍니다.

      이때 BaseWSGIServer Class에 대한 코드를 봐 볼까요?

      #https://github.com/pallets/werkzeug/blob/main/src/werkzeug/serving.py#L687C1-L687C34
      from http.server import HTTPServer
      class BaseWSGIServer(HTTPServer):
          def __init__(
              self,
              host: str,
              port: int,
              app: WSGIApplication,
              handler: type[WSGIRequestHandler] | None = None,
              passthrough_errors: bool = False,
              ssl_context: _TSSLContextArg | None = None,
              fd: int | None = None,
          ) -> None:
              #requesthandler가 설정되지 않았을 경우
              #Default WSGIRequestHandler를 사용하게 설정해둠
              if handler is None:
                  handler = WSGIRequestHandler
          ...
          def serve_forever(self, poll_interval: float = 0.5) -> None:
              try:
                  super().serve_forever(poll_interval=poll_interval)
              except KeyboardInterrupt:
                  pass
              finally:
                  self.server_close()

      이제 wsgiref를 어떻게 사용하는지 보이시나요?

      이전 WSGI포스팅 1편에서 보았듯


      WSGI는 serve_forever()를 실행하면서 selectors를 이용해서 Client의 requests를 받아들이고

      받아들인 requests를 requestsHandler를 사용해서 처리합니다.


      werkzeug가 wsgiref와의 차이점은

      wsgiref : serve_forever()가 BaseServer Class에 있는것을 바로 사용함

      werkzeug : serve_forever()를 overriding해서 사용 예외처리를 추가함

      정도가 되겠습니다.


      3_2. werkzeug 서버 Requests처리

      이후에 requests를 처리할때는 CGIRequestHandler와 유사하게 BaseHTTPRequestHandler를 상속받아서 사용하기 됩니다.

      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

      이때 do_POST, do_GET을 처리할 때 내부의 self.run_wsgi()를 사용합니다.

      #https://github.com/pallets/werkzeug/blob/main/src/werkzeug/serving.py#L240
      class WSGIRequestHandler(BaseHTTPRequestHandler):
          ...
          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()
              status_set: str | None = None
              headers_set: list[tuple[str, str]] | None = None
              status_sent: str | None = None
              headers_sent: list[tuple[str, str]] | None = None
              chunk_response: bool = False
              def write(data: bytes) -> None:
              ...
              def start_response(status, headers, exc_info=None):  # type: ignore
              ...
              def execute(app: WSGIApplication) -> None:
              ...
              try:
                  execute(self.server.app)
              ...       

      여기까지 어떻게 Flask 서버가 동작하는지 알아 보았습니다.

      하지만 여기까지만 봐서는 wsgiref와 차이점을 알기 어렵습니다.

      이때 @route decorator를 통해서 다수의 endpoint를 다룰 수 있게 됩니다.

      이 내용은 다음 시간에 이어서 다룹니다!

      댓글 0

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

      Teus 님의 최신 블로그

      더보기

      DEVOTEE 추천 블로그

      동영상 기고하기