FastAPI๋ž€?

FastAPI๋Š” Python์œผ๋กœ ๋งŒ๋“  ์ตœ์‹  ์›น ํ”„๋ ˆ์ž„์›Œํฌ์ด๋‹ค
๋น ๋ฅธ ์†๋„, ํƒ€์ž… ํžŒํŠธ ์ง€์›, ์ž๋™ ๋ฌธ์„œํ™”, ๋น„๋™๊ธฐ(Async) ์ง€์› ๋“ฑ์˜ ๊ฐ•์ ์ด ์žˆ๋‹ค
RESTful API, ์›น์†Œ์ผ“ ๋“ฑ ๋‹ค์–‘ํ•œ ๋ฐฑ์—”๋“œ ๊ธฐ๋Šฅ์„ ์‰ฝ๊ฒŒ ๊ฐœ๋ฐœํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค€๋‹ค


FastAPI์™€ ๊ด€๋ จ ์ฃผ์š” ์šฉ์–ด

ASGI (Asynchronous Server Gateway Interface)
  • Python์—์„œ ๋น„๋™๊ธฐ ์›น ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ์ธํ„ฐํŽ˜์ด์Šค (ํ”„๋กœํ† ์ฝœ, ํ‘œ์ค€)
  • WSGI[^1]์˜ ๋น„๋™๊ธฐ ํ™•์žฅํŒ์œผ๋กœ, ๋™๊ธฐ(Sync)์™€ ๋น„๋™๊ธฐ(Async) ๋ชจ๋‘ ์ง€์›
  • FastAPI, Starlette, Django(3.x ์ดํ›„) ๋“ฑ์ด 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 ์ „์ฒด ๊ตฌ์กฐ

graph TD
    Client["ํด๋ผ์ด์–ธํŠธ / ๋ธŒ๋ผ์šฐ์ € ๋˜๋Š” ์•ฑ)"] -->|ASGI| Uvicorn["Uvicorn - ASGI ์„œ๋ฒ„"]
    Uvicorn --> Starlette["Starlette - ASGI ํ”„๋ ˆ์ž„์›Œํฌ"]
    Starlette --> FastAPI["FastAPI - API ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง"]

์šด์˜ ํ™˜๊ฒฝ์—์„œ๋Š” Gunicorn์ด ์—ฌ๋Ÿฌ ๊ฐœ์˜ Uvicorn ํ”„๋กœ์„ธ์Šค๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ์—ญํ• ๋กœ ์ถ”๊ฐ€๋  ์ˆ˜ ์žˆ์Œ


์š”์•ฝ ํ‘œ

์šฉ์–ด์„ค๋ช…FastAPI์™€์˜ ๊ด€๊ณ„
ASGIPython ๋น„๋™๊ธฐ ์„œ๋ฒ„ ์ธํ„ฐํŽ˜์ด์ŠคFastAPI์˜ ๋™์ž‘ ๊ธฐ๋ฐ˜ ํ”„๋กœํ† ์ฝœ
StarletteASGI ๊ธฐ๋ฐ˜ ๊ฒฝ๋Ÿ‰ ์›น ํ”„๋ ˆ์ž„์›ŒํฌFastAPI์˜ ๊ธฐ๋ฐ˜ ๋ผˆ๋Œ€
UvicornASGI ์„œ๋ฒ„, FastAPI ์‹คํ–‰ ์„œ๋ฒ„FastAPI๋ฅผ ์‹คํ–‰์‹œ์ผœ์ฃผ๋Š” ์„œ๋ฒ„
Gunicorn๋ฉ€ํ‹ฐ ํ”„๋กœ์„ธ์Šค WSGI/ASGI ์„œ๋ฒ„Uvicorn๊ณผ ์กฐํ•ฉํ•˜์—ฌ ์šด์˜
FastAPIStarlette ๊ธฐ๋ฐ˜ ์ตœ์‹  API ํ”„๋ ˆ์ž„์›Œํฌ์‹ค์ œ API ์ž‘์„ฑ ์œ„์น˜

FastAPI Tutorial

ํŒจํ‚ค์ง€ ์„ค์น˜
pip install "fastapi[all]"
๊ฐ„๋‹จํ•œ API ๋งŒ๋“ค๊ธฐ
main.py
from fastapi import FastAPI
 
# Create a FastAPI instance
app = FastAPI()
 
 
# path '/' ๋กœ ๊ฐ€์„œ GET operation ์‹คํ–‰ํ–ˆ์„ ๋•Œ, ํ˜ธ์ถœ๋  ํŒŒ์ด์ฌ ํ•จ์ˆ˜
@app.get("/")
def read_root():
    return {"Hello": "World"}
์‹คํ–‰
# ๊ฐœ๋ฐœ
uvicorn main:app --reload
python -m fastapi dev app/main.py
 
# ์šด์˜(๋ฐฐํฌ)
gunicorn -k uvicorn.workers.UvicornWorker main:app
  • main : python script ์ด๋ฆ„
Path Parameter
path_param.py
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
query_param.py
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
# 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 item

FastAPI 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}
crud_path.py
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
crud_query.py
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)
crud_pydantic.py
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"}