FastAPI 교재 · 7편 · 데이터베이스

FastAPI 데이터베이스 — SQLAlchemy 연동

서버를 끄면 데이터가 통째로 사라진다면, 그건 아직 진짜 저장이 아니다. SQLAlchemy 로 파일 DB 를 붙여 데이터를 영구히 남겨 보자.

파이썬 서버가 데이터베이스 실린더에 연결되어 파이썬 클래스 객체를 테이블에 매핑하고 데이터가 영구 저장되는 SQLAlchemy ORM 컨셉 아이소메트릭 일러스트

지금까지 우리가 만든 API 들은 데이터를 어디에 담아 뒀을까. 솔직히 말하면 파이썬 변수 안에 담아 뒀다. items = [] 같은 리스트, users = {} 같은 딕셔너리에 차곡차곡 넣고 빼면서 "저장됐다" 고 여겼다. 입문 단계에서는 충분히 좋은 방법이고, 개념을 익히는 데 군더더기가 없다.

그런데 거기에는 치명적인 함정이 하나 있다. 7편은 그 함정을 정면으로 다룬다. 데이터베이스 — 이름은 거창하지만, 5편에서 맛본 get_db 패턴을 떠올리면 절반은 이미 와 있다. 이번 편을 마치면 SQLite 파일 하나로 진짜 DB 를 붙이고, 서버를 껐다 켜도 데이터가 그대로 남아 있는 API 를 갖게 된다.

1. 왜 데이터베이스가 필요한가 — 리스트는 휘발한다

다음 코드는 누구나 한 번쯤 짜 본 "메모리 저장" 방식이다. 회원을 리스트에 담고, 조회하면 그 리스트를 돌려준다.

users = [] # 그냥 파이썬 리스트 @app.post("/users") def create_user(name: str): users.append({"name": name}) return {"saved": name} @app.get("/users") def list_users(): return users # 잘 동작하는 것처럼 보인다

이 코드는 잘 돈다. 회원을 추가하면 리스트에 쌓이고, 조회하면 그대로 나온다. 문제는 서버를 한 번 껐다 켜는 순간 드러난다. 코드 수정, 배포, 서버 재시작, 단순한 크래시 — 무엇이든 프로세스가 다시 뜨면 그 users = [] 는 빈 리스트로 초기화된다. 어제 가입한 회원 1,000명이 흔적도 없이 사라진다.

이유는 단순하다. 파이썬 변수는 램(메모리) 에 산다. 메모리는 빠르지만 휘발성이다. 전원이 끊기거나 프로세스가 죽으면 내용물이 통째로 날아간다. 데이터를 오래 살리려면 디스크에 적어 두어야 하고, 그 일을 안전하고 빠르게 해 주는 전문 도구가 바로 데이터베이스다.

왜 그냥 파일이 아니라 DB 인가 — "그럼 텍스트 파일에 저장하면 되잖아?" 라고 할 수 있다. 가능은 하다. 하지만 동시에 두 요청이 같은 파일을 쓰면 내용이 깨지고(동시성), 100만 건 중 한 명을 찾으려면 파일 전체를 읽어야 하며(검색 속도), 쓰다가 멈추면 절반만 적힌다(무결성). 데이터베이스는 이 세 문제를 처음부터 해결해 둔 저장 전문가다.

이번 편은 가장 가벼운 SQLite 로 시작한다. SQLite 는 별도 서버 프로그램을 깔 필요 없이 파일 하나가 곧 데이터베이스 인 내장형 DB 다. 파이썬에 기본 포함돼 있어 설치도 필요 없고, app.db 라는 파일 한 개에 모든 테이블과 데이터가 담긴다. 학습과 소규모 서비스에 완벽하고, 나중에 PostgreSQL·MySQL 로 갈아탈 때도 코드 대부분을 그대로 쓴다.

2. 설치와 엔진·세션 — DB 로 가는 통로 만들기

파이썬에서 DB 를 다룰 때 거의 표준처럼 쓰이는 라이브러리가 SQLAlchemy 다. 설치는 한 줄이다.

$ pip install sqlalchemy

이제 DB 연결을 담당할 파일 하나를 만든다. database.py 라고 부르자. 여기서 등장하는 세 가지 부품 — 엔진, 세션, Base — 만 이해하면 나머지는 응용이다.

# database.py from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, declarative_base # 1) 엔진 — DB 파일로 가는 실제 연결 engine = create_engine( "sqlite:///./app.db", connect_args={"check_same_thread": False}, ) # 2) 세션 공장 — 요청마다 세션 하나씩 찍어낸다 SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) # 3) Base — 모델 클래스들이 상속할 부모 Base = declarative_base()

한 부품씩 풀어 본다. 엔진(engine) 은 DB 와 통하는 실제 연결 통로 다. "sqlite:///./app.db" 는 "현재 폴더의 app.db 파일을 SQLite DB 로 쓰겠다" 는 주소다. 만약 PostgreSQL 이었다면 이 한 줄만 "postgresql://..." 로 바뀐다 — 그래서 DB 교체가 쉽다.

check_same_thread: False 는 SQLite 에만 붙이는 옵션이다. SQLite 는 기본적으로 "한 연결은 만든 스레드에서만 써라" 고 막는데, FastAPI 는 여러 스레드에서 같은 연결을 다룰 수 있어 이 빗장을 풀어 줘야 한다. SQLite 한정 주문 이라고 외워 두면 된다.

세션(session) 은 DB 와의 한 번의 대화 묶음 이다. 조회·추가·수정을 모아 두었다가 한꺼번에 반영(commit)하거나 취소(rollback)한다. sessionmaker 는 그 세션을 찍어내는 공장 이고, 우리는 그 공장 이름을 SessionLocal 이라 지었다. SessionLocal() 을 호출할 때마다 새 세션이 하나 나온다. autocommit=Falseautoflush=False 는 "내가 명시적으로 commit() 하기 전엔 함부로 저장하지 마라" 는 안전한 기본 설정이다.

마지막 Base 는 잠시 뒤 만들 모델 클래스들이 공통으로 물려받을 부모 클래스 다. SQLAlchemy 는 이 Base 를 통해 "어떤 클래스가 어떤 테이블인지" 를 추적한다.

3. 모델 정의 — 파이썬 클래스가 곧 테이블

이제 핵심 개념인 ORM 을 만난다. ORM(Object-Relational Mapping)은 파이썬 클래스 ↔ DB 테이블, 클래스의 객체 ↔ 테이블의 한 줄(행) 을 자동으로 이어 주는 다리다. 우리는 SQL 문을 직접 쓰는 대신 평소처럼 파이썬 객체를 다루고, SQLAlchemy 가 그것을 INSERT·SELECT 같은 SQL 로 번역해 준다. "테이블을 파이썬 클래스로 그린다" 고 생각하면 정확하다.

# models.py from sqlalchemy import Column, Integer, String, Boolean from database import Base class User(Base): __tablename__ = "users" # 실제 테이블 이름 id = Column(Integer, primary_key=True, index=True) email = Column(String, unique=True, index=True) name = Column(String) is_active = Column(Boolean, default=True)

읽는 법은 직관적이다. class User(Base)Base 를 물려받았으니 SQLAlchemy 가 관리하는 모델이다. __tablename__ = "users" 로 실제 테이블 이름을 정하고, 그 아래 Column 하나하나가 테이블의 열(컬럼) 이 된다. id = Column(Integer, primary_key=True) 는 "정수형 id 열이고 이게 기본 키(각 행을 구별하는 고유 번호)" 라는 뜻이다.

옵션들도 이름값을 한다. primary_key=True 는 그 행의 고유 식별자, unique=True 는 같은 값이 두 번 못 들어오게(이메일 중복 가입 차단), index=True 는 그 열로 검색할 때 빠르도록 색인을 만들라는 지시, default=True 는 값을 안 주면 기본으로 채울 값이다. 이 한 클래스가 곧 users 테이블의 설계도 전체다.

설계도를 그렸으니 실제 테이블을 만들어야 한다. 그 한 줄은 이렇다.

# 앱이 시작될 때 한 번 실행 — 테이블이 없으면 만든다 from database import engine, Base import models # User 모델이 Base 에 등록되도록 불러온다 Base.metadata.create_all(bind=engine)

Base.metadata 는 지금까지 Base 를 상속한 모든 모델의 정보를 모아 둔 장부다. create_all(bind=engine) 은 그 장부를 보고 아직 없는 테이블만 골라 만든다. 이미 있으면 건드리지 않으니 여러 번 실행해도 안전하다. 이 한 줄을 실행하면 폴더에 app.db 파일이 생기고 그 안에 users 테이블이 들어앉는다.

주의 — create_all 은 "없는 것만" 만든다 — 이미 만들어진 테이블의 구조를 바꾸지는 못한다. 나중에 User 에 열을 하나 추가해도 create_all 은 기존 users 테이블을 그대로 둔다. 실서비스에서 테이블 구조를 바꾸는 일(마이그레이션)은 Alembic 같은 전용 도구가 맡는다. 지금은 "처음 테이블을 만들 때 쓰는 한 줄" 로만 알아 두면 충분하다.

4. get_db 의존성 — 라우트에 세션을 끼워 넣기

여기서 5편이 빛을 발한다. 우리는 그때 yield 의존성으로 "쓰기 전 준비 → 엔드포인트 실행 → 쓴 뒤 정리" 를 한 함수에 담는 법을 배웠고, 마지막에 "7편에서 이 get_db 패턴을 거의 그대로 다시 만난다" 고 예고했다. 약속한 그 순간이다.

# 세션을 열고, 넘겨주고, 끝나면 반드시 닫는다 from database import SessionLocal def get_db(): db = SessionLocal() # 준비: 세션 하나 생성 try: yield db # 엔드포인트로 넘김 finally: db.close() # 정리: 무슨 일이 있어도 닫는다

흐름은 5편과 똑같다. 요청이 들어오면 get_dbSessionLocal() 로 새 세션을 만들고, yield db 에서 잠시 멈춰 그 세션을 엔드포인트에 건넨다. 엔드포인트가 일을 끝내면 FastAPI 가 다시 get_db 로 돌아와 finallydb.close() 를 실행한다. 중간에 에러가 터져도 finally 라서 세션은 항상 닫힌다 — 연결 누수가 원천 차단된다. 요청마다 세션이 하나씩 깔끔하게 열리고 닫히는 것이다.

이제 라우트에서 이 세션을 받아 쓴다. 인자 자리에 Depends(get_db) 를 적기만 하면 된다.

from fastapi import FastAPI, Depends from sqlalchemy.orm import Session from database import get_db import models app = FastAPI() @app.get("/users") def list_users(db: Session = Depends(get_db)): users = db.query(models.User).all() # SELECT * FROM users return users @app.get("/users/{user_id}") def get_user(user_id: int, db: Session = Depends(get_db)): user = db.query(models.User).filter(models.User.id == user_id).first() return user

db: Session = Depends(get_db) 한 줄이 핵심이다. "이 라우트는 DB 세션이 필요해 — get_db 가 준비해 주는 걸 db 로 받을게" 라는 선언이다. 그 다음부터 db 는 평범한 세션 객체라 마음껏 질의할 수 있다. db.query(models.User).all() 은 "users 테이블의 모든 행" 을, .filter(...).first() 는 "조건에 맞는 첫 한 행" 을 가져온다. SQL 을 한 글자도 안 적었지만 SQLAlchemy 가 알아서 SELECT 문으로 번역해 실행한다.

주목할 점은 모든 라우트가 세션을 직접 만들지 않는다 는 것이다. 세션을 열고 닫는 책임은 전부 get_db 한 곳에 있고, 라우트는 그저 받아 쓰기만 한다. 이것이 5편 의존성 주입이 7편에서 결실을 맺는 지점이다 — 준비와 정리는 한 곳에, 사용은 어디서든.

조회는 했는데 저장(쓰기)은? — 이번 편은 연결 통로를 까는 데 집중했기에 예제가 대부분 db.query(...) 조회다. 새 회원을 실제로 넣으려면 db.add(user)db.commit()db.refresh(user) 의 3단계가 필요하고, 수정과 삭제도 짝이 있다. 이 생성·조회·수정·삭제 네 가지를 묶어 CRUD 라 부르는데, 바로 그게 다음 8편의 주제다.

5. 정리 — 데이터가 살아남는 구조

7편을 한 장의 지도로 압축하면 이렇다.

부품역할핵심 코드
엔진DB 파일로 가는 실제 연결 통로create_engine("sqlite:///./app.db")
세션 공장요청마다 세션 하나씩 생성SessionLocal = sessionmaker(bind=engine)
Base모델들이 상속할 부모 클래스Base = declarative_base()
모델파이썬 클래스 = DB 테이블class User(Base): __tablename__="users"
테이블 생성없는 테이블만 실제로 만든다Base.metadata.create_all(bind=engine)
get_db세션을 열고·넘기고·닫는 의존성Depends(get_db)

요약

리스트와 딕셔너리에 담은 데이터는 서버가 꺼지면 함께 사라진다. 그래서 우리는 SQLAlchemy 로 SQLite 파일 DB 를 붙였다. create_engine 으로 엔진(연결 통로)을, sessionmaker세션 공장을, declarative_base 로 모델의 부모 Base 를 만들고, User 클래스 하나로 테이블을 설계해 create_all 로 실제 테이블을 찍어 냈다. 핵심은 ORM — 파이썬 클래스가 곧 테이블이고, 객체가 곧 한 행이라 SQL 을 직접 쓰지 않는다. 그리고 5편에서 예고했던 get_db 의존성이 마침내 제 역할을 했다. db: Session = Depends(get_db) 한 줄이면 라우트는 요청마다 깨끗한 세션을 받아 쓰고, 세션을 닫는 뒷정리는 finally 가 책임진다. 이제 데이터는 서버를 껐다 켜도 살아남는다.

다음 편 예고 — CRUD 완성하기

조회만 해 본 세션으로 이번엔 생성·조회·수정·삭제를 전부 짠다. db.add · commit · refresh 의 정석 흐름으로 회원 API 를 완성한다.

© 2026 주나이테크(주) @JUNAITECH