포스트

Fastapi 서버 개발할 때 postgresql 대신 sqlite를 개발용으로 사용하기

https://youtu.be/PoZGMKsuxJI

[영상]

안녕하세요, 지난 번에 이런 포스팅을 작성했던 적이 있습니다.

[20250306] Fastapi와 함께 쓰는 DB로 초기 개발할 때는SQLite로, 이후 Postgresql로 이전하는 것이 유리함.

[20250306] Fastapi와 함께 쓰는 DB로 초기 개발할 때는SQLite로, 이후 Postgresql로 이전하는 것이 유리함. fastapi에서 postgresql 말고 sqlite 쓰는게 개발에서 유리한가요? 개발 후에 postgresql로 바꾸는건 쉽나… fastapi에서 postgresql 말고 sqlite 쓰는게 개발에서 유리한가요? 개발 후에 postgresql로 바꾸는건 쉽나…

오늘은 그 포스팅을 이어서 작성해보겠습니다.

sticker


  • 개발은 빠르고 간단하게 → SQLite
  • 운영은 확장성과 성능 고려해서 → PostgreSQL + asyncpg

처음엔 좋습니다. SQLite는 서버 없이 바로 쓸 수 있고, PostgreSQL은 스케일과 기능이 뛰어나니까요. 하지만 어느 순간, 이 두 환경의 차이가 의외의 문제들을 만들어냅니다.

이 글에서는 개발과 운영 환경의 차이로 생기는 실전 문제들과, 그걸 우아하게 해결할 수 있는 전략을 정리합니다.


✅ 왜 이 조합을 쓰는가?

환경DB장점
개발SQLite실행 즉시 사용 가능, 서버 불필요, 테스트 빠름
운영PostgreSQLJSONB, ARRAY, GIN 인덱스 등 강력한 기능 제공

⚠️ 실전에서 겪는 문제들

  1. JSONB vs JSON
DB타입설명
PostgreSQLJSONB빠르고 인덱싱 가능함
SQLiteJSON기본적으로 텍스트 수준의 지원
  • Column(JSONB)를 쓰면 SQLite에서 작동 안 함
  • JSON 연산자(@>, jsonb_path_query) 등도 SQLite 미지원
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from sqlalchemy import JSON
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.types import TypeDecorator
import json

class JsonType(TypeDecorator):
    impl = JSON
    cache_ok = True

    def load_dialect_impl(self, dialect):
        if dialect.name == "postgresql":
            return dialect.type_descriptor(JSONB())
        return dialect.type_descriptor(JSON())

    def process_bind_param(self, value, dialect):
        if value is None:
            return None
        if dialect.name == "sqlite":
            return json.dumps(value)
        return value

    def process_result_value(self, value, dialect):
        if value is None:
            return None
        if dialect.name == "sqlite" and isinstance(value, str):
            return json.loads(value)
        return value

모델에서 아래처럼 사용:

1
2
3
4
5
6
from sqlalchemy import Column

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    settings = Column(JsonType)
  • UUIDType, EnumType, ArrayType 등도 TypeDecorator로 감싸기
  • Enum은 Enum(String)으로 처리, UUID는 String으로 대체
1
2
3
4
5
6
7
8
9
10
11
if os.getenv("ENVIRONMENT") == "production":
    async def get_db() -> AsyncGenerator[AsyncSession, None]:
        async with SessionLocal() as session:
            yield session
else:
    def get_db() -> Generator[Session, None, None]:
        db = SessionLocal()
        try:
            yield db
        finally:
            db.close()
  • JSONB, UUID, ARRAY 같은 타입이 SQLite에서 에러 발생
  • Alembic이 다르게 인식해서 마이그레이션 충돌
  • SQLite에서는 마이그레이션 시 타입 대체 처리
  • 예: from sqlalchemy.dialects.sqlite import JSON as JSONB로 우회

🧩 실전 구조 제안

1
2
3
4
5
6
7
8
9
your_project/
├── app/
│   ├── db/
│   │   ├── types.py         ← JSONB/UUID 등 커스텀 타입들
│   │   ├── session.py       ← async/sync 분기 포함
│   ├── models/
│   │   ├── user.py          ← Column(JsonType) 등 적용
│   ├── core/
│   │   └── config.py        ← settings.ENVIRONMENT 등 분기 기준

.env 예:

1
ENVIRONMENT=development

🎯 결론 & 베스트 프랙티스

전략설명
커스텀 타입 래퍼JSONB, UUID, Enum 등 대응 가능하게 만들기
DB 엔진 모듈화engine_sync.py, engine_async.py 등으로 분리
get_db() 자동 분기async/sync 선택적으로 제공
마이그레이션 트릭Alembic에서 타입 우회 정의로 통일

FastAPI에서 SQLite를 개발용으로 쓰는 건 정말 편리합니다. 하지만 PostgreSQL과의 차이를 명확히 인식하고 미리 구조를 잘 짜두면, 운영 환경 전환 시 큰 시행착오 없이 자연스럽게 넘어갈 수 있습니다.


+2025.04.18 추가) ** **현재 시점에는 더 이상 “개발용”, “프로덕션용” db 프레임워크를 따로 나누지 않으려 합니다. 관련한 자세한 이유는 다음 블로그 포스팅에서 다루었습니다 :) https://blog.naver.com/devramyun/223838892076

[20250418] 왜 나는 더 이상 SQLite를 쓰지 않고 PostgreSQL로만 개발을 시작하기로 했는가 요약 0. 어차피 production에서는 비동기, 병목현상 등을 이유로 sqlite가 아닌 postgresql을 사용해야 함…. 요약 0. 어차피 production에서는 비동기, 병목현상 등을 이유로 sqlite가 아닌 postgresql을 사용해야 함….

감사합니다.

sticker

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.