FastAPI - Error Handling

Kjjeddยท2026๋…„ 1์›” 13์ผ

FastAPI

๋ชฉ๋ก ๋ณด๊ธฐ
13/16
post-thumbnail

๐Ÿšจ Error Handling โ€“ ์—๋Ÿฌ ์ฒ˜๋ฆฌ

API๋ฅผ ๋งŒ๋“ค ๋•Œ ๊ฐ€์žฅ ์œ„ํ—˜ํ•œ ์ˆœ๊ฐ„์€ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ๋‹ค.

์ •์ƒ ์‘๋‹ต์€ ๋ˆ„๊ตฌ๋‚˜ ์‹ ๊ฒฝ ์“ด๋‹ค. ํ•˜์ง€๋งŒ ์‹ค๋ฌด์˜ ํ’ˆ์งˆ์€ ์—๋Ÿฌ ์ฒ˜๋ฆฌ์—์„œ ๊ฐˆ๋ฆฐ๋‹ค.

์ด๋ฒˆ ๊ธ€์—์„œ๋Š” FastAPI์—์„œ ์—๋Ÿฌ๊ฐ€ ์™œ ํ•„์š”ํ•œ์ง€, HTTPException์€ ๋ฌด์—‡์ธ์ง€, ๊ทธ๋ฆฌ๊ณ  โ€œ์˜ฌ๋ฐ”๋ฅธ ์—๋Ÿฌโ€๋ž€ ๋ฌด์—‡์ธ์ง€๋ฅผ ๋‹จ๊ณ„์ ์œผ๋กœ ์ •๋ฆฌํ•œ๋‹ค.


๐Ÿ“Œ 1. ์—๋Ÿฌ(Error)๋ž€ ๋ฌด์—‡์ธ๊ฐ€

ํ”„๋กœ๊ทธ๋ž˜๋ฐ์—์„œ ์—๋Ÿฌ๋Š” ํ”„๋กœ๊ทธ๋žจ์˜ ์ •์ƒ์ ์ธ ํ๋ฆ„์ด ๊นจ์ง„ ์ƒํƒœ๋‹ค.

FastAPI ๊ธฐ์ค€์œผ๋กœ ๋ณด๋ฉด, ์—๋Ÿฌ๋Š” ํฌ๊ฒŒ ๋‘ ์ข…๋ฅ˜๋กœ ๋‚˜๋‰œ๋‹ค.

๊ตฌ๋ถ„ ์˜๋ฏธ ์˜ˆ์‹œ
ํด๋ผ์ด์–ธํŠธ ์—๋Ÿฌ ์š”์ฒญ์ด ์ž˜๋ชป๋จ 404, 400
์„œ๋ฒ„ ์—๋Ÿฌ ์„œ๋ฒ„ ๋‚ด๋ถ€ ๋ฌธ์ œ 500

์ค‘์š”ํ•œ ์›์น™์ด ํ•˜๋‚˜ ์žˆ๋‹ค.


๐Ÿ“š 2. HTTP ์ƒํƒœ ์ฝ”๋“œ ๋ณต์Šต

HTTP ์ƒํƒœ ์ฝ”๋“œ๋Š” ์„œ๋ฒ„๊ฐ€ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ณด๋‚ด๋Š” โ€œ๊ฒฐ๊ณผ ์š”์•ฝโ€์ด๋‹ค.

๋ฒ”์œ„ ์˜๋ฏธ ๋Œ€ํ‘œ ์ฝ”๋“œ
200๋ฒˆ๋Œ€ ์„ฑ๊ณต 200 OK, 201 Created
400๋ฒˆ๋Œ€ ํด๋ผ์ด์–ธํŠธ ์—๋Ÿฌ 400, 404
500๋ฒˆ๋Œ€ ์„œ๋ฒ„ ์—๋Ÿฌ 500

์˜ˆ๋ฅผ ๋“ค์–ด,

  • ์—†๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ–ˆ๋‹ค โ†’ 404
  • ์ž…๋ ฅ๊ฐ’์ด ์ด์ƒํ•˜๋‹ค โ†’ 400
  • ์ฝ”๋“œ๊ฐ€ ํ„ฐ์กŒ๋‹ค โ†’ 500

500 ์—๋Ÿฌ๋ฅผ ๊ทธ๋Œ€๋กœ ๋…ธ์ถœํ•˜๋Š” ๊ฒƒ์€ ์ตœ์•…์ด๋‹ค.


๐Ÿงช 3. ์—๋Ÿฌ ์ฒ˜๋ฆฌ ์—†์ด API๋ฅผ ๋งŒ๋“ค๋ฉด?

์ผ๋‹จ ์•„๋ฌด ์—๋Ÿฌ ์ฒ˜๋ฆฌ ์—†์ด API๋ฅผ ํ•˜๋‚˜ ๋งŒ๋“ค์–ด๋ณด์ž.

from fastapi import FastAPI

app = FastAPI()

items = {
    "foo": "ํฌ์•„์•„์•„์•„์•„์•…!"
}

@app.get("/items/{item_id}")
async def read_item(item_id: str):
    return {"item": items[item_id]}
    

์ด ์ƒํƒœ์—์„œ ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ?

์š”์ฒญ ๊ฒฐ๊ณผ
/items/foo 200 OK
/items/bar 500 Internal Server Error

๋ฌธ์ œ๋Š” ์—ฌ๊ธฐ ์žˆ๋‹ค.

โš ๏ธ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ž˜๋ชป ์š”์ฒญํ–ˆ๋Š”๋ฐ
์„œ๋ฒ„ ์—๋Ÿฌ(500)๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค

์ด๊ฑด ์™„์ „ํžˆ ์ž˜๋ชป๋œ API๋‹ค.


๐Ÿš‘ 4. HTTPException์œผ๋กœ ์˜ฌ๋ฐ”๋ฅธ ์—๋Ÿฌ ์ฒ˜๋ฆฌ

FastAPI๋Š” ์ด๋ฅผ ์œ„ํ•ด HTTPException์ด๋ผ๋Š” ๋„๊ตฌ๋ฅผ ์ œ๊ณตํ•œ๋‹ค.

from fastapi import FastAPI, HTTPException

app = FastAPI()

items = {
    "foo": "ํฌ์•„์•„์•„์•„์•„์•…!"
}

@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]}

์ด์ œ ๊ฒฐ๊ณผ๋Š” ์ด๋ ‡๊ฒŒ ๋ฐ”๋€๋‹ค.

์š”์ฒญ ์‘๋‹ต
/items/foo 200 OK
/items/bar 404 Not Found
โœ… ์ด์ œ ํด๋ผ์ด์–ธํŠธ๋Š”
โ€œ๋‚ด ์š”์ฒญ์ด ์ž˜๋ชป๋๊ตฌ๋‚˜โ€๋ฅผ ์ •ํ™•ํžˆ ์•Œ ์ˆ˜ ์žˆ๋‹ค

๐Ÿง  5. raise vs return

๐Ÿ”ธ return

  • ํ•จ์ˆ˜๋ฅผ ์ •์ƒ ์ข…๋ฃŒ
  • ์ดํ›„ ์ฝ”๋“œ ์‹คํ–‰ ๊ฐ€๋Šฅ

๐Ÿ”ธ raise

  • ์ฆ‰์‹œ ์‹คํ–‰ ์ค‘๋‹จ
  • ์ƒ์œ„ ํ˜ธ์ถœ ์Šคํƒ๊นŒ์ง€ ์ „ํŒŒ
if user_id <= 0:
    raise HTTPException(
        status_code=400,
        detail="User ID must be positive"
    )
๐Ÿ’ก raise๋Š”
โ€œ์—ฌ๊ธฐ์„œ ๋” ์ง„ํ–‰ํ•˜์ง€ ๋ง๊ณ  ์ฆ‰์‹œ ์‘๋‹ตํ•˜๋ผโ€๋Š” ์˜๋ฏธ

๐Ÿ“ฆ 6. HTTPException์˜ ๊ตฌ์„ฑ ์š”์†Œ

raise HTTPException(
    status_code=400,
    detail="Invalid request"
)
ํ•„๋“œ ์—ญํ• 
status_code HTTP ์ƒํƒœ ์ฝ”๋“œ
detail ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ „๋‹ฌํ•  ๋ฉ”์‹œ์ง€

detail์—๋Š” ๋ฌธ์ž์—ด๋ฟ ์•„๋‹ˆ๋ผ JSON์œผ๋กœ ๋ณ€ํ™˜ ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ๊ฐ’์„ ๋„ฃ์„ ์ˆ˜ ์žˆ๋‹ค.

raise HTTPException(
    status_code=404,
    detail={
        "error": "ITEM_NOT_FOUND",
        "message": "์•„์ดํ…œ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"
    }
)

๐Ÿ”„ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ํ๋ฆ„ ์‹œ๊ฐํ™”


  [ํด๋ผ์ด์–ธํŠธ ์š”์ฒญ]
        โ†“
[์—”๋“œํฌ์ธํŠธ ํ•จ์ˆ˜ ์‹คํ–‰]
        โ†“
	์กฐ๊ฑด ์‹คํŒจ
        โ†“
raise HTTPException
        โ†“
FastAPI๊ฐ€ ์‘๋‹ต ์ƒ์„ฑ
        โ†“
HTTP ์ƒํƒœ ์ฝ”๋“œ + ์—๋Ÿฌ ๋ฉ”์‹œ์ง€
        โ†“
	[ํด๋ผ์ด์–ธํŠธ]

๐Ÿงฑ 7. ์‚ฌ์šฉ์ž ์ •์˜ ์˜ˆ์™ธ ํด๋ž˜์Šค ๋งŒ๋“ค๊ธฐ

๐Ÿšจ ์ปค์Šคํ…€ ์˜ˆ์™ธ ํ•ธ๋“ค๋Ÿฌ (Custom Exception Handler)

์—๋Ÿฌ๋ฅผ if / return์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์‹œ์ž‘ํ•˜๋ฉด ์ฝ”๋“œ๋Š” ๊ธˆ๋ฐฉ ์ง€์ €๋ถ„ํ•ด์ง€๊ณ , ์‘๋‹ต ํ˜•์‹๋„ ์ œ๊ฐ๊ฐ์ด ๋œ๋‹ค.

FastAPI์˜ ์ „์—ญ ์˜ˆ์™ธ ํ•ธ๋“ค๋Ÿฌ๋Š” ์ด ๋ฌธ์ œ๋ฅผ ๊ตฌ์กฐ์ ์œผ๋กœ ํ•ด๊ฒฐํ•œ๋‹ค.

๐Ÿงฉ ์™œ ์ปค์Šคํ…€ ์˜ˆ์™ธ๊ฐ€ ํ•„์š”ํ•œ๊ฐ€

  • ๋‹จ์ˆœํ•œ 404, 400์„ ๋„˜๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ์—๋Ÿฌ๊ฐ€ ์กด์žฌํ•จ
  • ์—๋Ÿฌ ์‘๋‹ต ํฌ๋งท์„ API ์ „์ฒด์—์„œ ํ†ต์ผํ•˜๊ณ  ์‹ถ์Œ
  • ์—๋Ÿฌ ์ฒ˜๋ฆฌ ์ฝ”๋“œ๊ฐ€ ์—”๋“œํฌ์ธํŠธ์— ์„ž์ด๋Š” ๊ฑธ ๋ง‰๊ณ  ์‹ถ์Œ
โŒ ์ž˜๋ชป๋œ ๊ตฌ์กฐ
--------------------------------
if ์ž”์•ก ๋ถ€์กฑ:
    return {"error": "..."}  # ์—”๋“œํฌ์ธํŠธ๋งˆ๋‹ค ์ค‘๋ณต

โœ… ์˜ฌ๋ฐ”๋ฅธ ๊ตฌ์กฐ
--------------------------------
raise CustomException
โ†’ ์ „์—ญ ํ•ธ๋“ค๋Ÿฌ์—์„œ ์‘๋‹ต ์ƒ์„ฑ

๐Ÿงฑ ์‚ฌ์šฉ์ž ์ •์˜ ์˜ˆ์™ธ ๋งŒ๋“ค๊ธฐ

๋น„์ฆˆ๋‹ˆ์Šค ์˜๋ฏธ๋ฅผ ๋‹ด์€ ์˜ˆ์™ธ ํด๋ž˜์Šค๋ฅผ ์ง์ ‘ ๋งŒ๋“ ๋‹ค.

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

# ์‚ฌ์šฉ์ž ์ •์˜ ์˜ˆ์™ธ ํด๋ž˜์Šค
class InsufficientBalanceException(Exception):
    """์ž”์•ก ๋ถ€์กฑ ์˜ˆ์™ธ"""
    def __init__(self, balance: int, required: int):
        self.balance = balance      # ํ˜„์žฌ ์ž”์•ก
        self.required = required    # ํ•„์š”ํ•œ ๊ธˆ์•ก

app = FastAPI()

# ์˜ˆ์™ธ ํ•ธ๋“ค๋Ÿฌ ๋“ฑ๋ก
# InsufficientBalanceException์ด ๋ฐœ์ƒํ•˜๋ฉด ์ด ํ•จ์ˆ˜๊ฐ€ ์ฒ˜๋ฆฌํ•จ
@app.exception_handler(InsufficientBalanceException)
async def insufficient_balance_handler(
    request: Request, 
    exc: InsufficientBalanceException
):
    return JSONResponse(
        status_code=400,
        content={
            "error": "INSUFFICIENT_BALANCE",
            "message": "์ž”์•ก์ด ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค",
            "current_balance": exc.balance,
            "required_amount": exc.required,
            "shortage": exc.required - exc.balance
        }
    )

@app.post("/purchase/{item_id}")
async def purchase_item(item_id: str, user_balance: int = 1000):
    item_price = 1500
    if user_balance < item_price:
        # ์ปค์Šคํ…€ ์˜ˆ์™ธ ๋ฐœ์ƒ
        raise InsufficientBalanceException(
            balance=user_balance,
            required=item_price
        )
    return {"message": "๊ตฌ๋งค ์™„๋ฃŒ", "remaining": user_balance - item_price}

๐Ÿงญ ์˜ˆ์™ธ ํ๋ฆ„

[Client]
   |
   | POST /purchase/item1
   v
[Endpoint]
   |
   | ์ž”์•ก ์ฒดํฌ (1000 < 1500)
   |
   | raise InsufficientBalanceException
   v
[Exception Handler]
   |
   | JSONResponse ์ƒ์„ฑ
   v
[Client]
  400 Bad Request + ์ƒ์„ธ ์—๋Ÿฌ

๐ŸŽฏ ์ „์—ญ ์˜ˆ์™ธ ํ•ธ๋“ค๋Ÿฌ์˜ ์ง„์งœ ๊ฐ€์น˜

์ด์  ์„ค๋ช…
์ค‘๋ณต ์ œ๊ฑฐ ๋ชจ๋“  ์—”๋“œํฌ์ธํŠธ์—์„œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋กœ์ง ์ œ๊ฑฐ
์ผ๊ด€์„ฑ ์—๋Ÿฌ ์‘๋‹ต ํฌ๋งท ํ†ต์ผ
๊ด€์‹ฌ์‚ฌ ๋ถ„๋ฆฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง โ†” ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ถ„๋ฆฌ

โš ๏ธ ๊ธฐ๋ณธ ์˜ˆ์™ธ ํ•ธ๋“ค๋Ÿฌ ์˜ค๋ฒ„๋ผ์ด๋“œ

FastAPI๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋‘ ๊ฐ€์ง€๋ฅผ ์ž๋™ ์ฒ˜๋ฆฌํ•œ๋‹ค.

์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ์ 
HTTPException ๋ช…์‹œ์ ์œผ๋กœ raise
RequestValidationError ์š”์ฒญ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ์‹คํŒจ

๐Ÿงช RequestValidationError ์˜ค๋ฒ„๋ผ์ด๋”ฉ

๊ธฐ๋ณธ ๊ฒ€์ฆ ์—๋Ÿฌ๋Š” ์‚ฌ๋žŒ์ด ์ฝ๊ธฐ ํž˜๋“ค๊ธฐ ๋•Œ๋ฌธ์—, ์‘๋‹ต์„ ์˜ค๋ฒ„๋ผ์ด๋”ฉํ•˜์—ฌ ์‚ฌ์šฉํ•œ๋‹ค.

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

app = FastAPI()

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc: RequestValidationError):
    # ๊ฒ€์ฆ ์—๋Ÿฌ๋ฅผ ์‚ฌ์šฉ์ž ์นœํ™”์ ์ธ ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜
    errors = []
    for error in exc.errors():
        errors.append({
            "field": " โ†’ ".join(str(loc) for loc in error["loc"]),
            "message": error["msg"],
            "type": error["type"]
        })
    
    return JSONResponse(
        status_code=422,
        content={
            "error": "VALIDATION_ERROR",
            "message": "์ž…๋ ฅ๊ฐ’์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค",
            "details": errors
        }
    )

@app.get("/items/{item_id}")
async def read_item(item_id: int):  # int ํƒ€์ž… ๊ธฐ๋Œ€
    return {"item_id": item_id}

/items/abc ์š”์ฒญ ์‹œ ๊ธฐ๋ณธ ์‘๋‹ต vs ์ปค์Šคํ…€ ์‘๋‹ต
๊ธฐ๋ณธ ์‘๋‹ต:

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

์ปค์Šคํ…€ ์‘๋‹ต:

{
    "error": "VALIDATION_ERROR",
    "message": "์ž…๋ ฅ๊ฐ’์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค",
    "details": [{
        "field": "path โ†’ item_id",
        "message": "value is not a valid integer",
        "type": "type_error.integer"
    }]
}

๐Ÿ‘Œ ๊ฐ€๋…์„ฑ์ด ํ›จ์”ฌ ์ข‹์•„์ง„ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Œ.


โšก FastAPI vs Starlette HTTPException

FastAPI์˜ HTTPException์€ Starlette์˜ HTTPException์„ ์ƒ์†๋ฐ›๋Š”๋‹ค.
์˜ˆ์™ธ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๋“ฑ๋กํ•  ๋•Œ๋Š” Starlette์˜ HTTPException์œผ๋กœ ๋“ฑ๋กํ•ด์•ผ FastAPI ๋‚ด๋ถ€ ์ฝ”๋“œ๋‚˜ Starlette ํ”Œ๋Ÿฌ๊ทธ์ธ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ๋„ ์žก์„ ์ˆ˜ ์žˆ๋‹ค.

๊ตฌ๋ถ„ FastAPI Starlette
์šฉ๋„ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์ „์—ญ ํ•ธ๋“ค๋Ÿฌ
detail ํƒ€์ž… JSON ๊ฐ€๋Šฅ ๋ฌธ์ž์—ด

โœ… ์ฒดํฌ๋ฆฌ์ŠคํŠธ

  • ํด๋ผ์ด์–ธํŠธ ์—๋Ÿฌ๋Š” 400๋ฒˆ๋Œ€ ์‚ฌ์šฉ
  • 500 ์—๋Ÿฌ๋ฅผ ๊ทธ๋Œ€๋กœ ๋…ธ์ถœํ•˜์ง€ ์•Š๊ธฐ
  • ์—๋Ÿฌ ์‘๋‹ต ํฌ๋งท ํ†ต์ผ
  • Starlette HTTPException์œผ๋กœ ํ•ธ๋“ค๋Ÿฌ ๋“ฑ๋ก
profile
Gongbuhaja

0๊ฐœ์˜ ๋Œ“๊ธ€