FastAPI์ ํต์ฌ: Starlette ๐๐๐ ์ฌ์ธต ๋ถ์
James Reed
Infrastructure Engineer ยท Leapcell

FastAPI๋ ๋ณธ์ง์ ์ผ๋ก Starlette์ API ๋ํผ์ ๋๋ค. FastAPI๋ฅผ ์์ ํ ์ดํดํ๋ ค๋ฉด ๋จผ์ Starlette์ ์ดํดํด์ผ ํฉ๋๋ค.
1. ASGI ํ๋กํ ์ฝ
Uvicorn์ ๊ณตํต ์ธํฐํ์ด์ค๋ฅผ ํตํด ASGI ์ ํ๋ฆฌ์ผ์ด์ ๊ณผ ์ํธ ์์ฉํฉ๋๋ค. ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ค์ ์ฝ๋๋ฅผ ๊ตฌํํ์ฌ Uvicorn์ ํตํด ์ ๋ณด๋ฅผ ์ก์์ ํ ์ ์์ต๋๋ค.
async def app(scope, receive, send): # ๊ฐ์ฅ ๊ฐ๋จํ ASGI ์ ํ๋ฆฌ์ผ์ด์ assert scope['type'] == 'http' await send({ 'type': 'http.response.start', 'status': 200, 'headers': [ [b'content-type', b'text/plain'], ] }) await send({ 'type': 'http.response.body', 'body': b'Hello, world!', })
if __name__ == "__main__": # Uvicorn ์๋น์ค import uvicorn uvicorn.run(app, host="127.0.0.1", port=5000, log_level="info")
2. Starlette
Uvicorn์ผ๋ก Starlette์ ์์ํ๋ ค๋ฉด ๋ค์ ์ฝ๋๋ฅผ ์ฌ์ฉํฉ๋๋ค.
from starlette.applications import Starlette from starlette.middleware.gzip import GZipMiddleware app: Starlette = Starlette() @app.route("/") def demo_route() -> None: pass @app.websocket_route("/") def demo_websocket_route() -> None: pass @app.add_exception_handlers(404) def not_found_route() -> None: pass @app.on_event("startup") def startup_event_demo() -> None: pass @app.on_event("shutdown") def shutdown_event_demo() -> None: pass app.add_middleware(GZipMiddleware) if __name__ == "__main__": import uvicorn uvicorn.run(app, host="127.0.0.1", port=5000)
์ด ์ฝ๋๋ Starlette์ ์ด๊ธฐํํ๊ณ , ๋ผ์ฐํธ, ์์ธ ์ฒ๋ฆฌ๊ธฐ, ์ด๋ฒคํธ, ๋ฏธ๋ค์จ์ด๋ฅผ ๋ฑ๋กํ ๋ค์ uvicorn.run์ ์ ๋ฌํฉ๋๋ค. uvicorn.run ๋ฉ์๋๋ Starlette์ call ๋ฉ์๋๋ฅผ ํธ์ถํ์ฌ ์์ฒญ ๋ฐ์ดํฐ๋ฅผ ๋ณด๋
๋๋ค.
๋จผ์ Starlette ์ด๊ธฐํ๋ฅผ ๋ถ์ํด ๋ณด๊ฒ ์ต๋๋ค.
class Starlette: def __init__( self, debug: bool = False, routes: typing.Sequence[BaseRoute] = None, middleware: typing.Sequence[Middleware] = None, exception_handlers: typing.Dict[ typing.Union[int, typing.Type[Exception]], typing.Callable ] = None, on_startup: typing.Sequence[typing.Callable] = None, on_shutdown: typing.Sequence[typing.Callable] = None, lifespan: typing.Callable[["Starlette"], typing.AsyncGenerator] = None, ) -> None: """ :param debug: ๋๋ฒ๊ทธ ๊ธฐ๋ฅ ํ์ฑํ ์ฌ๋ถ ๊ฒฐ์ . :param route: HTTP ๋ฐ WebSocket ์๋น์ค๋ฅผ ์ ๊ณตํ๋ ๋ผ์ฐํธ ๋ชฉ๋ก. :param middleware: ๊ฐ ์์ฒญ์ ์ ์ฉ๋๋ ๋ฏธ๋ค์จ์ด ๋ชฉ๋ก. :param exception_handler: ์์ธ ์ฝ๋ฐฑ์ ์ ์ฅํ๋ ์ฌ์ , HTTP ์ํ ์ฝ๋๋ฅผ ํค๋ก, ์ฝ๋ฐฑ ํจ์๋ฅผ ๊ฐ์ผ๋ก ์ฌ์ฉ. :on_startup: ์์ ์ ํธ์ถ๋๋ ์ฝ๋ฐฑ ํจ์. :on_shutdown: ์ข ๋ฃ ์ ํธ์ถ๋๋ ์ฝ๋ฐฑ ํจ์. :lifespan: ASGI์ ๋ผ์ดํ์คํฌ ํจ์. """ # lifespan์ด ์ ๋ฌ๋๋ฉด on_startup๊ณผ on_shutdown์ ์ ๋ฌ๋ ์ ์์ต๋๋ค # Starlette์ ๊ธฐ๋ณธ์ ์ผ๋ก on_start_up๊ณผ on_shutdown์ Uvicorn ํธ์ถ์ ์ํ lifespan์ผ๋ก ๋ณํํ๊ธฐ ๋๋ฌธ์ ๋๋ค assert lifespan is None or ( on_startup is None and on_shutdown is None ), "'lifespan' ๋๋ 'on_startup'/'on_shutdown' ์ค ํ๋๋ฅผ ์ฌ์ฉํ๊ณ ๋ ๋ค ์ฌ์ฉํ์ง ๋ง์ญ์์ค." # ๋ณ์ ์ด๊ธฐํ self._debug = debug self.state = State() self.router = Router( routes, on_startup=on_startup, on_shutdown=on_shutdown, lifespan=lifespan ) self.exception_handlers = {} if exception_handlers is None else dict(exception_handlers) self.user_middleware = [] if middleware is None else list(middleware) # ๋ฏธ๋ค์จ์ด ๋น๋ self.middleware_stack = self.build_middleware_stack()
์ฝ๋์์ ๋ณผ ์ ์๋ฏ์ด ์ด๊ธฐํ๋ ์ด๋ฏธ ๋๋ถ๋ถ์ ์๊ตฌ ์ฌํญ์ ์ถฉ์กฑํฉ๋๋ค. ๊ทธ๋ฌ๋ ์ถ๊ฐ ๋ถ์์ด ํ์ํ ๋ฏธ๋ค์จ์ด๋ฅผ ๋น๋ํ๋ ํจ์๊ฐ ์์ต๋๋ค.
class Starlette: def build_middleware_stack(self) -> ASGIApp: debug = self.debug error_handler = None exception_handlers = {} # ์์ธ ์ฒ๋ฆฌ ์ฝ๋ฐฑ์ ํ์ฑํ์ฌ error_handler์ exception_handlers์ ์ ์ฅ # HTTP ์ํ ์ฝ๋ 500๋ง error_handler์ ์ ์ฅ๋ฉ๋๋ค for key, value in self.exception_handlers.items(): if key in (500, Exception): error_handler = value else: exception_handlers[key] = value # ๋ค์ํ ์ ํ์ ๋ฏธ๋ค์จ์ด ์ ๋ ฌ # ์ฒซ ๋ฒ์งธ ๋ ์ด์ด๋ ServerErrorMiddleware์ด๋ฉฐ, ์์ธ๊ฐ ๋ฐ๊ฒฌ๋ ๋ ์ค๋ฅ ์คํ์ ์ถ๋ ฅํ๊ฑฐ๋ ๋๋ฒ๊น ์ ์ฝ๊ฒ ์ํด ๋๋ฒ๊ทธ ๋ชจ๋์์ ์ค๋ฅ ํ์ด์ง๋ฅผ ํ์ํ ์ ์์ต๋๋ค. # ๋ ๋ฒ์งธ ๋ ์ด์ด๋ ์ฌ์ฉ์ ๋ฏธ๋ค์จ์ด ๋ ์ด์ด๋ก, ๋ฑ๋ก๋ ๋ชจ๋ ์ฌ์ฉ์ ๋ฏธ๋ค์จ์ด๊ฐ ์ ์ฅ๋ฉ๋๋ค. # ์ธ ๋ฒ์งธ ๋ ์ด์ด๋ ExceptionMiddleware๋ก, ์์ธ ์ฒ๋ฆฌ ๋ ์ด์ด์ด๋ฉฐ ๋ผ์ฐํธ ์คํ ์ค์ ๋ฐ์ํ๋ ๋ชจ๋ ์์ธ๋ฅผ ์ฒ๋ฆฌํฉ๋๋ค. middleware = [Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug)] + self.user_middleware + [ Middleware( ExceptionMiddleware, handlers=exception_handlers, debug=debug ) ] # ๋ง์ง๋ง์ผ๋ก ๋ฏธ๋ค์จ์ด๋ฅผ ์ฑ์ ๋ก๋ app = self.router for cls, options in reversed(middleware): # cls๋ ๋ฏธ๋ค์จ์ด ์์ฒด์ด๋ฉฐ, options๋ ์ฐ๋ฆฌ๊ฐ ์ ๋ฌํ๋ ๋งค๊ฐ๋ณ์์ ๋๋ค # ๋ฏธ๋ค์จ์ด ์์ฒด๋ ASGI APP์ด๋ฉฐ, ๋ฏธ๋ค์จ์ด๋ฅผ ๋ก๋ํ๋ ๊ฒ์ ๋ง์น ํ๋์ ASGI APP์ ๋ค๋ฅธ ํ๋์ ์ค์ฒฉํ๋ ๊ฒ๊ณผ ๊ฐ์ต๋๋ค. ๋งํธ๋ฃ์์นด ์ธํ์ฒ๋ผ์. app = cls(app=app, **options) # ๋ฏธ๋ค์จ์ด๊ฐ ์ค์ฒฉ๋ ๋ฐฉ์์ผ๋ก ๋ก๋๋๊ณ , call_next๋ฅผ ํตํด ์์ ASGI APP๋ฅผ ํธ์ถํ๋ฏ๋ก ์ญ์ ๋ฐฉ์์ ์ฌ์ฉํฉ๋๋ค. return app
๋ฏธ๋ค์จ์ด๋ฅผ ๋น๋ํ ํ ์ด๊ธฐํ๊ฐ ์๋ฃ๋๊ณ , uvicorn.run ๋ฉ์๋๊ฐ call ๋ฉ์๋๋ฅผ ํธ์ถํฉ๋๋ค.
class Starlette: async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: scope["app"] = self await self.middleware_stack(scope, receive, send)
์ด ๋ฉ์๋๋ ๊ฐ๋จํฉ๋๋ค. scope๋ฅผ ํตํด ์์ฒญ ํ๋ฆ์ ์ฑ์ ์ค์ ํ์ฌ ํ์ ํธ์ถ์ ์ฌ์ฉํ ์ ์๋๋ก ํ๊ณ , middleware_stack์ ํธ์ถํ์ฌ ์์ฒญ ์ฒ๋ฆฌ๋ฅผ ์์ํฉ๋๋ค. ์ด ๋ฉ์๋์ ๋ฏธ๋ค์จ์ด ์ด๊ธฐํ์์ Starlette์ ๋ฏธ๋ค์จ์ด ์ญ์ ASGI APP์์ ์ ์ ์์ต๋๋ค (๋ผ์ฐํธ๋ ํธ์ถ ์คํ์ ๊ฐ์ฅ ์๋์์ ASGI APP์์ ๋ณผ ์ ์์ต๋๋ค). ๋์์ Starlette์ ์์ธ ์ฒ๋ฆฌ๋ฅผ ๋ฏธ๋ค์จ์ด์ ์์ํ๋ฉฐ, ์ด๋ ๋ค๋ฅธ ์น ์ ํ๋ฆฌ์ผ์ด์
ํ๋ ์์ํฌ์์๋ ๋๋ฌผ๊ฒ ๋ณผ ์ ์์ต๋๋ค. Starlette์ ๊ฐ ๊ตฌ์ฑ ์์๊ฐ ๊ฐ๋ฅํ ํ ASGI APP๊ฐ ๋๋๋ก ์ค๊ณ๋์์์ ์ ์ ์์ต๋๋ค.
2. ๋ฏธ๋ค์จ์ด
์์ ์ธ๊ธํ๋ฏ์ด Starlette์์ ๋ฏธ๋ค์จ์ด๋ ASGI APP์ ๋๋ค. ๋ฐ๋ผ์ Starlette์ ๋ชจ๋ ๋ฏธ๋ค์จ์ด๋ ๋ค์ ํ์์ ์ถฉ์กฑํ๋ ํด๋์ค์ฌ์ผ ํฉ๋๋ค.
class BaseMiddleware: def __init__(self, app: ASGIApp) -> None: pass async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: pass
starlette.middleware์๋ ์ด ์๊ตฌ ์ฌํญ์ ์ถฉ์กฑํ๋ ๋ง์ ๋ฏธ๋ค์จ์ด ๊ตฌํ์ด ์์ต๋๋ค. ๊ทธ๋ฌ๋ ์ด ์ฅ์์๋ ๋ชจ๋ ๋ฏธ๋ค์จ์ด๋ฅผ ๋ค๋ฃจ์ง ์๊ณ , ๋ผ์ฐํธ์ ๊ฐ์ฅ ๊ฐ๊น์ด ๊ฒ๋ถํฐ ๊ฐ์ฅ ๋จผ ๊ฒ ์์ผ๋ก ๋ช ๊ฐ์ง ๋ํ์ ์ธ ๋ฏธ๋ค์จ์ด๋ฅผ ์ ํํ์ฌ ๋ถ์ํฉ๋๋ค.
2.1. ์์ธ ์ฒ๋ฆฌ ๋ฏธ๋ค์จ์ด - ExceptionMiddleware
์ฒซ ๋ฒ์งธ๋ ExceptionMiddleware์
๋๋ค. ์ฌ์ฉ์๋ ์ด ๋ฏธ๋ค์จ์ด์ ์ง์ ์ํธ ์์ฉํ์ง ์์ง๋ง (๋ฐ๋ผ์ starlette.middleware์ ํฌํจ๋์ง ์์), ๋ค์ ๋ฉ์๋๋ฅผ ํตํด ๊ฐ์ ์ ์ผ๋ก ์ํธ ์์ฉํฉ๋๋ค.
@app.app_exception_handlers(404) def not_found_route() -> None: pass
์ฌ์ฉ์๊ฐ ์ด ๋ฉ์๋๋ฅผ ์ฌ์ฉํ ๋ Starlette์ HTTP ์ํ ์ฝ๋๋ฅผ ํค๋ก, ์ฝ๋ฐฑ ํจ์๋ฅผ ๊ฐ์ผ๋ก ํ์ฌ ํด๋น ์ฌ์ ์ ์ฝ๋ฐฑ ํจ์๋ฅผ ์ฐ๊ฒฐํฉ๋๋ค. ExceptionMiddleware๊ฐ ๋ผ์ฐํธ ์์ฒญ ์ฒ๋ฆฌ์ ์์ธ๊ฐ ๋ฐ์ํ์์ ๊ฐ์งํ๋ฉด, ์์ธ ์๋ต์ HTTP ์ํ ์ฝ๋๋ฅผ ํตํด ํด๋น ์ฝ๋ฐฑ ํจ์๋ฅผ ์ฐพ๊ณ , ์์ฒญ๊ณผ ์์ธ๋ฅผ ์ฌ์ฉ์๊ฐ ๋ง์ดํธํ ์ฝ๋ฐฑ ํจ์์ ์ ๋ฌํ ๋ค์, ์ต์ข ์ ์ผ๋ก ์ฌ์ฉ์์ ์ฝ๋ฐฑ ํจ์ ๊ฒฐ๊ณผ๋ฅผ ์ด์ ASGI APP๋ก ๋ค์ ๋์ง๋๋ค. ๋ํ ExceptionMiddleware๋ ์์ธ ๋ฑ๋ก๋ ์ง์ํฉ๋๋ค. ๋ผ์ฐํธ์์ ๋ฐ์ํ๋ ์์ธ๊ฐ ๋ฑ๋ก๋ ์์ธ์ ์ผ์นํ๋ฉด ํด๋น ์์ธ ๋ฑ๋ก์ ๋ํ ์ฝ๋ฐฑ ํจ์๊ฐ ํธ์ถ๋ฉ๋๋ค. ์ด ํด๋์ค์ ์์ค ์ฝ๋์ ์ฃผ์์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
class ExceptionMiddleware: def __init__( self, app: ASGIApp, handlers: dict = None, debug: bool = False ) -> None: self.app = app self.debug = debug # TODO: ๋๋ฒ๊ทธ๊ฐ ์ค์ ๋ ๊ฒฝ์ฐ 404 ๊ฒฝ์ฐ๋ฅผ ์ฒ๋ฆฌํด์ผ ํฉ๋๋ค. # Starlette์ HTTP ์ํ ์ฝ๋์ Exception ์ ํ์ ๋ชจ๋ ์ง์ํฉ๋๋ค self._status_handlers = {} # type: typing.Dict[int, typing.Callable] self._exception_handlers = { HTTPException: self.http_exception } # type: typing.Dict[typing.Type[Exception], typing.Callable] if handlers is not None: for key, value in handlers.items(): self.add_exception_handler(key, value) def add_exception_handler( self, exc_class_or_status_code: typing.Union[int, typing.Type[Exception]], handler: typing.Callable, ) -> None: # Starlette ์ฑ ๋ฉ์๋๋ฅผ ํตํด ์ฌ์ฉ์๊ฐ ๋ง์ดํธํ ์์ธ ์ฝ๋ฐฑ์ ์ด ๋ฉ์๋๋ฅผ ํตํด ๊ฒฐ๊ตญ _status_handlers ๋๋ _exception_handler์ ๋ง์ดํธ๋ฉ๋๋ค. if isinstance(exc_class_or_status_code, int): self._status_handlers[exc_class_or_status_code] = handler else: assert issubclass(exc_class_or_status_code, Exception) self._exception_handlers[exc_class_or_status_code] = handler def _lookup_exception_handler( self, exc: Exception ) -> typing.Optional[typing.Callable]: # ๋ฑ๋ก๋ ์์ธ์ ๊ด๋ จ๋ ์ฝ๋ฐฑ ํจ์๋ฅผ ์กฐํํฉ๋๋ค. mro๋ฅผ ํตํด ์์ธ์ ๋ํ ํด๋น ์ฝ๋ฐฑ ํจ์๋ฅผ ์ฐพ์ต๋๋ค. # # ์ฌ์ฉ์๊ฐ ๊ธฐ๋ณธ ํด๋์ค๋ฅผ ๋ง์ดํธํ ์ ์์ผ๋ฉฐ, ๋์ค์ ์ด ๊ธฐ๋ณธ ํด๋์ค์ ํ์ ํด๋์ค๋ ๊ธฐ๋ณธ ํด๋์ค์ ๋ฑ๋ก๋ ์ฝ๋ฐฑ์ ํธ์ถํฉ๋๋ค. # ์๋ฅผ ๋ค์ด, ์ฌ์ฉ์๊ฐ ๊ธฐ๋ณธ ํด๋์ค๋ฅผ ๋ฑ๋กํ๊ณ , ๊ทธ๋ฐ ๋ค์ ๋ ๊ฐ์ ์์ธ, ์ฌ์ฉ์ ์์ธ์ ์์คํ ์์ธ๊ฐ ๋ชจ๋ ์ด ๊ธฐ๋ณธ ํด๋์ค์์ ์์๋ฉ๋๋ค. # ๋์ค์ ํจ์๊ฐ ์ฌ์ฉ์ ์์ธ ๋๋ ์์คํ ์์ธ๋ฅผ ๋ฐ์์ํค๋ฉด ๊ธฐ๋ณธ ํด๋์ค์ ๋ฑ๋ก๋ ํด๋น ์ฝ๋ฐฑ์ด ์คํ๋ฉ๋๋ค. for cls in type(exc).__mro__: if cls in self._exception_handlers: return self._exception_handlers[cls] return None async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: # ์ต์ํ ASGI ํธ์ถ ๋ฉ์๋ if scope["type"]!= "http": # WebSocket ์์ฒญ์ ์ง์ํ์ง ์์ต๋๋ค await self.app(scope, receive, send) return # ๋์ผํ ์๋ต์์ ์ฌ๋ฌ ์์ธ๊ฐ ๋ฐ์ํ๋ ๊ฒ์ ๋ฐฉ์ง response_started = False async def sender(message: Message) -> None: nonlocal response_started if message["type"] == "http.response.start": response_started = True await send(message) try: # ๋ค์ ASGI APP ํธ์ถ await self.app(scope, receive, sender) except Exception as exc: handler = None if isinstance(exc, HTTPException): # HTTPException์ธ ๊ฒฝ์ฐ ๋ฑ๋ก๋ HTTP ์ฝ๋ฐฑ ์ฌ์ ์์ ์ฐพ์ต๋๋ค. handler = self._status_handlers.get(exc.status_code) if handler is None: # ์ผ๋ฐ ์์ธ์ธ ๊ฒฝ์ฐ ์์ธ ์ฝ๋ฐฑ ์ฌ์ ์์ ์ฐพ์ต๋๋ค. handler = self._lookup_exception_handler(exc) if handler is None: # ํด๋น ์์ธ๋ฅผ ์ฐพ์ ์ ์์ผ๋ฉด ์๋ก ๋์ง๋๋ค. raise exc from None # ์๋ต๋น ํ๋์ ์์ธ๋ง ์ฒ๋ฆฌ if response_started: msg = "์ฒ๋ฆฌ๋ ์์ธ๋ฅผ ์ก์์ง๋ง ์๋ต์ด ์ด๋ฏธ ์์๋์์ต๋๋ค." raise RuntimeError(msg) from exc request = Request(scope, receive=receive) if asyncio.iscoroutinefunction(handler): response = await handler(request, exc) else: response = await run_in_threadpool(handler, request, exc) # ์ฝ๋ฐฑ ํจ์์์ ์์ฑ๋ ์๋ต์ผ๋ก ์์ฒญ ์ฒ๋ฆฌ await response(scope, receive, sender)
2.2. ์ฌ์ฉ์ ๋ฏธ๋ค์จ์ด
๋ค์์ ๊ฐ์ฅ ๋ง์ด ์ ํ๋ ์ฌ์ฉ์ ๋ฏธ๋ค์จ์ด์
๋๋ค. starlette.middleware๋ฅผ ์ฌ์ฉํ ๋ ์ผ๋ฐ์ ์ผ๋ก BaseHTTPMiddleware๋ผ๋ ๋ฏธ๋ค์จ์ด์์ ์์ํ์ฌ ๋ค์ ์ฝ๋์ ๋ฐ๋ผ ํ์ฅํฉ๋๋ค.
class DemoMiddleware(BaseHTTPMiddleware): def __init__( self, app: ASGIApp, ) -> None: super(DemoMiddleware, self).__init__(app) async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: # ์ด์ response: Response = await call_next(request) # ์ดํ return response
์์ฒญ ์ ์ฌ์ ์ฒ๋ฆฌ๋ฅผ ์ํํ๋ ค๋ฉด before ๋ธ๋ก์ ๊ด๋ จ ์ฝ๋๋ฅผ ์์ฑํฉ๋๋ค. ์์ฒญ ํ ์ฒ๋ฆฌ๋ฅผ ์ํํ๋ ค๋ฉด after ๋ธ๋ก์ ์ฝ๋๋ฅผ ์์ฑํฉ๋๋ค. ์ฌ์ฉํ๊ธฐ ๋งค์ฐ ๊ฐ๋จํ๋ฉฐ, ๋ชจ๋ ๋์ผํ ๋ฒ์์ ์์ต๋๋ค. ์ฆ, ์ด ๋ฉ์๋์ ๋ณ์๋ฅผ ์ปจํ
์คํธ๋ ๋์ ๋ณ์๋ฅผ ํตํด ์ ๋ฌํ ํ์๊ฐ ์์ต๋๋ค (Django ๋๋ Flask์ ๋ฏธ๋ค์จ์ด ๊ตฌํ์ ์ ํ ์ ์ด ์๋ค๋ฉด Starlette ๊ตฌํ์ ์ฐ์ํจ์ ์ดํดํ ๊ฒ์
๋๋ค).
์ด์ ์ด๋ป๊ฒ ๊ตฌํ๋์๋์ง ์ดํด๋ณด๊ฒ ์ต๋๋ค. ์ฝ๋๋ ์ฝ 60์ค๋ก ๋งค์ฐ ๊ฐ๋จํ์ง๋ง ์ฃผ์์ด ๋ง์ด ํฌํจ๋์ด ์์ต๋๋ค.
class BaseHTTPMiddleware: def __init__(self, app: ASGIApp, dispatch: DispatchFunction = None) -> None: # ๋ค์ ๋ ๋ฒจ ASGI ์ฑ ํ ๋น self.app = app # ์ฌ์ฉ์๊ฐ dispatch๋ฅผ ์ ๋ฌํ๋ฉด ์ฌ์ฉ์๊ฐ ์ ๋ฌํ ํจ์๋ฅผ ์ฌ์ฉํ๊ณ , ๊ทธ๋ ์ง ์์ผ๋ฉด ์์ฒด dispatch๋ฅผ ์ฌ์ฉํฉ๋๋ค. # ์ผ๋ฐ์ ์ผ๋ก ์ฌ์ฉ์๋ BaseHTTPMiddleware์์ ์์ํ์ฌ dispatch ๋ฉ์๋๋ฅผ ์ฌ์ ์ํฉ๋๋ค. self.dispatch_func = self.dispatch if dispatch is None else dispatch async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: """ ASGI ํ์ค ํจ์ ์๊ทธ๋์ฒ๋ฅผ ๊ฐ๋ ํจ์๋ก, ASGI ์์ฒญ์ด ์ฌ๊ธฐ์ ์ง์ ํจ์ ๋ํ๋ ๋๋ค. """ if scope["type"]!= "http": # ์ ํ์ด http๊ฐ ์๋๋ฉด ๋ฏธ๋ค์จ์ด๊ฐ ์ ๋ฌ๋์ง ์์ต๋๋ค (์ฆ, WebSocket์ ์ง์๋์ง ์์). # WebSocket์ ์ง์ํ๋ ค๋ฉด ์ด ๋ฐฉ์์ผ๋ก ๋ฏธ๋ค์จ์ด๋ฅผ ๊ตฌํํ๊ธฐ๊ฐ ๋งค์ฐ ์ด๋ ต์ต๋๋ค. rap ํ๋ ์์ํฌ๋ฅผ ๊ตฌํํ ๋ WebSocket๊ณผ ์ ์ฌํ ํธ๋ํฝ์ ๋ํ ๋ฏธ๋ค์จ์ด ์ฒ๋ฆฌ๋ฅผ ๋ฌ์ฑํ๊ธฐ ์ํด ์ผ๋ถ ๊ธฐ๋ฅ์ ํฌ์ํ์ต๋๋ค. await self.app(scope, receive, send) return # scope์์ ์์ฒญ ๊ฐ์ฒด ์์ฑ request = Request(scope, receive=receive) # dispatch ๋ก์ง, ์ฆ ์ฌ์ฉ์์ ์ฒ๋ฆฌ ๋ก์ง ์ง์ # ์ด ๋ก์ง์์ ์ป์ ์๋ต์ ์ค์ ๋ก call_next ํจ์์ ์ํด ์์ฑ๋๋ฉฐ, dispatch ํจ์๋ ์ ๋ฌ ์ญํ ๋ง ํฉ๋๋ค. response = await self.dispatch_func(request, self.call_next) # ์์ฑ๋ ์๋ต์ ๋ฐ๋ผ ์์ ๊ณ์ธต์ผ๋ก ๋ฐ์ดํฐ ๋ฐํ await response(scope, receive, send) async def call_next(self, request: Request) -> Response: loop = asyncio.get_event_loop() # ํ ์์ฐ ๋ฐ ์๋น ๋ชจ๋ธ์ ํตํด ๋ค์ ๋ ๋ฒจ ๋ฉ์์ง ์ป๊ธฐ queue: "asyncio.Queue[typing.Optional[Message]]" = asyncio.Queue() scope = request.scope # request.receive ๊ฐ์ฒด๋ฅผ ํตํด uvicorn์ receive ๊ฐ์ฒด ์ ๋ฌ # ์ฌ๊ธฐ์ ์ฌ์ฉ๋๋ receive ๊ฐ์ฒด๋ ์ฌ์ ํ uvicorn์ผ๋ก ์ด๊ธฐํ๋ receive ๊ฐ์ฒด์ ๋๋ค. receive = request.receive send = queue.put async def coro() -> None: try: await self.app(scope, receive, send) finally: # ์ด put ์์ ์ get ์ธก์์ ์ฐจ๋จ๋์ง ์๋๋ก ๋ณด์ฅํฉ๋๋ค. await queue.put(None) # loop.create_task๋ฅผ ํตํด ๋ค๋ฅธ ์ฝ๋ฃจํด์์ ๋ค์ ASGI APP ์คํ task = loop.create_task(coro()) # ๋ค์ ASGI APP์ ๋ฐํ ๋๊ธฐ message = await queue.get() if message is None: # ์ป์ ๊ฐ์ด ๋น์ด ์์ผ๋ฉด ๋ค์ ASGI APP๊ฐ ์๋ต์ ๋ฐํํ์ง ์์์์ ์๋ฏธํ๋ฉฐ, ์ค๋ฅ๊ฐ ๋ฐ์ํ์ ์ ์์ต๋๋ค. # task.result()๋ฅผ ํธ์ถํ๋ฉด ์ฝ๋ฃจํด์ ์์ธ๊ฐ ์๋ ๊ฒฝ์ฐ ์ฝ๋ฃจํด์ ์ค๋ฅ๊ฐ ๋ฐ์ํฉ๋๋ค. task.result() # ์์ธ๊ฐ ๋ฐ์ํ์ง ์์ผ๋ฉด ์ฌ์ฉ์ ์ค๋ฅ์ผ ์ ์์ต๋๋ค (์: ๋น ์๋ต ๋ฐํ). # ์ด๋ ํด๋ผ์ด์ธํธ์ ์๋ต์ ๋ฐํํ ์ ์์ผ๋ฏ๋ก, 500 ์๋ต ์์ฑ์ ์ฉ์ดํ๊ฒ ํ๊ธฐ ์ํด ์์ธ๋ฅผ ๋ฐ์์์ผ์ผ ํฉ๋๋ค. raise RuntimeError("์๋ต์ด ๋ฐํ๋์ง ์์์ต๋๋ค.") # ASGI๊ฐ ์๋ต์ ์ฒ๋ฆฌํ ๋ ์ฌ๋ฌ ๋จ๊ณ๋ก ์ํ๋ฉ๋๋ค. ์ผ๋ฐ์ ์ผ๋ก ์์ queue.get์ ์๋ต์ ์ป๋ ์ฒซ ๋ฒ์งธ ๋จ๊ณ์ ๋๋ค. assert message["type"] == "http.response.start" async def body_stream() -> typing.AsyncGenerator[bytes, None]: # ๋ค๋ฅธ ์ฒ๋ฆฌ๋ body_stream ํจ์์ ์์๋ฉ๋๋ค. # ์ด ๋ฉ์๋๋ ๋จ์ํ ๋ฐ์ดํฐ ์คํธ๋ฆผ์ ๊ณ์ ๋ฐํํฉ๋๋ค. while True: message = await queue.get() if message is None: break assert message["type"] == "http.response.body" yield message.get("body", b"") task.result() # body_stream ํจ์๋ฅผ Response ๋ฉ์๋์ ๋ฃ์ต๋๋ค. # ์๋ต ์์ฒด๋ ASGI APP์ ์ ์ฌํ ํด๋์ค์ ๋๋ค.
2.3. ServerErrorMiddleware
ServerErrorMiddleware๋ ExceptionMiddleware์ ๋งค์ฐ ์ ์ฌํฉ๋๋ค (๋ฐ๋ผ์ ์ด ๋ถ๋ถ์ ๋ ์ด์ ์์ธํ ์ค๋ช ํ์ง ์์ต๋๋ค). ์ ์ฒด ๋ก์ง์ ๋๋ถ๋ถ ๋์ผํฉ๋๋ค. ๊ทธ๋ฌ๋ ๋ฑ๋ก๋ HTTP ์ํ ์ฝ๋๊ฐ 500์ผ ๋๋ง ์ฝ๋ฐฑ์ด ServerErrorMiddleware์ ๋ฑ๋ก๋ฉ๋๋ค.
@app.exception_handlers(500) def not_found_route() -> None: pass
ServerErrorMiddleware๋ ASGI APP์ ์ต์์ ๋ ๋ฒจ์ ์์ต๋๋ค. ํ์ ์์ธ ์ฒ๋ฆฌ ์์ ์ ๋งก์ต๋๋ค. ํด์ผ ํ ์ผ์ ๊ฐ๋จํฉ๋๋ค. ๋ค์ ๋ ๋ฒจ ASGI APP ์ฒ๋ฆฌ ์ค์ ์์ธ๊ฐ ๋ฐ์ํ๋ฉด ํ์ ๋ก์ง์ผ๋ก ๋ค์ด๊ฐ๋๋ค.
-
- ๋๋ฒ๊ทธ๊ฐ ํ์ฑํ๋ ๊ฒฝ์ฐ ๋๋ฒ๊ทธ ํ์ด์ง๋ฅผ ๋ฐํํฉ๋๋ค.
-
- ๋ฑ๋ก๋ ์ฝ๋ฐฑ์ด ์์ผ๋ฉด ๋ฑ๋ก๋ ์ฝ๋ฐฑ์ ์คํํฉ๋๋ค.
-
- ์์ ๋ ๋ค ์๋ ๊ฒฝ์ฐ 500 ์๋ต์ ๋ฐํํฉ๋๋ค.
3. ๋ผ์ฐํธ
Starlette์์ ๋ผ์ฐํธ๋ ๋ ๋ถ๋ถ์ผ๋ก ๋๋ฉ๋๋ค. ํ๋๋ ๋ฏธ๋ค์จ์ด ๋ ๋ฒจ ์๋์ ์๋ ์ ๊ฐ Real App์ Router๋ผ๊ณ ๋ถ๋ฅด๋ ๋ถ๋ถ์ ๋๋ค. ์ฃผ์ ๋ก๋ ์กฐํ ๋ฐ ์ผ์น, ์ ํ๋ฆฌ์ผ์ด์ ์์ ๋ฐ ์ข ๋ฃ ์ฒ๋ฆฌ ๋ฑ ๋ฏธ๋ค์จ์ด๋ฅผ ์ ์ธํ Starlette์ ๊ฑฐ์ ๋ชจ๋ ๊ฒ์ ๋ด๋นํฉ๋๋ค. ๋ค๋ฅธ ํ๋๋ Router์ ๋ฑ๋ก๋ ๋ผ์ฐํธ๋ก ๊ตฌ์ฑ๋ฉ๋๋ค.
3.1. Router
Router๋ ๊ฐ๋จํฉ๋๋ค. ์ฃผ์ ์ฑ ์์ ๋ผ์ฐํธ๋ฅผ ๋ก๋ํ๊ณ ์ผ์น์ํค๋ ๊ฒ์ ๋๋ค. ๋ผ์ฐํธ ๋ก๋ ๋ถ๋ถ์ ์ ์ธํ ์์ค ์ฝ๋์ ์ฃผ์์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
class Router: def __init__( self, routes: typing.Sequence[BaseRoute] = None, redirect_slashes: bool = True, default: ASGIApp = None, on_startup: typing.Sequence[typing.Callable] = None, on_shutdown: typing.Sequence[typing.Callable] = None, lifespan: typing.Callable[[typing.Any], typing.AsyncGenerator] = None, ) -> None: # Starlette ์ด๊ธฐํ์์ ์ ๋ณด ๋ก๋ self.routes = [] if routes is None else list(routes) self.redirect_slashes = redirect_slashes self.default = self.not_found if default is None else default self.on_startup = [] if on_startup is None else list(on_startup) self.on_shutdown = [] if on_shutdown is None else list(on_shutdown) async def default_lifespan(app: typing.Any) -> typing.AsyncGenerator: await self.startup() yield await self.shutdown() # ์ด๊ธฐํ๋ lifespan์ด ๋น์ด ์์ผ๋ฉด on_startup๊ณผ on_shutdown์ lifespan์ผ๋ก ๋ณํ self.lifespan_context = default_lifespan if lifespan is None else lifespan async def not_found(self, scope: Scope, receive: Receive, send: Send) -> None: """๋ผ์ฐํธ๊ฐ ์ผ์นํ์ง ์์ ๋ ์คํ๋๋ ๋ก์ง""" if scope["type"] == "websocket": # WebSocket ์ผ์น ์คํจ websocket_close = WebSocketClose() await websocket_close(scope, receive, send) return # starlette ์ ํ๋ฆฌ์ผ์ด์ ๋ด๋ถ์์ ์คํ ์ค์ด๋ผ๋ฉด ์์ธ๋ฅผ ๋ฐ์์์ผ # ๊ตฌ์ฑ ๊ฐ๋ฅํ ์์ธ ์ฒ๋ฆฌ๊ธฐ๊ฐ ์๋ต ๋ฐํ์ ์ฒ๋ฆฌํ ์ ์๋๋ก ํฉ๋๋ค. ์ผ๋ฐ ASGI ์ฑ์ ๊ฒฝ์ฐ ์๋ต์ ์ง์ ๋ฐํํฉ๋๋ค. if "app" in scope: # starlette.applications์ __call__ ๋ฉ์๋์์ Starlette์ด ์์ ์ scope์ ์ ์ฅํ๋ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค. # ์ฌ๊ธฐ์ ์์ธ๋ฅผ ๋ฐ์์ํค๋ฉด ServerErrorMiddleware์์ ํฌ์ฐฉํ ์ ์์ต๋๋ค. raise HTTPException(status_code=404) else: # Starlette์ด ์๋ ํธ์ถ์ ๊ฒฝ์ฐ ์ค๋ฅ ์ง์ ๋ฐํ response = PlainTextResponse("Not Found", status_code=404) await response(scope, receive, send) async def lifespan(self, scope: Scope, receive: Receive, send: Send) -> None: """ ์ ํ๋ฆฌ์ผ์ด์ ์์ ๋ฐ ์ข ๋ฃ ์ด๋ฒคํธ๋ฅผ ๊ด๋ฆฌํ ์ ์๋๋ก ํ๋ ASGI ๋ผ์ดํ์คํฌ ๋ฉ์์ง ์ฒ๋ฆฌ """ # ๋ผ์ดํ์คํฌ ์คํ ๋ก์ง. ์คํ ์ Starlette์ ASGI ์๋ฒ์ ํต์ ํฉ๋๋ค. ํ์ง๋ง ํ์ฌ ์ด ์ฝ๋์๋ ์์ง ๊ฐ๋ฐ๋์ง ์์ ์ผ๋ถ ๊ธฐ๋ฅ์ด ์์ ์ ์์ต๋๋ค. first = True app = scope.get("app") await receive() try: if inspect.isasyncgenfunction(self.lifespan_context): async for item in self.lifespan_context(app): assert first, "Lifespan context yielded multiple times." first = False await send({"type": "lifespan.startup.complete"}) await receive() else: for item in self.lifespan_context(app): # type: ignore assert first, "Lifespan context yielded multiple times." first = False await send({"type": "lifespan.startup.complete"}) await receive() except BaseException: if first: exc_text = traceback.format_exc() await send({"type": "lifespan.startup.failed", "message": exc_text}) raise else: await send({"type": "lifespan.shutdown.complete"}) async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: """ Router ํด๋์ค์ ์ฃผ์ ์ง์ ์ . """ # http, websocket, lifespan ์ ํ๋ง ์ง์ assert scope["type"] in ("http", "websocket", "lifespan") # scope์ ๋ผ์ฐํฐ ์ด๊ธฐํ if "router" not in scope: scope["router"] = self if scope["type"] == "lifespan": # ๋ผ์ดํ์คํฌ ๋ก์ง ์คํ await self.lifespan(scope, receive, send) return partial = None # ๋ผ์ฐํธ ๋งค์นญ ์ํ for route in self.routes: match, child_scope = route.matches(scope) if match == Match.FULL: # ์ ์ฒด ์ผ์น (URL ๋ฐ ๋ฉ์๋ ์ผ์น)์ธ ๊ฒฝ์ฐ # ๊ทธ๋ฐ ๋ค์ ์ผ๋ฐ ๋ผ์ฐํธ ์ฒ๋ฆฌ ์ํ scope.update(child_scope) await route.handle(scope, receive, send) return elif match == Match.PARTIAL and partial is None: # ๋ถ๋ถ ์ผ์น (URL ์ผ์น, ๋ฉ์๋ ๋ถ์ผ์น)์ธ ๊ฒฝ์ฐ # ํด๋น ๊ฐ์ ์ ์งํ๊ณ ๊ณ์ ์ผ์น์ํต๋๋ค. partial = route partial_scope = child_scope if partial is not None: # ๋ถ๋ถ ์ผ์น๊ฐ ์๋ ๋ผ์ฐํธ๊ฐ ์๋ ๊ฒฝ์ฐ ๋ํ ๊ณ์ ์คํํ์ง๋ง, ๋ผ์ฐํธ๋ HTTP ๋ฉ์๋ ์ค๋ฅ๋ฅผ ๋ฐํํฉ๋๋ค. scope.update(partial_scope) await partial.handle(scope, receive, send) return if scope["type"] == "http" and self.redirect_slashes and scope["path"]!= "/": # ์ผ์นํ์ง ์๋ ์ํฉ, ๋ฆฌ๋๋ ์ ํ๋จ redirect_scope = dict(scope) if scope["path"].endswith("/"): redirect_scope["path"] = redirect_scope["path"].rstrip("/") else: redirect_scope["path"] = redirect_scope["path"] + "/" for route in self.routes: match, child_scope = route.matches(redirect_scope) if match!= Match.NONE: # ๋ค์ ์ผ์น์ํต๋๋ค. ๊ฒฐ๊ณผ๊ฐ ๋น์ด ์์ง ์์ผ๋ฉด ๋ฆฌ๋๋ ์ ์๋ต ์ ์ก redirect_url = URL(scope=redirect_scope) response = RedirectResponse(url=str(redirect_url)) await response(scope, receive, send) return
# ์์ ์ด๋ ๊ณผ์ ๋ ํํธํ์ง ์์ผ๋ฉด ๋ผ์ฐํธ๋ฅผ ์ฐพ์ ์ ์๋ค๋ ๋ป์
๋๋ค. ์ด๋ ๊ธฐ๋ณธ ๋ผ์ฐํธ๊ฐ ์คํ๋๋ฉฐ, ๊ธฐ๋ณธ ๊ธฐ๋ณธ ๋ผ์ฐํธ๋ 404 ์ฐพ์ ์ ์์์
๋๋ค.
await self.default(scope, receive, send)
Router ์ฝ๋๋ ์๋นํ ๊ฐ๋จํฉ๋๋ค. ๋๋ถ๋ถ์ ์ฝ๋๋ `call` ๋ฉ์๋์ ์ง์ค๋์ด ์์ต๋๋ค. ๊ทธ๋ฌ๋ ๋ผ์ฐํธ๋ฅผ ์ฟผ๋ฆฌํ๊ธฐ ์ํด ์ฌ๋ฌ ๋ฒ ์ํํ๋ฉฐ, ๊ฐ ๋ผ์ฐํธ๋ ์ผ์น ์ฌ๋ถ๋ฅผ ํ๋จํ๊ธฐ ์ํด ์ ๊ท ํํ์์ ์คํํฉ๋๋ค. ์ด๋ค ์ฌ๋๋ค์ ์ด ์คํ ์๋๊ฐ ๋๋ฆฌ๋ค๊ณ ์๊ฐํ ์ ์์ต๋๋ค. ์ ๋ ๊ทธ๋ ๊ฒ ์๊ฐํ๊ณ , ๋ผ์ฐํธ ํธ๋ฆฌ๋ฅผ ๊ตฌํํ์ฌ ๋์ฒดํ์ต๋๋ค (์์ธํ ๋ด์ฉ์ `route_trie.py` ์ฐธ์กฐ). ํ์ง๋ง ์ฑ๋ฅ ํ
์คํธ ๊ฒฐ๊ณผ, ๋ผ์ฐํธ ์๊ฐ 50๊ฐ๋ฅผ ์ด๊ณผํ์ง ์์ผ๋ฉด ๋ฃจํ ๋งค์นญ ์ฑ๋ฅ์ด ๋ผ์ฐํธ ํธ๋ฆฌ๋ณด๋ค ์ข์ต๋๋ค. 100๊ฐ๋ฅผ ์ด๊ณผํ์ง ์์ผ๋ฉด ๋์ ๋๋ฑํฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ ์ผ๋ฐ์ ์ธ ์ํฉ์์ ์ง์ ํ๋ ๋ผ์ฐํธ ์๋ 100๊ฐ๋ฅผ ์ด๊ณผํ์ง ์์ต๋๋ค. ๋ฐ๋ผ์ ์ด ๋ผ์ฐํธ ์ผ์น ์ฑ๋ฅ์ ๋ํด ๊ฑฑ์ ํ ํ์๊ฐ ์์ต๋๋ค. ์ฌ์ ํ ๊ฑฑ์ ๋๋ค๋ฉด `Mount`๋ฅผ ์ฌ์ฉํ์ฌ ๋ผ์ฐํธ๋ฅผ ๊ทธ๋ฃนํํ์ฌ ์ผ์น ์๋ฅผ ์ค์ผ ์ ์์ต๋๋ค.
### 3.2. ๊ธฐํ ๋ผ์ฐํธ
`Mount`๋ `BaseRoute`์์ ์์ํ๋ฉฐ, `HostRoute`, `WebSocketRoute`์ ๊ฐ์ ๋ค๋ฅธ ๋ผ์ฐํธ๋ ๋ง์ฐฌ๊ฐ์ง์
๋๋ค. ์ ์ฌํ ๋ฉ์๋๋ฅผ ์ ๊ณตํ๋ฉฐ ๊ตฌํ์ ์ฝ๊ฐ์ ์ฐจ์ด๊ฐ ์์ต๋๋ค (์ฃผ๋ก ์ด๊ธฐํ, ๋ผ์ฐํธ ๋งค์นญ, ์ญ๋ฐฉํฅ ์กฐํ). ๋จผ์ `BaseRoute`๋ฅผ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
```python
class BaseRoute:
def matches(self, scope: Scope) -> typing.Tuple[Match, Scope]:
# ํ์ค ๋งค์นญ ํจ์ ์๊ทธ๋์ฒ. ๊ฐ Route๋ (Match, Scope) ํํ์ ๋ฐํํด์ผ ํฉ๋๋ค.
# Match์๋ 3๊ฐ์ง ์ ํ์ด ์์ต๋๋ค:
# NONE: ์ผ์น ์์.
# PARTIAL: ๋ถ๋ถ ์ผ์น (URL ์ผ์น, ๋ฉ์๋ ๋ถ์ผ์น).
# FULL: ์ ์ฒด ์ผ์น (URL ๋ฐ ๋ฉ์๋ ์ผ์น).
# Scope๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ค์๊ณผ ๊ฐ์ ํ์์ผ๋ก ๋ฐํ๋์ง๋ง Mount๋ ๋ ๋ง์ ๋ด์ฉ์ ๋ฐํํฉ๋๋ค:
# {"endpoint": self.endpoint, "path_params": path_params}
raise NotImplementedError() # pragma: no cover
def url_path_for(self, name: str, **path_params: str) -> URLPath:
# ์ด๋ฆ์ ๋ฐ๋ผ ์ญ๋ฐฉํฅ ์กฐํ ์์ฑ
raise NotImplementedError() # pragma: no cover
async def handle(self, scope: Scope, receive: Receive, send: Send) -> None:
# Router์ ์ํด ์ผ์น๋ ํ ํธ์ถ ๊ฐ๋ฅํ ํจ์
raise NotImplementedError() # pragma: no cover
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
"""
๋ผ์ฐํธ๋ ๋
๋ฆฝ์ ์ธ ASGI ์ฑ์ผ๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค.
์ด๊ฒ์ ๋ค์ ๊ธฐ์ดํ ๊ฒฝ์ฐ๋ก, ๊ฑฐ์ ํญ์ Router ๋ด์์ ์ฌ์ฉ๋์ง๋ง
์ผ๋ถ ๋๊ตฌ ๋ฐ ์ต์ ์ฑ์ ์ ์ฉํ ์ ์์ต๋๋ค.
"""
# ๋ผ์ฐํธ๊ฐ ๋
๋ฆฝ์ ์ธ ASGI APP๋ก ํธ์ถ๋ ๊ฒฝ์ฐ, ์์ฒด์ ์ผ๋ก ๋งค์นญํ๊ณ ์๋ตํฉ๋๋ค.
match, child_scope = self.matches(scope)
if match == Match.NONE:
if scope["type"] == "http":
response = PlainTextResponse("Not Found", status_code=404)
await response(scope, receive, send)
elif scope["type"] == "websocket":
websocket_close = WebSocketClose()
await websocket_close(scope, receive, send)
return
scope.update(child_scope)
await self.handle(scope, receive, send)
BaseRoute๋ ๋ง์ ๊ธฐ๋ฅ์ ์ ๊ณตํ์ง ์์ผ๋ฉฐ ๋ค๋ฅธ ๋ผ์ฐํธ๋ ์ด๋ฅผ ํ์ฅํ์ฌ ์ฌ์ฉํฉ๋๋ค.
- Route: ํ์ค HTTP ๋ผ์ฐํธ์ ๋๋ค. HTTP URL ๋ฐ HTTP ๋ฉ์๋๋ฅผ ํตํด ๋ผ์ฐํธ ๋งค์นญ์ ๋ด๋นํ๋ฉฐ, HTTP ๋ผ์ฐํธ๋ฅผ ํธ์ถํ๋ ๋ฉ์๋๋ฅผ ์ ๊ณตํฉ๋๋ค.
- WebSocketRoute: ํ์ค WebSocket ๋ผ์ฐํธ์
๋๋ค. HTTP URL์ ๋ฐ๋ผ ๋ผ์ฐํธ๋ฅผ ๋งค์นญํ๊ณ ,
starlette.websocket์WebSocket์ ํตํด ์ธ์ ์ ์์ฑํ์ฌ ํด๋น ํจ์์ ์ ๋ฌํฉ๋๋ค. - Mount: ๋ผ์ฐํธ๋ฅผ ์ค์ฒฉํ์ฌ ์บก์ํํฉ๋๋ค. ๋งค์นญ ๋ฐฉ์์ URL์ ์ ๋์ด ๋งค์นญ์ด๋ฉฐ, ๊ท์น์ ์ถฉ์กฑํ๋ ๋ค์ ๋ ๋ฒจ ASGI APP๋ก ์์ฒญ์ ์ ๋ฌํฉ๋๋ค. ๋ค์ ๋ ๋ฒจ ASGI APP๊ฐ Router์ธ ๊ฒฝ์ฐ, ํธ์ถ ์ฒด์ธ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค: Router->Mount->Router->Mount->Router->Route.
Mount๋ฅผ ์ฌ์ฉํ์ฌ ๋ผ์ฐํธ๋ฅผ ๊ทธ๋ฃนํํ๊ณ ๋งค์นญ ์๋๋ฅผ ๋์ผ ์ ์์ต๋๋ค. ๊ถ์ฅํฉ๋๋ค. ๋ํ ์์ฒญ์ ๋ค๋ฅธ ASGI APP (์: Starlette->ASGI Middleware->Mount->Other Starlette->...)๋ก ๋ถ๋ฐฐํ ์๋ ์์ต๋๋ค. - Host: ์ฌ์ฉ์ ์์ฒญ์
Host์ ๋ฐ๋ผ ํด๋น ASGI APP (๋ผ์ฐํธ, ๋ง์ดํธ, ๋ฏธ๋ค์จ์ด ๋ฑ)๋ก ์์ฒญ์ ๋ถ๋ฐฐํฉ๋๋ค.
4. ๊ธฐํ ๊ตฌ์ฑ ์์
์์์ ๋ณด์๋ฏ์ด Starlette์ ๋๋ถ๋ถ์ ๊ตฌ์ฑ ์์๋ ASGI APP๋ก ์ค๊ณ๋์ด ํธํ์ฑ์ด ๋งค์ฐ ๋์ต๋๋ค. ์ฑ๋ฅ์ด ์ฝ๊ฐ ํฌ์๋์ง๋ง ํธํ์ฑ์ ๋งค์ฐ ๋ฐ์ด๋ฉ๋๋ค. ๋ค๋ฅธ ๊ตฌ์ฑ ์์๋ ์ด๋ ์ ๋ ASGI APP์ ์ ์ฌํ๊ฒ ์ค๊ณ๋์์ต๋๋ค. ๋ค๋ฅธ ๊ตฌ์ฑ ์์๋ฅผ ์๊ฐํ๊ธฐ ์ ์ ๋จผ์ Starlette์ ์ ์ฒด ํ๋ก์ ํธ ๊ตฌ์กฐ๋ฅผ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
โโโ middleware # ๋ฏธ๋ค์จ์ด
โโโ applications.py # ์์ ์ ํ๋ฆฌ์ผ์ด์
โโโ authentication.py # ์ธ์ฆ ๊ด๋ จ
โโโ background.py # ๋ฐฑ๊ทธ๋ผ์ด๋ ์์
์บก์ํ, ์๋ต์ด ๋ฐํ๋ ํ ์คํ๋จ
โโโ concurrency.py # ์ผ๋ถ ์๊ท๋ชจ asyncio ๊ด๋ จ ์บก์ํ. ์ ๋ฒ์ ์์๋ anyio ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ง์ ์ฌ์ฉ ๋์ ์ฌ์ฉํฉ๋๋ค.
โโโ config.py # ๊ตฌ์ฑ
โโโ convertors.py # ์ผ๋ถ ์ ํ ๋ณํ ๋ฉ์๋
โโโ datastructures.py # Url, Header, Form, QueryParam, State ๋ฑ ์ผ๋ถ ๋ฐ์ดํฐ ๊ตฌ์กฐ
โโโ endpoints.py # cbv๋ฅผ ์ง์ํ๋ ๋ผ์ฐํธ ๋ฐ ์ฝ๊ฐ ๋ ๊ณ ๊ธ WebSocket ์บก์ํ
โโโ exceptions.py # ์์ธ ์ฒ๋ฆฌ
โโโ formparsers.py # Form, File ๋ฑ ํ์ฑ
โโโ graphql.py # GraphQL ๊ด๋ จ ์ฒ๋ฆฌ ๋ด๋น
โโโ __init__.py
โโโ py.typed # Starlette์ ํ์ํ ํ์
ํํธ
โโโ requests.py # ์์ฒญ, ์ฌ์ฉ์๊ฐ ๋ฐ์ดํฐ๋ฅผ ์ป๊ธฐ ์ํจ
โโโ responses.py # ์๋ต, ํค๋ ๋ฐ ์ฟ ํค ์ด๊ธฐํ, ๋ค์ํ ์๋ต ํด๋์ค์ ๋ฐ๋ผ ์๋ต ๋ฐ์ดํฐ๋ฅผ ์์ฑํ๊ณ , ๊ทธ ๋ค์ ํด๋์ค ASGI ํธ์ถ ์ธํฐํ์ด์ค๋ฅผ ๊ฐ์ง๋๋ค. ์ด ์ธํฐํ์ด์ค๋ Uvicorn ์๋น์ค์ ASGI ํ๋กํ ์ฝ์ ๋ณด๋
๋๋ค. ๋ณด๋ธ ํ ๋ฐฑ๊ทธ๋ผ์ด๋ ์์
์ด ์์ผ๋ฉด ์๋ฃ๋ ๋๊น์ง ์คํ๋ฉ๋๋ค.
โโโ routing.py # ๋ผ์ฐํ
โโโ schemas.py # OpenApi ๊ด๋ จ ์คํค๋ง
โโโ staticfiles.py # ์ ์ ํ์ผ
โโโ status.py # HTTP ์ํ ์ฝ๋
โโโ templating.py # Jinja ๊ธฐ๋ฐ ํ
ํ๋ฆฟ ์๋ต
โโโ testclient.py # ํ
์คํธ ํด๋ผ์ด์ธํธ
โโโ types.py # ํ์
โโโ websockets.py # WebSocket
์์ ๋ง์ ํ์ผ์ด ์์ผ๋ฉฐ, ๋ช ๊ฐ์ง ๊ฐ๋จํ ํ์ผ์ ๊ฑด๋๋๋๋ค.
4.1. Request
Request ํด๋์ค๋ ๋งค์ฐ ๊ฐ๋จํฉ๋๋ค. HttpConnection์์ ์์ํฉ๋๋ค. ์ด ํด๋์ค๋ ์ฃผ๋ก ASGI ํ๋กํ ์ฝ์์ ์ ๊ณต๋๋ Scope๋ฅผ ํ์ฑํ์ฌ URL ๋ฐ ๋ฉ์๋์ ๊ฐ์ ์ ๋ณด๋ฅผ ์ถ์ถํฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ Request ํด๋์ค๋ ์์ฒญ ๋ฐ์ดํฐ๋ฅผ ์ฝ๊ณ ๋ฐ์ดํฐ๋ฅผ ๋ฐํํ๋ ๊ธฐ๋ฅ (HTTP 1.1์ ์๋ฒ๊ฐ ๊ณ ๊ฐ์๊ฒ ๋ฐ์ดํฐ๋ฅผ ํธ์ํ๋ ๊ฒ์ ์ง์)์ ์ถ๊ฐํฉ๋๋ค. ๊ทธ์ค ๋ฐ์ดํฐ๋ฅผ ์ฝ๋ ๊ฒ์ ํต์ฌ ํจ์์ธ stream์ ์์กดํฉ๋๋ค. ์์ค ์ฝ๋๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
async def stream(self) -> typing.AsyncGenerator[bytes, None]: # ์ด๋ฏธ ์ฝ์์ผ๋ฉด ์บ์์์ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ if hasattr(self, "_body"): yield self._body yield b"" return if self._stream_consumed: raise RuntimeError("Stream consumed") self._stream_consumed = True while True: # ASGI ์ปจํ ์ด๋์ receive ๋ฃจํ์์ ์ง์์ ์ผ๋ก ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ message = await self._receive() if message["type"] == "http.request": body = message.get("body", b"") if body: # ๋น์ด ์์ง ์์ผ๋ฉด ๋ฐ์ดํฐ ๋ฐํ yield body if not message.get("more_body", False): # ๋ชจ๋ body ๋ฐ์ดํฐ๊ฐ ์ป์ด์ก์์ ์๋ฏธ break elif message["type"] == "http.disconnect": # ํด๋ผ์ด์ธํธ์์ ์ฐ๊ฒฐ์ด ๋ซํ์์ ์๋ฏธ self._is_disconnected = true # ์์ธ ๋ฐ์. `await request.body()` ๋๋ `await request.json()`์ ํธ์ถํ๋ ์ฌ์ฉ์๋ ์์ธ๋ฅผ ๋ฐ์์ํต๋๋ค. raise ClientDisconnect() # ๋์ ํ์ํ๊ธฐ ์ํด ๋น ๋ฐ์ดํธ ๋ฐํ yield b""
์ด ๊ตฌํ์ ๊ฐ๋จํ์ง๋ง ์์ ๋ฒ๊ทธ๊ฐ ์์ต๋๋ค. Nginx ๋๋ ๋ค๋ฅธ ์น ์๋น์ค์ ์ต์ํ ์ฌ๋๋ค์ ์ผ๋ฐ์ ์ธ ์ค๊ฐ ์๋ฒ๋ ๋ณธ๋ฌธ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ์ง ์๊ณ ์ ๋ฌํ๊ธฐ๋ง ํ๋ค๋ ๊ฒ์ ์๊ณ ์์ต๋๋ค. ASGI๋ ๋ง์ฐฌ๊ฐ์ง์
๋๋ค. URL๊ณผ ํค๋๋ฅผ ์ฒ๋ฆฌํ ํ Uvicorn์ ASGI APP๋ฅผ ํธ์ถํ๊ณ send ๋ฐ receive ๊ฐ์ฒด๋ฅผ ์๋๋ก ์ ๋ฌํฉ๋๋ค. ์ด ๋ ๊ฐ์ฒด๋ ์ฌ๋ฌ ASGI APP๋ฅผ ๊ฑฐ์ณ ์ฌ์ฉ์๊ฐ ํจ์์์ ์ฌ์ฉํ ์ ์๋ ๋ผ์ฐํธ ASGI APP์ ๋๋ฌํฉ๋๋ค. ๋ฐ๋ผ์ Request์์ ์์ ๋ receive ๊ฐ์ฒด๋ Uvicorn์ ์ํด ์ด๊ธฐํ๋ฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ receive์ ๋ฐ์ดํฐ ์์ค๋ asyncio.Queue ํ์์ ์ต๋๋ค. ๋ฏธ๋ค์จ์ด ๋ถ์์์ ๊ฐ ASGI APP๋ scope์ receive๋ฅผ ๊ธฐ๋ฐ์ผ๋ก Request ๊ฐ์ฒด๋ฅผ ์์ฑํฉ๋๋ค. ์ด๋ ๊ฐ ๊ณ์ธต ASGI APP์ Request ๊ฐ์ฒด๊ฐ ์ผ์นํ์ง ์์์ ์๋ฏธํฉ๋๋ค. ๋ฏธ๋ค์จ์ด๊ฐ Request ๊ฐ์ฒด๋ฅผ ํธ์ถํ์ฌ ๋ณธ๋ฌธ์ ์ฝ์ผ๋ฉด receive๋ฅผ ํตํด ํ์ ๋ฐ์ดํฐ๋ฅผ ๋ฏธ๋ฆฌ ์๋นํ์ฌ ํ์ ASGI APP๊ฐ Request ๊ฐ์ฒด๋ฅผ ํตํด ๋ณธ๋ฌธ ๋ฐ์ดํฐ๋ฅผ ์ฝ์ ์ ์๊ฒ ๋ฉ๋๋ค. ์ด ๋ฌธ์ ์ ๋ํ ์ํ ์ฝ๋๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
import asyncio from starlette.applications import Starlette from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint from starlette.requests import Request from starlette.responses import JSONResponse, Response app: Starlette = Starlette() class DemoMiddleware(BaseHTTPMiddleware): async def dispatch( self, request: Request, call_next: RequestResponseEndpoint, ) -> Response: print(request, await request.body()) return await call_next(request) app.add_middleware(DemoMiddleware) @app.route("/") async def demo(request: Request) -> JSONResponse: try: await asyncio.wait_for(request.body(), 1) return JSONResponse({"result": True}) except asyncio.TimeoutError: return JSONResponse({"result": False}) if __name__ == "__main__": import uvicorn # type: ignore uvicorn.run(app)
์คํํ๊ณ ์์ฒญ ๊ฒฐ๊ณผ๋ฅผ ํ์ธํฉ๋๋ค.
-> curl http://127.0.0.1:8000
{"result":false}
์์์ ๋ณด๋ฏ์ด ๊ฒฐ๊ณผ๋ false์ด๋ฉฐ, ์ด๋ request.body ์คํ์ด receive ํ๊ฐ ์ด๋ฏธ ๋น์ด ์๊ณ ๋ฐ์ดํฐ๋ฅผ ๊ฒ์ํ ์ ์๊ธฐ ๋๋ฌธ์ ์๊ฐ ์ด๊ณผ๋์์์ ์๋ฏธํฉ๋๋ค. ์๊ฐ ์ด๊ณผ๊ฐ ์์ผ๋ฉด ์ด ์์ฒญ์ ๋ฉ์ถ ๊ฒ์
๋๋ค.
์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋จผ์ Request๊ฐ ๋ณธ๋ฌธ์ ์ป๋ ๋ฐฉ๋ฒ์ ์ดํด๋ด
๋๋ค. ์ฌ์ฉ์๊ฐ ๋ณธ๋ฌธ์ ์ฌ๋ฌ ๋ฒ ์ป์ ์ ์๊ณ ๋ฐ์ดํฐ๊ฐ ๋์ผํ๊ธฐ ๋๋ฌธ์ ๊ตฌํ ์์ด๋์ด๋ ๋ฐ์ดํฐ๋ฅผ ๊ฒ์ํ ํ ์บ์ฑํ๋ ๊ฒ์
๋๋ค. ์ด ์์ด๋์ด๋ฅผ ๋ฐ๋ผ๊ฐ๋๋ค. ๋ฐ์ดํฐ๋ฅผ receive๋ฅผ ํตํด ๊ฒ์ํ๋ฏ๋ก, ๋ฐ์ดํฐ๋ฅผ ์ฝ์ ํ receive ํจ์๋ฅผ ๊ตฌ์ฑํ ์ ์์ต๋๋ค. ์ด ํจ์๋ ASGI ํต์ ํ๋กํ ์ฝ๊ณผ ์ ์ฌํ ๋ฐ์ดํฐ๋ฅผ ๋ฐํํ๊ณ ์์ ํ ๋ณธ๋ฌธ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ง๊ณ ์์ต๋๋ค (Request.stream ๊ตฌ์ฑ์ ์ถฉ์กฑ).
async def proxy_get_body(request: Request) -> bytes: async def receive() -> Message: return {"type": "http.request", "body": body} body = await request.body() request._receive = receive return body
์ดํ, ASGI APP์ ๋ชจ๋ ๊ณ์ธต์์ ๋ณธ๋ฌธ ๋ฐ์ดํฐ๋ฅผ ์ป์ด์ผ ํ๋ ๊ฒฝ์ฐ ์ด ํจ์๋ฅผ ํธ์ถํ์ฌ ํ์ ASGI APP๊ฐ ๋ณธ๋ฌธ ๋ฐ์ดํฐ๋ฅผ ์ป๋ ๋ฅ๋ ฅ์ ์ํฅ์ ๋ฏธ์น์ง ์๊ณ ๋ณธ๋ฌธ ๋ฐ์ดํฐ๋ฅผ ์ป์ ์ ์์ต๋๋ค.
5. ์์ฝ
์ง๊ธ๊น์ง Starlette์ ์ฌ๋ฌ ์ค์ํ ๊ธฐ๋ฅ ์ฝ๋๋ฅผ ๋ถ์ํ์ต๋๋ค. Starlette์ ํ๋ฅญํ ๋์์ธ ์ปจ์ ์ ์ง๋ ๋ฐ์ด๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋๋ค. ์์ผ๋ก ์์ ๋ง์ ํ๋ ์์ํฌ๋ฅผ ์์ฑํ๋ ๋ฐ ๋์์ด ๋ ๊ฒ์ด๋ฏ๋ก Starlette ์์ค ์ฝ๋๋ฅผ ์ง์ ์ฝ์ด๋ณด์๋ ๊ฒ์ ์ถ์ฒํฉ๋๋ค.
Leapcell: ์น ํธ์คํ , ๋น๋๊ธฐ ํ์คํฌ ๋ฐ Redis๋ฅผ ์ํ ์ฐจ์ธ๋ ์๋ฒ๋ฆฌ์ค ํ๋ซํผ
๋ง์ง๋ง์ผ๋ก FastAPI๋ฅผ ๋ฐฐํฌํ๋ ๋ฐ ๊ฐ์ฅ ์ ํฉํ ํ๋ซํผ์ธ Leapcell์ ๊ณต์ ํ๊ณ ์ถ์ต๋๋ค.
1. ๋ค๊ตญ์ด ์ง์
- JavaScript, Python, Go ๋๋ Rust๋ก ๊ฐ๋ฐํ์ธ์.
2. ๋ฌด๋ฃ๋ก ๋ฌด์ ํ ํ๋ก์ ํธ ๋ฐฐํฌ
- ์ฌ์ฉํ ๋งํผ๋ง ์ง๋ถํ์ธ์. ์์ฒญ ์, ์๊ธ ์์.
3. ๋น๊ตํ ์ ์๋ ๋น์ฉ ํจ์จ์ฑ
- ์ฌ์ฉํ ๋งํผ ์ง๋ถํ๋ฉฐ ์ ํด ์๊ธ ์์.
- ์: $25๋ก 6.94M๊ฐ์ ์์ฒญ์ 60ms ํ๊ท ์๋ต ์๊ฐ ์ง์.
4. ๊ฐ์ํ๋ ๊ฐ๋ฐ์ ๊ฒฝํ
- ์ง๊ด์ ์ธ UI๋ก ์ฌ์ด ์ค์ .
- ์์ ์๋ํ๋ CI/CD ํ์ดํ๋ผ์ธ ๋ฐ GitOps ํตํฉ.
- ์คํ ๊ฐ๋ฅํ ์ธ์ฌ์ดํธ๋ฅผ ์ํ ์ค์๊ฐ ๋ฉํธ๋ฆญ ๋ฐ ๋ก๊น .
5. ์ฌ์ด ํ์ฅ์ฑ ๋ฐ ๊ณ ์ฑ๋ฅ
- ์๋ ํ์ฅ์ ํตํด ๋์ ๋์์ฑ์ ์ฝ๊ฒ ์ฒ๋ฆฌ.
- ์ ๋ก ์ด์ ์ค๋ฒํค๋ - ๋น๋์๋ง ์ง์คํ์ธ์.
๋ฌธ์์์ ๋ ์์ธํ ์์๋ณด์ธ์!
Leapcell ํธ์ํฐ: https://x.com/LeapcellHQ



