FastAPI 데이터베이스 — SQLAlchemy 연동
서버를 끄면 데이터가 통째로 사라진다면, 그건 아직 진짜 저장이 아니다. SQLAlchemy 로 파일 DB 를 붙여 데이터를 영구히 남겨 보자.
지금까지 우리가 만든 API 들은 데이터를 어디에 담아 뒀을까. 솔직히 말하면 파이썬 변수 안에 담아 뒀다. items = [] 같은 리스트, users = {} 같은 딕셔너리에 차곡차곡 넣고 빼면서 "저장됐다" 고 여겼다. 입문 단계에서는 충분히 좋은 방법이고, 개념을 익히는 데 군더더기가 없다.
그런데 거기에는 치명적인 함정이 하나 있다. 7편은 그 함정을 정면으로 다룬다. 데이터베이스 — 이름은 거창하지만, 5편에서 맛본 get_db 패턴을 떠올리면 절반은 이미 와 있다. 이번 편을 마치면 SQLite 파일 하나로 진짜 DB 를 붙이고, 서버를 껐다 켜도 데이터가 그대로 남아 있는 API 를 갖게 된다.
1. 왜 데이터베이스가 필요한가 — 리스트는 휘발한다
다음 코드는 누구나 한 번쯤 짜 본 "메모리 저장" 방식이다. 회원을 리스트에 담고, 조회하면 그 리스트를 돌려준다.
이 코드는 잘 돈다. 회원을 추가하면 리스트에 쌓이고, 조회하면 그대로 나온다. 문제는 서버를 한 번 껐다 켜는 순간 드러난다. 코드 수정, 배포, 서버 재시작, 단순한 크래시 — 무엇이든 프로세스가 다시 뜨면 그 users = [] 는 빈 리스트로 초기화된다. 어제 가입한 회원 1,000명이 흔적도 없이 사라진다.
이유는 단순하다. 파이썬 변수는 램(메모리) 에 산다. 메모리는 빠르지만 휘발성이다. 전원이 끊기거나 프로세스가 죽으면 내용물이 통째로 날아간다. 데이터를 오래 살리려면 디스크에 적어 두어야 하고, 그 일을 안전하고 빠르게 해 주는 전문 도구가 바로 데이터베이스다.
이번 편은 가장 가벼운 SQLite 로 시작한다. SQLite 는 별도 서버 프로그램을 깔 필요 없이 파일 하나가 곧 데이터베이스 인 내장형 DB 다. 파이썬에 기본 포함돼 있어 설치도 필요 없고, app.db 라는 파일 한 개에 모든 테이블과 데이터가 담긴다. 학습과 소규모 서비스에 완벽하고, 나중에 PostgreSQL·MySQL 로 갈아탈 때도 코드 대부분을 그대로 쓴다.
2. 설치와 엔진·세션 — DB 로 가는 통로 만들기
파이썬에서 DB 를 다룰 때 거의 표준처럼 쓰이는 라이브러리가 SQLAlchemy 다. 설치는 한 줄이다.
이제 DB 연결을 담당할 파일 하나를 만든다. database.py 라고 부르자. 여기서 등장하는 세 가지 부품 — 엔진, 세션, 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=False 와 autoflush=False 는 "내가 명시적으로 commit() 하기 전엔 함부로 저장하지 마라" 는 안전한 기본 설정이다.
마지막 Base 는 잠시 뒤 만들 모델 클래스들이 공통으로 물려받을 부모 클래스 다. SQLAlchemy 는 이 Base 를 통해 "어떤 클래스가 어떤 테이블인지" 를 추적한다.
3. 모델 정의 — 파이썬 클래스가 곧 테이블
이제 핵심 개념인 ORM 을 만난다. ORM(Object-Relational Mapping)은 파이썬 클래스 ↔ DB 테이블, 클래스의 객체 ↔ 테이블의 한 줄(행) 을 자동으로 이어 주는 다리다. 우리는 SQL 문을 직접 쓰는 대신 평소처럼 파이썬 객체를 다루고, SQLAlchemy 가 그것을 INSERT·SELECT 같은 SQL 로 번역해 준다. "테이블을 파이썬 클래스로 그린다" 고 생각하면 정확하다.
읽는 법은 직관적이다. 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 테이블의 설계도 전체다.
설계도를 그렸으니 실제 테이블을 만들어야 한다. 그 한 줄은 이렇다.
Base.metadata 는 지금까지 Base 를 상속한 모든 모델의 정보를 모아 둔 장부다. create_all(bind=engine) 은 그 장부를 보고 아직 없는 테이블만 골라 만든다. 이미 있으면 건드리지 않으니 여러 번 실행해도 안전하다. 이 한 줄을 실행하면 폴더에 app.db 파일이 생기고 그 안에 users 테이블이 들어앉는다.
User 에 열을 하나 추가해도 create_all 은 기존 users 테이블을 그대로 둔다. 실서비스에서 테이블 구조를 바꾸는 일(마이그레이션)은 Alembic 같은 전용 도구가 맡는다. 지금은 "처음 테이블을 만들 때 쓰는 한 줄" 로만 알아 두면 충분하다.
4. get_db 의존성 — 라우트에 세션을 끼워 넣기
여기서 5편이 빛을 발한다. 우리는 그때 yield 의존성으로 "쓰기 전 준비 → 엔드포인트 실행 → 쓴 뒤 정리" 를 한 함수에 담는 법을 배웠고, 마지막에 "7편에서 이 get_db 패턴을 거의 그대로 다시 만난다" 고 예고했다. 약속한 그 순간이다.
흐름은 5편과 똑같다. 요청이 들어오면 get_db 가 SessionLocal() 로 새 세션을 만들고, yield db 에서 잠시 멈춰 그 세션을 엔드포인트에 건넨다. 엔드포인트가 일을 끝내면 FastAPI 가 다시 get_db 로 돌아와 finally 의 db.close() 를 실행한다. 중간에 에러가 터져도 finally 라서 세션은 항상 닫힌다 — 연결 누수가 원천 차단된다. 요청마다 세션이 하나씩 깔끔하게 열리고 닫히는 것이다.
이제 라우트에서 이 세션을 받아 쓴다. 인자 자리에 Depends(get_db) 를 적기만 하면 된다.
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 를 완성한다.