TIL 06-03 Fast API 기초

김덕협·2026년 6월 3일

TIL

목록 보기
19/41

FastAPI 학습 노트

FastAPI 핵심 개념 정리


목차

  1. FastAPI란?
  2. Django / Flask와 비교
  3. 서버 실행
  4. HTTP 메서드 (GET · POST · PUT · DELETE)
  5. 타입 힌트와 Pydantic
  6. 응답 모델 (response_model)
  7. 에러 처리 (HTTPException)
  8. 비동기 (Async)
  9. DB 연결 (SQLAlchemy)
  10. 의존성 주입 (Depends)
  11. JWT 인증
  12. 프로젝트 구조 · 라우터 분리

1. FastAPI란?

Python으로 API 서버를 만드는 현대적인 웹 프레임워크 (2018년 출시).

3가지 핵심 특징:

  • 빠른 성능 — Node.js 수준의 속도, 비동기(async) 완전 지원
  • 자동 문서화 — 코드 작성 시 Swagger UI(/docs)가 자동 생성
  • 타입 기반 검증 — Python 타입 힌트로 데이터 자동 검증 (Pydantic)

가장 간단한 예시:

from fastapi import FastAPI

app = FastAPI()

@app.get("/hello")
def say_hello():
    return {"message": "안녕하세요!"}

2. Django / Flask와 비교

DjangoFlaskFastAPI
목적웹사이트 전체범용API 서버 전문
속도보통보통매우 빠름
비동기부분 지원미지원(기본)완전 지원
자동 문서화없음없음자동 생성
학습 난이도높음낮음낮음~중간
대표 용도인스타그램, 핀터레스트프로토타입AI/ML API, 마이크로서비스

언제 무엇을 쓰나?

  • 블로그, 쇼핑몰처럼 웹사이트 전체 → Django
  • 아주 간단한 API 빠르게 테스트 → Flask
  • 모바일 앱 / AI 서비스 백엔드 API → FastAPI

3. 서버 실행

# 패키지 설치
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.mainapp/main.py 파일 (점이 폴더 구분자)

실행 후 접속:

  • http://localhost:8000 — API 서버
  • http://localhost:8000/docs — Swagger 자동 문서화

4. HTTP 메서드

데코레이터 하나로 메서드가 결정됨.

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/5item_id: int (중괄호로 선언)
쿼리 파라미터/items?q=사과q: str = None (기본값 있으면 자동으로 쿼리)
바디POST bodyitem: 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(): ...

5. 타입 힌트와 Pydantic

타입 힌트

Python 3.5+에서 변수나 함수에 타입을 명시하는 문법.

def greet(name: str) -> str:
    return "Hello " + name

FastAPI는 타입 힌트를 보고 자동으로:
1. URL 파라미터를 해당 타입으로 변환
2. 잘못된 타입 입력 시 422 에러 자동 응답
3. /docs에 타입 정보 자동 표시

Pydantic

타입 힌트를 기반으로 데이터를 검증해주는 라이브러리. 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에선 구조적으로 불가능.


6. 응답 모델 (response_model)

응답 데이터의 형태를 지정. 민감한 필드(비밀번호 등)를 응답에서 제외할 때 필수.

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       (응답용)

7. 에러 처리 (HTTPException)

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 상태코드 정리:

코드의미언제 쓰나
400Bad Request요청 형식이 잘못됨
401Unauthorized로그인 안 됨
403Forbidden권한 없음
404Not Found리소스 없음
422UnprocessablePydantic 검증 실패 (자동)
500Server Error서버 내부 오류

8. 비동기 (Async)

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를 쓰나?

  • DB 조회 (PostgreSQL, MongoDB 등) ✅
  • 외부 API 호출 (httpx, aiohttp) ✅
  • 파일 읽기/쓰기 ✅
  • 무거운 계산 (이미지 처리, ML 연산) → def 사용 (FastAPI가 자동으로 별도 스레드 처리)

9. DB 연결 (SQLAlchemy)

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 역할 비교:

역할DjangoFastAPI
DB 테이블 정의models.pymodels.py (SQLAlchemy)
입출력 검증serializers.pyschemas.py (Pydantic)
DB 연결 설정settings.pydatabase.py
마이그레이션makemigrations / migrateAlembic (별도 도구)

10. 의존성 주입 (Depends)

"이 함수를 실행하기 전에 저 함수를 먼저 실행하고, 그 결과를 여기 넣어줘."

# get_db()를 먼저 실행하고, 결과(세션)를 db에 주입
def create_user(db: Session = Depends(get_db)):
    ...

실제 쓰임새 3가지:

1. DB 세션 주입

@app.get("/users")
def get_users(db: Session = Depends(get_db)):
    return db.query(User).all()

2. 로그인 인증 체크

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

3. Depends 중첩 — 관리자 체크

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와 비교:

목적DjangoFastAPI
로그인 체크@login_requiredDepends(get_current_user)
권한 체크@permission_requiredDepends(get_admin_user)
DB 세션자동 (내장)Depends(get_db)

11. JWT 인증

로그인 시 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_authenticatedDepends(get_current_user)
로그아웃세션 삭제클라이언트에서 토큰 삭제
적합한 환경전통적인 웹사이트API 서버, 모바일 앱

12. 프로젝트 구조 · 라우터 분리

추천 폴더 구조

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

users/router.py

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)

users/service.py

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

main.py — 라우터 등록

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/...

prefix와 tags

router = APIRouter(prefix="/users", tags=["users"])
# prefix="/users" → @router.get("/")가 실제로는 GET /users/
# tags=["users"]  → /docs에서 엔드포인트를 그룹으로 묶어서 표시

Django와 1:1 비교

DjangoFastAPI
urls.pymain.py + include_router()
views.pyrouter.py + service.py
models.pymodels.py (SQLAlchemy)
serializers.pyschemas.py (Pydantic)
apps.py폴더 자체가 도메인 단위

다음에 해볼 것들

  • Alembic으로 DB 마이그레이션 관리
  • JWT Refresh Token 구현
  • CORS 설정 (fastapi.middleware.cors)
  • 테스트 코드 작성 (pytest + httpx)
  • 배포 (Docker + nginx + gunicorn)
profile
뭘봐

0개의 댓글