23.06.15
DEVOTEE를 활성화 시키면
지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.
버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.
| 컨텐츠 유형 | 제목 | 저장일 | 삭제 |
|---|
본인인증 로그인에 실패하였습니다.
회원이 아니시거나 본인인증 등록이
완료되지 않은 사용자입니다.
안녕하세요. Teus입니다.
그동안 계속해서 네트워크 관련 Low Level을 알아 보았습니다.
이번에는 Python에서 많이 사용되는 WebFrameWork인 Flask에 대해서 알아보는 두번째 시간입니다!
지난 포스팅에서
Flask는 werkzeug서버를 이용하고, werkzeug는 wsgiref를 이용한다고 했습니다.
하지만 이걸로만은 부족하죠?
분명 Flask는 다양한 url_rule, EndPoint를 설정하고, 하나의 서버에서 이 EndPoint를 모두 다룰 수가 있습니다.
그러면 어떻게 다양한 EndPoint를 다룰 수 있는지 이 부분에 대해서 알아보겠습니다.
#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결국 여기까지보면
Decorator를 달아도 함수 자체는 변하지 않는다.
이 과정에서 self.add_url_rule()을 통과한다
werkzeug.routing.Rule Object를 만들어서 werkzeug.routing.Map Object에 추가한다.
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로 돌아가 보겠습니다.
이전 포스팅에서 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인것을 알 수 있습니다.
⬆
⬆
⬆
그래서 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는 동작하게 됩니다.👀
유저가 입력한 rule을 werkzeug.routing.RuleObject로 만듦
werkzeug.routing.Map에다가 rule을 저장시킴
request가 도착하면 해당 request에 대응되는 requestHandler를 실행시킴
WSGIRequestHandler의 self.run_wsgi()를 실행시킴
request정보를 바탕으로 werkzeug.routing.Map에서 대응되는 Rule을 빼옴
Rule.endpoint에 대응되는 view_func를 실행시킴
view_func의 결과값을 Response Object로 만들어서 client한태 보내줌.
Flask를 이해하는데 도움되셨길 바랍니다!
감사합니다!
DEVOTEE를 활성화 시키면
지금 작성한 댓글에 AI가 댓글을 달아줍니다.