백엔드 API는 프론트와 통신하는 다리로, 요청을 받고 응답을 보내준다. 요청으로 보내져오는 모든 요소가 정상적이면 좋겠지만, 잡지 못한 예외나 특정 상황에서 문제가 발생했을 때 적절하게 응답을 보내주어야 한다. 상태코드/예외처리는 FastAPI에서 API 품질을 좌우하는 핵심이어서 신중하고 깊게 다루어야 보다 좋은 서비스를 구현할 수 있다.
상태 코드 (기본)
상태 코드 | 의미 | 설명 |
200 | OK | 일반 성공 (GET 등) |
201 | Created | 생성 성공 (POST로 리소스 생성) |
204 | No Content | 성공했지만 바디 없음 (DELETE 자주) |
400 | Bad Request | 요청 자체가 말이 안 됨 (규칙 위반) |
401 | Unauthorized | 인증 필요 (로그인 안 함) |
403 | Forbidden | 인증은 됐지만 권한 없음 |
404 | Not Found | 대상 리소스 없음 |
409 | Conflict | 충돌 (중복 생성 등) |
422 | Unprocessable Entity | 타입/스키마 검증 실패 (FastAPI가 자동으로 많이 씀) |
500 | Internal Server Error | 서버 내부 에러 |
FastAPI의 엔드포인트에서 상태 코드를 설정하는 가장 쉬운 방법은 라우터 파일에서 엔드포인트에 붙이는 것이다.
@router.post("/sum", response_model=SumResponse, status_code=status.HTTP_201_CREATED)
def sum_numbers(body: SumRequest, offset: int = Depends(get_offset)) -> SumResponse:
"""Endpoint to sum two numbers."""
return SumResponse(result=body.a + body.b + offset)
Python
복사
추가로 가장 쉬운 예외 처리는 HTTPException을 사용하여 의도적으로 실패 하게끔 할 수 있다.
@router.post("/div", response_model=DivResponse)
def divide_numbers(body: DivRequest) -> DivResponse:
"""Endpoint to divide two numbers."""
if body.b == 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Division by zero is not allowed.",
)
return DivResponse(result=body.a / body.b)
Python
복사
위의 에러는 다음과 같이 응답으로 표현 된다.
{ "detail": "Division by zero is not allowed." }
JSON
복사
하지만, 엔드포인트가 많아지고 작업하는 사람이 많아질수록 에러 응답 포맷을 통일할 필요가 있다.
위에서 작성한 것처럼 { “detail”: “…” } 도 괜찮지만 명확한 에러 코드와 그 메시지를 같이 보내준다.
{
"code": "INVALID_ARGUMENT",
"message": "Division by zero is not allowed."
}
JSON
복사
에러 응답 포맷 통일 방법
먼저, main.py와 동일한 depth에 errors.py를 작성한다.
"""Custom exceptions for the application."""
from dataclasses import dataclass
@dataclass
class AppError(Exception):
"""Base class for application errors."""
status_code: int
code: str
message: str
Python
복사
AppError 클래스는 Exception을 상속하는 클래스이다. raise AppError(…) 형식으로도 사용할 수 있으며, FastAPI는 Exception을 상속한 객체가 raise 되면 예외 핸들러가 잡아서 HTTP 응답으로 바꿔줄 수 있다.
@dataclass를 쓰는 이유는 이 데코레이터를 사용하고 내부 필드를 정의하면 자동으로 init을 만들어준다.
@dataclass
class AppError(Exception):
status_code: int
code: str
message: str
Python
복사
이 코드가 다음과 같이 자동 생성한다.
class AppError(Exception):
def __init__(self, status_code: int, code: str, message: str):
self.status_code = status_code
self.code = code
self.message = message
Python
복사
이렇게 에러 핸들러 클래스를 생성했으면 main.py에 예외 핸들러를 등록해야한다.
"""This is the main entry point for the FastAPI application.
It initializes the FastAPI app and includes the routers for different endpoints.
"""
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from app.errors import AppError
from app.routers.math import router as math_router
app = FastAPI(title="FastAPI with Routers")
app.include_router(math_router)
@app.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError):
"""Custom exception handler for AppError.
Args:
request (Request): The incoming request that caused the error.
exc (AppError): The exception instance containing error details.
"""
return JSONResponse(
status_code=exc.status_code, content={"code": exc.code, "message": exc.message}
)
Python
복사
@app.exception_handler(AppError)
Python
복사
FastAPI 객체인 app에 에러 핸들러를 등록하는 곳이다. AppError 타입 예외가 발생하면 이 아래에 있는 함수를 실행한다.
async def app_error_handler(request: Request, exc: AppError):
Python
복사
•
request: Request는 어떤 요청에서 이 에러가 발생했는지 정보를 담는 파라미터이고, URL, 헤더, 바디 등에 접근할 수 있다. 지금 이 핸들러에서는 사용하지 않지만 후에 로그를 확인할 때 유용하다.
•
exc: AppError는 raise한 예외 객체이다.
•
async를 사용한 이유는 FastAPI가 기본적으로 비동기 서버 위에서 동작하기 때문이다. 즉, FastAPI는 async 기반 프레임워크이다. 예외 핸들러가 async인 이유는 이 핸들러 안에서 DB 조회, 로그 기록, 외부 API 호출, 파일 쓰기와 같은 비동기 작업을 할 가능성이 있기 때문이다.
return JSONResponse(
status_code=exc.status_code, content={"code": exc.code, "message": exc.message}
)
Python
복사
이 부분은 HTTP 응답을 직접 만들어서 반환하는 부분이다.
전체적인 흐름을 봤을 때 예를 들어 라우터에서 아래와 같은 에러가 발생했다고 한다.
raise AppError(
status_code=400,
code="INVALID_ARGUMENT",
message="b must not be 0"
)
Python
복사
이렇게 하면 내부에서 아래와 같은 순서로 에러를 처리한다.
1. AppError 발생
2. FastAPI가 이 타입의 핸들러를 찾음
3. app_error_handler 실행
4. JSONResponse 반환
5. 클라이언트에 응답
Plain Text
복사
이 구조가 중요한 이유는 에러 포맷을 통일하여 모든 API가 같은 구조로 에러를 반환하게 하고, 프론트에서 code 값 기준으로 분기가 가능하게 쉽게 하고, 로깅 / 모니터링에 용이하며 나중에 ValidationError나 PermissionError, DBError와 같은 핸들러를 추가로 등록하여 확장 가능하게 한다.
에러 핸들러 추가
지금까지는 AppError를 FastAPI 앱에 등록하여 에러 발생 시 app_error_handler가 에러를 처리하게끔 했다. 하지만, 422 에러와 같이 FastAPI가 자동으로 내리는 에러에 대해서는 포맷이 일치하지 않는다. 따라서 이 에러에 대해서도 포맷을 맞추기 위해 app에 핸들러를 추가로 등록할 수 있다.
from fastapi.exceptions import RequestValidationError
...
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
"""Custom exception handler for request validation errors."""
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
content={
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": exc.errors(),
},
)
Python
복사
예를 들어 /math/div를 실행하기 위해서는 a와 b 둘 다 필요한데 b가 없을 경우 FastAPI 앱은 422 에러를 raise 한다. 이 때 FastAPI는 등록한 핸들러인 validation_exception_handler를 사용하여 에러를 처리한다.
마찬가지로 HTTPException(FastAPI 기본 404/400/401/403 등)도 포맷을 통일할 수 있다.
from starlette.exceptions import HTTPException as StarletteHTTPException
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
"""Custom exception handler for HTTP exceptions."""
message = exc.detail if isinstance(exc.detail, str) else "An HTTP error occurred."
return JSONResponse(
status_code=exc.status_code,
content={"code": f"HTTP_{exc.status_code}", "message": message},
)
Python
복사