1 문제 — 어디에 저장할 것인가
사내 AI Agent 플랫폼 (MINERVA) 리팩토링 맥락에서 발화·대화·A/B 실험 메트릭 을 어디에 저장할지 결정이 필요했다. 관계자 입장이 세 갈래로 갈렸다.
- MS 아키텍트 권고: Cosmos DB (JSON 원문 그대로 적재).
- DS 팀 초기 직관: PostgreSQL 로 시작, 비정형·규모 커지면 MongoDB 로 전환.
- 최종 결론: Azure Database for PostgreSQL (Flexible Server) 시작 + 조건부 Cosmos DB for PostgreSQL (Citus).
이 글은 “왜 이 결론에 도달했는가” 를 data scientist 관점에서 정리한다. DS 배경이라 SQL 은 익숙하지만 NoSQL · 클라우드 DB 과금 구조 · 운영 현실에 감각이 약할 수 있는 사람 이 읽고 결정 프레임을 얻을 수 있도록 쓴다.
2 “비정형이면 NoSQL” 프레임의 한계
DS 교과서 · 튜토리얼에서 흔히 보는 이분법:
- 정형 데이터 → RDB (PostgreSQL, MySQL)
- 비정형 · 스키마 유연 → NoSQL (MongoDB)
- 대규모 · 글로벌 → Cosmos DB / DynamoDB
이 프레임은 2010 년대 초반 PostgreSQL 이 아직 JSON 을 제대로 다루지 못하던 시절에 형성되었다. 지난 10 년 동안 PostgreSQL 이 JSONB, GIN 인덱스, 배열 타입, full-text search, pgvector 를 흡수하면서 상당 부분 낡았다.
2.1 PostgreSQL JSONB 가 해결하는 것
JSONB 는 JSON 을 이진 포맷으로 저장 하는 컬럼 타입이다. 텍스트 JSON 과 달리 내부 키에 인덱스를 걸 수 있고 쿼리 시 파싱 비용이 없다.
CREATE TABLE utterances (
id UUID PRIMARY KEY,
user_id UUID,
created_at TIMESTAMPTZ,
arm_id TEXT, -- A/B 실험 arm
payload JSONB -- 유연 영역: citations, retrieved_chunks, latency 등
);
-- GIN 인덱스: JSONB 내부 키 검색을 빠르게
CREATE INDEX idx_payload_gin ON utterances USING GIN (payload);
-- 쿼리 예
SELECT
payload->>'query' AS query,
payload->'citations' AS citations,
(payload->>'latency_ms')::float AS latency
FROM utterances
WHERE arm_id = 'treatment'
AND payload @> '{"has_citation": true}' -- JSONB containment
AND created_at > NOW() - INTERVAL '7 days';MongoDB 문서 쿼리와 거의 같은 유연성을 준다. 차이점:
| 축 | PostgreSQL JSONB | MongoDB |
|---|---|---|
| 스키마 변경 빈도 | 기본 필드는 정형, 유연 영역만 JSONB | 모든 필드가 유연 |
| 수평 확장 | 단일 인스턴스 + 읽기 replica (수백 GB 까지 편안) | 샤딩이 기본 내장 |
| 분석 쿼리 | 네이티브 SQL — 조인·window·CTE | aggregation pipeline ($group/$match) — 문법 복잡 |
| 트랜잭션 | ACID 완전 지원 | 4.0+ 멀티문서 트랜잭션 지원 (제약 있음) |
발화 데이터의 현실: 대부분의 LLM 앱에서 발화 스키마는 월 단위로 크게 바뀌지 않는다. 바뀌는 건 보통 “새 metric 필드 추가” 수준이고 이건 JSONB 하위 키 추가로 끝난다. 스키마가 진짜 매일 뒤집히는 케이스 (예: 외부 파트너 데이터 수집, IoT 센서 이벤트) 가 아니면 JSONB 로 충분하다.
3 분석 관점에서 PostgreSQL 이 유리한 이유
A/B 실험 분석은 본질적으로 GROUP BY + window function + join 이다. LLM 앱 평가 쿼리의 전형적 예:
-- arm별 평균 지표 + 신뢰구간
SELECT
arm_id,
COUNT(*) AS n,
AVG((payload->>'latency_ms')::float) AS mean_latency,
STDDEV((payload->>'latency_ms')::float) AS sd_latency,
AVG((payload->>'relevance_score')::float) AS mean_relevance,
PERCENTILE_CONT(0.95) WITHIN GROUP
(ORDER BY (payload->>'latency_ms')::float) AS p95_latency
FROM utterances
WHERE created_at > NOW() - INTERVAL '14 days'
GROUP BY arm_id;
-- 사용자별 첫 turn vs 이후 turn 비교 (window)
SELECT
user_id,
created_at,
payload->>'relevance_score' AS score,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) AS turn_idx
FROM utterances;MongoDB 도 할 수는 있지만:
- aggregation pipeline 문법 이
$group,$match,$project,$lookup등으로 복잡. - 팀에서 분석하려면 결국 pandas 로 덤프해 돌리는 빈도가 높아져 IO 부담이 커진다.
- SQL 을 쓰는 DS 팀에게는 러닝 커브가 발생한다.
이건 엔지니어링 선택이 아니라 팀 생산성 문제다. DS 가 직접 쿼리를 쓸 조직이라면 SQL 기반이 압도적으로 편하다.
4 Cosmos DB 의 실체 — 한 제품이 아니라 플랫폼
여기서 많이 혼동한다. 처음엔 “Cosmos DB = MongoDB 류 문서 DB” 로 단순화하기 쉽지만, 실제로 Cosmos DB 는 여러 API 를 지원하는 플랫폼 이다. 어떤 API 를 쓰느냐에 따라 내부 모델과 이전 비용이 전혀 다르다.
| API | 내부 모델 | SQL 유지? | PG 에서 이전 비용 |
|---|---|---|---|
| Cosmos DB for NoSQL (Core) | 문서 (JSON) + 전용 쿼리 언어 | X | 높음 (패러다임 전환) |
| Cosmos DB for MongoDB | MongoDB wire protocol | X | 높음 |
| Cosmos DB for Cassandra | wide column | X | 매우 높음 |
| Cosmos DB for PostgreSQL | Citus (분산 PG) | O | 낮음 |
| Cosmos DB for Gremlin | 그래프 | X | 매우 높음 |
| Cosmos DB for Table | Key-Value | X | 낮음 (단순 KV 만) |
즉 “PG 로 시작했다가 규모 나면 Cosmos 로 간다” 가 반드시 SQL → 문서 DB 패러다임 전환을 의미하지 않는다. Cosmos DB for PostgreSQL (Citus 기반) 로 가면 SQL · JSONB · 분석 쿼리가 그대로 유지되고, 단지 테이블이 여러 노드에 샤딩 (분산 저장) 될 뿐이다.
4.1 Citus 가 무엇인가
Citus 는 PostgreSQL 확장으로, 하나의 논리 테이블을 여러 물리 노드에 분산 시키는 기능이다. 테이블 생성 시 “이 컬럼을 샤딩 키로 써라” 선언하면 Citus 가 알아서 처리한다.
CREATE TABLE utterances (...);
SELECT create_distributed_table('utterances', 'user_id');
-- 이제 utterances는 user_id 해시에 따라 여러 노드에 분산 저장됨SELECT 쿼리는 Citus 가 각 노드에 분산해 실행하고 결과를 합친다. 애플리케이션 코드는 거의 바뀌지 않는다. Cosmos DB for PostgreSQL 은 이 Citus 를 Azure 가 관리형으로 운영해 주는 것이다.
5 Cosmos DB 의 비용 구조 — RU/s
Cosmos DB for NoSQL 계열은 Request Unit per second (RU/s) 로 과금된다. 요청 하나당 RU 소모량이 달라서 비용 예측이 까다롭다.
| 작업 | 대략 RU 소모 |
|---|---|
| 1KB 문서 읽기 | 1 RU |
| 1KB 문서 쓰기 | 5 RU |
| 쿼리 (필터) | 스캔한 문서 수 × 복잡도 비례 |
Provisioned Throughput 모드는 실제 사용량과 무관하게 예약한 RU/s 만큼 과금 된다. 즉 피크 대비 프로비저닝하면 유휴 시간에도 비용이 나간다. Autoscale · Serverless 옵션이 있지만 각각 다른 제약 (최소 RU, 컨테이너 사이즈 한도) 이 있어 가볍게 돌리기 어렵다.
동일 워크로드 기준 실무 체감:
- Cosmos DB for NoSQL: Azure Database for PostgreSQL 대비 2~5 배 비용 이 흔히 나온다.
- trace · 로그같이 append-heavy 한 워크로드는 쓰기 RU 가 5 배 들어 더 나쁘다.
5.1 왜 MS 아키텍트는 Cosmos DB 를 권장했나
제품 자체는 훌륭하다. 글로벌 배포, 자동 인덱싱, 다중 API, 강력한 SLA. 기술적으로 “할 수 있다” 는 말은 맞다.
다만 벤더 영업 인센티브 측면에서도 Cosmos DB 가 Azure Database for PostgreSQL 보다 ARR (연간 반복 매출) 가 크고 MACC (Microsoft Azure Consumption Commitment) 소진에도 유리하다. 이건 비난할 일이 아니라 구조적 현실 이다. MS 아키텍트는 MS Azure 제품 풀 안에서 최선을 제안하지, “차라리 오픈소스 PG 를 셀프 호스팅하세요” 라는 제안은 하지 않는다.
결론: 아키텍처 결정은 벤더 권고를 필터링해서 받아야 한다. 요구사항 (규모·쿼리 패턴·팀 역량·비용) 을 자기 기준으로 먼저 정의하고, 그 위에서 벤더 옵션을 평가한다.
6 추천 스테이징 경로
“한번에 최종 아키텍처로 가기” 가 아니라 “현재 규모에 맞는 것으로 시작 + 명확한 전환 트리거” 전략이 합리적이다.
| 단계 | 저장소 | 언제 | 상대 비용 |
|---|---|---|---|
| 1 | Azure Database for PostgreSQL (Flexible Server) | 지금 ~ 운영 오픈 초기 | 1× |
| 2 | 동일 + 읽기 replica + pgvector 확장 | 분석 워크로드 분리 · 임베딩 검색 필요 | 2× |
| 3 | Cosmos DB for PostgreSQL (Citus) | 단일 PG 쓰기 TPS 한계 돌파 | 5~10× |
대부분의 사내 LLM 앱은 1~2 단계에서 수년을 버틴다. 3 단계까지 가는 건 연간 발화 TB+ 규모다.
6.1 1 단계: Azure Database for PostgreSQL — Flexible Server
왜 raw PostgreSQL 이 아니라 Azure managed 버전인가:
- 백업 · PITR (Point-In-Time Recovery) · 장애조치 자동.
- Azure Monitor 메트릭 · 로깅 통합.
- Entra ID 인증 지원 (별도 DB 계정 없이 AD 계정으로 로그인).
- VNet 통합 (Private Endpoint).
- pgvector, PostGIS, pg_cron 같은 주요 확장 native 지원.
- HA 옵션 (동기 복제 standby) 을 스위치로 켬.
클라이언트 관점에선 psql 로 접속하면 똑같은 PostgreSQL 이다. 운영 부담만 Azure 가 가져간다.
6.2 2 단계: pgvector 로 분석 DB 에 임베딩 합치기
pgvector 는 PostgreSQL 에 벡터 컬럼 타입과 유사도 연산자 를 추가하는 확장이다.
CREATE EXTENSION vector;
CREATE TABLE utterances (
id UUID PRIMARY KEY,
query TEXT,
query_emb vector(1536), -- OpenAI embedding 차원
payload JSONB,
created_at TIMESTAMPTZ
);
CREATE INDEX ON utterances USING ivfflat (query_emb vector_cosine_ops);
-- "이 질문과 비슷한 과거 질문 top 10"
SELECT query, 1 - (query_emb <=> $1) AS similarity
FROM utterances
ORDER BY query_emb <=> $1
LIMIT 10;이걸 분석 DB 에 넣어두면:
- “사용자들이 반복하는 질문 클러스터” 분석.
- “실패한 응답과 비슷한 패턴의 다른 응답” 추적.
- RAG 성능 저하 원인을 벡터 공간에서 탐색.
주의: 이건 RAG 검색용 Vector Store (Azure AI Search) 와는 역할이 다르다. AI Search 는 실시간 검색 최적화용이고, pgvector 는 분석 DB 의 부가 기능으로 “오프라인 탐색” 에 쓴다. 중복이 아니라 역할 분리다.
6.3 3 단계: Cosmos DB for PostgreSQL (Citus)
단일 PG 인스턴스 상한 체감:
- 저장: Flexible Server 최대 32TB.
- 쓰기 TPS: 수천 ~ 수만 (하드웨어 · 워크로드 의존).
- 동시 연결: 수천 (PgBouncer 같은 커넥션 풀러 필수).
사내 LLM 앱에서 이 한계에 실제로 부딪히는 경우는 드물다. 사용자 수천 명 × 하루 발화 수십 건 = 연간 수백 GB 수준이고, Flexible Server General Purpose 4vCPU/16GB 면 충분하다.
7 전환 트리거 기준 — “규모 커지면” 을 구체 조건으로
추상적으로 “규모 커지면 전환” 이라고 두면 실제 판단이 어렵다. 다음 조건 중 둘 이상 동시 충족 을 전환 검토 기준으로 삼는다.
- JSONB + GIN 으로 잡을 수 없는 쿼리 패턴이 3 개 이상 누적 (예: 깊이 5+ 중첩 조건부 집계).
- 쓰기 TPS 가 단일 인스턴스 최대값의 60% 초과 상태로 주 단위 지속.
- 분석 쿼리가 읽기 replica 에서도 p95 > 5 초로 악화.
- 발화 스키마가 월 단위로 top-level 구조 변경 (JSONB 하위 키 추가 수준 아님).
- 연간 스토리지 증가율이 Flexible Server 최대치 (32TB) 도달을 2 년 내로 투영.
1~3 은 기술 지표, 4~5 는 도메인 변화 신호. 하나만 걸리면 PG 내부에서 튜닝 (파티셔닝 · 인덱싱 · read replica) 으로 해결 가능. 두 개 이상이면 구조적 한계로 본다.
8 역할 분리 — 모든 데이터를 한 DB 에 넣지 않는다
실무 최적 설계는 DB 하나가 아니라 유형별 역할 분리 다.
| 데이터 유형 | 저장소 | 이유 |
|---|---|---|
| 발화 · 대화 · A/B arm · citation | PostgreSQL (JSONB) | 분석 · 조인 · 집계, 장기 보존 |
| 원시 trace / LLM raw I/O | Blob Storage + App Insights | append-only, 대용량, 드물게 조회 |
| 세션 캐시 | Redis 또는 PG 테이블 | 휘발성, 고속 조회 |
| Vector (RAG 검색) | Azure AI Search | 검색 최적화 |
| Vector (분석용) | PostgreSQL + pgvector | 분석 DB 부가 |
“모든 것을 Cosmos DB 에 넣어라” 가 over-engineering 인 이유: Cosmos DB 는 모든 유형을 수용할 수 있지만, RU/s 과금 특성상 append-heavy trace 를 넣으면 비용이 기하급수로 오른다. Trace 를 Blob + App Insights 로 보내면 TB 당 수 달러, Cosmos DB 로 넣으면 TB 당 수백 달러 이상.
역할 분리 = 각 저장소의 강점에 맞는 데이터만 넣는 것. 단일 DB 로 단순화하려는 유혹은 흔히 “개발 편의” 를 명분으로 하지만, 장기적으로는 비용 · 성능 양쪽에서 손해다.
9 Data Scientist 관점의 교훈
이 논의에서 재정리한 세 가지를 원칙으로 정리한다.
9.1 1. “비정형 → NoSQL” 은 2010 년대 프레임이다
2020 년대에는 PG JSONB 가 상당히 많은 비정형 케이스를 커버하고, 분석 편의성을 양보하지 않는다. DS 가 자주 쓰는 프레임을 10 년 주기로 한 번씩 재검토 할 가치가 있다.
9.2 2. Cosmos DB 는 제품명이 아니라 플랫폼이다
“PG → Cosmos” 가 반드시 패러다임 전환은 아니다. Cosmos for PostgreSQL (Citus) 로 가면 SQL · JSONB · 분석 쿼리가 유지된다. 제품명을 단일 블랙박스로 보지 말고 내부 API · 엔진이 무엇인지 들여다보는 습관이 중요하다.
9.3 3. 벤더 권고는 필터링해서 듣는다
MS 아키텍트의 Cosmos DB 권고는 기술적으로 가능한 제안이지만, 비용 · 운영 · 팀 역량 관점에서 맞는 답이 아닐 수 있다. 회의록에 “우리 요구사항 기준 PG Flexible Server 로 결정, 근거: 분석 편의성 · 비용 · JSONB 성숙도” 한 줄 남겨두는 것이 나중 감사 · 보고에 도움이 된다.
엔지니어링은 DS 가 종종 “상식 없음” 을 느끼는 영역이지만, 결정 프레임을 “비용 · 성능 · 팀 생산성을 변수로 둔 최적화 문제” 로 보면 DS 의 분석 감각이 오히려 강점이 된다. 결정 근거를 수치로 적어두는 습관이 그대로 통한다.
10 한 줄 요약
“비정형이면 MongoDB / Cosmos” 는 낡은 프레임이다. PostgreSQL JSONB + GIN 인덱스가 LLM 앱 발화 데이터의 비정형성을 충분히 커버하고, 분석 쿼리 (A/B arm 비교 · window · join) 편의성에서 압도적 우위를 보인다. Azure Database for PostgreSQL (Flexible Server) 로 시작 → 필요 시 읽기 replica + pgvector → 진짜 규모가 나면 Cosmos DB for PostgreSQL (Citus, SQL 유지) 로 무중단 확장. 단일 DB 에 모든 것을 넣지 말고 trace 는 Blob + App Insights, 세션은 Redis/PG 로 역할을 분리 한다. 벤더 권고는 MACC 소진 · ARR 구조를 감안해 요구사항 기준으로 필터링 해서 받는다.
11 관련 주제
Azure 시리즈
- Azure - DBA Introduction
- Azure - Server-Based DB Management
- Azure - Enterprise Architecture Overview
- Azure AI Search 개요 및 구성 — Vector Store 역할 분리의 맞은편
- Azure AI Search — Vector Search 심층 가이드
데이터 아키텍처
AWS 비교
MINERVA 프로젝트 맥락