Skip to content

์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ

๋‹น์‹ ์˜ API๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์˜ค๋ฅ˜๋ฅผ ์•Œ๋ ค์•ผํ•˜๋Š” ๋‹ค์–‘ํ•œ ์ƒํ™ฉ๋“ค์ด ์žˆ์Šต๋‹ˆ๋‹ค.

์—ฌ๊ธฐ์„œ ํด๋ผ์ด์–ธํŠธ๋Š” ํ”„๋ก ํŠธ์—”๋“œ๊ฐ€ ์žˆ๋Š” ๋ธŒ๋ผ์šฐ์ €, ๋‹ค๋ฅธ ์‚ฌ๋žŒ์ด ์ž‘์„ฑํ•œ ์ฝ”๋“œ, IoT ๊ธฐ๊ธฐ ๋“ฑ์ด ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋‹ค์Œ์˜ ์‚ฌ์‹ค์„ ์ „๋‹ฌํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค:

  • ํด๋ผ์ด์–ธํŠธ๊ฐ€ ํ•ด๋‹น ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๊ธฐ์— ์ถฉ๋ถ„ํ•œ ๊ถŒํ•œ์„ ๊ฐ€์ง€์ง€ ์•Š์•˜๋‹ค๋Š” ์‚ฌ์‹ค
  • ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ž์›์— ์ ‘๊ทผํ•  ์ˆ˜ ์—†๋‹ค๋Š” ์‚ฌ์‹ค
  • ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ ‘๊ทผํ•˜๋ ค๊ณ  ํ•˜๋Š” ํ•ญ๋ชฉ์ด ์กด์žฌํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” ์‚ฌ์‹ค
  • ๊ธฐํƒ€ ๋“ฑ๋“ฑ

์ด๋Ÿฌํ•œ ๊ฒฝ์šฐ ์ผ๋ฐ˜์ ์œผ๋กœ 4xx(400์—์„œ 499๊นŒ์ง€)์˜ HTTP ์ƒํƒœ ์ฝ”๋“œ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

์ด๊ฒƒ์€ 2xx(200์—์„œ 299๊นŒ์ง€)์˜ HTTP ์ƒํƒœ ์ฝ”๋“œ์™€ ์œ ์‚ฌํ•ฉ๋‹ˆ๋‹ค. "2xx" ์ƒํƒœ ์ฝ”๋“œ๋“ค์€ ์š”์ฒญ์ด "์„ฑ๊ณต"์ ์ด์—ˆ๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

400๋ฒˆ๋Œ€์˜ ์ƒํƒœ ์ฝ”๋“œ๋“ค์€ ํด๋ผ์ด์–ธํŠธ๋กœ๋ถ€ํ„ฐ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Œ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

์ง€๊ธˆ๊นŒ์ง€ ๋ณธ "404 Not Found" ์˜ค๋ฅ˜๋“ค์„ ๋– ์˜ฌ๋ ค๋ณด์„ธ์š”(๊ทธ๋ฆฌ๊ณ  ๊ทธ์— ๋Œ€ํ•œ ๋†๋‹ด๋“ค๋„์š”)!

HTTPException ์‚ฌ์šฉ

ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ HTTP ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•˜๊ธฐ ์œ„ํ•ด HTTPException์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

HTTPException ์ž„ํฌํŠธ

from fastapi import FastAPI, HTTPException

app = FastAPI()

items = {"foo": "The Foo Wrestlers"}


@app.get("/items/{item_id}")
async def read_item(item_id: str):
    if item_id not in items:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"item": items[item_id]}

์ฝ”๋“œ์—์„œ HTTPException ๋ฐœ์ƒ์‹œํ‚ค๊ธฐ

HTTPException์€ API์— ๋Œ€ํ•œ ์ถ”๊ฐ€์ ์ธ ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•œ ์ผ๋ฐ˜์ ์ธ ํŒŒ์ด์ฌ ์˜ˆ์™ธ์ž…๋‹ˆ๋‹ค.

ํŒŒ์ด์ฌ ์˜ˆ์™ธ์ด๊ธฐ ๋•Œ๋ฌธ์—, ๋ฐ˜ํ™˜(return)ํ•˜์ง€ ์•Š๊ณ  ๋ฐœ์ƒ(raise)์‹œํ‚ต๋‹ˆ๋‹ค.

์ด๊ฒƒ์€ ๋งŒ์•ฝ ๋‹น์‹ ์ด ๊ฒฝ๋กœ ๋™์ž‘ ํ•จ์ˆ˜์˜ ๋‚ด๋ถ€์—์„œ ํ˜ธ์ถœํ•˜๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ ๋‚ด๋ถ€์— ์žˆ๊ณ , ํ•ด๋‹น ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ ๋‚ด๋ถ€์—์„œ HTTPException์„ ๋ฐœ์ƒ์‹œํ‚ค๋Š” ๊ฒฝ์šฐ, ๊ฒฝ๋กœ ๋™์ž‘ ํ•จ์ˆ˜์˜ ๋‚˜๋จธ์ง€ ๋ถ€๋ถ„์„ ์‹คํ–‰ํ•˜๋Š” ๋Œ€์‹  ์ฆ‰์‹œ ์š”์ฒญ์— ๋Œ€ํ•œ ์ž‘์—…์„ ์ค‘๋‹จํ•˜๊ณ  HTTPException์— ๋”ฐ๋ฅธ HTTP ์˜ค๋ฅ˜๋ฅผ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ „์†กํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ํ•จ์— ์žˆ์–ด ๊ฐ’์„ ๋ฐ˜ํ™˜(return)ํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ๋ฐœ์ƒ์‹œํ‚ค๋Š” ๊ฒƒ์˜ ์ด์ ์€ ์ข…์† ๋ฐ ๋ณด์•ˆ(Dependencies and Security) ์„น์…˜์—์„œ ๊นŠ๊ฒŒ ๋‹ค๋ฃฐ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์ผ๋ก€๋กœ, ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ํ•ญ๋ชฉ์˜ ID๋ฅผ ์š”์ฒญํ•˜๋Š” ๊ฒฝ์šฐ, ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฐฉ๋ฒ•์œผ๋กœ ์ƒํƒœ ์ฝ”๋“œ 404์™€ ํ•จ๊ป˜ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

from fastapi import FastAPI, HTTPException

app = FastAPI()

items = {"foo": "The Foo Wrestlers"}


@app.get("/items/{item_id}")
async def read_item(item_id: str):
    if item_id not in items:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"item": items[item_id]}

๊ฒฐ๊ณผ ์‘๋‹ต

ํด๋ผ์ด์–ธํŠธ๊ฐ€ http://example.com/items/foo(item_id๊ฐ€ "foo"์ธ ํ•ญ๋ชฉ)๋กœ ์š”์ฒญ์„ ๋ณด๋ƒˆ๋‹ค๋ฉด, ํด๋ผ์ด์–ธํŠธ๋Š” HTTP ์ƒํƒœ์ฝ”๋“œ 200๊ณผ ๋‹ค์Œ๊ณผ ๊ฐ™์€ JSON ์‘๋‹ต์„ ๋ฐ›๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค:

{  
    "item": "The Foo Wrestlers"
}

ํ•˜์ง€๋งŒ ํด๋ผ์ด์–ธํŠธ๊ฐ€ http://example.com/items/bar (item_id๊ฐ€ "bar"์ธ ํ•ญ๋ชฉ ์—†์Œ)๋กœ ์š”์ฒญ์„ ๋ณด๋‚ธ๋‹ค๋ฉด, ํด๋ผ์ด์–ธํŠธ๋Š” HTTP ์ƒํƒœ์ฝ”๋“œ 404("์ฐพ์„ ์ˆ˜ ์—†์Œ(not found)" ์˜ค๋ฅ˜)์™€ ๋‹ค์Œ์˜ JSON ์‘๋‹ต์„ ๋ฐ›๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค:

{  
    "detail": "Item not found"
}

ํŒ

HTTPException์„ ๋ฐœ์ƒ์‹œํ‚ฌ ๋•Œ, str ๋ฟ ์•„๋‹ˆ๋ผ JSON์œผ๋กœ ๋ณ€ํ™˜ ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ๊ฐ’์„ detail ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

dict, list ๋“ฑ์„ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋“ค์€ FastAPI์— ์˜ํ•ด ์ž๋™์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋˜๊ณ  JSON์œผ๋กœ ๋ณ€ํ™˜๋ฉ๋‹ˆ๋‹ค.

์‚ฌ์šฉ์ž ์ •์˜ ํ—ค๋” ์ถ”๊ฐ€

HTTP ์˜ค๋ฅ˜์— ์‚ฌ์šฉ์ž ์ •์˜ ํ—ค๋”๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ์œ ์šฉํ•œ ๊ฒฝ์šฐ๋“ค์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ๋“ค์–ด, ๋ช‡๋ช‡ ๋ณด์•ˆ ๋ฌธ์ œ์˜ ๊ฒฝ์šฐ ๊ทธ๋Ÿฌํ•ฉ๋‹ˆ๋‹ค.

์ฝ”๋“œ์—์„œ ์ด๊ฒƒ์„ ์ง์ ‘ ์‚ฌ์šฉํ•  ํ•„์š”๋Š” ์—†์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ๊ณ ๊ธ‰ ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ ํ•„์š”ํ•œ ๊ฒฝ์šฐ, ์‚ฌ์šฉ์ž ์ •์˜ ํ—ค๋”๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

from fastapi import FastAPI, HTTPException

app = FastAPI()

items = {"foo": "The Foo Wrestlers"}


@app.get("/items-header/{item_id}")
async def read_item_header(item_id: str):
    if item_id not in items:
        raise HTTPException(
            status_code=404,
            detail="Item not found",
            headers={"X-Error": "There goes my error"},
        )
    return {"item": items[item_id]}

์‚ฌ์šฉ์ž ์ •์˜ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ธฐ(exception handler) ์„ค์น˜

Starlette๊ณผ ๋™์ผํ•œ ์˜ˆ์™ธ ์œ ํ‹ธ๋ฆฌํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž ์ •์˜ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ธฐ๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹น์‹  ๋˜๋Š” ๋‹น์‹ ์ด ์‚ฌ์šฉํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์‚ฌ์šฉ์ž ์ •์˜ ์˜ˆ์™ธ์ธ UnicornException์„ ๋ฐœ์ƒ(raise) ์‹œํ‚ค๊ณ ์ž ํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด๋ด…์‹œ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ๋‹น์‹ ์€ FastAPI๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ•ด๋‹น ์˜ˆ์™ธ๋ฅผ ์ „์—ญ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค.

@app.exception_handler()๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž ์˜ˆ์™ธ๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse


class UnicornException(Exception):
    def __init__(self, name: str):
        self.name = name


app = FastAPI()


@app.exception_handler(UnicornException)
async def unicorn_exception_handler(request: Request, exc: UnicornException):
    return JSONResponse(
        status_code=418,
        content={"message": f"Oops! {exc.name} did something. There goes a rainbow..."},
    )


@app.get("/unicorns/{name}")
async def read_unicorn(name: str):
    if name == "yolo":
        raise UnicornException(name=name)
    return {"unicorn_name": name}

์—ฌ๊ธฐ์„œ /unicorns/yolo๋ฅผ ์š”์ฒญํ•˜๋ฉด, ๊ฒฝ๋กœ ๋™์ž‘์€ UnicornException์„ ๋ฐœ์ƒ(raise)์‹œํ‚ฌ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์ด๊ฒƒ์€ unicorn_exception_handler ์— ์˜ํ•ด ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ ๋‹น์‹ ์€ HTTP ์ƒํƒœ์ฝ”๋“œ๊ฐ€ 418์ด๊ณ , ๋‹ค์Œ๊ณผ ๊ฐ™์€ JSON ๋‚ด์šฉ์„ ๊ฐ€์ง„ ์˜ค๋ฅ˜๋ฅผ ๋ฐ›๊ฒŒ๋ฉ๋‹ˆ๋‹ค:

{"message": "Oops! yolo did something. There goes a rainbow..."}

๊ธฐ์ˆ  ์„ธ๋ถ€์‚ฌํ•ญ

from starlette.requests import Request์™€ from starlette.responses import JSONResponse ์—ญ์‹œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

FastAPI๋Š” ๊ฐœ๋ฐœ์ž์ธ ๋‹น์‹ ์˜ ํŽธ์˜๋ฅผ ์œ„ํ•ด fastapi.responses ์™€ ๋™์ผํ•œ starlette.responses ๋„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋Œ€๋ถ€๋ถ„์˜ ์‘๋‹ต๋“ค์€ Starlette๋กœ๋ถ€ํ„ฐ ์ง์ ‘ ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค. ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ Request ๋„ ๊ทธ๋Ÿฌํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋ณธ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ธฐ ์žฌ์ •์˜

FastAPI ์—๋Š” ๊ธฐ๋ณธ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ธฐ๋“ค์ด ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ธฐ๋“ค์€ ๋‹น์‹ ์ด HTTPException์„ ๋ฐœ์ƒ(raise)์‹œํ‚ค๊ฑฐ๋‚˜ ์š”์ฒญ์— ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์„ ๋•Œ ๊ธฐ๋ณธ JSON ์‘๋‹ต๋“ค์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.

์ด ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ธฐ๋“ค์„ ์ง์ ‘ ์žฌ์ •์˜ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์š”์ฒญ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์˜ˆ์™ธ ์žฌ์ •์˜

์š”์ฒญ์— ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์„ ๋•Œ, FastAPI๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ RequestValidationError๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.

๋˜ํ•œ ์ด์— ๋Œ€ํ•œ ๊ธฐ๋ณธ์ ์ธ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ธฐ๋„ ํฌํ•จํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฅผ ์žฌ์ •์˜ ํ•˜๊ธฐ ์œ„ํ•ด, RequestValidationError๋ฅผ ์ž„ํฌํŠธํ•œ ํ›„@app.exception_handler(RequestValidationError) ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ์™€ ํ•จ๊ป˜ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ธฐ์— ์‚ฌ์šฉํ•˜์„ธ์š”.

์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ธฐ๋Š” Request์™€ ์˜ˆ์™ธ๋ฅผ ์ „๋‹ฌ๋ฐ›์Šต๋‹ˆ๋‹ค.

from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.responses import PlainTextResponse
from starlette.exceptions import HTTPException as StarletteHTTPException

app = FastAPI()


@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request, exc):
    return PlainTextResponse(str(exc.detail), status_code=exc.status_code)


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
    return PlainTextResponse(str(exc), status_code=400)


@app.get("/items/{item_id}")
async def read_item(item_id: int):
    if item_id == 3:
        raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
    return {"item_id": item_id}

์ด์ œ /items/foo๋กœ ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด, ๊ธฐ๋ณธ JSON ์˜ค๋ฅ˜๋ฅผ ๋ฐ›๋Š” ๋Œ€์‹ :

{
    "detail": [
        {
            "loc": [
                "path",
                "item_id"
            ],
            "msg": "value is not a valid integer",
            "type": "type_error.integer"
        }
    ]
}

๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ…์ŠคํŠธ๋ฅผ ๋ฐ›๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค:

1 validation error
path -> item_id
  value is not a valid integer (type=type_error.integer)

RequestValidationError vs ValidationError

์ฃผ์˜

์ด ๋ถ€๋ถ„์€ ๋‹น์‹ ์—๊ฒŒ ์ง€๊ธˆ ์ค‘์š”ํ•˜์ง€ ์•Š๋‹ค๋ฉด ๋„˜์–ด๊ฐ€๋„ ๋ฌด๊ด€ํ•œ ๊ธฐ์ˆ  ์„ธ๋ถ€์‚ฌํ•ญ์ž…๋‹ˆ๋‹ค.

RequestValidationError ๋Š” Pydantic์˜ ValidationError์˜ ํ•˜์œ„ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.

FastAPI๊ฐ€ ์ด๊ฒƒ์„ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋‹น์‹ ์ด response_model์—์„œ Pydantic ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•˜๊ณ , ๋‹น์‹ ์˜ ๋ฐ์ดํ„ฐ์— ์˜ค๋ฅ˜๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ ๋กœ๊ทธ์—์„œ ์˜ค๋ฅ˜๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ํด๋ผ์ด์–ธํŠธ์™€ ์‚ฌ์šฉ์ž๋Š” ์ด๋ฅผ ๋ณผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋Œ€์‹ , ํด๋ผ์ด์–ธํŠธ๋Š” HTTP ์ƒํƒœ ์ฝ”๋“œ 500 ๊ณผ ํ•จ๊ป˜ "๋‚ด๋ถ€ ์„œ๋ฒ„ ์˜ค๋ฅ˜(Internal Server Error)"๋ฅผ ์ „๋‹ฌ ๋ฐ›์Šต๋‹ˆ๋‹ค.

๋งŒ์•ฝ ํด๋ผ์ด์–ธํŠธ์˜ ์š”์ฒญ(request)์ด ์•„๋‹Œ ์‘๋‹ต(response)์ด๋‚˜ ์ฝ”๋“œ ์–ด๋”˜๊ฐ€์— Pydantic์˜ ValidationError ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ, ์ด๊ฒƒ์€ ์ฝ”๋“œ ๋‚ด์— ๋ฒ„๊ทธ๊ฐ€ ์žˆ์Œ์„ ์˜๋ฏธํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

๋‹น์‹ ์ด ํ•ด๋‹น ๋ฒ„๊ทธ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๋™์•ˆ ๋ณด์•ˆ ์ทจ์•ฝ์ ์ด ๋…ธ์ถœ๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ํด๋ผ์ด์–ธํŠธ์™€ ์‚ฌ์šฉ์ž๋Š” ์˜ค๋ฅ˜์— ๊ด€ํ•œ ๋‚ด๋ถ€ ์ •๋ณด์— ์ ‘๊ทผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

HTTPException ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ธฐ ์žฌ์ •์˜

๊ฐ™์€ ๋ฐฉ์‹์œผ๋กœ HTTPException ์ฒ˜๋ฆฌ๊ธฐ๋ฅผ ์žฌ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, ์ด ์˜ค๋ฅ˜๋“ค์— ๋Œ€ํ•ด JSON ๋Œ€์‹  ์ผ๋ฐ˜ ํ…์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ ์ž ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.responses import PlainTextResponse
from starlette.exceptions import HTTPException as StarletteHTTPException

app = FastAPI()


@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request, exc):
    return PlainTextResponse(str(exc.detail), status_code=exc.status_code)


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
    return PlainTextResponse(str(exc), status_code=400)


@app.get("/items/{item_id}")
async def read_item(item_id: int):
    if item_id == 3:
        raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
    return {"item_id": item_id}

๊ธฐ์ˆ  ์„ธ๋ถ€์‚ฌํ•ญ

from starlette.responses import JSONResponse ์—ญ์‹œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

FastAPI๋Š” ๊ฐœ๋ฐœ์ž์ธ ๋‹น์‹ ์˜ ํŽธ์˜๋ฅผ ์œ„ํ•ด fastapi.responses์™€ ๋™์ผํ•œ starlette.responses๋„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋Œ€๋ถ€๋ถ„์˜ ์‘๋‹ต๋“ค์€ Starlette๋กœ๋ถ€ํ„ฐ ์ง์ ‘ ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค.

RequestValidationError ๋ณธ๋ฌธ ์‚ฌ์šฉ

RequestValidationError ๋Š” ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ฐ์ดํ„ฐ์™€ ํ•จ๊ป˜ ๋ฐ›์€ body๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.

๋‹น์‹ ์€ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ฐœ๋ฐœํ•˜๋Š” ๋™์•ˆ ๋ณธ๋ฌธ์„ ๋กœ๊ทธ์— ๊ธฐ๋กํ•˜๊ณ , ๋””๋ฒ„๊น…ํ•˜๊ณ , ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ฐ˜ํ™˜ํ•˜๋Š” ์ž‘์—… ๋“ฑ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๋ฐ์— ์ด๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

from fastapi import FastAPI, Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from pydantic import BaseModel

app = FastAPI()


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    return JSONResponse(
        status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
        content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
    )


class Item(BaseModel):
    title: str
    size: int


@app.post("/items/")
async def create_item(item: Item):
    return item

๋‹ค์Œ๊ณผ ๊ฐ™์ด ์œ ํšจํ•˜์ง€ ์•Š์€ ํ•ญ๋ชฉ์„ ์ „์†กํ•˜๋ฉด:

{
  "title": "towel",
  "size": "XL"
}

๋ฐ์ดํ„ฐ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Œ์„ ์•Œ๋ ค์ฃผ๋Š”, ์ „๋‹ฌ๋ฐ›์€ ๋ณธ๋ฌธ์„ ํฌํ•จํ•œ ์‘๋‹ต์„ ๋ฐ›๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

{
  "detail": [
    {
      "loc": [
        "body",
        "size"
      ],
      "msg": "value is not a valid integer",
      "type": "type_error.integer"
    }
  ],
  "body": {
    "title": "towel",
    "size": "XL"
  }
}

FastAPI์˜ HTTPException vs Starlette์˜ HTTPException

FastAPI์—๋Š” ์ž์ฒด์ ์ธ HTTPException ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  FastAPI์˜ HTTPException ์˜ค๋ฅ˜ ํด๋ž˜์Šค๋Š” Starlette์˜ HTTPException ์˜ค๋ฅ˜ ํด๋ž˜์Šค๋ฅผ ์ƒ์†๋ฐ›์Šต๋‹ˆ๋‹ค.

์œ ์ผํ•œ ์ฐจ์ด์ ์€, FastAPI์˜ HTTPException ์„ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ ์‘๋‹ต์— ํ—ค๋”๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์ด๊ฒƒ์€ OAuth 2.0 ๋ฐ ๋ช‡๋ช‡ ๋ณด์•ˆ ์œ ํ‹ธ๋ฆฌํ‹ฐ์— ๋‚ด๋ถ€์ ์œผ๋กœ ํ•„์š”/์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ, ํ‰์†Œ์™€ ๊ฐ™์ด ์ฝ”๋“œ์—์„œ FastAPI์˜ HTTPException ์„ ๊ณ„์† ๋ฐœ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ธฐ๋ฅผ ๋“ฑ๋กํ•  ๋•Œ์—๋Š”, Starlette์˜ HTTPException ์— ๋Œ€ํ•˜์—ฌ ๋“ฑ๋กํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด Starlette์˜ ๋‚ด๋ถ€ ์ฝ”๋“œ ๋˜๋Š” Starlette ํ™•์žฅ(extension) ๋˜๋Š” ํ”Œ๋Ÿฌ๊ทธ์ธ์˜ ์ผ๋ถ€๊ฐ€ Starlette HTTPException์„ ๋ฐœ์ƒ์‹œํ‚ฌ ๋•Œ, ํ•ด๋‹น ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ธฐ๊ฐ€ ์ด๋ฅผ ํฌ์ฐฉํ•˜๊ณ  ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์•„๋ž˜ ์˜ˆ์‹œ์—์„œ, ๋‘ ๊ฐœ์˜ HTTPException ์„ ๊ฐ™์€ ์ฝ”๋“œ์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด, Starlette์˜ ์˜ˆ์™ธ๋Š” StarletteHTTPException ๋กœ ์ด๋ฆ„์„ ๋ณ€๊ฒฝํ•˜์˜€์Šต๋‹ˆ๋‹ค.

from starlette.exceptions import HTTPException as StarletteHTTPException

FastAPI์˜ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ธฐ ์žฌ์‚ฌ์šฉ

์–ด๋– ํ•œ ๋ฐฉ์‹์œผ๋กœ๋“  ์˜ˆ์™ธ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด์„œ, FastAPI์˜ ๋™์ผํ•œ ๊ธฐ๋ณธ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ธฐ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

fastapi.exception_handlers ๋กœ๋ถ€ํ„ฐ ๊ธฐ๋ณธ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ธฐ๋ฅผ ์ž„ํฌํŠธํ•˜๊ณ  ์žฌ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค:

from fastapi import FastAPI, HTTPException
from fastapi.exception_handlers import (
    http_exception_handler,
    request_validation_exception_handler,
)
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException

app = FastAPI()


@app.exception_handler(StarletteHTTPException)
async def custom_http_exception_handler(request, exc):
    print(f"OMG! An HTTP error!: {repr(exc)}")
    return await http_exception_handler(request, exc)


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
    print(f"OMG! The client sent invalid data!: {exc}")
    return await request_validation_exception_handler(request, exc)


@app.get("/items/{item_id}")
async def read_item(item_id: int):
    if item_id == 3:
        raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
    return {"item_id": item_id}

์ƒ๊ธฐ ์˜ˆ์‹œ์—์„œ, ๊ฐ์ •์ด ๋งค์šฐ ๋งŽ์ด ์„ž์ธ ๋ฉ”์‹œ์ง€์™€ ํ•จ๊ป˜ ์˜ค๋ฅ˜๋ฅผ ๋‹จ์ˆœํžˆ print ํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์ด๋ฅผ ํ†ตํ•ด ์˜ˆ์™ธ๋ฅผ ์‚ฌ์šฉํ•œ ํ›„, ๊ธฐ๋ณธ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ธฐ๋ฅผ ๋‹ค์‹œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์•Œ์•˜์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.