2026년 4월 29일 수요일
글 목록
Lv.1 입문PostgreSQL / MongoDB
15분 읽기Lv.1 입문
시리즈PostgreSQL vs MongoDB · 파트 2/3시리즈 허브 보기

PostgreSQL vs MongoDB — Part 2: 실전 시나리오별 데이터베이스 선택 기준

PostgreSQL vs MongoDB — Part 2: 실전 시나리오별 데이터베이스 선택 기준

Part 1에서 살펴본 설계 철학을 실제 프로젝트 판단 기준으로 확장합니다. 결제·정산처럼 트랜잭션이 생명인 도메인, 문서마다 구조가 다른 가변 프로필, 리포팅·대시보드, 폭발적 이벤트 로그, Nest.js + TypeScript 백엔드 — 5가지 실전 시나리오별로 PostgreSQL과 MongoDB 중 어느 쪽이 자연스러운 선택인지 구체적인 코드 예제와 함께 설명합니다. 각 시나리오 끝에는 짧은 판단 기준 질문을 달아 독자가 자기 도메인에 바로 대입해 볼 수 있게 했습니다. 마지막으로 DB 선택에서 흔히 저지르는 실수 3가지를 짚고, Part 3에서 다룰 하이브리드 아키텍처로 자연스럽게 넘어갑니다.

시리즈 구성

목차

  1. 들어가며: "어떤 DB 쓸까요?"에 답하는 법
  2. 시나리오 1 — 결제·정산 시스템: 트랜잭션이 생명이다
  3. 시나리오 2 — 사용자 프로필·콘텐츠: 구조가 매번 다르다면
  4. 시나리오 3 — 리포팅·대시보드: SQL의 독보적인 영역
  5. 시나리오 4 — 실시간 이벤트·로그: 쓰기가 폭발적으로 많다면
  6. 시나리오 5 — Nest.js + TypeScript 백엔드: 타입 안전성 우선
  7. 흔히 저지르는 선택 실수 3가지
  8. 마치며: Part 3 예고

1. 들어가며

Part 1에서 두 DB의 철학적 차이를 살펴봤다. 이번엔 현실로 내려온다.

실제 프로젝트에서 "PostgreSQL이 좋다", "MongoDB가 좋다"는 말은 맥락 없이는 무의미하다. 도메인이 무엇인지, 데이터가 어떻게 생겼는지, 팀이 무엇에 익숙한지에 따라 답이 달라지기 때문이다.

이번 파트에서는 실무에서 자주 마주치는 5가지 시나리오를 중심으로, 각 상황에서 어떤 DB를 선택하고 왜 그렇게 판단하는지를 구체적으로 풀어본다.


2. 시나리오 1 — 결제·정산 시스템

"트랜잭션이 생명이다"

선택: PostgreSQL

결제나 정산처럼 데이터 무결성이 최우선인 도메인은 PostgreSQL이 압도적으로 유리하다. 이유는 단순하다 — 돈이 걸린 문제에서 "절반만 반영된 상태"는 용납이 안 되기 때문이다.

예를 들어, A 계좌에서 B 계좌로 이체하는 로직을 생각해 보자. 이 작업은 반드시 두 행이 함께 업데이트되거나, 아예 둘 다 업데이트되지 않아야 한다.

BEGIN;

UPDATE accounts
  SET balance = balance - 50000
  WHERE id = 'user_A';

UPDATE accounts
  SET balance = balance + 50000
  WHERE id = 'user_B';

-- 어느 한 쪽에서 오류 발생 시 전체 롤백
COMMIT;

PostgreSQL의 ACID 트랜잭션 모델은 이 패턴을 수십 년간 검증해 왔다. MongoDB도 v4.0부터 멀티 도큐먼트 트랜잭션을 지원하지만, 이는 문서 DB의 설계 철학과 방향이 다르며 성능 오버헤드도 존재한다.

MongoDB를 굳이 사용한다면 복잡한 트랜잭션을 애플리케이션 레이어에서 직접 처리해야 한다. 가능은 하지만, DB 레이어에서 이미 해결되는 문제를 굳이 위로 올릴 이유가 없다.

판단 질문: "이 작업이 실패했을 때, 절반만 적용된 상태가 존재해선 안 되는가?" — YES라면 PostgreSQL.


3. 시나리오 2 — 사용자 프로필·콘텐츠

"구조가 매번 다르다면"

선택: MongoDB

SaaS 제품이나 CMS처럼 사용자마다 입력 필드가 다르거나, 콘텐츠 타입에 따라 구조가 전혀 달라지는 경우가 있다. 예를 들어 채용 플랫폼을 생각해 보자.

  • 개발자 프로필에는 githubUrl, techStack[], openSourceContribs가 있다.
  • 디자이너 프로필에는 portfolioUrl, tools[], dribbbleHandle이 있다.
  • 마케터 프로필에는 campaignHistory[], certifications[]가 있다.

관계형 DB에서 이를 표현하려면 선택지가 좁다. 공통 컬럼에 JSON 컬럼을 혼용하거나, 타입별 테이블을 따로 만들거나 — 둘 다 어딘가 어색하다.

MongoDB에서는 자연스럽다:

// 개발자 프로필
{
  "_id": "user_101",
  "type": "developer",
  "name": "김백엔드",
  "techStack": ["Node.js", "PostgreSQL", "Redis"],
  "githubUrl": "https://github.com/kimbackend",
  "openSourceContribs": 12
}

// 디자이너 프로필
{
  "_id": "user_202",
  "type": "designer",
  "name": "이프론트",
  "tools": ["Figma", "Framer"],
  "portfolioUrl": "https://lefront.design",
  "dribbbleHandle": "@lefront"
}

각 도큐먼트가 독립적인 구조를 갖되, 공통 필드(_id, type, name)로 검색과 필터링이 가능하다. 새로운 직군이 생겨도 기존 도큐먼트를 건드릴 필요가 없다.

단, 한 가지 주의가 필요하다. "스키마가 없다"는 건 "아무렇게나 저장해도 된다"는 뜻이 아니다. MongoDB의 Schema Validation 기능으로 최소한의 필드 타입과 필수 값은 강제하는 것이 좋다. 그렇지 않으면 6개월 후 데이터가 뒤죽박죽이 되어 있을 것이다.

판단 질문: "도큐먼트마다 구조가 다르고, 그 구조가 앞으로도 계속 변화할 것인가?" — YES라면 MongoDB.


4. 시나리오 3 — 리포팅·대시보드

"SQL의 독보적인 영역"

선택: PostgreSQL

"지난 30일간 지역별 매출 상위 10개 카테고리를 보여주세요."

이런 요구사항이 나오는 순간, SQL은 그야말로 날개를 단다.

SELECT
  r.name           AS region,
  c.name           AS category,
  SUM(o.total_price) AS revenue,
  COUNT(DISTINCT o.user_id) AS unique_buyers
FROM orders o
JOIN users u     ON o.user_id = u.id
JOIN regions r   ON u.region_id = r.id
JOIN products p  ON o.product_id = p.id
JOIN categories c ON p.category_id = c.id
WHERE o.created_at >= NOW() - INTERVAL '30 days'
  AND o.status = 'completed'
GROUP BY r.id, c.id
ORDER BY revenue DESC
LIMIT 10;

이 쿼리 하나로 끝이다. 인덱스만 잘 설계되어 있다면 수백만 건의 데이터도 빠르게 처리한다.

MongoDB의 집계 파이프라인(Aggregation Pipeline)으로도 유사한 결과를 낼 수 있다. 하지만 복잡한 다중 조인과 집계가 섞인 쿼리를 MongoDB로 표현하면 코드가 상당히 길어지고, SQL에 익숙한 팀에게는 가독성도 훨씬 떨어진다.

// 같은 결과를 MongoDB Aggregation Pipeline으로 작성하면
db.orders.aggregate([
  { $match: { status: "completed", created_at: { $gte: thirtyDaysAgo } } },
  { $lookup: { from: "users",     localField: "user_id",    foreignField: "_id", as: "user" } },
  { $unwind: "$user" },
  { $lookup: { from: "products",  localField: "product_id", foreignField: "_id", as: "product" } },
  { $unwind: "$product" },
  // ... 계속 이어짐
]);

Metabase, Superset, Grafana 같은 BI 도구들은 대부분 SQL을 기반으로 한다. 리포팅 요구사항이 강한 프로젝트에서 PostgreSQL은 사실상 기본값이다.

판단 질문: "이해관계자들이 정기적으로 데이터 리포트를 요청할 것인가?" — YES라면 PostgreSQL.


5. 시나리오 4 — 실시간 이벤트·로그

"쓰기가 폭발적으로 많다면"

선택: MongoDB (또는 전용 솔루션)

사용자 행동 이벤트, 앱 로그, IoT 센서 데이터처럼 초당 수천 건 이상의 쓰기가 발생하고, 각 이벤트의 구조가 제각각인 경우가 있다.

// 이벤트마다 페이로드 구조가 다르다
{ "type": "page_view",    "url": "/products",         "duration_ms": 1240, "device": "mobile" }
{ "type": "button_click", "element": "add_to_cart",   "product_id": "p_99" }
{ "type": "error",        "code": 500,                "stack": "TypeError: ...", "context": {} }

이런 데이터를 관계형 테이블에 억지로 넣으려면, 공통 컬럼 외 나머지를 JSON 컬럼에 쑤셔 넣게 된다 — 그 순간 PostgreSQL의 강점인 "명시적 스키마"의 의미가 퇴색된다. MongoDB의 유연한 도큐먼트 모델이 이 패턴에 훨씬 자연스럽게 맞는다.

단, 쓰기 볼륨이 정말 극단적으로 크다면 MongoDB보다도 Apache Kafka + ClickHouse 또는 Elasticsearch 같은 전용 솔루션을 검토하는 게 맞다. MongoDB는 "어느 정도 유연하고 빠른 쓰기"를 원할 때의 선택이지, 모든 이벤트 스트리밍 문제의 답은 아니다.

판단 질문: "이벤트 구조가 다양하고, 쓰기가 많으며, 나중에 집계해서 볼 데이터인가?" — MongoDB. "실시간 스트리밍 + 분석이 핵심"이라면 전용 솔루션 병행 검토.


6. 시나리오 5 — Nest.js + TypeScript 백엔드

"타입 안전성 우선"

기본값: PostgreSQL + Prisma

Nest.js와 TypeScript 조합으로 백엔드를 구성할 때, 특별한 이유가 없다면 PostgreSQL + Prisma를 기본값으로 삼는다. 이유는 명확하다.

Prisma의 스키마 파일 하나로 DB 구조 정의, 타입 자동 생성, 마이그레이션이 한 번에 처리된다. 코드와 DB 스키마가 항상 동기화되고, 잘못된 필드명이나 타입 불일치를 컴파일 타임에 잡아낼 수 있다.

// prisma/schema.prisma
model User {
  id        String      @id @default(cuid())
  email     String      @unique
  name      String
  orders    Order[]
  createdAt DateTime    @default(now())
}

model Order {
  id        String      @id @default(cuid())
  total     Decimal
  status    OrderStatus
  user      User        @relation(fields: [userId], references: [id])
  userId    String
}
// 자동 생성된 타입으로 완전한 타입 안전성
const orders = await prisma.order.findMany({
  where: { status: 'COMPLETED', user: { email: 'dev@example.com' } },
  include: { user: true },
});
// orders는 완전히 타입이 추론된 상태

MongoDB + Mongoose도 TypeScript를 지원하고, Prisma 역시 MongoDB를 지원한다. 하지만 문서 DB 특유의 중첩 타입을 TypeScript로 완벽히 표현하려면 생각보다 많은 보일러플레이트가 필요하다. 데이터가 명확히 관계형이라면 PostgreSQL이 TypeScript와 더 자연스럽게 어울린다.

판단 질문: "Nest.js + TypeScript 환경에서 데이터가 구조적이고 관계가 명확한가?" — PostgreSQL + Prisma를 기본값으로.


7. 흔히 저지르는 선택 실수 3가지

실수 1: "MongoDB는 스키마가 없으니까 빠르게 시작할 수 있다"

맞는 말이다 — 초기에는. 하지만 프로덕션에서 데이터가 쌓이기 시작하면, 인덱스 없이 도큐먼트를 넣어온 댓가를 치르게 된다. MongoDB에서도 어떤 필드로 조회할지, 도큐먼트 크기는 얼마나 될지, 어떤 필드는 필수인지 처음부터 고민해야 한다. "스키마리스 = 설계 불필요"는 위험한 오해다.

실수 2: "관계형이 더 올바른 설계다"

기술 커뮤니티에는 "NoSQL은 스케일 때문에 유행한 것이고, 결국 관계형이 정답"이라는 시각이 있다. 하지만 데이터가 진짜 문서 형태이고 항상 통째로 읽힌다면, 굳이 그걸 쪼개서 여러 테이블에 넣을 이유가 없다. 이념이 아니라 데이터의 형태와 접근 패턴이 기준이 되어야 한다.

실수 3: "잘 돌아가고 있는 DB를 갈아엎는다"

현재 시스템이 잘 작동하고 있다면, 다른 DB가 유행한다고 해서 마이그레이션을 서두를 필요가 없다. 마이그레이션은 명확한 문제(예: MongoDB로 복잡한 리포팅이 고통스럽다, PostgreSQL에서 스키마 변경이 너무 잦아 병목이 된다)가 있을 때 하는 것이다. 기술 선택의 조급함은 시간과 비용을 잡아먹는다.


8. 마치며

5가지 시나리오를 요약하면 다음과 같다.

시나리오추천핵심 이유
결제·정산PostgreSQLACID 트랜잭션, 데이터 무결성
가변 프로필·콘텐츠MongoDB유연한 스키마, 구조 다양성
리포팅·대시보드PostgreSQLSQL 집계, BI 도구 연동
이벤트·로그MongoDB유연한 페이로드, 빠른 쓰기
Nest.js + TypeScriptPostgreSQL + Prisma타입 안전성, 명확한 관계

결국 질문은 하나다: 도메인의 모양을 먼저 보라.

그리고 그 중간 어딘가라면? — Part 3에서 이야기할 하이브리드 전략으로 넘어갈 차례다.

Part 3 예고:

  • PostgreSQL + MongoDB를 함께 쓰는 하이브리드 아키텍처
  • 언제 두 DB를 분리하고, 언제 하나로 통일할까
  • 실전 마이그레이션: MongoDB에서 PostgreSQL로, 그리고 그 반대
  • 2026년 주목할 대안들: PlanetScale, SurrealDB, EdgeDB

이 글 공유하기

시리즈 내비게이션

PostgreSQL vs MongoDB

2 / 3 · 3

같은 주제 더 보기·대표 시리즈로 시작

English

최신 글을 RSS로 받아보세요

뉴스레터 오픈 전에는 RSS로 먼저 업데이트를 받아보실 수 있습니다.

RSS 구독 안내 보기