왜 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가 search를 book_id의 문자열 값으로 인식할 수 있다. book_id를 int로 선언하면 search는 int 변환에 실패해서 /books/search로 올바르게 라우팅된다. 타입 힌트가 라우팅 우선순위 문제를 해결한다.
model_dump() vs dict()
Pydantic v2부터 .dict()는 deprecated이고 .model_dump()를 써야 한다. exclude_none=True와 exclude_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
'튜토리얼 > 백엔드' 카테고리의 다른 글
| [PHP] 파일 업로드 시스템 만들기 — 다중 업로드, 파일 교체, 다운로드 스트리밍 (0) | 2026.03.05 |
|---|---|
| [PHP] REST API 서버 만들기 — 프레임워크 없이 순수 PHP로 CRUD API 구현 (0) | 2026.03.04 |
| [PHP] 게시판 CRUD 만들기 — SQLite, CSRF 방어, 소유권 검사, 페이지네이션 (0) | 2026.03.04 |
| [PHP] 로그인/회원가입 시스템 만들기 — PDO SQLite, 세션, CSRF 방어 (0) | 2026.03.04 |