반응형

파이썬 FastAPI 예외 처리, 테스트 및 프로젝트 구조화

실무에서 진짜 중요한 FastAPI 프로젝트의 마무리 3대장!
예외 처리, 테스트, 구조화로 완성도를 끌어올려보세요.

 

 

안녕하세요, 여러분!

오늘은 FastAPI를 활용한 프로젝트에서 완성도와 유지보수성을 높이는 핵심 주제 3가지를 함께 살펴보려 합니다.

FastAPI는 정말 빠르고 유연한 웹 프레임워크지만, 규모가 커지면 예외 처리와 테스트, 그리고 깔끔한 프로젝트 구조가 필수죠.

초반엔 하나의 파일에 모든 걸 몰아넣는 게 편하게 느껴질 수 있지만, 진짜 문제는 그 이후부터입니다.

에러가 터지거나, 기능이 많아지고, 팀과 협업하는 상황이 오면 코드 관리가 점점 지옥이 되거든요. 😓

그래서 오늘은 그런 혼돈을 예방할 수 있는 3가지 실전 기술을 소개하려고 합니다.

하나씩 차근차근 설명드릴게요. 초보자도 충분히 따라올 수 있으니 걱정 마세요!

 

1. 효율적인 프로젝트 구조 설계하기 🏗️

1.1 모듈화의 필요성과 폴더 구조

FastAPI 프로젝트가 커지기 시작하면 코드가 한 파일에 몰려 있으면 유지보수가 매우 어려워져요.

기능이 많아질수록 파일을 나누고, 책임을 분리하고, 폴더 구조를 체계적으로 구성하는 것이 필수입니다.

FastAPI에서 추천하는 기본적인 디렉터리 구조는 다음과 같습니다:

app/
├── main.py          # FastAPI 앱 실행 진입점
├── models.py        # SQLAlchemy 모델 정의
├── schemas.py       # Pydantic 스키마 정의
├── database.py      # DB 설정, 연결 관리
├── routers/
│   ├── __init__.py
│   ├── users.py     # 사용자 관련 API
│   └── todos.py     # 할일 관련 API
└── core/
    └── config.py    # 환경 변수 및 설정 관리

 

각 파일과 폴더가 맡는 역할이 명확하죠?

특히 routers 폴더는 기능별 라우트를 나누기 좋고, core 폴더에는 설정 파일을 정리할 수 있어요.

1.2 APIRouter로 구조화하는 방법

FastAPI에서는 APIRouter를 활용해서 라우팅을 기능 단위로 나눌 수 있어요.

예를 들어 todos 관련 API는 routers/todos.py

다음과 같이 구성합니다:

from fastapi import APIRouter, Depends, HTTPException
from .. import models, schemas
from ..database import get_db
from sqlalchemy.orm import Session

router = APIRouter(prefix="/todos", tags=["Todos"])

@router.get("/", response_model=List[schemas.TodoItem])
def list_todos(db: Session = Depends(get_db)):
    return db.query(models.Todo).all()

 

이제 main.py에서는 이렇게 간단히 router를 불러와 등록하면 돼요:

from fastapi import FastAPI
from .routers import users, todos

app = FastAPI()
app.include_router(users.router)
app.include_router(todos.router)

 

이런 식으로 각 기능은 독립적으로 관리되고, main에서는 전체 앱을 조립하는 역할만 하게 됩니다.

마치 블록처럼요!

1.3 모듈 간 의존성과 순환참조 방지

구조화할 때 주의할 점 중 하나는 순환 참조(Circular Import)입니다. 예를 들어 models.pyschemas.py를 import하고, schemas.py가 다시 models.py를 참조하면 문제 발생! 😵‍💫

해결 방법은 의외로 간단해요.

보통 schemas.py는 오직 필드 선언에만 집중하고, models.py는 비즈니스 로직 중심으로 구성합니다.

서로 직접 참조하지 않게 하고, 실제 연결은 routers/*.py 파일에서 이루어지도록 조정하면 돼요.

추가로 환경 설정은 core/config.py에 넣고, .env 파일과 pydantic.BaseSettings를 함께 쓰면 환경별로 설정을 유연하게 다룰 수 있어요.

 

 

2. APIRouter를 활용한 모듈화 📦

2.1 APIRouter란 무엇인가요?

FastAPI에서 APIRouter는 말 그대로 “라우터” 역할을 해요.

여러 개의 API 경로들을 하나로 묶어주는 객체인데, 마치 미니 FastAPI 인스턴스처럼 동작해요.

FastAPI의 구조화된 애플리케이션을 만들기 위해 필수적인 도구라고 볼 수 있죠.

main.py 하나에 모든 API 경로를 넣으면 작고 단순할 땐 괜찮지만, 규모가 커지면 지옥 같은 수정의 나락이 펼쳐집니다. 😱

그래서 각 기능별로 라우터를 나눠주는 것이 좋아요.

2.2 APIRouter 사용 예제

routers/todos.py 파일에서 할 일(Todo) 목록을 관리하는 API를 만든다고 가정해볼게요:

from fastapi import APIRouter, Depends, HTTPException
from typing import List
from .. import models, schemas
from ..database import get_db
from sqlalchemy.orm import Session

router = APIRouter(prefix="/todos", tags=["Todos"])

@router.get("/", response_model=List[schemas.TodoItem])
def list_todos(db: Session = Depends(get_db)):
    return db.query(models.Todo).all()

 

이제 main.py에 가서 해당 라우터를 연결하면 됩니다:

from fastapi import FastAPI
from .routers import todos

app = FastAPI()
app.include_router(todos.router)

 

prefix="/todos" 덕분에 /todos로 시작하는 모든 경로가 이 라우터에 포함됩니다.

tags는 자동 문서화(swagger UI)에서 그룹 이름처럼 사용되죠.

2.3 APIRouter의 이점 💡

  • 기능별로 API를 독립적으로 관리 가능 → 유지보수 용이
  • Swagger 문서에서 각 API 그룹을 구분해 보여줌 → 테스트 용이
  • 다른 파일과 독립적으로 테스트 가능 → 유닛 테스트 구성에도 유리

프로젝트가 점점 커질수록 APIRouter를 쓰는 구조는 선택이 아니라 필수가 됩니다.

기획자나 다른 개발자가 함께 보는 문서화된 API를 만드는 데도 매우 유리하거든요.

 

 

3. FastAPI의 예외 처리 방법 알아보기 ⚠️

3.1 HTTPException의 기본 사용법

FastAPI에서 예외를 처리할 때 가장 기본이 되는 클래스가 바로 HTTPException이에요.

이걸 이용하면 코드에서 명시적으로 에러 응답을 보낼 수 있죠.

예를 들면 이렇습니다:

from fastapi import HTTPException

if not user:
    raise HTTPException(status_code=404, detail="User not found")

 

이 코드가 실행되면 FastAPI는 자동으로 {"detail": "User not found"} 같은 JSON 응답과 함께 404 상태 코드를 반환합니다.

3.2 자주 쓰는 예외 처리 시나리오

  • 조회할 데이터가 없을 때 → raise HTTPException(status_code=404)
  • 사용자 인증 실패 시 → status_code=401 또는 403
  • 클라이언트의 잘못된 요청 → status_code=400과 적절한 메시지

detail에 넣는 메시지는 문자열이나 JSON 형태가 가능해서 에러 코드와 설명을 같이 담을 수도 있어요. 예:

raise HTTPException(
    status_code=400,
    detail={"error": "Invalid email", "code": 1001}
)

 

이렇게 하면 클라이언트에서 에러 코드를 받아서 처리하기 쉬워지죠.

3.3 FastAPI의 자동 유효성 검사

FastAPI는 입력 데이터의 유효성 검사를 자동으로 해줍니다.

예를 들어 Pydantic 모델에서 title: str로 지정해두면, 숫자나 null이 들어왔을 때 자동으로 422 Unprocessable Entity 오류를 반환해요.

다만, 값은 타입이 맞더라도 비즈니스 로직에 어긋나는 경우는 직접 HTTPException으로 처리해줘야 합니다.

예를 들어 제목이 빈 문자열이면 아래처럼 처리할 수 있어요:

if todo.title.strip() == "":
    raise HTTPException(status_code=400, detail="Title cannot be empty")

 

이처럼 자동 유효성 검사 + 비즈니스 검증을 조합하면, 훨씬 견고한 API를 만들 수 있어요.

 

 

4. 전역 예외 핸들러 활용법 💥

4.1 커스텀 예외 핸들러란?

FastAPI는 예외 처리기를 전역으로 등록할 수 있어요.

즉, 특정 예외가 발생했을 때 공통된 형식의 응답을 보내고 싶다면 핸들러를 따로 정의해서 자동으로 처리되게 할 수 있다는 말이죠.

예를 들어 데이터베이스의 제약 조건 위반 같은 경우, 매번 처리하기보다는 한 번에 묶어서 처리하면 편하겠죠?

4.2 SQLAlchemy 예외 핸들링 예제

SQLAlchemy에서 가장 자주 만나는 예외 중 하나는 IntegrityError입니다.

중복된 키 삽입, not null 위반 등에서 발생하는 예외죠.

이걸 전역으로 처리해봅시다.

from fastapi.responses import JSONResponse
from sqlalchemy.exc import IntegrityError
from fastapi import Request, FastAPI

app = FastAPI()

@app.exception_handler(IntegrityError)
async def integrity_error_handler(request: Request, exc: IntegrityError):
    return JSONResponse(
        status_code=400,
        content={"detail": "데이터베이스 제약 조건 위반 (중복, null 등)"}
    )

 

이제 어떤 라우터에서든 IntegrityError가 발생하면 자동으로 이 핸들러가 실행되어 공통된 메시지를 보내게 됩니다. 무척 깔끔하죠? 👍

4.3 RequestValidationError 커스터마이징

FastAPI는 입력 검증에 실패하면 기본적으로 422 오류와 함께 RequestValidationError를 발생시켜요. 그런데 이 에러 메시지가 너무 상세하거나 개발자스러워서, 사용자 입장에서는 당황스러울 수 있어요.

그럴 땐 아래처럼 기본 예외 핸들러를 오버라이딩해서 에러 메시지를 심플하게 바꿔줄 수 있습니다:

from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from fastapi.exception_handlers import request_validation_exception_handler

@app.exception_handler(RequestValidationError)
async def custom_validation_handler(request, exc):
    return JSONResponse(
        status_code=422,
        content={"detail": "입력 형식이 잘못되었습니다. 필수 값을 확인하세요."}
    )

 

물론 실제 서비스에서는 유형별 메시지 분기exc.errors()를 순회하여 더 정교하게 구성할 수도 있어요.

4.4 실무에서의 활용 포인트 💡

  • 모든 라우터에서 반복되는 예외 처리 코드 제거
  • 사용자에게 친절하고 일관된 오류 메시지 제공
  • 로깅과 모니터링에 유리한 구조 구성 가능

전역 예외 핸들러는 단순히 “에러를 막는” 게 아니라 일관성 있고 신뢰감 있는 서비스를 만드는 첫걸음입니다.

 

 

5. TestClient를 사용한 테스트 자동화 🧪

5.1 FastAPI의 TestClient란?

FastAPI는 Starlette를 기반으로 만들어졌기 때문에 TestClient라는 테스트 도구를 제공합니다.

이 도구는 실제 서버를 실행하지 않고도 app 객체를 직접 호출하여 테스트를 할 수 있도록 해주는 매우 강력한 기능이에요.

보통 pytest와 함께 사용되며, REST API의 요청/응답 시나리오를 자동화하는 데 아주 적합합니다.

즉, 매번 브라우저나 Postman으로 테스트하지 않아도 된다는 거죠!

5.2 기본 테스트 코드 작성법

아래는 간단한 예시입니다.

test_main.py 파일을 만들고 다음과 같은 테스트 코드를 작성해볼 수 있어요:

from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)

def test_create_todo():
    response = client.post("/todos", json={"title": "Test", "description": "Test Desc"})
    assert response.status_code == 201
    assert response.json()["title"] == "Test"

def test_read_todo_not_found():
    response = client.get("/todos/999")
    assert response.status_code == 404

 

위 테스트는 할일을 생성하고, 존재하지 않는 할일을 조회할 때 404를 반환하는지를 검증합니다.

.get, .post, .delete 등을 통해 실제 요청을 시뮬레이션할 수 있고, response.status_coderesponse.json()으로 응답 값을 비교합니다.

5.3 테스트에서 자주 검증하는 항목들

  • 상태 코드가 올바르게 반환되는가?
  • 응답 데이터의 필드/값이 기대한 대로 구성되어 있는가?
  • 에러 발생 시 에러 메시지가 정확히 반환되는가?

5.4 실무 팁 💡

  • 테스트 함수는 반드시 test_로 시작해야 pytest에서 인식합니다.
  • 반복되는 검증 로직은 함수로 분리해 재사용성을 높입니다.
  • 정상 케이스뿐 아니라 에러 케이스도 테스트하세요!

 

테스트는 개발자의 실수를 사전에 방지해주는 방패입니다.

특히 협업하거나 유지보수가 필요한 프로젝트에서는 테스트 코드의 유무가 신뢰성과 품질의 기준이 되기도 해요.

 

 

6. 테스트 환경의 DB 처리 전략 🗄️

6.1 테스트에 실제 DB를 쓰면 안 되는 이유

실제 개발용 데이터베이스를 테스트에 사용하면 어떤 일이 벌어질까요?

예기치 않게 데이터를 삭제하거나 오염시킬 수 있습니다. 😱

특히 DELETEDROP 같은 쿼리를 테스트하는 경우에는 더더욱 위험하죠.

그래서 테스트 환경에서는 격리된 테스트 전용 DB를 사용하는 것이 원칙입니다.

6.2 메모리 SQLite DB 사용 예제

FastAPI에서 SQLite의 메모리 DB는 테스트 환경을 빠르게 구성할 수 있는 좋은 도구입니다.

아래는 예시 설정입니다:

# test_db.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.models import Base

SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:"

engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

def override_get_db():
    db = TestingSessionLocal()
    try:
        yield db
    finally:
        db.close()

 

그리고 main.py 또는 conftest.py에서 FastAPI의 의존성을 오버라이드 해줍니다:

from app.main import app
from app.test_db import override_get_db

app.dependency_overrides[get_db] = override_get_db

 

이렇게 하면 테스트에서 사용하는 get_db()가 실제 DB가 아닌 임시 메모리 DB로 교체됩니다.

테스트마다 새로운 환경에서 시작되니 데이터 충돌 걱정 없이 깔끔하게 실행 가능해요.

6.3 데이터 초기화 전략

테스트 전에 테이블을 생성하고 데이터를 초기화해주는 것도 중요합니다.

아래처럼 setup_function 혹은 pytest.fixture를 사용해 테스트 시작 전에 초기 세팅을 할 수 있어요.

import pytest
from app.test_db import engine, Base

@pytest.fixture(scope="function", autouse=True)
def setup_and_teardown():
    Base.metadata.create_all(bind=engine)
    yield
    Base.metadata.drop_all(bind=engine)

 

이렇게 하면 테스트마다 DB가 깨끗하게 리셋되기 때문에, 데이터 충돌로 인한 테스트 실패를 방지할 수 있어요.

6.4 실전 활용 팁 💡

  • 테스트용 DB URL을 별도로 구성하고 설정 파일로 분리하세요.
  • 테스트 중 발생하는 로그는 최소화하여 결과에 집중할 수 있도록 합니다.
  • CI 환경에서는 --disable-warnings 옵션을 이용해 깔끔한 출력 유지

 

테스트의 핵심은 "예측 가능한 상태에서 테스트가 반복 가능해야 한다"는 것이에요.

이를 위해 테스트용 DB는 선택이 아닌 필수 전략입니다.

 

 

마무리하며 🌱

이번 글에서는 FastAPI 프로젝트의 후반부에서 반드시 챙겨야 할 예외 처리, 테스트, 구조화 전략에 대해 하나씩 짚어봤습니다.

 

단순히 기능을 구현하는 것을 넘어서, 확장성과 유지보수성을 고려한 설계가 왜 중요한지를 느끼셨을 거예요.

특히 예외 처리에서는 HTTPException과 전역 핸들러의 활용, 테스트에서는 TestClient와 SQLite 메모리 DB의 유용함, 그리고 APIRouter를 통한 구조화는 프로젝트의 ‘완성도’를 좌우합니다.

코드가 잘 돌아가는 것도 좋지만, 에러에 강하고 테스트가 보장된 프로젝트가 진짜 안정적인 프로젝트예요.

FastAPI를 쓰면 API를 정말 빠르게 만들 수 있지만, 그 위에 신뢰성과 관리 용이성이라는 무기를 더해보세요. 그게 바로 한 단계 높은 개발자의 길이니까요. 😉

 

이제 여러분의 FastAPI 프로젝트는 단순한 샘플이 아닌, 실제 서비스로 연결될 수 있는 기반이 갖춰졌습니다. 이제 자신 있게 확장하고 테스트하고, 에러를 두려워하지 마세요!

반응형
반응형

파이썬 FastAPI 고급 ORM – 관계 모델링과 다중 테이블 연동

여러분의 FastAPI 앱이 점점 커지고 있나요?
단일 테이블로는 한계가 있다 느낄 때,
지금이 바로 관계형 모델링을 배워야 할 타이밍입니다!

 

 

안녕하세요, 개발자 여러분 😊
오늘은 FastAPI와 SQLAlchemy ORM을 활용하여 "다중 테이블 관계 설정"Pydantic 스키마 연계까지 다뤄볼 거예요.

특히 User와 Todo 같은 실용적인 예제를 중심으로 일대다(1:N), 다대다(N:M) 관계를 어떻게 모델링하는지, 그리고 이를 CRUD API로 어떻게 연결하는지 구체적으로 살펴봅니다.

FastAPI를 사용하다 보면 단일 테이블로 구현한 간단한 예제는 금방 만들 수 있지만, 실제 애플리케이션에서는 테이블 간의 관계가 필수입니다. 오늘은 이걸 제대로 배워볼 거예요.

 

1. 테이블 간 관계 설정의 기초 🧩

FastAPI에서 데이터베이스를 제대로 다루기 위해선 SQLAlchemy ORM을 이용한 관계형 모델링이 거의 필수예요.

특히 다수의 테이블이 얽힌 구조에서는 일대다(One-to-Many), 다대다(Many-to-Many) 같은 관계 설정이 핵심이 됩니다.

예를 들어 한 명의 사용자가 여러 개의 Todo를 소유하거나, 여러 사용자가 여러 프로젝트에 동시에 참여할 수 있다면 관계형 설계가 반드시 필요하죠.

1.1 일대다 관계란 무엇인가요?

가장 흔한 관계가 바로 1:N 관계예요.

한 사람이 여러 개의 블로그 글을 작성하는 경우처럼요.

이를 SQLAlchemy에서는 다음과 같은 방식으로 표현합니다:

from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    username = Column(String, unique=True, index=True)
    posts = relationship("Post", back_populates="author")

class Post(Base):
    __tablename__ = "posts"
    id = Column(Integer, primary_key=True)
    title = Column(String)
    content = Column(String)
    author_id = Column(Integer, ForeignKey("users.id"))
    author = relationship("User", back_populates="posts")

 

여기서 핵심은 ForeignKeyrelationship입니다.

Post.author_id는 User 테이블의 id를 참조하는 외래 키이고, 두 모델은 back_populates로 서로를 참조합니다.

이 설정 덕분에 user.posts로 해당 사용자의 모든 게시글을 가져올 수 있고, post.author로 작성자 정보를 조회할 수 있어요.

1.2 다대다 관계는 어떻게 구성할까요?

조금 더 복잡한 구조인 N:M 관계는 중간 테이블(association table)을 만들어야 합니다.

예를 들어 사용자와 프로젝트는 다음과 같이 설정할 수 있습니다.

association_table = Table(
    "user_project",
    Base.metadata,
    Column("user_id", ForeignKey("users.id"), primary_key=True),
    Column("project_id", ForeignKey("projects.id"), primary_key=True),
)

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    username = Column(String)
    projects = relationship("Project", secondary=association_table, back_populates="members")

class Project(Base):
    __tablename__ = "projects"
    id = Column(Integer, primary_key=True)
    title = Column(String)
    members = relationship("User", secondary=association_table, back_populates="projects")

 

이처럼 secondary 파라미터를 이용해 중간 테이블을 지정하고, 관계를 서로 연결하면 user.projects, project.members로 양방향 접근이 가능해요.

실제 현업에서도 협업 앱, 팀 관리 시스템 등에서 자주 쓰이는 구조입니다.

📋 일대다 vs 다대다 – 요약 비교

구분 일대다 (1:N) 다대다 (N:M)
예시 사용자 – 게시글 사용자 – 프로젝트
테이블 구성 두 개의 테이블 + 외래 키 세 개의 테이블 (중간 테이블 포함)
SQLAlchemy 설정 ForeignKey, relationship association_table, secondary, relationship

이제 관계 설정의 기초를 이해하셨다면, 다음 단계에서는 실제로 FastAPI에서 SQLAlchemy ORM 모델을 작성하고, 이 관계가 어떻게 작동하는지 하나씩 코드를 통해 살펴볼 거예요.

 

 

2. 관계를 표현하는 SQLAlchemy 모델 구조 🏗️

자, 이제 본격적으로 모델을 구성해볼 시간이에요.

FastAPI에서 SQLAlchemy ORM을 사용할 때는 클래스를 기반으로 테이블을 정의하고,

relationship()ForeignKey()를 통해 테이블 간의 관계를 표현합니다.

 

우리가 구현할 시나리오는 아주 현실적이에요:

사용자(User)가 여러 개의 할 일(Todo)을 가질 수 있는 일대다(1:N) 구조입니다.

2.1 사용자(User)와 할 일(Todo) 모델 정의

아래 코드는 SQLAlchemy ORM을 사용한 관계형 모델 정의 예시입니다.

중요한 건 각 모델이 서로를 어떻게 참조하고 있는지예요.

from sqlalchemy import Column, Integer, String, Boolean, ForeignKey
from sqlalchemy.orm import relationship
from database import Base  # Base는 SQLAlchemy의 declarative_base()로 정의

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True, index=True)
    username = Column(String, unique=True, index=True)
    todos = relationship("Todo", back_populates="owner")

class Todo(Base):
    __tablename__ = "todos"
    id = Column(Integer, primary_key=True, index=True)
    title = Column(String, index=True)
    description = Column(String, nullable=True)
    done = Column(Boolean, default=False)
    owner_id = Column(Integer, ForeignKey("users.id"))
    owner = relationship("User", back_populates="todos")

 

이 구조에서 User.todos는 해당 사용자가 작성한 모든 Todo 항목을 리스트로 반환하며, Todo.owner는 해당 항목의 작성자(User 객체)를 가리킵니다.

이렇게 하면 ORM 객체 간 탐색이 매우 쉬워져요.

🧠 관계형 구조의 장점은?

  • ORM 객체 간 탐색이 쉬워져 복잡한 쿼리를 단순화할 수 있습니다.
  • Pydantic과 함께 사용할 때 직관적인 JSON 변환이 가능합니다.
  • 모델 간의 의존성 설계가 명확해져 유지보수가 쉬워집니다.

2.2 모델 구조 정리 예시

테이블 컬럼 비고
users id, username username은 unique + index
todos id, title, description, done, owner_id owner_id는 users.id를 ForeignKey로 참조

이제 관계 설정이 완료된 ORM 모델 구조를 완성했어요!

 

다음 단계에서는 Pydantic 스키마를 통해 ORM 객체와 어떻게 연동하고 응답 데이터를 어떻게 구성하는지 다뤄볼게요.

이 부분이 바로 FastAPI의 강력함이 드러나는 순간입니다 💪

 

 

3. Pydantic 스키마와 ORM 연계하기 🔄

FastAPI의 가장 큰 장점 중 하나는 Pydantic을 활용한 데이터 유효성 검증과 직렬화예요.

특히 ORM 모델과 Pydantic BaseModel을 함께 사용하면, 우리가 만든 SQLAlchemy 객체를 JSON으로 변환하거나 사용자 입력을 유효성 있게 처리하는 게 정말 간단해집니다.

핵심은 orm_mode 설정을 통해 ORM 객체를 바로 변환할 수 있도록 만드는 거예요.

3.1 Todo 스키마 구성

먼저 Todo 항목을 표현할 Pydantic 스키마를 작성해보겠습니다.

아래처럼 기본 정보와 orm_mode 설정만 해주면 FastAPI에서 ORM 객체를 응답 모델로 사용할 수 있게 됩니다.

from pydantic import BaseModel
from typing import Optional

class TodoItem(BaseModel):
    id: int
    title: str
    description: Optional[str] = None
    done: bool

    class Config:
        orm_mode = True

 

이제 response_model=TodoItem으로 설정한 API에서 SQLAlchemy Todo 객체를 반환하면, FastAPI가 자동으로 이 객체를 JSON으로 변환해줍니다.

놀랍게도 따로 변환 로직을 만들 필요가 없어요.

3.2 User 입력/출력 스키마 분리

FastAPI에서는 API의 목적에 따라 입력용과 출력용 스키마를 분리해서 사용하는 게 일반적이에요.

예를 들어 사용자 등록 시엔 비밀번호를 받아야 하지만, 사용자 정보를 응답할 땐 비밀번호가 절대 노출되어선 안 되죠.

class UserCreate(BaseModel):
    username: str
    password: str  # 실제로는 해싱 필요

class User(BaseModel):
    id: int
    username: str

    class Config:
        orm_mode = True

 

이처럼 분리함으로써 보안은 물론 API 명세도 깔끔해지고 유지보수가 쉬워져요.

특히 비밀번호 필드는 출력을 철저히 막는 게 기본입니다.

실습에서는 해싱을 생략하지만, 실제 서비스에서는 절대 평문 저장하면 안 된다는 거, 꼭 기억해주세요!

3.3 관계 필드를 포함한 응답 스키마

관계를 활용한 응답을 구성할 때는 중첩 모델(Nested Model)을 사용할 수 있어요.

예를 들어 사용자 조회 시 해당 사용자의 Todo 목록을 함께 반환하고 싶다면 아래처럼 구성합니다.

from typing import List

class UserWithTodos(BaseModel):
    id: int
    username: str
    todos: List[TodoItem] = []

    class Config:
        orm_mode = True

 

주의할 점은 데이터량이 많아지면 응답 속도나 용량에 영향을 줄 수 있으므로 꼭 필요한 경우에만 중첩 구조를 사용하는 게 좋습니다.

예를 들어 Todo가 수천 개라면...? 페이지네이션이 필요하겠죠 😅

✅ 정리: ORM ↔ Pydantic 연동 요약

구분 내용
입력 스키마 필수 입력값 (비밀번호 포함 가능)
출력 스키마 응답에 포함할 필드만 선택
orm_mode ORM 객체를 자동으로 Pydantic으로 변환 가능하게 함
중첩 스키마 관계 객체를 포함하고 싶을 때 List, Optional 등으로 표현

이제 ORM과 스키마 간의 연결 고리를 완성했습니다.

 

다음은 이걸 활용해서 실제 API를 만들고, 사용자와 Todo 항목을 어떻게 엮는지 실습으로 들어가볼게요.

 

 

4. 사용자 기반 Todo API 만들기 ⚙️

지금부터는 앞서 만든 User ↔ Todo 관계를 실제 API에서 어떻게 다룰 수 있는지 구체적으로 알아볼게요.

예제를 통해 사용자와 연동된 Todo 항목을 만들고, 조회하고, 연결 관계를 유지하면서 데이터를 다룰 수 있게 해볼 거예요.

4.1 사용자 등록 API

사용자 등록은 POST /users로 진행합니다.

중복 사용자명을 체크하고, 평문 비밀번호를 그대로 저장합니다(실습 단순화용).

@app.post("/users", response_model=User)
def create_user(user: UserCreate, db: Session = Depends(get_db)):
    existing_user = db.query(User).filter(User.username == user.username).first()
    if existing_user:
        raise HTTPException(status_code=400, detail="이미 존재하는 사용자명입니다.")
    new_user = User(username=user.username)
    db.add(new_user)
    db.commit()
    db.refresh(new_user)
    return new_user

 

여기선 비밀번호 해싱 없이 단순하게 처리하지만, 실제 서비스라면 반드시 bcrypt 등으로 암호화해야 한다는 점 잊지 마세요.

4.2 사용자 기반 Todo 생성

이제 Todo 항목을 만들면서 사용자와 연결해볼게요.

POST /users/{user_id}/todos로 요청 시 해당 사용자의 소유 Todo를 생성하는 방식입니다.

@app.post("/users/{user_id}/todos", response_model=TodoItem)
def create_todo_for_user(user_id: int, todo: TodoItemCreate, db: Session = Depends(get_db)):
    user = db.query(User).filter(User.id == user_id).first()
    if not user:
        raise HTTPException(status_code=404, detail="해당 사용자를 찾을 수 없습니다.")
    new_todo = Todo(**todo.dict(), owner_id=user_id)
    db.add(new_todo)
    db.commit()
    db.refresh(new_todo)
    return new_todo

 

이 API는 user_id를 URL로 받고, 실제로 존재하는 사용자인지 확인한 후 그 사용자에게 Todo 항목을 연결해요. 데이터 무결성을 유지하는 좋은 예입니다.

4.3 사용자 별 Todo 목록 조회

GET /users/{user_id}/todos 요청으로 해당 사용자의 할 일 목록을 조회할 수 있어요. List[TodoItem] 형태로 응답하므로 클라이언트 입장에서도 처리하기 쉽습니다.

@app.get("/users/{user_id}/todos", response_model=List[TodoItem])
def get_user_todos(user_id: int, db: Session = Depends(get_db)):
    user = db.query(User).filter(User.id == user_id).first()
    if not user:
        raise HTTPException(status_code=404, detail="사용자가 존재하지 않습니다.")
    return user.todos

🚀 라우팅 구조 요약

  • POST /users → 사용자 생성
  • POST /users/{user_id}/todos → Todo 생성 (소유자 연결)
  • GET /users/{user_id}/todos → Todo 목록 조회

 

이러한 RESTful한 구조는 실무에서도 아주 자주 사용되는 패턴입니다.

리소스 기반 URL 설계HTTP 메서드 의미를 자연스럽게 활용할 수 있어 API의 일관성과 가독성을 높여줘요.

 

 

5. 관계 응답과 데이터 최적화 전략 📦

데이터베이스 관계를 설정하고 API를 만들다 보면, 관계 필드를 API 응답에 포함해야 할 때가 많아요.

예를 들어 사용자 정보를 조회할 때 해당 사용자가 등록한 모든 할 일 목록까지 함께 보내주고 싶은 경우가 있죠.

이럴 땐 orm_mode=True 설정과 함께 Nested Pydantic 모델을 사용하면 됩니다.

5.1 관계 필드 포함한 사용자 응답 예시

앞서 정의한 UserWithTodos 스키마를 활용하면 사용자와 해당 사용자의 할 일을 한꺼번에 응답할 수 있어요.

@app.get("/users/{user_id}", response_model=UserWithTodos)
def get_user_detail(user_id: int, db: Session = Depends(get_db)):
    user = db.query(User).filter(User.id == user_id).first()
    if not user:
        raise HTTPException(status_code=404, detail="사용자를 찾을 수 없습니다.")
    return user

 

이 API는 매우 직관적이고, 프론트엔드 입장에서도 한 번의 요청으로 사용자와 그에 따른 할 일을 모두 받아볼 수 있으니 편리하죠.

하지만 여기에는 중요한 함정이 있어요…

5.2 Lazy Loading의 한계

SQLAlchemy는 기본적으로 관계 데이터를 지연 로딩(Lazy Loading)합니다.

즉, 관계 필드를 실제로 접근하기 전까지는 쿼리를 실행하지 않죠.

이게 편리하긴 한데, 반복되는 쿼리 발생으로 N+1 문제가 생길 수 있어요.

예를 들어 1명의 사용자를 불러오고 todos를 포함해 응답하려면, user.todos 접근 시마다 새로운 SELECT 쿼리가 실행되기 때문에 비효율적입니다.

이런 상황에선 selectinload() 같은 Eager Loading 전략을 사용하면 좋아요.

from sqlalchemy.orm import selectinload

@app.get("/users/{user_id}", response_model=UserWithTodos)
def get_user_with_todos(user_id: int, db: Session = Depends(get_db)):
    user = db.query(User).options(selectinload(User.todos)).filter(User.id == user_id).first()
    if not user:
        raise HTTPException(status_code=404, detail="해당 사용자가 없습니다.")
    return user

 

selectinload()를 옵션에 포함시키면, 사용자와 할 일 목록을 단 두 번의 쿼리로 한번에 가져올 수 있어서 효율적이에요.

5.3 중첩 응답의 설계 전략

모든 관계 데이터를 항상 응답에 포함시키는 건 좋은 전략이 아닐 수 있어요.

데이터가 커질수록 성능에 악영향을 줄 수 있거든요.

그래서 보통은 아래와 같은 전략을 추천해요.

  • 단순 응답: 기본적으로 ID, 이름 등 요약된 정보만 제공
  • 선택적 확장: 필요할 때만 쿼리 파라미터(ex: ?include=todos)로 전체 정보 요청
  • 페이지네이션 도입: 할 일 목록이 많다면 offset/limit 사용

 

🔍 이런 경우 주의하세요!

관계가 양방향(back_populates)으로 설정되어 있는 경우 순환 참조 문제가 생길 수 있어요.

예를 들어 User → Todo → User → Todo... 식으로 무한 루프가 발생할 수 있으니, 이런 경우엔 관계 필드를 Optional로 정의하거나 재귀 깊이를 제어하는 방법을 활용해야 합니다.

이처럼 관계 응답은 강력한 기능이지만 설계 전략이 명확해야 성능과 보안 모두 잡을 수 있어요.

 

다음 단계에서는 실제 서비스에서 모델 구조가 변경되었을 때 어떻게 대응하는지, 마이그레이션 도구 Alembic을 소개해볼게요!

 

 

6. (선택) Alembic으로 마이그레이션 관리하기 📜

여러분, ORM 모델을 작성하고 데이터베이스와 연동하는 단계까지는 성공했지만, 현실은 언제나 변화하죠. 테이블 구조가 바뀌면 어떻게 해야 할까요?

매번 수동으로 SQL ALTER TABLE 문을 쓰기엔 너무 번거롭고 위험하기까지 합니다.

그래서 등장한 게 바로 Alembic입니다!

6.1 Alembic이란 무엇인가요?

Alembic은 SQLAlchemy 프로젝트의 공식 마이그레이션 도구예요.

우리가 ORM 모델을 수정하면, 이를 자동 감지해 버전 기반 마이그레이션 파일로 만들어주고, 데이터베이스에 적용하거나 되돌리는 작업을 쉽게 해줍니다.

  • 모델 → DB 테이블 생성
  • 모델 수정 → 마이그레이션 스크립트 자동 생성
  • 변경 사항 추적 및 롤백 가능

6.2 설치 및 초기 설정

pip install alembic
alembic init alembic

 

위 명령어를 입력하면 alembic 폴더와 설정 파일이 생성돼요.

그다음 env.py 파일에서 SQLAlchemy Base 모델과 데이터베이스 URL을 연결해줘야 해요.

6.3 자동 마이그레이션 생성

alembic revision --autogenerate -m "Add owner_id to Todo"
alembic upgrade head

 

이렇게 하면 모델 변경 사항을 기반으로 마이그레이션 파일이 생성되고, upgrade head 명령으로 DB에 실제 적용할 수 있어요.

테이블이 추가되거나 컬럼이 변경될 때도 자동으로 반영되니 정말 유용하죠.

💡 팁: 버전 관리 전략

  • 모든 모델 변경 시 마이그레이션 생성 필수!
  • downgrade로 이전 버전으로 복구 가능 (테스트 환경에서 유용)
  • 버전 파일은 Git으로 함께 관리!

 

이번 글에서는 실습에 Alembic을 직접 적용하지는 않았지만, FastAPI + SQLAlchemy를 실제 서비스에 도입할 때는 반드시 필요한 도구입니다.

공식 문서를 꼭 참고해 보시고, 여러분의 프로젝트에 도입해보세요!

 

지금까지 FastAPI에서 SQLAlchemy를 활용한 고급 ORM 관계 모델링다중 테이블 연동에 대해 하나하나 살펴보았습니다.

단순한 CRUD를 넘어서 관계를 정의하고, Pydantic 모델과 연계하고, 사용자 기반의 RESTful API를 만드는 실전 흐름까지 따라오셨다면 정말 큰 도약을 하신 거예요!

처음에는 관계 설정이 어렵게 느껴질 수 있지만, 코드를 따라 치고, 하나씩 요청을 만들어보면 생각보다 명확해진답니다.

특히 ORM 객체와 Pydantic 스키마의 분리, Lazy vs Eager Loading 개념, API 응답 최적화 전략은 실무에서도 자주 마주하게 되니 이번 기회에 확실히 익혀두시길 추천드려요.

앞으로 여러분의 FastAPI 프로젝트에 더 복잡한 모델 구조나 비즈니스 로직을 추가하게 되면, 이 관계 모델링이 탄탄한 기반이 되어줄 거예요.

그리고 모델이 복잡해질수록 Alembic을 통한 마이그레이션 관리는 필수입니다. 꼭 연습해보세요!

 

다음 글에서는 사용자 인증(Authentication)과 권한 부여(Authorization), OAuth2 등 보안 요소를 어떻게 FastAPI에 적용하는지 다룰 예정이에요.

궁금하셨던 분들 많으시죠? 😉 

반응형
반응형

파이썬 FastAPI 데이터베이스 연동
: SQLAlchemy ORM 시작

여러분, 웹 API를 만들었는데 데이터를 저장할 방법이 없어서 매번 초기화된다고요?
그렇다면 지금이 바로 ORM을 배워야 할 순간입니다!

 

 

안녕하세요, 여러분! 😊

FastAPI로 REST API를 구현하면서 데이터를 메모리나 리스트에만 저장했다면, 이제 다음 단계로 나아갈 차례입니다.

바로 데이터베이스 연동이죠.

특히 실습 환경에서는 간편한 SQLite로, 실전 배포 단계에서는 PostgreSQL이나 MySQL을 사용하게 될 텐데요.

오늘은 그 첫걸음으로 SQLAlchemy ORM을 FastAPI에 통합하는 방법을 알아보겠습니다.

ORM이 뭐고, 왜 쓰는지부터 실제 코드 예제까지 차근차근 풀어볼게요.

초보자도 이해할 수 있도록! 그럼 바로 시작해볼까요? 😄

 

1. 관계형 데이터베이스와 ORM 기초 📘

1.1 관계형 데이터베이스란 무엇인가요?

웹 애플리케이션에서 가장 기본적이면서도 중요한 요소 중 하나는 데이터 저장입니다.

사용자의 정보, 게시글, 댓글 등은 휘발성 메모리보다 영구 저장이 가능한 데이터베이스에 보관해야 하죠.

그리고 그중에서도 가장 널리 쓰이는 건 관계형 데이터베이스(Relational Database)입니다.

관계형 데이터베이스는 데이터를 표(table)의 형태로 저장하고, 그 안에는 열(컬럼)행(레코드)이 존재합니다.

예를 들어 사용자의 정보를 저장할 때는 다음과 같은 테이블이 만들어집니다.

id username email
1 minsu minsu@example.com
2 jiyoon jiyoon@example.com

또한 외래 키(Foreign Key)라는 개념을 이용해서 테이블 간 관계를 설정할 수 있어요.

예를 들어

사용자 테이블과 게시글 테이블이 있다면, 게시글 테이블은 작성자의 ID를 외래 키로 저장함으로써 '누가 쓴 글인지'를 연결할 수 있는 겁니다.

1.2 ORM(Object Relational Mapping)이란?

그런데 여러분, SQL 쿼리 직접 작성해보셨나요?

INSERT INTO, SELECT, UPDATE 같은 명령어들 말이죠. 배우는 건 어렵지 않지만, 반복적으로 작성하다 보면 지치기 마련이에요. 😓

특히 Python처럼 객체 지향 언어에서는 SQL보다 객체를 다루는 게 더 자연스럽죠.

그래서 등장한 게 바로 ORM (Object-Relational Mapping)입니다.

ORM은 객체와 데이터베이스를 자동으로 매핑해주는 기술로, 클래스를 정의하고 객체를 조작하면 자동으로 SQL이 실행돼요.

즉, SQL을 거의 몰라도 DB를 다룰 수 있는 거죠.

  • Python 코드로 SQL 없이 DB 조작 가능
  • 모델 클래스 정의 → 자동으로 테이블 생성
  • 쿼리 결과도 객체로 반환되어 사용이 간편함

ORM을 사용하면 생산성은 높이고, 오류는 줄이고 코드의 일관성까지 챙길 수 있어요.

물론 SQL을 완전히 모르고 개발하는 건 위험하지만, 기본 SQL을 익힌 후 ORM을 활용하면 정말 편리합니다. 😎

자, 이제 ORM의 세계를 본격적으로 들어가 볼 시간이에요.

다음 섹션에서는 FastAPI와 함께 사용할 ORM 라이브러리인 SQLAlchemy를 소개해볼게요!

 

 

2. SQLAlchemy ORM의 기본 구조 이해하기 🧩

2.1 SQLAlchemy란?

SQLAlchemy는 Python 생태계에서 가장 널리 쓰이는 ORM 도구 중 하나입니다.

단순히 ORM 기능뿐 아니라, SQL 생성기, DB 연결기, 트랜잭션 관리자까지 포괄적으로 포함된 종합 프레임워크죠.

SQLAlchemy는 크게 두 가지 레벨로 나뉘는데요.

하나는 Core (SQL 표현 언어), 다른 하나는 ORM (객체 관계 매핑)입니다.

저희는 이 중 ORM을 활용하여 FastAPI와 연결할 거예요.

2.2 SQLAlchemy ORM의 구조

ORM 방식으로 SQLAlchemy를 사용할 때는 다음과 같은 구성요소들을 기억해 두셔야 해요.

각각의 역할이 명확하게 나뉘어 있어서 한 번 구조를 이해해두면 이후 확장도 쉬워집니다.

구성 요소 설명
Engine 데이터베이스와의 연결을 생성하는 객체 (URL 기반으로 DB 접근)
Session ORM이 실제로 DB와 통신할 때 사용하는 작업 단위 (add, commit, query 등 처리)
Base 모든 ORM 모델이 상속받을 추상 클래스 (테이블 메타데이터 포함)
Model 데이터베이스 테이블과 매핑되는 Python 클래스 (컬럼과 필드 정의)

위 구성요소들을 바탕으로 SQLAlchemy로 앱을 구축할 수 있어요.

FastAPI와 통합할 때는 이 구조를 기본으로 시작하게 됩니다.

2.3 SQLAlchemy ORM의 동작 흐름

  1. Engine 생성: create_engine()로 데이터베이스 연결 설정
  2. Session 생성: sessionmaker를 이용해 세션 팩토리 구성
  3. Base 선언: declarative_base()로 모델의 부모 클래스 생성
  4. 모델 정의: 클래스를 통해 테이블 구조와 컬럼 정의
  5. CRUD 작업: 세션을 통해 데이터 생성/조회/수정/삭제 수행

이 흐름을 기억해두면, 이후 FastAPI에서 API 요청 → ORM 처리 → DB 반영 흐름을 구현할 때 큰 도움이 됩니다.

 

그럼 이제 실제로 FastAPI 프로젝트에 SQLAlchemy를 어떻게 통합하는지 본격적으로 코드를 통해 알아볼까요?

다음 섹션에서는 실습 위주로 하나씩 따라가며 설명드릴게요.

 

 

3. FastAPI 프로젝트에 SQLAlchemy 통합 🔗

3.1 SQLite로 로컬 개발 환경 구성하기

FastAPI와 SQLAlchemy를 연동할 때, 처음부터 복잡한 PostgreSQL 같은 DB를 연결하면 진입장벽이 높아질 수 있어요.

그래서 우리는 초보자에게 아주 친숙한 SQLite를 먼저 사용합니다. 설치할 필요도 없고, 파일 하나만 있으면 실행되니까요!

우선 SQLAlchemy 패키지를 설치해요. 터미널에서 아래 명령어를 실행합니다:

pip install sqlalchemy

그 다음, FastAPI 프로젝트 루트 디렉토리에 database.py라는 파일을 만들고

아래처럼 작성해 주세요:

# database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base

SQLALCHEMY_DATABASE_URL = "sqlite:///./app.db"  # 현재 디렉토리에 app.db 생성
engine = create_engine(
    SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)

SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
Base = declarative_base()
  • create_engine: SQLite 파일로 연결되는 엔진 객체 생성
  • sessionmaker: 세션 팩토리. 세션 객체를 생성하여 DB 작업에 사용
  • declarative_base: 모든 모델이 상속받는 베이스 클래스 생성

3.2 DB 세션을 위한 의존성 주입 함수 만들기

FastAPI의 핵심 기능 중 하나인 의존성 주입을 활용해서 각 API 요청마다 자동으로 DB 세션을 열고 닫는 구조를 만들 수 있어요.

아래 코드를 database.py 파일 하단에 추가해보세요:

from sqlalchemy.orm import Session
from fastapi import Depends

def get_db():
    db: Session = SessionLocal()
    try:
        yield db
    finally:
        db.close()

이제 FastAPI 라우터 함수에서는 다음처럼 db: Session = Depends(get_db) 형태로 세션을 자동 주입받을 수 있습니다.

아주 편리하죠? 😄

3.3 구조 정리 – 폴더와 파일 구성은 이렇게

SQLAlchemy를 FastAPI 프로젝트에 통합할 때는 구조화가 중요합니다.

아래처럼 파일을 구성하면 확장성과 유지보수에 훨씬 유리해요.

📁 app/
├── main.py
├── database.py        # DB 연결 설정 및 세션 함수
├── models.py          # ORM 모델 클래스 정의
├── schemas.py         # Pydantic 스키마
└── crud.py            # DB CRUD 함수

이제 다음 단계에서는 실제로 모델을 정의하고 테이블을 생성하는 작업을 해볼 거예요.

드디어 DB와 코드가 연결되는 진짜 실습에 돌입합니다!

 

 

4. ORM 모델 정의와 테이블 생성 🏗️

4.1 모델 클래스 정의하기

ORM에서 가장 중요한 부분 중 하나는 모델 클래스 정의입니다.

여기서 "모델"은 데이터베이스의 테이블과 매핑되는 Python 클래스를 의미해요.

예를 들어 Todo 리스트를 저장하고 싶다면 Todo 모델을 만들어야겠죠?

이제 models.py 파일을 새로 만들어 아래와 같이 작성해봅시다:

# models.py
from sqlalchemy import Column, Integer, String, Boolean
from .database import Base

class Todo(Base):
    __tablename__ = "todos"
    
    id = Column(Integer, primary_key=True, index=True)
    title = Column(String, index=True)
    description = Column(String, nullable=True)
    done = Column(Boolean, default=False)
  • __tablename__: 이 모델이 매핑될 실제 DB 테이블 이름
  • id: 기본 키. 자동 증가 설정
  • title, description: 문자열 컬럼, nullable=True로 NULL 허용
  • done: 완료 여부를 나타내는 불리언(Boolean) 컬럼

4.2 테이블 생성 – 코드로 자동화하기

모델 클래스를 정의했으면, 다음은 실제 SQLite DB 파일에 테이블을 생성해야 합니다.

별도로 SQL을 작성하지 않고도 간단히 처리할 수 있어요.

FastAPI 앱의 진입점인 main.py에 아래 코드를 추가해 주세요:

# main.py
from fastapi import FastAPI
from . import models
from .database import engine

models.Base.metadata.create_all(bind=engine)

app = FastAPI()

create_all() 함수는 Base를 상속받은 모든 모델 클래스의 테이블을 데이터베이스에 자동으로 생성해줍니다.

이미 존재하는 테이블은 건너뛰기 때문에 안심하고 실행해도 돼요.

이제 서버를 실행하면, app.db 파일 안에 todos 테이블이 생기고, 우리가 정의한 컬럼들이 포함돼 있을 거예요. SQLite 브라우저를 통해 직접 확인해보는 것도 재미있겠죠? 😉

4.3 한눈에 보는 코드 구성 요약

파일 내용
database.py DB 연결 엔진, 세션 팩토리, Base 클래스 정의
models.py Todo 모델 클래스 정의
main.py FastAPI 앱 시작 시 테이블 자동 생성

이제 데이터베이스에 연결하고 테이블까지 만들었으니, 본격적으로 데이터를 넣고 꺼내는 CRUD 엔드포인트를 구현해 볼 차례예요.

 

다음 단계에서는 FastAPI 라우터와 SQLAlchemy 세션을 연결해 실제 데이터를 다뤄볼 거예요.

 

 

5. DB 세션 관리 및 CRUD 구현 🛠️

5.1 DB 세션 주입하기 (Dependency Injection)

FastAPI의 가장 멋진 기능 중 하나는 의존성 주입입니다.

각 API 요청에서 DB 세션을 쉽게 주입받을 수 있고, 요청이 끝나면 자동으로 세션을 닫아줘요. 🤩

앞서 만든 get_db() 함수를 기억하시죠? 이를 FastAPI 라우트 함수에 아래처럼 적용하면 됩니다:

from fastapi import Depends
from sqlalchemy.orm import Session
from .database import get_db

이제 API 핸들러에서 db: Session = Depends(get_db)를 인자로 추가하면 DB 연결이 자동으로 처리됩니다.

5.2 CRUD 기능 구현하기

이제 진짜 핵심인 CRUD 구현을 해볼게요!

예시로 Todo 항목을 데이터베이스에서 생성하고 불러오는 코드부터 시작합니다.

먼저 Pydantic 스키마부터 설정할게요:

# schemas.py
from pydantic import BaseModel

class TodoItemCreate(BaseModel):
    title: str
    description: str | None = None

class TodoItem(BaseModel):
    id: int
    title: str
    description: str | None = None
    done: bool

    class Config:
        orm_mode = True

이제 main.py 또는 router 파일에 API 핸들러를 작성해봅시다.

(1) Todo 생성

@app.post("/todos", response_model=schemas.TodoItem)
def create_todo(item: schemas.TodoItemCreate, db: Session = Depends(get_db)):
    db_todo = models.Todo(title=item.title, description=item.description)
    db.add(db_todo)
    db.commit()
    db.refresh(db_todo)
    return db_todo

(2) Todo 단일 조회

@app.get("/todos/{id}", response_model=schemas.TodoItem)
def read_todo(id: int, db: Session = Depends(get_db)):
    todo = db.query(models.Todo).filter(models.Todo.id == id).first()
    if not todo:
        raise HTTPException(status_code=404, detail="Todo not found")
    return todo

(3) Todo 전체 조회

@app.get("/todos", response_model=list[schemas.TodoItem])
def read_all_todos(db: Session = Depends(get_db)):
    return db.query(models.Todo).all()

(4) Todo 상태 업데이트

@app.put("/todos/{id}", response_model=schemas.TodoItem)
def update_done_status(id: int, done: bool, db: Session = Depends(get_db)):
    todo = db.query(models.Todo).filter(models.Todo.id == id).first()
    if not todo:
        raise HTTPException(status_code=404, detail="Todo not found")
    todo.done = done
    db.commit()
    db.refresh(todo)
    return todo

(5) Todo 삭제

@app.delete("/todos/{id}", status_code=204)
def delete_todo(id: int, db: Session = Depends(get_db)):
    todo = db.query(models.Todo).filter(models.Todo.id == id).first()
    if not todo:
        raise HTTPException(status_code=404, detail="Todo not found")
    db.delete(todo)
    db.commit()

5.3 정리: FastAPI + SQLAlchemy 조합의 강력함

  • DB 연결과 세션 관리를 코드 몇 줄로 자동화
  • ORM 모델과 Pydantic 스키마를 분리해 가독성과 유지보수 향상
  • API와 DB가 자연스럽게 연결되어 생산성 UP

이제 API를 실행해보면 SQLite 파일에 데이터가 저장되고, 브라우저나 API 클라이언트에서 요청을 보내도 데이터가 유지되는 걸 확인할 수 있어요.

이건 정말 개발자에게 기쁨이죠! 🎉

 

 

6. 실습 과제: SQLite를 이용한 Todo API 완성 🧪

6.1 실습 목표 정리

이번 섹션에서는 지금까지 배운 내용을 바탕으로 하나의 완성된 Todo API 프로젝트를 직접 만들어보는 게 목표예요. 🚀

FastAPI, SQLAlchemy, SQLite를 연결하여 CRUD 엔드포인트를 직접 구현하고 테스트하는 과정을 통해 백엔드 개발의 핵심을 체감할 수 있습니다.

6.2 실습 내용 정리 – 구현할 기능 목록 ✅

  • POST /todos → Todo 항목 생성
  • GET /todos → 전체 Todo 목록 조회
  • GET /todos/{id} → 특정 Todo 항목 조회
  • PUT /todos/{id}?done=true → 완료 상태 업데이트
  • DELETE /todos/{id} → Todo 항목 삭제

6.3 테스트 방법 및 점검 포인트 🔍

API는 Swagger UI를 통해 쉽게 테스트할 수 있어요.

http://127.0.0.1:8000/docs 주소를 브라우저에 입력하면, 자동 생성된 문서를 통해 각 요청을 테스트해볼 수 있습니다.

 

그리고 아래 항목들을 직접 확인해보세요:

  • POST 요청 후 실제 app.db 파일에 데이터가 저장되는가?
  • 서버를 재시작해도 데이터가 보존되는가?
  • 상태코드 (200, 201, 204, 404 등) 가 올바르게 반환되는가?

6.4 실습 마무리 및 다음 단계 안내 🧭

이번 실습을 통해 FastAPI 애플리케이션에 SQLAlchemy를 통합하고, 데이터를 영구 저장하는 진짜 백엔드 앱을 만들 수 있게 되었습니다. 🎉

 

다음 단계에서는 이 API를 바탕으로 프론트엔드와 연결하거나, 배포 환경을 준비하거나,

다른 DB (예: PostgreSQL)로 전환하는 연습을 해보면 좋겠죠? 😉

 

 

마무리🚀

지금까지 우리는 FastAPI와 SQLAlchemy를 활용해 데이터베이스와 연동되는 Todo API를 구축해봤습니다.

단순한 in-memory 데이터 저장 방식에서 벗어나, 이제는 SQLite 파일을 통해 데이터를 영구 저장하고 관리할 수 있는 수준으로 발전했어요. 🧱

ORM을 도입하면서 코드의 구조가 훨씬 깔끔해지고, 유지보수도 쉬워졌다는 걸 직접 체험해보셨을 겁니다.

특히 FastAPI의 의존성 주입과 SQLAlchemy의 강력한 매핑 기능이 얼마나 잘 어우러지는지도 느껴지셨을 거예요.

 

이제 여러분은 다음 단계로 나아갈 준비가 되었습니다.

PostgreSQL 같은 다른 관계형 DB로 확장하거나, Alembic을 통한 마이그레이션 관리, 혹은 Docker 환경에서의 운영 환경 구축 등 다양한 실전 영역에 도전해보세요!

 

🎯 오늘 다룬 기술을 응용하면 여러분만의 Todo 웹앱, 게시판, 메모장 등 다양한 백엔드 서비스로 발전시킬 수 있어요.  다음 편에서는 FastAPI와 프론트엔드(예: Streamlit)를 연동하는 방법도 소개할 예정입니다!

 

 

반응형

+ Recent posts