FastAPI 시작 가이드 — 현대적 Python REST API

FastAPI란?

FastAPI는 파이썬으로 고성능 REST API를 빠르게 만들 수 있는 현대적 웹 프레임워크입니다. Python 타입 힌트를 기반으로 자동 유효성 검사, 자동 API 문서화(Swagger/OpenAPI)를 제공하며, 비동기(async/await)를 네이티브로 지원합니다.

Flask보다 빠르고, Django REST Framework보다 간결하며, Node.js/Go 수준의 성능을 제공한다는 점에서 빠르게 인기를 얻고 있습니다. 이 글에서는 설치부터 CRUD API 구현, 의존성 주입, 에러 핸들링까지 다룹니다.

설치와 첫 API

pip install fastapi uvicorn[standard]
# fastapi: 웹 프레임워크
# uvicorn: ASGI 서버 (비동기 서버)
# main.py
from fastapi import FastAPI

app = FastAPI(
    title="블로그 API",
    description="블로그 포스트 관리 API",
    version="1.0.0",
)

@app.get("/")
async def root():
    """API 상태 확인"""
    return {"message": "블로그 API가 동작 중입니다", "status": "ok"}

@app.get("/hello/{name}")
async def hello(name: str):
    """이름을 받아 인사합니다."""
    return {"message": f"안녕하세요, {name}님!"}
# 서버 실행
uvicorn main:app --reload --port 8000

# 자동 생성된 API 문서 확인
# Swagger UI: http://localhost:8000/docs
# ReDoc: http://localhost:8000/redoc

서버를 실행하면 타입 힌트 정보를 기반으로 Swagger UI 문서가 자동 생성됩니다. 별도의 문서화 작업 없이 API를 테스트하고 공유할 수 있습니다.

Pydantic 모델 — 요청/응답 스키마

FastAPI는 Pydantic 모델을 사용하여 요청 본문의 유효성을 자동으로 검사합니다. 잘못된 데이터가 들어오면 422 에러와 함께 상세한 에러 메시지를 반환합니다.

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from datetime import datetime

app = FastAPI()

# Pydantic 모델 — 요청/응답 스키마 정의
class PostCreate(BaseModel):
    """포스트 생성 요청"""
    title: str = Field(min_length=1, max_length=200,
                       description="포스트 제목")
    content: str = Field(min_length=10,
                         description="포스트 본문 (10자 이상)")
    tags: list[str] = Field(default_factory=list,
                            description="태그 목록")

class PostResponse(BaseModel):
    """포스트 응답"""
    id: int
    title: str
    content: str
    tags: list[str]
    created_at: datetime

# 인메모리 DB (예시용)
posts_db: dict[int, dict] = {}
next_id = 1

@app.post("/posts", response_model=PostResponse,
           status_code=201)
async def create_post(post: PostCreate):
    """새 포스트를 생성합니다."""
    global next_id
    now = datetime.now()
    new_post = {
        "id": next_id,
        "title": post.title,
        "content": post.content,
        "tags": post.tags,
        "created_at": now,
    }
    posts_db[next_id] = new_post
    next_id += 1
    return new_post

@app.get("/posts/{post_id}", response_model=PostResponse)
async def get_post(post_id: int):
    """포스트를 조회합니다."""
    if post_id not in posts_db:
        raise HTTPException(status_code=404,
                            detail="포스트를 찾을 수 없습니다")
    return posts_db[post_id]

PostCreate 모델에 정의된 제약 조건(min_length, max_length)을 위반하면 FastAPI가 자동으로 422 에러를 반환합니다. 유효성 검사 코드를 직접 작성할 필요가 없습니다.

쿼리 파라미터와 페이지네이션

from fastapi import Query

@app.get("/posts", response_model=list[PostResponse])
async def list_posts(
    skip: int = Query(default=0, ge=0,
                      description="건너뛸 항목 수"),
    limit: int = Query(default=10, ge=1, le=100,
                       description="가져올 항목 수"),
    tag: str | None = Query(default=None,
                            description="태그로 필터링"),
):
    """포스트 목록을 조회합니다 (페이지네이션 지원)."""
    all_posts = list(posts_db.values())

    # 태그 필터링
    if tag:
        all_posts = [p for p in all_posts if tag in p["tags"]]

    # 페이지네이션 적용
    return all_posts[skip : skip + limit]

# GET /posts?skip=0&limit=5&tag=python

Query를 사용하면 파라미터의 기본값, 범위 제한, 설명을 선언적으로 정의할 수 있고, 이 정보가 Swagger 문서에 자동 반영됩니다.

의존성 주입 (Dependency Injection)

FastAPI의 의존성 주입 시스템은 인증, DB 세션 관리 등 횡단 관심사를 깔끔하게 분리합니다.

from fastapi import Depends, Header, HTTPException

# 인증 의존성
async def verify_api_key(
    x_api_key: str = Header(description="API 인증 키")
) -> str:
    """API 키를 검증합니다."""
    valid_keys = {"key-abc-123", "key-xyz-789"}
    if x_api_key not in valid_keys:
        raise HTTPException(
            status_code=401,
            detail="유효하지 않은 API 키입니다"
        )
    return x_api_key

# 보호된 엔드포인트 — Depends로 의존성 주입
@app.delete("/posts/{post_id}")
async def delete_post(
    post_id: int,
    api_key: str = Depends(verify_api_key),  # 인증 필수
):
    """포스트를 삭제합니다 (인증 필요)."""
    if post_id not in posts_db:
        raise HTTPException(status_code=404,
                            detail="포스트를 찾을 수 없습니다")
    deleted = posts_db.pop(post_id)
    return {"message": f"포스트 '{deleted['title']}' 삭제됨"}

# DB 세션 의존성 예시
async def get_db_session():
    """DB 세션을 생성하고 종료합니다."""
    session = {"connected": True}  # 실제로는 DB 연결
    try:
        yield session  # yield로 세션 제공
    finally:
        session["connected"] = False  # 요청 완료 후 정리

Depends를 사용하면 인증 로직이 각 엔드포인트에서 분리되어 코드 중복이 사라지고, 테스트 시 의존성을 쉽게 교체할 수 있습니다.

에러 핸들링

from fastapi import Request
from fastapi.responses import JSONResponse

class PostNotFoundError(Exception):
    """포스트를 찾을 수 없을 때 발생하는 예외"""
    def __init__(self, post_id: int):
        self.post_id = post_id

# 커스텀 예외 핸들러 등록
@app.exception_handler(PostNotFoundError)
async def post_not_found_handler(
    request: Request, exc: PostNotFoundError
):
    return JSONResponse(
        status_code=404,
        content={
            "error": "POST_NOT_FOUND",
            "message": f"포스트 ID {exc.post_id}를 찾을 수 없습니다",
            "path": str(request.url),
        },
    )

# 사용 예시
@app.get("/posts/{post_id}/detail")
async def get_post_detail(post_id: int):
    if post_id not in posts_db:
        raise PostNotFoundError(post_id)
    return posts_db[post_id]

# 응답 예시:
# {
#   "error": "POST_NOT_FOUND",
#   "message": "포스트 ID 99를 찾을 수 없습니다",
#   "path": "http://localhost:8000/posts/99/detail"
# }

미들웨어 — 요청/응답 공통 처리

import time
from fastapi.middleware.cors import CORSMiddleware

# CORS 설정
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],  # 프론트엔드 도메인
    allow_methods=["*"],
    allow_headers=["*"],
)

# 커스텀 미들웨어: 응답 시간 측정
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    start = time.perf_counter()
    response = await call_next(request)
    elapsed = time.perf_counter() - start
    response.headers["X-Process-Time"] = f"{elapsed:.4f}"
    return response

실전 팁

  • 프로젝트 구조: 라우터(APIRouter)로 엔드포인트를 모듈별로 분리합니다
  • 환경변수: pydantic-settings로 설정값을 타입 안전하게 관리합니다
  • 비동기 DB: sqlalchemy[asyncio] 또는 tortoise-orm으로 비동기 DB 접근을 사용합니다
  • 테스트: httpx.AsyncClient + pytest-asyncio로 비동기 테스트를 작성합니다
  • 배포: uvicorn 대신 gunicorn -k uvicorn.workers.UvicornWorker로 프로덕션 배포합니다
  • 버전 관리: URL 프리픽스(/api/v1/)로 API 버전을 관리합니다
  • 응답 모델: response_model을 항상 지정하여 응답 스키마를 명시합니다

이 글이 도움이 되었나요?