[DB] MongoDB로 시작해 PostgreSQL로 — DB를 고른 게 아니라 스택이 골라줬다

부제: MongoDB에서 출발해 MariaDB를 저울에 올렸다가 PostgreSQL에 내려앉기까지, 결정타는 DB 자체가 아니었다
AI 에이전트 서버를 설계하면서 DB를 세 번 고민했다.
처음엔 MongoDB로 문서를 다 썼고, 하루 만에 PostgreSQL로 갈아탔고, 그 사이에 MariaDB 같은 다른 RDB도 저울에 올렸다.
돌아보면 세 번 모두, 결정을 내린 건 DB의 성능표가 아니라 그 주변에 있는 것들이었다.
출발은 MongoDB였다
첫 설계는 MongoDB Atlas였다. 이유는 그럴듯했다.
- 문서 모델이 데이터와 맞았다. 세션마다
user,device,custom같은 자유형 덩어리를 담아야 했고, 필드는 유연하게 열어달라는 요구가 있었다. 스키마를 못 박기 어려운 AI 앱 데이터엔 문서 DB가 자연스러워 보였다. - 벡터 검색이 내장돼 있었다. Atlas Vector Search가 있으니 임베딩 검색을 위해 DB를 하나 더 둘 필요가 없었다.
- LangGraph가 MongoDB를 지원했다. 대화 상태를 저장하는 체크포인터로 MongoDBSaver를 그대로 쓸 수 있었다.
DB 하나로 문서 저장·벡터 검색·대화 상태까지 다 덮인다 — 출발점으로 나쁘지 않았다.
하루 만에 뒤집은 건 감사 로그였다
이 프로젝트는 코드보다 설계 문서를 먼저 쓰는 방식으로 진행했는데, 첫 번째로 커밋된 문서가 아키텍처가 아니라 ISO 27001 보안 설계였다. 개인 건강 데이터를 다루는 서비스라 보안 요구사항을 먼저 못 박아야 했고, 그 안에 "감사 로그 3년 보관"이 법적 요구로 들어 있었다.
여기서 MongoDB가 걸렸다. MongoDB의 감사 로깅은 유료 영역이다 — Atlas 클라우드나 Enterprise 에디션의 기능이라, 우리가 쓰려던 로컬 도커 배포(Atlas Local)에서는 쓸 수 없었다. 반면 PostgreSQL은 pgAudit 확장을 무료로 붙일 수 있다.
그래서 오전에 MongoDB 기준으로 커밋한 설계 문서를, 오후에 전부 PostgreSQL로 고쳤다. 마이그레이션 비용은 문서 치환 몇 시간이 전부였다 — 코드가 한 줄도 없는 단계였기 때문이다. 같은 결정을 반년 뒤에 내렸다면 대공사였을 것이다.
그리고 갈아타면서 잃은 게 없는지 확인해보니, MongoDB를 골랐던 이유마다 PostgreSQL 쪽에 대응물이 있었다.
| MongoDB를 골랐던 이유 | PostgreSQL의 대답 |
|---|---|
| 자유형 문서(user/device/custom) | JSONB — 지금 스키마에 JSONB 컬럼 25개 |
| Atlas Vector Search | pgvector (HNSW) |
| MongoDBSaver 체크포인터 | PostgresSaver (마찬가지로 공식) |
| ObjectId의 시간순 정렬 | UUID v7 (몇 시간 뒤 후속 결정으로 채택) |
| — | pgAudit ← 여기서 저울이 기울었다 |
동률이었기에 갈아탈 수 있었고, 동률이 아니게 만든 한 가지(무료 감사 로깅)가 결정타였다.
그럼 왜 MariaDB는 아니었나
RDB로 방향을 틀었을 때 PostgreSQL만 후보였던 건 아니다. MariaDB 같은 다른 RDB도 저울에 올랐다.
그리고 이건 분명히 해두고 싶은데 — MariaDB가 모자란 DB라서 떨어진 게 아니다. 감사 로깅만 해도 MariaDB는 server_audit 플러그인을 무료로 제공하니, MongoDB를 탈락시킨 그 기준으로는 MariaDB도 통과였다. 벡터 검색도 MariaDB Vector가 있다.
떨어진 이유는 DB 바깥에 있었다.
첫째, LangGraph의 공식 프로덕션 체크포인터가 Postgres 하나다. 공식 패키지는 SQLite(개발용)와 Postgres뿐이고, MySQL/MariaDB용은 개인이 유지하는 커뮤니티 구현이 전부다. 체크포인터는 대화 상태 전체를 맡는 컴포넌트다 — 여기가 깨지면 모든 대화가 깨진다. 그 무게를 단일 메인테이너의 서드파티 패키지에 걸 수는 없었다.
둘째, 관측 스택이 이미 Postgres를 요구했다. LLM 관측용으로 Langfuse를 셀프호스팅하기로 했는데, Langfuse는 저장소로 PostgreSQL이 필수다. 실제로 지금 우리 PostgreSQL 인스턴스의 초기화 스크립트에는 앱 계정(agent_rw/agent_ro)과 나란히 langfuse 계정이 만들어진다. 즉 앱 DB를 MariaDB로 골랐어도 Postgres를 한 대 더 띄워야 했다. 그 순간 DB 엔진 두 개를 운영하고, 백업 전략도 두 벌이 된다.
셋째, JSON 인덱싱. MongoDB에서 가져온 유연 스키마가 JSONB 25개 컬럼으로 살아남았고, 그중엔 임의 키로 검색해야 해서 GIN 인덱스를 건 컬럼도 있다. MySQL은 JSON 경로마다 생성 컬럼(generated column)을 만들어야 인덱스가 걸리고, MariaDB의 JSON 타입은 사실상 LONGTEXT의 별칭이다. "어떤 키가 올지 모르는 JSON에 인덱스"는 PostgreSQL의 영역이었다.
넷째, 한국어 검색 조합. RAG 하이브리드 검색이 트라이그램 유사도(pg_trgm), 형태소 사전 토큰화 기반 전문검색(tsvector/ts_rank), 벡터 검색(pgvector)을 SQL 한 방에서 섞는다. 부품 하나하나는 다른 DB에도 비슷한 게 있지만, 세 개를 같은 쿼리에서 조합하는 배관은 PostgreSQL 기준으로 짜여 있었다.
정리하면 — 체크포인터가 Postgres를 가리키고, 관측 스택이 Postgres를 요구하고, 스키마 유연성과 검색 조합이 Postgres에서만 온전히 성립했다. DB를 고른 게 아니라, 이미 확정된 스택이 DB를 골라준 것이다.
정리
- 출발은 MongoDB — 자유형 문서·내장 벡터검색·공식 체크포인터, 이유는 충분했다
- 컴플라이언스(감사 로그 3년 보관)가 뒤집었다 — MongoDB 감사 로깅은 유료 영역, pgAudit은 무료
- 갈아타는 비용은 문서 치환뿐이었다 — 코드 전에 설계 문서로 결정을 돌린 덕
- MongoDB의 장점은 전부 PostgreSQL 안에 대응물이 있었다 — JSONB 25컬럼, pgvector, PostgresSaver, UUID v7
- MariaDB는 좋은 RDB였지만, LangGraph 공식 체크포인터·Langfuse 필수 요구·JSON 인덱싱·검색 조합이 전부 Postgres를 가리켰다 — 골라도 Postgres를 한 대 더 띄워야 했다
DB 선택을 성능 벤치마크로 시작하면 끝이 안 난다.
내 스택이 이미 어느 DB를 기본값으로 잡고 있는지부터 보면, 답은 생각보다 빨리 좁혀진다.