본문 바로가기
튜토리얼/백엔드

[Python] FastAPI로 REST API 서버 만들기 — Pydantic, CRUD, 자동 문서화

by 루까(Luka) 2026. 3. 4.
반응형

왜 FastAPI인가

Python 백엔드 프레임워크 하면 Flask나 Django가 먼저 떠오른다. FastAPI는 2019년에 등장했지만 이미 Star 수에서 Flask를 앞질렀다. 이유는 세 가지다.

첫째, 빠르다. Starlette + uvicorn 기반 비동기 서버라 Node.js, Go 수준의 처리량이 나온다. 둘째, 타입 힌트 기반 자동 검증이다. Pydantic 모델을 정의하면 요청 파싱, 유효성 검사, 에러 메시지가 자동으로 처리된다. 셋째, 자동 문서화다. 코드를 작성하면 /docs에서 Swagger UI가 즉시 열린다.

Flask와 비교하면:

항목 Flask FastAPI
성능 WSGI (동기) ASGI (비동기)
입력 검증 직접 구현 Pydantic 자동 처리
문서화 별도 설정 /docs 자동 생성
타입 힌트 선택 핵심 설계 원칙
학습 곡선 낮음 낮음

이번 튜토리얼은 도서 관리 API다. CRUD 전체와 검색, 필터링, 페이지네이션까지 구현한다.


앱 구조

23-python-fastapi/
├── main.py          # FastAPI 앱
└── requirements.txt # fastapi, uvicorn

엔드포인트:

메서드 경로 설명
GET / 서버 상태
GET /books 목록 조회 (limit, offset, genre, min_rating)
GET /books/search 제목·저자 검색
GET /books/{id} 단일 조회
POST /books 추가
PUT /books/{id} 수정
DELETE /books/{id} 삭제
GET /books/genre/{genre} 장르별 목록

핵심 구현

Pydantic 모델

class BookBase(BaseModel):
    title:  str   = Field(..., min_length=1, max_length=200)
    author: str   = Field(..., min_length=1, max_length=100)
    price:  float = Field(..., gt=0)
    rating: int   = Field(..., ge=1, le=5)
    genre:  Optional[str] = None

class BookCreate(BookBase):
    pass

class BookUpdate(BaseModel):
    # 모든 필드 Optional — 입력한 필드만 수정
    title:  Optional[str]   = None
    author: Optional[str]   = None
    price:  Optional[float] = None
    rating: Optional[int]   = None
    genre:  Optional[str]   = None

class BookResponse(BookBase):
    id: int
    created_at: str

BookBase에 공통 필드를 모아두고, Create / Update / Response를 각각 분리한다. Field()min_length, gt, ge, le는 Pydantic이 자동으로 검증한다. 조건을 어기면 FastAPI가 422 응답과 함께 어떤 필드가 잘못됐는지 자동으로 설명한다.


앱 초기화

app = FastAPI(
    title="도서 관리 API",
    description="FastAPI 튜토리얼 — CRUD + 검색 + 자동 문서화",
    version="1.0.0",
)

title, description, version이 그대로 /docs Swagger UI에 표시된다.


GET 목록 — 쿼리 파라미터

@app.get("/books", response_model=list[BookResponse])
def list_books(
    limit:      int           = Query(10, ge=1, le=100),
    offset:     int           = Query(0, ge=0),
    genre:      Optional[str] = Query(None),
    min_rating: int           = Query(1, ge=1, le=5),
):
    books = list(_books.values())
    if genre:
        books = [b for b in books if b.get("genre") == genre]
    books = [b for b in books if b["rating"] >= min_rating]
    return books[offset: offset + limit]

Query()로 기본값, 유효성 범위를 선언한다. response_model=list[BookResponse]는 응답을 자동으로 필터링하고 직렬화한다. 내부 dict에 _secret 같은 필드가 있어도 BookResponse에 없으면 응답에 포함되지 않는다.


POST 생성 — 요청 바디

@app.post("/books", response_model=BookResponse, status_code=201)
def create_book(book: BookCreate):
    global _next_id
    new_book = {
        **book.model_dump(),
        "id": _next_id,
        "created_at": datetime.now().isoformat(),
    }
    _books[_next_id] = new_book
    _next_id += 1
    return new_book

함수 파라미터가 Pydantic 모델 타입이면 FastAPI가 요청 바디로 인식한다. model_dump()로 딕셔너리로 변환하고 id, created_at을 추가한다. status_code=201은 성공 응답 코드를 변경한다.


PUT 수정 — 부분 업데이트

@app.put("/books/{book_id}", response_model=BookResponse)
def update_book(book_id: int, updates: BookUpdate):
    book = _get_book_or_404(book_id)
    for field, value in updates.model_dump(exclude_none=True).items():
        book[field] = value
    return book

model_dump(exclude_none=True)None인 필드를 제외하고 딕셔너리로 반환한다. 입력된 필드만 업데이트되므로 PATCH 방식처럼 동작한다.


HTTPException — 404 처리

def _get_book_or_404(book_id: int) -> dict:
    book = _books.get(book_id)
    if not book:
        raise HTTPException(status_code=404, detail=f"ID {book_id} 책을 찾을 수 없습니다.")
    return book

HTTPException을 raise하면 FastAPI가 적절한 JSON 에러 응답을 자동으로 반환한다. {"detail": "..."} 형태로 응답된다.


실행 및 테스트

pip3 install -r requirements.txt
uvicorn main:app --reload

서버가 http://localhost:8000에서 실행된다. --reload는 코드 변경 시 자동 재시작.

자동 문서화:

  • http://localhost:8000/docs → Swagger UI (직접 API 테스트 가능)
  • http://localhost:8000/redoc → ReDoc (읽기 좋은 문서)

curl 테스트:

# 목록 조회
curl http://localhost:8000/books?limit=3

# 장르 필터 + 최소 평점
curl "http://localhost:8000/books?genre=소설&min_rating=4"

# 단일 조회
curl http://localhost:8000/books/1

# 추가
curl -X POST http://localhost:8000/books \
  -H "Content-Type: application/json" \
  -d '{"title":"새 책","author":"저자","price":15000,"rating":4,"genre":"개발"}'

# 수정
curl -X PUT http://localhost:8000/books/1 \
  -H "Content-Type: application/json" \
  -d '{"price": 20000}'

# 삭제
curl -X DELETE http://localhost:8000/books/1

# 검색
curl "http://localhost:8000/books/search?q=클린"

삽질 기록

/books/search 라우팅 충돌

/books/{book_id}/books/search를 함께 쓰면 FastAPI가 searchbook_id의 문자열 값으로 인식할 수 있다. book_idint로 선언하면 search는 int 변환에 실패해서 /books/search로 올바르게 라우팅된다. 타입 힌트가 라우팅 우선순위 문제를 해결한다.

model_dump() vs dict()

Pydantic v2부터 .dict()는 deprecated이고 .model_dump()를 써야 한다. exclude_none=Trueexclude_unset=True의 차이도 있다. exclude_none=True는 값이 None인 모든 필드 제외, exclude_unset=True는 클라이언트가 아예 보내지 않은 필드만 제외한다. 부분 업데이트엔 exclude_unset=True가 더 정확하지만 이번 튜토리얼에선 exclude_none=True로 단순화했다.

uvicorn 직접 실행 vs python main.py

uvicorn main:app --reload 방식이 표준이다. if __name__ == "__main__": uvicorn.run(...) 패턴은 스크립트로 직접 실행할 때 편의를 위해 넣어두지만, --reload가 필요하면 CLI에서 직접 실행해야 한다.


마무리

FastAPI의 핵심은 타입 힌트 = 문서 + 검증 + 직렬화라는 점이다. 별도의 설정 없이 Pydantic 모델 하나로 입력 검증, 에러 응답, Swagger 문서가 모두 자동화된다. Flask로 같은 기능을 구현하려면 marshmallow나 Flask-RESTX 같은 라이브러리를 추가로 붙여야 한다.

실제 서비스에서는 인메모리 저장소 대신 SQLAlchemy + PostgreSQL이나 SQLModel을 연결하면 된다. 구조 자체는 이번 튜토리얼과 동일하다.


기술 스택: Python 3.x · FastAPI 0.115.6 · uvicorn 0.32.1 · Pydantic v2
소스 코드: GitHub

반응형