FastAPI๋ž€?

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


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

ASGI (Asynchronous Server Gateway Interface)
  • Python์—์„œ ๋น„๋™๊ธฐ ์›น ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ์ธํ„ฐํŽ˜์ด์Šค (ํ”„๋กœํ† ์ฝœ, ํ‘œ์ค€)
  • WSGI์˜ ๋น„๋™๊ธฐ ํ™•์žฅํŒ์œผ๋กœ, ๋™๊ธฐ(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 ์ „์ฒด ๊ตฌ์กฐ

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

    classDef entry fill:#f8fafc,stroke:#94a3b8,color:#1e293b,stroke-width:1.4px;
    classDef runtime fill:#eff6ff,stroke:#60a5fa,color:#1e3a8a,stroke-width:1.4px;
    classDef app fill:#f0fdf4,stroke:#34d399,color:#14532d,stroke-width:1.6px;
    classDef ops fill:#fff7ed,stroke:#fb923c,color:#9a3412,stroke-width:1.4px,stroke-dasharray: 4 3;
    linkStyle 0 stroke:#64748b,stroke-width:1.6px;
    linkStyle 1 stroke:#64748b,stroke-width:1.6px;
    linkStyle 2 stroke:#64748b,stroke-width:1.6px;
    linkStyle 3 stroke:#fb923c,stroke-width:1.4px,stroke-dasharray: 4 3;

์šด์˜ ํ™˜๊ฒฝ์—์„œ๋Š” 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"}