FastAPI 핵심 개념 정리
Python으로 API 서버를 만드는 현대적인 웹 프레임워크 (2018년 출시).
3가지 핵심 특징:
/docs)가 자동 생성가장 간단한 예시:
from fastapi import FastAPI
app = FastAPI()
@app.get("/hello")
def say_hello():
return {"message": "안녕하세요!"}
| Django | Flask | FastAPI | |
|---|---|---|---|
| 목적 | 웹사이트 전체 | 범용 | API 서버 전문 |
| 속도 | 보통 | 보통 | 매우 빠름 |
| 비동기 | 부분 지원 | 미지원(기본) | 완전 지원 |
| 자동 문서화 | 없음 | 없음 | 자동 생성 |
| 학습 난이도 | 높음 | 낮음 | 낮음~중간 |
| 대표 용도 | 인스타그램, 핀터레스트 | 프로토타입 | AI/ML API, 마이크로서비스 |
언제 무엇을 쓰나?
# 패키지 설치
pip install fastapi uvicorn
# 기본 실행
uvicorn main:app --reload
# uv 사용 시 (가상환경 자동 적용)
uv run uvicorn app.main:app --reload
명령어 분해:
main — Python 파일 이름 (main.py)app — 파일 안의 app = FastAPI() 객체 이름--reload — 코드 수정 시 자동 재시작 (개발용)app.main — app/main.py 파일 (점이 폴더 구분자)실행 후 접속:
http://localhost:8000 — API 서버http://localhost:8000/docs — Swagger 자동 문서화데코레이터 하나로 메서드가 결정됨.
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
# GET — 데이터 조회
@app.get("/items")
def get_items():
return [{"id": 1, "name": "사과"}]
# 경로 파라미터 (/items/5)
@app.get("/items/{item_id}")
def get_item(item_id: int):
return {"id": item_id}
# 쿼리 파라미터 (/items?q=사과)
@app.get("/items")
def search_items(q: str = None):
return {"query": q}
# POST — 데이터 생성
class Item(BaseModel):
name: str
price: float
@app.post("/items", status_code=201)
def create_item(item: Item):
return {"message": "생성됨", "item": item}
# PUT — 데이터 수정 (전체 교체)
@app.put("/items/{item_id}")
def update_item(item_id: int, item: Item):
return {"id": item_id, "updated": item}
# DELETE — 데이터 삭제
@app.delete("/items/{item_id}")
def delete_item(item_id: int):
return {"message": f"{item_id}번 아이템 삭제됨"}
파라미터 3가지 구분:
| 종류 | 예시 URL | 코드 |
|---|---|---|
| 경로 파라미터 | /items/5 | item_id: int (중괄호로 선언) |
| 쿼리 파라미터 | /items?q=사과 | q: str = None (기본값 있으면 자동으로 쿼리) |
| 바디 | POST body | item: Item (Pydantic 모델) |
Django DRF와 비교:
# Django DRF
class ItemView(APIView):
def get(self, request): ...
def post(self, request): ...
# FastAPI — 함수 단위로 훨씬 간결
@app.get("/items")
def get_items(): ...
@app.post("/items")
def create_item(): ...
Python 3.5+에서 변수나 함수에 타입을 명시하는 문법.
def greet(name: str) -> str:
return "Hello " + name
FastAPI는 타입 힌트를 보고 자동으로:
1. URL 파라미터를 해당 타입으로 변환
2. 잘못된 타입 입력 시 422 에러 자동 응답
3. /docs에 타입 정보 자동 표시
타입 힌트를 기반으로 데이터를 검증해주는 라이브러리. POST 요청의 body 데이터를 다룰 때 주로 사용.
from pydantic import BaseModel
from typing import Optional
class User(BaseModel):
name: str # 필수
age: int # 필수
bio: Optional[str] = None # 선택 (없어도 됨)
is_admin: bool = False # 기본값 False
자동 검증 예시:
{"name": "김철수", "age": 25} → 통과{"name": "김철수", "age": "스물다섯"} → 422 에러 (age가 문자열){"name": "김철수"} → 422 에러 (age 누락)Django DRF와 비교:
# Django DRF — 검증을 직접 호출해야 함
class UserSerializer(serializers.Serializer):
name = serializers.CharField()
age = serializers.IntegerField()
serializer = UserSerializer(data=request.data)
if serializer.is_valid(): # 직접 호출
data = serializer.validated_data
return Response(serializer.errors) # 에러 처리도 직접
# FastAPI — 함수 인자에 모델만 쓰면 자동으로 다 됨
@app.post("/users")
def create_user(user: User): # 검증, 변환, 에러 응답 자동
return user
is_valid()깜빡하면 검증 안 된 데이터가 통과되는 Django의 버그 가능성이 FastAPI에선 구조적으로 불가능.
응답 데이터의 형태를 지정. 민감한 필드(비밀번호 등)를 응답에서 제외할 때 필수.
from pydantic import BaseModel
class UserCreate(BaseModel): # 입력용 (POST body)
name: str
email: str
password: str
class UserResponse(BaseModel): # 응답용 (password 없음)
id: int
name: str
email: str
model_config = {"from_attributes": True}
@app.post("/users", response_model=UserResponse)
def create_user(user: UserCreate):
saved_user = save_to_db(user)
return saved_user # password 있어도 응답엔 자동 제외
자주 쓰는 옵션:
# 리스트 응답
@app.get("/users", response_model=list[UserResponse])
def get_users(): ...
# None 필드 제외
@app.get("/users/{id}", response_model=UserResponse,
response_model_exclude_none=True)
def get_user(id: int): ...
# 특정 필드 제외
@app.get("/users/{id}", response_model=UserResponse,
response_model_exclude={"email"})
def get_user(id: int): ...
실무 모델 분리 패턴:
UserBase ← 공통 필드 (name, email)
├── UserCreate ← UserBase + password (입력용)
├── UserUpdate ← 수정 시 필요한 필드 (수정용)
└── UserResponse ← UserBase + id (응답용)
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/items/{item_id}")
def get_item(item_id: int):
item = db.get(item_id)
if not item:
raise HTTPException(status_code=404, detail="아이템을 찾을 수 없어요")
return item
자주 쓰는 상황별 에러:
raise HTTPException(status_code=404, detail="존재하지 않는 리소스")
raise HTTPException(status_code=401, detail="로그인이 필요해요")
raise HTTPException(status_code=403, detail="접근 권한이 없어요")
raise HTTPException(status_code=400, detail="잘못된 요청이에요")
# 헤더 추가
raise HTTPException(
status_code=401,
detail="로그인이 필요해요",
headers={"WWW-Authenticate": "Bearer"},
)
전역 에러 핸들러:
from fastapi import Request
from fastapi.responses import JSONResponse
@app.exception_handler(404)
async def not_found_handler(request: Request, exc):
return JSONResponse(
status_code=404,
content={"message": "페이지를 찾을 수 없어요", "path": str(request.url)}
)
커스텀 예외 클래스:
class ItemNotFoundError(Exception):
def __init__(self, item_id: int):
self.item_id = item_id
@app.exception_handler(ItemNotFoundError)
async def item_not_found_handler(request: Request, exc: ItemNotFoundError):
return JSONResponse(
status_code=404,
content={"message": f"{exc.item_id}번 아이템이 없어요"}
)
HTTP 상태코드 정리:
| 코드 | 의미 | 언제 쓰나 |
|---|---|---|
| 400 | Bad Request | 요청 형식이 잘못됨 |
| 401 | Unauthorized | 로그인 안 됨 |
| 403 | Forbidden | 권한 없음 |
| 404 | Not Found | 리소스 없음 |
| 422 | Unprocessable | Pydantic 검증 실패 (자동) |
| 500 | Server Error | 서버 내부 오류 |
DB/API 대기 시간에 다른 요청을 처리 → 같은 서버로 더 많은 요청 처리 가능.
동기 vs 비동기 흐름:
[동기] 요청1 처리 끝나야 요청2 시작
요청1: [코드실행] → [DB 대기 3초...] → [완료]
요청2: [코드실행] → ...
[비동기] DB 대기 중에 다른 요청 처리
요청1: [코드실행] → [DB 대기 중...] → [완료]
요청2: [코드실행] → [DB 대기 중...] → [완료]
FastAPI 코드:
# 비동기 — async def + await
@app.get("/users/{id}")
async def get_user(id: int):
user = await db.fetch(id) # DB 기다리는 동안 다른 요청 처리
return user
# 동기 — DB 응답 올 때까지 멈춤 (블로킹)
@app.get("/users/{id}")
def get_user(id: int):
user = db.fetch(id)
return user
언제 async를 쓰나?
httpx, aiohttp) ✅def 사용 (FastAPI가 자동으로 별도 스레드 처리)Django는 ORM이 내장이지만, FastAPI는 직접 연결 필요. 가장 많이 쓰는 조합은 SQLAlchemy.
파일 구조:
app/
├── database.py ← DB 연결 설정
├── models.py ← SQLAlchemy 테이블 모델
├── schemas.py ← Pydantic 입출력 모델
└── main.py ← 라우터
database.py:
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, DeclarativeBase
DATABASE_URL = "sqlite:///./app.db" # PostgreSQL이면 "postgresql://..."
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(bind=engine)
class Base(DeclarativeBase):
pass
def get_db():
db = SessionLocal()
try:
yield db # 요청 처리 동안 세션 유지
finally:
db.close() # 요청 끝나면 자동으로 닫힘
models.py (DB 테이블):
from sqlalchemy.orm import Mapped, mapped_column
from .database import Base
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
email: Mapped[str] = mapped_column(unique=True)
password: Mapped[str]
schemas.py (Pydantic 입출력):
from pydantic import BaseModel
class UserCreate(BaseModel):
name: str
email: str
password: str
class UserResponse(BaseModel):
id: int
name: str
email: str
model_config = {"from_attributes": True} # SQLAlchemy 모델 변환 허용
main.py (라우터에서 DB 사용):
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from . import models, schemas
from .database import engine, get_db
models.Base.metadata.create_all(bind=engine) # 테이블 자동 생성
app = FastAPI()
@app.post("/users", response_model=schemas.UserResponse)
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
db_user = models.User(**user.model_dump())
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
@app.get("/users/{user_id}", response_model=schemas.UserResponse)
def get_user(user_id: int, db: Session = Depends(get_db)):
user = db.query(models.User).filter(models.User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="유저를 찾을 수 없어요")
return user
Django vs FastAPI 역할 비교:
| 역할 | Django | FastAPI |
|---|---|---|
| DB 테이블 정의 | models.py | models.py (SQLAlchemy) |
| 입출력 검증 | serializers.py | schemas.py (Pydantic) |
| DB 연결 설정 | settings.py | database.py |
| 마이그레이션 | makemigrations / migrate | Alembic (별도 도구) |
"이 함수를 실행하기 전에 저 함수를 먼저 실행하고, 그 결과를 여기 넣어줘."
# get_db()를 먼저 실행하고, 결과(세션)를 db에 주입
def create_user(db: Session = Depends(get_db)):
...
실제 쓰임새 3가지:
@app.get("/users")
def get_users(db: Session = Depends(get_db)):
return db.query(User).all()
def get_current_user(token: str = Header(...)):
user = verify_token(token)
if not user:
raise HTTPException(401, "로그인이 필요해요")
return user
@app.get("/my-profile")
def my_profile(current_user = Depends(get_current_user)):
return current_user
def get_admin_user(current_user = Depends(get_current_user)):
if not current_user.is_admin:
raise HTTPException(403, "관리자만 접근 가능해요")
return current_user
@app.delete("/users/{user_id}")
def delete_user(
user_id: int,
db: Session = Depends(get_db),
admin = Depends(get_admin_user),
):
...
router = APIRouter(
prefix="/admin",
dependencies=[Depends(get_admin_user)] # 모든 엔드포인트에 자동 적용
)
Django와 비교:
| 목적 | Django | FastAPI |
|---|---|---|
| 로그인 체크 | @login_required | Depends(get_current_user) |
| 권한 체크 | @permission_required | Depends(get_admin_user) |
| DB 세션 | 자동 (내장) | Depends(get_db) |
로그인 시 JWT 토큰 발급 → 이후 요청마다 헤더에 토큰 포함 → 서버가 검증.
설치:
uv add python-jose[cryptography] passlib[bcrypt]
auth.py:
from datetime import datetime, timedelta
from jose import jwt, JWTError
from passlib.context import CryptContext
from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
SECRET_KEY = "your-secret-key" # 실제로는 환경변수로!
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
pwd_context = CryptContext(schemes=["bcrypt"])
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/login")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain: str, hashed: str) -> bool:
return pwd_context.verify(plain, hashed)
def create_access_token(data: dict) -> str:
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode["exp"] = expire
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
def get_current_user(token: str = Depends(oauth2_scheme)):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id = payload.get("sub")
if not user_id:
raise HTTPException(status_code=401, detail="유효하지 않은 토큰")
return user_id
except JWTError:
raise HTTPException(status_code=401, detail="유효하지 않은 토큰")
main.py:
from fastapi.security import OAuth2PasswordRequestForm
@app.post("/login")
def login(form: OAuth2PasswordRequestForm = Depends()):
user = get_user_by_email(form.username)
if not user or not verify_password(form.password, user.password):
raise HTTPException(status_code=401, detail="이메일 또는 비밀번호가 틀려요")
token = create_access_token({"sub": str(user.id)})
return {"access_token": token, "token_type": "bearer"}
# 로그인 필요한 라우터
@app.get("/my-profile")
def my_profile(user_id: str = Depends(get_current_user)):
return {"user_id": user_id}
실제 요청 흐름:
# 1. 로그인 → 토큰 받기
POST /login
{ "username": "test@test.com", "password": "1234" }
→ { "access_token": "eyJhbGci...", "token_type": "bearer" }
# 2. 토큰으로 보호된 API 호출
GET /my-profile
Authorization: Bearer eyJhbGci...
→ { "user_id": "1" }
환경변수로 SECRET_KEY 관리 (실무 필수):
# .env
SECRET_KEY=super-secret-key-here
# config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
secret_key: str
class Config:
env_file = ".env"
settings = Settings()
Django 세션 vs FastAPI JWT:
| Django 세션 | FastAPI JWT | |
|---|---|---|
| 상태 저장 | 서버 DB에 저장 | 토큰 자체에 포함 |
| 로그인 확인 | request.user.is_authenticated | Depends(get_current_user) |
| 로그아웃 | 세션 삭제 | 클라이언트에서 토큰 삭제 |
| 적합한 환경 | 전통적인 웹사이트 | API 서버, 모바일 앱 |
my-project/
├── app/
│ ├── main.py ← FastAPI 앱 생성, 라우터 등록
│ ├── database.py ← DB 연결 설정
│ ├── auth.py ← JWT 인증 로직
│ ├── config.py ← 환경변수 설정
│ │
│ ├── users/ ← 유저 도메인
│ │ ├── router.py ← GET /users, POST /users ...
│ │ ├── models.py ← SQLAlchemy User 테이블
│ │ ├── schemas.py ← Pydantic UserCreate, UserResponse
│ │ └── service.py ← 비즈니스 로직
│ │
│ └── posts/ ← 게시글 도메인
│ ├── router.py
│ ├── models.py
│ ├── schemas.py
│ └── service.py
│
├── .env
└── pyproject.toml
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from ..database import get_db
from ..auth import get_current_user
from . import schemas, service
router = APIRouter(prefix="/users", tags=["users"])
@router.get("/", response_model=list[schemas.UserResponse])
def get_users(db: Session = Depends(get_db)):
return service.get_all_users(db)
@router.get("/me", response_model=schemas.UserResponse)
def get_me(db: Session = Depends(get_db), user_id: str = Depends(get_current_user)):
return service.get_user(db, user_id)
@router.post("/", response_model=schemas.UserResponse, status_code=201)
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
return service.create_user(db, user)
from sqlalchemy.orm import Session
from fastapi import HTTPException
from . import models, schemas
from ..auth import hash_password
def get_all_users(db: Session):
return db.query(models.User).all()
def get_user(db: Session, user_id: str):
user = db.query(models.User).filter(models.User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="유저를 찾을 수 없어요")
return user
def create_user(db: Session, user: schemas.UserCreate):
db_user = models.User(
name=user.name,
email=user.email,
password=hash_password(user.password),
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
from fastapi import FastAPI
from .database import engine, Base
from .users.router import router as users_router
from .posts.router import router as posts_router
Base.metadata.create_all(bind=engine)
app = FastAPI()
app.include_router(users_router) # /users/...
app.include_router(posts_router) # /posts/...
router = APIRouter(prefix="/users", tags=["users"])
# prefix="/users" → @router.get("/")가 실제로는 GET /users/
# tags=["users"] → /docs에서 엔드포인트를 그룹으로 묶어서 표시
| Django | FastAPI |
|---|---|
urls.py | main.py + include_router() |
views.py | router.py + service.py |
models.py | models.py (SQLAlchemy) |
serializers.py | schemas.py (Pydantic) |
apps.py | 폴더 자체가 도메인 단위 |
fastapi.middleware.cors)pytest + httpx)Docker + nginx + gunicorn)