TL;DR
- ํ์ ํํธยท๋น๋๊ธฐยท์๋ ๋ฌธ์ํ๋ฅผ ์ง์ํ๋ Python ์น ํ๋ ์์ํฌ
- ASGI, Starlette, Uvicorn, Gunicorn์ ์ญํ ๊ณผ ๊ด๊ณ ์ ๋ฆฌ
- Path/Query Parameter์ Pydantic ๊ธฐ๋ฐ CRUD ๊ตฌํ ์์ ์ ๊ณต
FastAPI๋?
FastAPI๋ Python์ผ๋ก ๋ง๋ ์ต์ ์น ํ๋ ์์ํฌ์ด๋ค
๋น ๋ฅธ ์๋, ํ์
ํํธ ์ง์, ์๋ ๋ฌธ์ํ, ๋น๋๊ธฐ(Async) ์ง์ ๋ฑ์ ๊ฐ์ ์ด ์๋ค
RESTful API, ์น์์ผ ๋ฑ ๋ค์ํ ๋ฐฑ์๋ ๊ธฐ๋ฅ์ ์ฝ๊ฒ ๊ฐ๋ฐํ ์ ์๊ฒ ํด์ค๋ค
FastAPI์ ๊ด๋ จ ์ฃผ์ ์ฉ์ด
ASGI (Asynchronous Server Gateway Interface)
- Python์์ ๋น๋๊ธฐ ์น ์๋น์ค๋ฅผ ๋ง๋ค๊ธฐ ์ํ ์ธํฐํ์ด์ค (ํ๋กํ ์ฝ, ํ์ค)
- WSGI์ ๋น๋๊ธฐ ํ์ฅํ์ผ๋ก, ๋๊ธฐ(Sync)์ ๋น๋๊ธฐ(Async) ๋ชจ๋ ์ง์
- FastAPI, Starlette, Django(3.x ์ดํ) ๋ฑ์ด ASGI๋ฅผ ์ง์
- CGI โ WSGI โ ASGI๋ก ์ด์ด์ง๋ ๋ฐ์ ๊ณผ์ ์ ์น ์๋ฒ ์ํคํ ์ฒ์ ์งํ ์ฐธ๊ณ
Starlette
- Starlette๋ ASGI ๊ธฐ๋ฐ์ ์ด๊ฒฝ๋ ์น ํ๋ ์์ํฌ
- FastAPI๋ Starlette ์์ ๋ง๋ค์ด์ก์ผ๋ฉฐ, Starlette๊ฐ FastAPI์ ๋ผ๋ ์ญํ
- ๋ผ์ฐํ , ๋ฏธ๋ค์จ์ด, ์์ฒญ/์๋ต ์ฒ๋ฆฌ ๋ฑ ์น ํ๋ ์์ํฌ์ ํต์ฌ ๊ธฐ๋ฅ์ ์ ๊ณต
Uvicorn
- Uvicorn์ ASGI ์๋ฒ๋ก, FastAPI (๋๋ Starlette) ์ฑ์ ์คํ์์ผ์ฃผ๋ ์๋ฒ
- ๋น๋๊ธฐ (Async) ์ง์, ๋น ๋ฅด๊ณ ๊ฐ๋ฒผ์
- Flask์์์ WSGI ์๋ฒ (์: gunicorn, uwsgi ๋ฑ)์ ๋น์ทํ ์ญํ
Gunicorn
- Gunicorn์ WSGI ์๋ฒ๋ก ๋๋ฆฌ ์ฌ์ฉ๋์ด์์ผ๋, ์ต๊ทผ์๋ ASGI ์ ํ๋ฆฌ์ผ์ด์ ๋ ์ง์
- Uvicorn ์์ปค(worker)์ ์กฐํฉํด์ ์ด์ ํ๊ฒฝ์์ ๋ง์ด ์ฌ์ฉ
- Gunicorn์ ๋ฉํฐ ํ๋ก์ธ์ค ๋ฐฉ์์ ์ง์ํ์ฌ, ์ฌ๋ฌ Uvicorn ์ธ์คํด์ค๋ฅผ ๊ด๋ฆฌ
FastAPI ์ ์ฒด ๊ตฌ์กฐ
flowchart TD Client(["ํด๋ผ์ด์ธํธ<br/>๋ธ๋ผ์ฐ์ ๋๋ ์ฑ"]):::entry Uvicorn["Uvicorn<br/>ASGI ์๋ฒ"]:::runtime Starlette["Starlette<br/>ASGI ํ๋ ์์ํฌ"]:::runtime FastAPI[FastAPI<br/>API ๋น์ฆ๋์ค ๋ก์ง]:::app Gunicorn["Gunicorn<br/>Uvicorn worker ๊ด๋ฆฌ"]:::ops Client -->|ASGI| Uvicorn Uvicorn --> Starlette Starlette --> FastAPI Gunicorn -. ์ด์ ํ๊ฒฝ .-> Uvicorn
์ด์ ํ๊ฒฝ์์๋ Gunicorn์ด ์ฌ๋ฌ ๊ฐ์ Uvicorn ํ๋ก์ธ์ค๋ฅผ ๊ด๋ฆฌํ๋ ์ญํ ๋ก ์ถ๊ฐ๋ ์ ์์
์์ฝ ํ
| ์ฉ์ด | ์ค๋ช | FastAPI์์ ๊ด๊ณ |
|---|---|---|
| ASGI | Python ๋น๋๊ธฐ ์๋ฒ ์ธํฐํ์ด์ค | FastAPI์ ๋์ ๊ธฐ๋ฐ ํ๋กํ ์ฝ |
| Starlette | ASGI ๊ธฐ๋ฐ ๊ฒฝ๋ ์น ํ๋ ์์ํฌ | FastAPI์ ๊ธฐ๋ฐ ๋ผ๋ |
| Uvicorn | ASGI ์๋ฒ, FastAPI ์คํ ์๋ฒ | FastAPI๋ฅผ ์คํ์์ผ์ฃผ๋ ์๋ฒ |
| Gunicorn | ๋ฉํฐ ํ๋ก์ธ์ค WSGI/ASGI ์๋ฒ | Uvicorn๊ณผ ์กฐํฉํ์ฌ ์ด์ |
| FastAPI | Starlette ๊ธฐ๋ฐ ์ต์ API ํ๋ ์์ํฌ | ์ค์ API ์์ฑ ์์น |
์์ฒญ์ ์ด๋ป๊ฒ ๋์์ ์ฒ๋ฆฌ๋๋ โ event loop์ coroutine
FastAPI๊ฐ โ๋น ๋ฅด๋คโ๊ณ ํ ๋ ํต์ฌ์ ์์ฒญ์ ์ฒ๋ฆฌํ๋ ๋์์ฑ ๋ชจ๋ธ์ ์๋ค. ํํ ์คํด๊ฐ โ์์ฒญ ํ๋๋น ํ๋ก์ธ์ค(๋๋ ์ค๋ ๋) ํ๋โ์ธ๋ฐ, ASGI ๊ธฐ๋ฐ FastAPI๋ ๊ทธ๋ ์ง ์๋ค.
์์ฒญ = ํ๋ก์ธ์ค๊ฐ ์๋๋ผ event loop ์์ coroutine
- Gunicorn/Uvicorn์ด ๋์ฐ๋ worker ํ๋ก์ธ์ค ์๋ ๊ณ ์ ์ด๋ค(๋ณดํต
CPU ์ฝ์ด ร 2 + 1). ์์ฒญ ์์ ๋ฌด๊ดํ๋ค. - ๊ฐ worker ์์๋ event loop ํ๋(๋จ์ผ ์ค๋ ๋)๊ฐ ์๊ณ , ๊ทธ ํ๋๊ฐ ์๋ง์ ์์ฒญ์ ๋์์ ๊ตด๋ฆฐ๋ค.
- ๋ค์ด์จ ์์ฒญ์ worker๋ค์ ๋ถ์ฐ๋๊ณ , ํ worker ์์์ ๊ฐ ์์ฒญ์ coroutine ํ๋๋ก event loop์ ์นํ ๋๋ค.
์ฆ ๊ตฌ์กฐ๋ ํ๋ก์ธ์ค ๋ช ๊ฐ ร event loop 1๊ฐ ร ์์ฒญ ์๋ฐฑ ๊ฐ๋ค. ์์ฒญ๋ง๋ค ํ๋ก์ธ์ค๋ฅผ ๋ง๋๋ ๊ฒ ์๋๋ผ ์์์ ํ๋ก์ธ์ค๊ฐ ๋ค์ ์์ฒญ์ ๋ค์คํํ๋ค. ๋๊ธฐ ํ๋ ์์ํฌ๊ฐ worker ํ๋๋ก ํ ๋ฒ์ ์์ฒญ ํ๋์ฉ ์ฒ๋ฆฌํ๋ ๊ฒ๊ณผ ๋๋น๋๋ค.
async def์ def๋ ์ฒ๋ฆฌ ๊ฒฝ๋ก๊ฐ ๋ค๋ฅด๋ค
async def์๋ํฌ์ธํธ โ event loop ์์์ coroutine์ผ๋ก ์ง์ ์คํ.- ๋๊ธฐ
def์๋ํฌ์ธํธ โ loop๋ฅผ ๋ง์ง ์์ผ๋ ค๊ณ FastAPI๊ฐ ๋ณ๋ threadpool๋ก ๋ฐ์ด๋ธ๋ค(run_in_threadpool).
๊ทธ๋์ ์๋ํฌ์ธํธ๋ถํฐ ๊ทธ ์์์ ๋ถ๋ฅด๋ I/O(DBยท์ธ๋ถ API)๊น์ง ์ ๋ถ async def๋ก ์ด์ด์ผ ์์ฒญ์ด ๋๊น์ง coroutine์ผ๋ก ๊ตด๋ฌ๊ฐ๋ค.
์ async๊ฐ ์ค์ํ๊ฐ โ I/O ๋๊ธฐ๋ฅผ ๊ฒน์น๋ค
FastAPI๋ก ๋ง๋๋ ์๋น์ค๋ ๋๊ฐ I/O-bound๋ค โ DB ์ฟผ๋ฆฌ, ์ธ๋ถ API ํธ์ถ, ์คํธ๋ฆฌ๋ฐ ์๋ต์์ ๋๋ถ๋ถ์ ์๊ฐ์ ๊ธฐ๋ค๋ฆฐ๋ค.
- ํ ์์ฒญ์ด DB ์๋ต์
await๋ก ๊ธฐ๋ค๋ฆฌ๋ ๋์ ๊ฐ์ event loop๊ฐ ๋ค๋ฅธ ์์ฒญ์ ์งํํ๋ค. ๋๊ธฐ๊ฐ ๊ฒน์ณ worker ํ๋๊ฐ ๋ง์ ์ฌ์ฉ์๋ฅผ ๊ฐ๋นํ๋ค. - ๊ฒฐ์ ์ ํจ์ โ ๊ฒฝ๋ก ์ด๋๊ฐ์ ๋๊ธฐ blocking ํธ์ถ(๋๊ธฐ DB ๋๋ผ์ด๋ฒ, ๋ฌด๊ฑฐ์ด CPU ์ฐ์ฐ)์ด ํ๋๋ผ๋ ๋ผ๋ฉด ๊ทธ ์๊ฐ event loop ์ ์ฒด๊ฐ ๋ฉ์ถฐ ๊ทธ worker์ ๋ค๋ฅธ ๋ชจ๋ ์์ฒญ์ด ์ ์งํ๋ค. ๊ทธ๋์ DB ๋๋ผ์ด๋ฒยทํด๋ผ์ด์ธํธ๋ฅผ async(
asyncpg๋ฑ)๋ก ๋ง์ถ๋ค. - async๋ ๊ฐ๋ณ ์์ฒญ์ ๋น ๋ฅด๊ฒ ๋ง๋ค์ง ์๋๋ค. ์ฟผ๋ฆฌ ํ๋์ ์ง์ฐ์ ๊ทธ๋๋ก๊ณ , ์ด๋์ ์ฌ๋ฌ ์์ฒญ์ ํ loop๋ก ๊ฒน์ณ ๋ฐ๋ ์๋ฒ ์ ์ฒด ์ฒ๋ฆฌ๋์์ ๋์จ๋ค. ๋ค์ค ์ฌ์ฉ์ยทI/O-bound ์๋น์ค์ผ์๋ก ํจ๊ณผ๊ฐ ํฌ๋ค.
์คํธ๋ฆฌ๋ฐ(SSEยทWebSocket)์์ ํนํ ํฌ๋ค. ์ค๋ ์ด๋ ค ์๋ ์คํธ๋ฆผ์ด async์์ ์ ๋ coroutine ํ๋๋ผ ๊ฑฐ์ ๊ณต์ง์ง๋ง, ๋๊ธฐ + ์ฐ๊ฒฐ๋น ์ค๋ ๋์๋ค๋ฉด ์ด๋ฆฐ ์คํธ๋ฆผ๋ง๋ค ์ค๋ ๋๋ฅผ ์ก์ ๋์ ์ ์์๊ฐ ์กฐ๊ธ๋ง ๋์ด๋ ๊ณ ๊ฐ๋๋ค.
๋ฐ๋๋ก ์์ฒญ ์ฒ๋ฆฌ๊ฐ CPU-bound(๋ฌด๊ฑฐ์ด ๊ณ์ฐ)๋ผ๋ฉด ๋จ์ผ event loop๋ก๋ ๋ณ๋ ฌ์ด ์ ๋๋ฏ๋ก, worker ํ๋ก์ธ์ค๋ฅผ ๋๋ฆฌ๊ฑฐ๋ ๊ณ์ฐ์ ๋ณ๋ process๋ก ๋บ๋ค.
FastAPI Tutorial
ํจํค์ง ์ค์น
pip install "fastapi[all]"
๊ฐ๋จํ API ๋ง๋ค๊ธฐ
from fastapi import FastAPI
# Create a FastAPI instance
app = FastAPI()
# path '/' ๋ก ๊ฐ์ GET operation ์คํํ์ ๋, ํธ์ถ๋ ํ์ด์ฌ ํจ์
@app.get("/")
def read_root():
return {"Hello": "World"}์คํ
๋ชจ๋๋ช
:๋ณ์๋ช
ํ์์ uvicorn/gunicorn CLI๊ฐ ์ฌ์ฉํ๋ ๋ฌธ๋ฒ์ด๋ค. main:app์ด๋ฉด main.py ์์ app ๋ณ์๋ฅผ ๊ฐ๋ฆฌํจ๋ค. ์๋ธ ๋๋ ํ ๋ฆฌ์ ์์ผ๋ฉด app.main:app์ฒ๋ผ ์ ์ผ๋ก ๊ตฌ๋ถํ๋ค.
# ๊ฐ๋ฐ โ ์ฑ๊ธ ํ๋ก์ธ์ค, ์ฝ๋ ๋ณ๊ฒฝ ์ ์๋ ์ฌ์์
uvicorn main:app --reload
python -m fastapi dev app/main.py# ์ด์(๋ฐฐํฌ) โ Gunicorn์ด ๋ฉํฐ ํ๋ก์ธ์ค๋ฅผ ๊ด๋ฆฌํ๊ณ , ๊ฐ ์์ปค์์ Uvicorn์ด ASGI๋ฅผ ์ฒ๋ฆฌ
gunicorn -k uvicorn.workers.UvicornWorker main:app| ์ต์ | ์๋ฏธ |
|---|---|
-k (--worker-class) | ์์ปค ํด๋์ค ์ง์ . ๊ธฐ๋ณธ๊ฐ์ sync(WSGI)์ด๋ฏ๋ก ASGI ์ฑ์ ๋ฐ๋์ ์ง์ ํ์ |
-w 4 | ์์ปค ์. ๋ณดํต CPU ์ฝ์ด ร 2 + 1 |
-b 0.0.0.0:8000 | ๋ฐ์ธ๋ ์ฃผ์ |
Gunicorn (ํ๋ก์ธ์ค ๋งค๋์ )
โโโ Uvicorn Worker 1 โ ASGI ์ฒ๋ฆฌ
โโโ Uvicorn Worker 2 โ ASGI ์ฒ๋ฆฌ
โโโ Uvicorn Worker 3 โ ASGI ์ฒ๋ฆฌ
Uvicorn ๋จ๋ ์ผ๋ก๋ ์ฑ๊ธ ํ๋ก์ธ์ค๋ผ CPU ์ฝ์ด๋ฅผ ๋ค ํ์ฉํ์ง ๋ชปํ๋ค. Gunicorn์ด ์ฌ๋ฌ Uvicorn ์์ปค๋ฅผ ๋์์ ๊ด๋ฆฌํ๋ ๊ตฌ์กฐ๋ค.
Path Parameter
from fastapi import FastAPI
# Create a FastAPI instance
app = FastAPI()
# item_id๊ฐ Path Parameter. function์ argument๋ก ์ ๋ฌ
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id}Query Parameter
from fastapi import FastAPI
# Create a FastAPI instance
app = FastAPI()
fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]
# function parameter์ ํฌํจ๋์ง๋ง, Path Operation์ ํฌํจ X
# http://localhost:8000/items/?skip=0&limit=10 ์ผ๋ก ์ ๊ทผ
@app.get("/items/")
def read_item(skip: int = 0, limit: int = 10):
return fake_items_db[skip : skip + limit]Multiple Path and Query Parameters
# multi_param.py
from typing import Union
from fastapi import FastAPI
# Create a FastAPI instance
app = FastAPI()
# Path Parameter + Query Parameter
@app.get("/users/{user_id}/items/{item_id}")
def read_user_item(user_id: int, item_id: str, q: Union[str, None] = None, short: bool = False):
item = {"item_id": item_id, "owner_id": user_id}
if q:
item.update({"q": q})
if not short:
item.update(
{"description": "This is an amazing item that has a long description"},
)
return itemFastAPI CRUD
- CREATE / READ / UPDATE / DELETE ๊ธฐ๋ฅ์ Path Parameter, Query Parameter, Pydantic์ ํ์ฉํด ๊ตฌํ ๊ฐ๋ฅ
Path Parameter CRUD
- CREATE :
POST /users/name/{name}/nickname/{nickname} - READ :
GET /users/name/{name} - UPDATE :
PUT /users/name/{name}/nickname/{nickname} - DELETE :
DELETE /users/name/{name}
from fastapi import FastAPI, HTTPException
# Create a FastAPI instance
app = FastAPI()
# User database
USER_DB = {}
# Fail response
NAME_NOT_FOUND = HTTPException(status_code=400, detail="Name not found.")
@app.post("/users/name/{name}/nickname/{nickname}")
def create_user(name: str, nickname: str):
USER_DB[name] = nickname
return {"status": "success"}
@app.get("/users/name/{name}")
def read_user(name: str):
if name not in USER_DB:
raise NAME_NOT_FOUND
return {"nickname": USER_DB[name]}
@app.put("/users/name/{name}/nickname/{nickname}")
def update_user(name: str, nickname: str):
if name not in USER_DB:
raise NAME_NOT_FOUND
USER_DB[name] = nickname
return {"status": "success"}
@app.delete("/users/name/{name}")
def delete_user(name: str):
if name not in USER_DB:
raise NAME_NOT_FOUND
del USER_DB[name]
return {"status": "success"}Query Parameter CRUD
- CREATE :
POST /users?name=hello&nickname=world - READ :
GET /users?name=hello - UPDATE :
PUT /users?name=hello&nickname=world2 - DELETE :
DELETE /users?name=hello
from fastapi import FastAPI, HTTPException
# Create a FastAPI instance
app = FastAPI()
# User database
USER_DB = {}
# Fail response
NAME_NOT_FOUND = HTTPException(status_code=400, detail="Name not found.")
@app.post("/users")
def create_user(name: str, nickname: str):
USER_DB[name] = nickname
return {"status": "success"}
@app.get("/users")
def read_user(name: str):
if name not in USER_DB:
raise NAME_NOT_FOUND
return {"nickname": USER_DB[name]}
@app.put("/users")
def update_user(name: str, nickname: str):
if name not in USER_DB:
raise NAME_NOT_FOUND
USER_DB[name] = nickname
return {"status": "success"}
@app.delete("/users")
def delete_user(name: str):
if name not in USER_DB:
raise NAME_NOT_FOUND
del USER_DB[name]
return {"status": "success"}Pydantic CRUD
- ๊ธฐ๋ฅ
- ๋ฐ์ดํฐ ๊ฒ์ฆ ๋ฐ ํ์
์์ ์ฑ (BaseModel)
- CreateIn : ์ ๋ ฅ๋ฐ๋ ๋ฐ์ดํฐ ํํ ์ง์
- CreateOut : ๋ฐํํ๊ณ ์ ํ๋ ๋ฐ์ดํฐ ํํ ์ง์
- ์ฝ๋ ๊ฐ๋ ์ฑ ๋ฐ ์ ์ง๋ณด์์ฑ ํฅ์
- ๋ณด์ ๋ฐ ์ ๋ณด ๋ ธ์ถ ์ต์ํ : Response Model ๋ณ๋ ์ง์ ํด์ ๋น๋ฐ๋ฒํธ์ ๊ฐ์ ๋ฏผ๊ฐํ ์ ๋ณด ์๋ต์์ ์ ์ธ ๊ฐ๋ฅ
- ์๋ ๋ณํ ๋ฐ ์ง๋ ฌํ (Python โ JSON)
- ๋ฐ์ดํฐ ๊ฒ์ฆ ๋ฐ ํ์
์์ ์ฑ (BaseModel)
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
class CreateIn(BaseModel):
name: str
nickname: str
class CreateOut(BaseModel):
status: str
id: int
# Create a FastAPI instance
app = FastAPI()
# User database
USER_DB = {}
# Fail response
NAME_NOT_FOUND = HTTPException(status_code=400, detail="Name not found.")
@app.post("/users", response_model=CreateOut)
def create_user(user: CreateIn):
USER_DB[user.name] = user.nickname
user_dict = user.dict()
user_dict["status"] = "success"
user_dict["id"] = len(USER_DB)
return user_dict
@app.get("/users")
def read_user(name: str):
if name not in USER_DB:
raise NAME_NOT_FOUND
return {"nickname": USER_DB[name]}
@app.put("/users")
def update_user(name: str, nickname: str):
if name not in USER_DB:
raise NAME_NOT_FOUND
USER_DB[name] = nickname
return {"status": "success"}
@app.delete("/users")
def delete_user(name: str):
if name not in USER_DB:
raise NAME_NOT_FOUND
del USER_DB[name]
return {"status": "success"}