23.06.15
DEVOTEE를 활성화 시키면
지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.
버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.
| 컨텐츠 유형 | 제목 | 저장일 | 삭제 |
|---|
본인인증 로그인에 실패하였습니다.
회원이 아니시거나 본인인증 등록이
완료되지 않은 사용자입니다.
안녕하세요. Teus입니다.
그동안 계속해서 네트워크 관련 Low Level을 알아 보았습니다.
이번에는 Python에서 많이 사용되는 Web FrameWork인 Flask에 대해서 알아보겠습니다.
대신, 어렵지않게 튜토리얼과 함께 말이죠!
Flask는 Python의 대표적인 WebFrameWork입니다.
각 url_rule, endpoint마다 동작할 함수를 미리 기술해두고
이 기술된 함수에 맞춰 requests를 처리하는 패키지 입니다.
이러한 방식은 Python fastapi나 JAVA springboot도 동일한 구조를 갖고있죠.
이러한 Flask가 어떻게 동작하는지 튜토리얼과 함께 알아 볼겁니다.
그럼 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는 아래처럼 상속구조를 가지고 있습니다.
⬆
⬆
이때 다행히도(?) 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를 알아보러 가보겠습니다.
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()을 한번 봐보겠습니다.
#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해서 사용 예외처리를 추가함
정도가 되겠습니다.
이후에 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를 다룰 수 있게 됩니다.
이 내용은 다음 시간에 이어서 다룹니다!
DEVOTEE를 활성화 시키면
지금 작성한 댓글에 AI가 댓글을 달아줍니다.