uv를 사용한 FastAPI 도서 관리 API 만들기
1단계: uv 설치 및 프로젝트 초기화
- uv 설치
# uv 설치 brew install uv
- 프로젝트 초기화
uv init fastapi-practice cd fastapi-practice
2단계: 필요한 의존성 추가
# FastAPI 관련 패키지들 (따옴표 필수!)
uv add fastapi "uvicorn[standard]"
# 데이터베이스 관련
uv add sqlalchemy "psycopg[binary]" alembic
# 환경변수 관리
uv add python-dotenv
# 개발용 도구
uv add --dev pytest httpx
주의: zsh 쉘에서는 []
가 포함된 패키지명을 반드시 따옴표로 감싸야 합니다!
3단계: DB - Docker 설정 (PostgreSQL)
version: '3.8'
services:
postgres:
image: postgres:15
environment:
POSTGRES_DB: fastapi_practice
POSTGRES_USER: user
POSTGRES_PASSWORD: password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d fastapi_practice"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
4단계: 프로젝트 구조 및 환경 설정
mkdir app
touch app/__init__.py app/main.py app/database.py app/models.py app/schemas.py app/crud.py app/config.py
- env 알아서 설정
# 데이터베이스 설정 DATABASE_URL=postgresql://user:password@localhost:5432/fastapi_practice #또는 개별 설정 DB_HOST=localhost DB_PORT=5432 DB_NAME=fastapi_practice DB_USER=user DB_PASSWORD=password #애플리케이션 설정 APP_NAME="도서 관리 API" APP_VERSION="1.0.0" DEBUG=True
- gitignore 알아서 잘 설정
5단계: 환경변수 설정 파일(config.py)
import os
from dotenv import load_dotenv
# .env 파일 로드
load_dotenv()
class Settings:
# 데이터베이스 설정
DATABASE_URL: str = os.getenv("DATABASE_URL", "")
# 개별 DB 설정 (필요시 사용)
DB_HOST: str = os.getenv("DB_HOST", "")
DB_PORT: int = int(os.getenv("DB_PORT", ""))
DB_NAME: str = os.getenv("DB_NAME", "")
DB_USER: str = os.getenv("DB_USER", "")
DB_PASSWORD: str = os.getenv("DB_PASSWORD", "")
# 애플리케이션 설정
APP_NAME: str = os.getenv("APP_NAME", "도서 관리 API")
APP_VERSION: str = os.getenv("APP_VERSION", "1.0.0")
DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true"
# 개별 설정으로 DATABASE_URL 구성하는 메서드
@property
def database_url_from_parts(self) -> str:
return f"postgresql://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
# 설정 인스턴스 생성
settings = Settings()
6단계: 데이터베이스 연결 파일(database.py)
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from .config import settings
SQLALCHEMY_DATABASE_URL = settings.DATABASE_URL
# SQLite의 경우 추가 설정
if SQLALCHEMY_DATABASE_URL.startswith("sqlite"):
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False} # SQLite용 설정
)
else:
# PostgreSQL용 설정
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
pool_pre_ping=True, # 연결 상태 확인
pool_recycle=300, # 5분마다 연결 재생성
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# 의존성 주입용 함수
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
7단계: 데이터베이스 모델 정의(models.py)
from sqlalchemy import Column, Integer, String, Text, DateTime
from sqlalchemy.sql import func
from .database import Base
class Book(Base):
__tablename__ = "books"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(200), nullable=False, index=True)
author = Column(String(100), nullable=False)
description = Column(Text, nullable=True)
isbn = Column(String(13), unique=True, nullable=True)
published_year = Column(Integer, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
8단계: Pydantic 스키마 정의(schema.py)
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
class BookBase(BaseModel):
title: str = Field(..., min_length=1, max_length=200)
author: str = Field(..., min_length=1, max_length=100)
description: Optional[str] = None
isbn: Optional[str] = Field(None, regex=r'^\d{13}$')
published_year: Optional[int] = Field(None, ge=1000, le=2024)
class BookCreate(BookBase):
pass
class BookUpdate(BookBase):
title: Optional[str] = Field(None, min_length=1, max_length=200)
author: Optional[str] = Field(None, min_length=1, max_length=100)
class BookResponse(BookBase):
id: int
created_at: datetime
updated_at: Optional[datetime]
class Config:
from_attributes = True
9단계: CRUD 로직 작성(crud.py)
from sqlalchemy.orm import Session
from typing import List, Optional
from . import models, schemas
def get_book(db: Session, book_id: int) -> Optional[models.Book]:
return db.query(models.Book).filter(models.Book.id == book_id).first()
def get_books(db: Session, skip: int = 0, limit: int = 100) -> List[models.Book]:
return db.query(models.Book).offset(skip).limit(limit).all()
def create_book(db: Session, book: schemas.BookCreate) -> models.Book:
db_book = models.Book(**book.dict())
db.add(db_book)
db.commit()
db.refresh(db_book)
return db_book
def update_book(db: Session, book_id: int, book: schemas.BookUpdate) -> Optional[models.Book]:
db_book = db.query(models.Book).filter(models.Book.id == book_id).first()
if db_book:
update_data = book.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(db_book, field, value)
db.commit()
db.refresh(db_book)
return db_book
def delete_book(db: Session, book_id: int) -> bool:
db_book = db.query(models.Book).filter(models.Book.id == book_id).first()
if db_book:
db.delete(db_book)
db.commit()
return True
return False
def get_book_by_isbn(db: Session, isbn: str) -> Optional[models.Book]:
return db.query(models.Book).filter(models.Book.isbn == isbn).first()
10단계: FastAPI 앱 작성(main.py)
from fastapi import FastAPI, HTTPException, Depends, status
from sqlalchemy.orm import Session
from typing import List
from . import crud, models, schemas
from .database import SessionLocal, engine, get_db
from .config import settings
# 데이터베이스 테이블 생성
models.Base.metadata.create_all(bind=engine)
app = FastAPI(
title=settings.APP_NAME,
description="간단한 도서 관리 시스템",
version=settings.APP_VERSION,
debug=settings.DEBUG
)
@app.get("/", tags=["Root"])
def read_root():
return {"message": "도서 관리 API에 오신 것을 환영합니다!"}
@app.get("/books", response_model=List[schemas.BookResponse], tags=["Books"])
def get_books(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
"""모든 도서 목록을 조회합니다."""
books = crud.get_books(db, skip=skip, limit=limit)
return books
@app.post("/books", response_model=schemas.BookResponse, status_code=status.HTTP_201_CREATED, tags=["Books"])
def create_book(book: schemas.BookCreate, db: Session = Depends(get_db)):
"""새로운 도서를 추가합니다."""
# ISBN 중복 체크
if book.isbn and crud.get_book_by_isbn(db, isbn=book.isbn):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="이미 존재하는 ISBN입니다."
)
return crud.create_book(db=db, book=book)
@app.get("/books/{book_id}", response_model=schemas.BookResponse, tags=["Books"])
def get_book(book_id: int, db: Session = Depends(get_db)):
"""특정 도서를 조회합니다."""
db_book = crud.get_book(db, book_id=book_id)
if db_book is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="도서를 찾을 수 없습니다."
)
return db_book
@app.put("/books/{book_id}", response_model=schemas.BookResponse, tags=["Books"])
def update_book(book_id: int, book: schemas.BookUpdate, db: Session = Depends(get_db)):
"""특정 도서 정보를 수정합니다."""
db_book = crud.update_book(db, book_id=book_id, book=book)
if db_book is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="도서를 찾을 수 없습니다."
)
return db_book
@app.delete("/books/{book_id}", tags=["Books"])
def delete_book(book_id: int, db: Session = Depends(get_db)):
"""특정 도서를 삭제합니다."""
if not crud.delete_book(db, book_id=book_id):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="도서를 찾을 수 없습니다."
)
return {"message": "도서가 성공적으로 삭제되었습니다."}
11단계: 실행 및 테스트
PostgreSQL 컨테이너를 실행합니다:
docker-compose up -d
uv를 사용한 서버 실행
# uv 방식 (권장)
uv run uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
api 테스트
# 도서 추가
curl -X POST "http://localhost:8000/books" \
-H "Content-Type: application/json" \
-d '{
"title": "파이썬 완전 정복",
"author": "김개발자",
"description": "파이썬 기초부터 실전까지",
"isbn": "1234567890123",
"published_year": 2024
}'
# 도서 목록 조회
curl "http://localhost:8000/books"
# 특정 도서 조회
curl "http://localhost:8000/books/1"
최종 프로젝트 구조
fastapi-practice/
├── .env # 환경변수 (git에 올리지 않음)
├── .env.example # 환경변수 예시
├── .gitignore
├── docker-compose.yml
├── pyproject.toml # uv가 자동 생성
├── uv.lock # uv가 자동 생성
└── app/
├── __init__.py
├── config.py # 환경변수 설정
├── database.py # DB 연결
├── main.py # FastAPI 앱
├── models.py # SQLAlchemy 모델
├── schemas.py # Pydantic 스키마
└── crud.py # CRUD 로직
'TIL' 카테고리의 다른 글
[250712 TIL] 파이썬 모듈 시스템 궁금증 (0) | 2025.07.12 |
---|---|
[250712 TIL] fastapi 제너레이터 궁금증 (0) | 2025.07.12 |
[240705 TIL] 깃헙 여러 계정 사용(맥) (5) | 2025.07.05 |
[250617 TIL] Docker dangling 이미지 삭제 (0) | 2025.06.17 |
[250616 TIL] JSON-LD (0) | 2025.06.17 |