모던 파이썬 실전 #1 FastAPI 시작과 셋업
모던 파이썬 기초 → 중급 → 고급 21편에서 다진 도구를 FastAPI 한 프로젝트로 종합합니다. 타입 힌트, dataclass/Pydantic, async/await, 의존성 주입, 테스트까지 — 지금까지 익힌 도구를 한곳에 모읍니다.
- #1 시작과 셋업, OpenAPI ← 이번 글
- #2 라우팅, Pydantic 모델, 의존성 주입
- #3 DB 연동 — SQLAlchemy 2.x + Alembic
- #4 인증 — OAuth2 패스워드 플로우 + JWT
- #5 비동기와 백그라운드 작업
- #6 테스트와 배포 — pytest, Docker, Railway/Fly
만드는 것은 작은 할 일 API — 사용자 회원가입/로그인, 할 일 CRUD, 비동기 작업 처리, 테스트, 배포까지 정리합니다. 매 글은 그 위에 한 단계씩 쌓는 구성입니다.
왜 FastAPI인가 #
Python 웹 프레임워크는 여럿입니다. Django, Flask, FastAPI, Litestar, Starlette 등. 각자 강점이 있습니다.
| Django | Flask | FastAPI | |
|---|---|---|---|
| 스타일 | 풀스택 (배터리 포함) | 마이크로 | 마이크로 + 타입 |
| 비동기 | 부분 (4.0+) | 외부 라이브러리 | 네이티브 |
| 타입 힌트 활용 | 보조적 | 보조적 | 핵심 동작 |
| 자동 문서 (OpenAPI) | 별도 라이브러리 | 별도 | 빌트인 |
| 학습 곡선 | 가파름 | 매우 평탄 | 평탄 |
| 데이터 검증 | DRF + 직접 | 직접 | Pydantic 자동 |
FastAPI의 강점은 타입 힌트가 그대로 동작한다는 점입니다. 함수 시그니처를 적으면 그게 곧:
- 입력 검증
- 직렬화/역직렬화
- OpenAPI 스키마 (Swagger UI 자동 생성)
- 에디터 자동완성
- 정적 타입 검사
이 다섯이 한 번에 일어납니다. 다른 프레임워크는 각각을 별도 설정으로 잡아야 합니다. 모던 파이썬 시리즈에서 다진 타입 힌트가 가장 잘 통하는 영역이 FastAPI이고, 그래서 트랙의 수렴점으로 잡았습니다.
프로젝트 셋업 #
기초 #1의 uv 흐름으로 시작합니다.
uv init todo-api --python 3.14
cd todo-api의존성 설치 #
uv add "fastapi[standard]"fastapi[standard] 한 줄에 따라오는 것들:
- FastAPI — 프레임워크 본체
- uvicorn — ASGI 서버 (개발/프로덕션)
- pydantic — 데이터 검증
- httpx — 테스트용 클라이언트 (다음 글들에서)
- email-validator —
EmailStr검증 - python-multipart — 폼/파일 업로드
- fastapi-cli —
fastapiCLI 명령
다음으로 개발용 도구.
uv add --dev pytest pytest-asyncio ruff pyright고급 #7에서 본 도구상자에 더해, FastAPI 테스트의 표준은 pytest + httpx 조합입니다 (#6에서 자세히).
첫 엔드포인트 #
from fastapi import FastAPI
app = FastAPI(
title="Todo API",
version="0.1.0",
description="모던 파이썬 실전 시리즈의 예제 API",
)
@app.get("/")
def read_root() -> dict[str, str]:
return {"hello": "fastapi"}이게 끝입니다. 7 줄의 파이썬으로 HTTP 서버가 됩니다.
실행 #
uv run fastapi dev app/main.pyfastapi dev가 하는 일:
- uvicorn을 띄우고
- 코드 변경 시 자동 리로드
- 디버그 로그
- LAN 노출 안 함 (
127.0.0.1만)
브라우저에서 http://127.0.0.1:8000을 열면:
{"hello": "fastapi"}더 중요한 — 자동 문서 #
같은 서버의 /docs로 가보세요. Swagger UI가 자동으로 떠 있습니다. 엔드포인트 목록, 입력 스키마, 응답 모델, 바로 호출해 볼 수 있는 “Try it out” 버튼까지 정리합니다. 코드 한 줄도 더 안 적었습니다.
/redoc으로 가면 ReDoc 스타일 문서도 같이 있습니다.
이게 FastAPI가 말하는 자동 문서의 정체입니다. 함수 시그니처 + 타입 힌트 + docstring이 그대로 OpenAPI 명세로 변환됩니다.
더 풍부한 엔드포인트 #
from fastapi import FastAPI
app = FastAPI(title="Todo API", version="0.1.0")
@app.get("/")
def read_root() -> dict[str, str]:
"""루트 — 헬스체크용."""
return {"hello": "fastapi"}
@app.get("/items/{item_id}")
def read_item(item_id: int) -> dict[str, int]:
"""ID로 단일 아이템 조회."""
return {"item_id": item_id}
@app.get("/items")
def list_items(skip: int = 0, limit: int = 10) -> dict[str, int]:
"""페이지네이션 쿼리 파라미터."""
return {"skip": skip, "limit": limit}여기서 일어나는 자동 변환:
- 경로 매개변수
{item_id}→ 함수 인자item_id: intint가 아니면 422 Unprocessable Entity 자동 응답
- 쿼리 매개변수
skip,limit→ 함수의 기본값 있는 인자?skip=10&limit=20형태로 자동 파싱
- 반환 타입
-> dict[str, int]→ JSON 응답 - docstring → Swagger UI의 설명
int 타입을 적었는데 자동으로 변환,검증이 되는 게 핵심입니다. Flask 라면:
@app.route("/items/<int:item_id>")
def read_item(item_id):
return jsonify({"item_id": item_id})<int:item_id>같이 라우터에 타입을 적고, 검증 실패 응답을 직접 처리해야 합니다. FastAPI는 함수 시그니처가 라우터 명세입니다.
Path, Query — 더 세밀한 검증
#
from fastapi import FastAPI, Path, Query
from typing import Annotated
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(
item_id: Annotated[int, Path(ge=1, le=10000, description="아이템 ID")],
q: Annotated[str | None, Query(min_length=3, max_length=50)] = None,
):
return {"item_id": item_id, "q": q}Path(), Query()가 검증 규칙과 메타데이터를 추가합니다.
ge=1—>= 1le=10000—<= 10000min_length=3— 최소 3자description=...— Swagger 문서에 표시
Annotated[T, ...] 패턴이 고급 #6에서 본 그것입니다 — 타입에 메타데이터를 묶는 표준. FastAPI의 거의 모든 의존성 주입이 이 패턴 위에 만들어졌습니다.
응답 — 자동 직렬화 #
from datetime import datetime, timezone
from pydantic import BaseModel
class Todo(BaseModel):
id: int
title: str
done: bool
created_at: datetime
@app.get("/todos/1")
def get_todo() -> Todo:
return Todo(
id=1,
title="FastAPI 학습",
done=False,
created_at=datetime.now(timezone.utc),
)응답:
{
"id": 1,
"title": "FastAPI 학습",
"done": false,
"created_at": "2026-05-01T12:00:00+00:00"
}datetime 같은 비-JSON 타입도 자동 변환 됩니다. Pydantic이 직렬화를 담당합니다. 이걸 직접 짜면 라우트마다 변환 코드를 써야 하지만, FastAPI는 반환 타입만 적으면 끝.
Todo 모델에 대한 자세한 내용은 #2에서.
에러 — HTTPException
#
from fastapi import HTTPException
todos: dict[int, Todo] = {1: Todo(...)}
@app.get("/todos/{todo_id}")
def get_todo(todo_id: int) -> Todo:
if todo_id not in todos:
raise HTTPException(status_code=404, detail="Todo not found")
return todos[todo_id]raise HTTPException(...) 한 줄로 JSON 에러 응답이 나갑니다.
{"detail": "Todo not found"}표준 HTTP 상태 코드 (404, 400, 500 등)와 함께. 다음 글들에서 사용자 정의 예외 핸들러도 다룹니다.
프로젝트 구조 — 처음부터 단정하게 #
작은 API 라도 처음부터 모듈/패키지로 나누면 기초 #7에서 본 흐름이 그대로 살아납니다.
todo-api/
├── pyproject.toml
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI 인스턴스, 라우터 등록
│ ├── core/
│ │ ├── config.py # 환경 변수
│ │ └── security.py # JWT, 패스워드 (#4)
│ ├── api/
│ │ ├── __init__.py
│ │ ├── deps.py # 의존성 주입 (#2, #4)
│ │ ├── todos.py # 할 일 라우터
│ │ └── auth.py # 인증 라우터 (#4)
│ ├── models/ # SQLAlchemy 모델 (#3)
│ │ └── todo.py
│ ├── schemas/ # Pydantic 스키마 (#2)
│ │ └── todo.py
│ └── db/ # DB 연결 (#3)
│ └── session.py
├── alembic/ # 마이그레이션 (#3)
└── tests/ # pytest (#6)
└── test_todos.py이번 글에서는 app/main.py 한 파일이지만, 다음 글부터 한 디렉터리씩 채워 나갑니다.
환경 변수 — pydantic-settings
#
설정값(DB URL, JWT 시크릿 등)을 코드에 박지 말고 환경 변수로 분리하는 게 표준입니다. FastAPI 진영의 표준 도구는 pydantic-settings.
uv add pydantic-settingsfrom pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
app_name: str = "Todo API"
database_url: str = "sqlite+aiosqlite:///./todo.db"
jwt_secret: str = "dev-only-change-me"
jwt_expire_minutes: int = 60
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
settings = Settings().env 파일에서 자동으로 읽고, 환경 변수가 있으면 그걸로 덮어씁니다. 타입 검증까지 포함이라 잘못된 환경 변수는 시작 시점에 막힙니다.
JWT_SECRET=production-secret-here
DATABASE_URL=postgresql+asyncpg://user:pw@localhost/todo.env는 git에서 제외 (.gitignore에 추가). 보안 주의.
빠르게 정리한 첫 글의 결과물 #
from fastapi import FastAPI, HTTPException
from app.core.config import settings
app = FastAPI(
title=settings.app_name,
version="0.1.0",
)
@app.get("/")
def read_root() -> dict[str, str]:
return {"app": settings.app_name, "status": "ok"}
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "healthy"}uv run fastapi dev app/main.py
# → http://127.0.0.1:8000
# → http://127.0.0.1:8000/docs엔드포인트 두 개 + 자동 문서 + 환경 변수 분리. 다음 글의 출발점입니다.
정리 #
이번 글에서 잡은 것:
- 왜 FastAPI — 타입 힌트가 검증/직렬화/문서/자동완성에 그대로 동작
uv add "fastapi[standard]"한 줄에 uvicorn/pydantic/httpx 다 따라옴fastapi dev로 자동 리로드 개발 모드- 함수 시그니처가 라우터 명세 — 경로/쿼리 매개변수 타입 변환 자동
/docs,/redoc— OpenAPI/Swagger UI 자동 생성Annotated[T, Path(...)],Annotated[T, Query(...)]로 세밀한 검증- Pydantic 모델 반환 — datetime 등 자동 직렬화
HTTPException으로 에러 응답pydantic-settings로 환경 변수 분리- 처음부터 모듈/패키지 구조
다음 글(#2 라우팅, Pydantic, 의존성 주입)에서는 APIRouter로 라우트를 분리하고, Pydantic 스키마를 본격적으로 사용하고, 의존성 주입으로 라우트 간 공통 로직(인증된 사용자, DB 세션 등)을 깔끔하게 풀어내는 패턴을 다룹니다.