Skip to main content
backend2025년 11월 30일10분 소요

올바른 데이터베이스 선택: 실제 프로젝트를 위한 관계형 vs NoSQL

실제 요구사항에 따라 PostgreSQL, MongoDB, Firebase, Redis 및 기타 데이터베이스 중에서 선택하기 위한 실용적인 의사결정 프레임워크.

databasepostgresqlmongodb
올바른 데이터베이스 선택: 실제 프로젝트를 위한 관계형 vs NoSQL

"그때그때 다르다"는 모든 데이터베이스 질문에 대한 솔직한 답변이지만, 동시에 쓸모없는 답변이기도 합니다. 프로젝트를 시작하고 데이터베이스를 선택해야 할 때, 장단점 목록보다 더 구체적인 것이 필요합니다. 데이터 형태, 쿼리 패턴, 팀 전문성, 스케일링 기대치, 운영 복잡성 등 실제 요구사항을 고려하는 의사결정 프레임워크가 필요합니다.

저는 다양한 프로덕션 프로젝트에서 PostgreSQL, MongoDB, Firebase/Firestore, Redis, SQLite를 사용해왔습니다. 각 선택은 해당 컨텍스트에 적합했지만, 몇몇은 잘못되어 마이그레이션해야 했습니다. 이러한 결정 뒤에 숨겨진 패턴은 어떤 벤치마크 비교보다 유용합니다.

의사결정 프레임워크

개별 데이터베이스를 살펴보기 전에 프로젝트에 대한 다음 다섯 가지 질문에 답하십시오.

1. 데이터 형태는 어떻습니까?

이것은 "관계형 vs 문서형"에 대한 것이 아닙니다. 데이터 엔티티가 서로 어떻게 관련되어 있는지에 대한 것입니다.

고도로 관계형: 사용자는 주문을 가지고 있고, 주문은 항목을 가지고 있으며, 항목은 카테고리에 속하고, 카테고리는 계층 구조를 가집니다. 데이터 모델을 그렸을 때 많은 연결을 가진 그래프처럼 보인다면 강력한 관계형 기능이 필요합니다.

문서 지향형: 각 엔티티는 비교적 독립적입니다. 블로그 게시물은 제목, 본문, 태그 및 작성자 정보를 포함합니다. 엔티티 유형 간에 조인할 필요가 거의 없습니다.

키-값: 키로 저장하고 검색해야 합니다. 세션 데이터, 구성, 기능 플래그.

시계열: 이벤트, 메트릭, 로그. 쓰기 중심, 추가 전용, 시간 범위별 쿼리.

2. 쿼리 패턴은 어떻습니까?

알려진 쿼리: 애플리케이션이 실행할 쿼리를 정확히 알고 있습니다. 전자상거래: "사용자 주문 가져오기", "카테고리별 제품 찾기", "이번 달 총 수익 계산". 알려진 쿼리는 인덱스와 쿼리 계획으로 최적화할 수 있는 관계형 데이터베이스에 유리합니다.

임시 쿼리: 사용자가 예측할 수 없는 방식으로 검색, 필터링 및 집계를 수행할 수 있습니다. 분석 대시보드, 검색 기능, 보고 도구. 이러한 쿼리는 유연한 쿼리 엔진에 유리합니다.

간단한 조회: 대부분의 읽기는 "ID로 문서 가져오기"입니다. 문서 데이터베이스와 키-값 저장소가 이 분야에서 뛰어납니다.

3. 일관성 요구사항은 무엇입니까?

강력한 일관성: 뱅킹, 재고, 오래된 데이터를 읽는 것이 실제 문제를 야기하는 모든 경우. ACID 트랜잭션을 지원하는 관계형 데이터베이스가 안전한 선택입니다.

최종 일관성: 소셜 피드, 분석, 캐싱. 성능과 가용성을 위해 약간 오래된 데이터를 읽는 것을 허용할 수 있습니다.

4. 스케일링 경로는 어떻습니까?

수직 스케일링으로 충분: 대부분의 애플리케이션. 데이터베이스가 성장할 여유가 있는 단일 머신에 맞는다면 어떤 데이터베이스든 작동합니다. 최신 서버의 PostgreSQL은 수백만 개의 행을 문제없이 처리합니다.

수평 스케일링이 필요: 여러 머신에 데이터를 분산해야 합니다. 이는 대부분의 개발자가 생각하는 것보다 드물지만, 필요할 때는 데이터베이스 선택이 제한됩니다.

5. 팀은 무엇을 알고 있습니까?

이 요소는 과소평가됩니다. 데이터 모델에 MongoDB가 이론적으로 더 적합하더라도 PostgreSQL 전문가 팀은 PostgreSQL을 사용할 때 더 생산적일 것입니다. 새로운 데이터베이스를 배우는 비용(익숙하지 않은 오류 디버깅, 운영 모범 사례 학습, 성능 특성 이해)은 실제적이고 중요합니다.

PostgreSQL: 기본 선택

확실하지 않다면 PostgreSQL을 선택하십시오. 이는 백엔드 엔지니어들 사이에서 논란의 여지가 있는 의견이 아니라 합의된 의견입니다. PostgreSQL은 관계형 데이터, JSON 문서, 전체 텍스트 검색, 지리 공간 쿼리 및 시계열 데이터를 처리합니다. 이들 중 어느 하나에서 최고는 아니지만, 대부분의 애플리케이션에서 단일 데이터베이스로 사용하기에 충분히 좋습니다.

강점

ACID 트랜잭션. 재고를 차감하고 주문을 생성하거나, 계좌 간에 돈을 이체하는 등 여러 테이블을 원자적으로 업데이트해야 할 때 PostgreSQL은 정확성을 보장합니다.

풍부한 쿼리 기능. 윈도우 함수, CTE(Common Table Expressions), 재귀 쿼리, 래터럴 조인. SQL은 단순히 SELECT * FROM이 아닙니다. 데이터를 애플리케이션 계층으로 이동하지 않고도 복잡한 분석을 표현할 수 있는 강력한 쿼리 언어입니다.

-- Find each user's most recent order with running total
WITH ranked_orders AS (
  SELECT
    user_id,
    order_id,
    total,
    created_at,
    ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) as rn,
    SUM(total) OVER (PARTITION BY user_id ORDER BY created_at) as running_total
  FROM orders
)
SELECT * FROM ranked_orders WHERE rn = 1;

JSONB 컬럼. 데이터의 일부가 사용자 환경 설정, 동적 양식 응답, 타사 웹훅 페이로드와 같이 진정으로 스키마가 없는 경우, JSONB로 저장하고 전체 인덱싱 지원을 통해 쿼리할 수 있습니다.

-- Store and query semi-structured data
CREATE TABLE products (
  id SERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  attributes JSONB DEFAULT '{}'
);

-- Index a specific JSON path
CREATE INDEX idx_products_color ON products USING GIN ((attributes->'color'));

-- Query JSON data
SELECT name FROM products
WHERE attributes->>'color' = 'blue'
AND (attributes->>'weight')::numeric < 10;

확장 생태계. 지리 공간 데이터를 위한 PostGIS, 퍼지 텍스트 검색을 위한 pg_trgm, 시계열 데이터를 위한 TimescaleDB, AI 임베딩을 위한 pgvector. 확장 모델은 PostgreSQL이 기본 데이터베이스를 교체하지 않고도 특수 워크로드에 적응할 수 있음을 의미합니다.

PostgreSQL이 올바른 선택이 아닌 경우

  • 수평 스케일링을 통한 대규모 쓰기 처리량. PostgreSQL은 수직적으로 잘 스케일링되지만(더 큰 머신), 수평 샤딩은 복잡합니다. 초당 수백만 개의 이벤트를 수십 대의 머신에 걸쳐 써야 한다면, 목적에 맞게 구축된 솔루션을 고려하십시오.
  • 스키마 변경이 매일 발생하는 빠른 프로토타이핑. 스타트업의 초기 단계에서 데이터 모델이 매주 근본적으로 변경될 때, 마이그레이션 오버헤드는 스키마 없는 옵션에 비해 속도를 늦출 수 있습니다.
  • 팀에 SQL 경험이 전혀 없고 프로젝트 일정상 학습할 시간이 없는 경우. 드물지만 이런 경우도 있습니다.

MongoDB: 문서가 합리적일 때

MongoDB는 마땅히 받아야 할 비판보다 더 많은 비판을 받습니다. 초기 마케팅("스키마 없음! 웹 스케일!")은 비현실적인 기대를 불러일으켰고, 많은 개발자들이 PostgreSQL이 더 나았을 사용 사례에 MongoDB를 사용했습니다. 하지만 MongoDB가 올바른 선택인 진정한 시나리오도 있습니다.

MongoDB가 승리할 때

콘텐츠 관리 시스템. 각 콘텐츠 조각은 다른 구조를 가집니다. 기사는 비디오와 다른 필드를 가지며, 비디오는 팟캐스트와 다른 필드를 가집니다. MongoDB에서는 이 모든 것이 다른 형태를 가진 동일한 컬렉션의 문서입니다. PostgreSQL에서는 많은 nullable 컬럼을 가진 넓은 테이블을 만들거나, JSON 컬럼을 사용하거나(이 시점에서 PostgreSQL에서 MongoDB의 패러다임을 사용하는 것), 복잡한 조인을 가진 테이블당 유형을 생성해야 합니다.

이벤트 소싱 및 로깅. 대부분 쓰기 및 읽기만 하고, 거의 업데이트되지 않으며, 조인되지 않는 문서로 높은 쓰기 처리량을 가집니다.

임베디드 문서는 조인을 줄입니다. 주문이 항상 항목을 필요로 하고, 항목이 주문 없이 액세스되지 않는다면, 주문 문서 내에 항목을 임베딩하는 것은 가장 일반적인 쿼리가 두 테이블 간의 조인 대신 단일 문서 조회임을 의미합니다.

// MongoDB document with embedded sub-documents
{
  _id: ObjectId("..."),
  customer: {
    name: "John Doe",
    email: "john@example.com"
  },
  items: [
    { product: "Widget", quantity: 2, price: 9.99 },
    { product: "Gadget", quantity: 1, price: 24.99 }
  ],
  total: 44.97,
  status: "shipped",
  createdAt: ISODate("2026-03-01T10:00:00Z")
}

수평 스케일링은 일급 기능입니다. MongoDB의 샤딩은 내장되어 있으며 잘 문서화되어 있습니다. 많은 노드에 데이터를 분산해야 하는 경우(높은 처리량으로 수억 개의 문서), MongoDB는 PostgreSQL보다 더 우아하게 이를 처리합니다.

MongoDB가 잘못된 경우

  • 고도로 관계형 데이터. 모든 쿼리에서 $lookup 집계(MongoDB의 조인 버전)를 작성하고 있다면 잘못된 데이터베이스를 선택한 것입니다.
  • 컬렉션 간 트랜잭션이 자주 필요한 경우. MongoDB는 다중 문서 트랜잭션을 지원하지만, 이는 지연 시간과 복잡성을 추가합니다. 핵심 작업에 컬렉션 간 원자성이 필요한 경우 PostgreSQL의 트랜잭션 모델이 더 자연스럽습니다.
  • 실제로 스키마가 필요한 경우. "스키마 없음"은 자유롭게 들리지만, 데이터베이스에서 읽는 모든 코드 조각이 암묵적으로 스키마라는 것을 깨닫기 전까지는 그렇습니다. 데이터베이스에서 강제하는 스키마가 없으면 유효성 검사를 애플리케이션 계층으로 이동하게 되는데, 여기서는 오류가 발생하기 쉽고 일관되게 적용하기 어렵습니다.

Firebase 및 Firestore: 빠른 개발

Firebase Realtime Database와 Firestore는 특정 틈새 시장에 적합합니다. 데이터 모델링의 순수성보다 개발 속도가 더 중요하고, 백엔드가 복잡한 비즈니스 로직 엔진이라기보다는 주로 데이터 지속성 및 동기화 계층인 애플리케이션입니다.

Firebase가 빛을 발할 때

실시간 동기화를 지원하는 모바일 우선 앱. Firebase의 SDK는 오프라인 캐싱, 실시간 리스너 및 충돌 해결을 기본적으로 처리합니다. PostgreSQL로 이를 처음부터 구축하려면 WebSocket 계층, 캐싱 전략 및 상당한 사용자 지정 코드가 필요합니다.

백엔드 엔지니어가 없는 소규모 팀. Firebase는 데이터베이스 서버 관리, API 계층 구축, 인증 구현 및 파일 저장소 처리의 필요성을 없앱니다. MVP를 구축하는 두 명의 팀에게 이러한 운영 오버헤드 감소는 출시 여부를 결정하는 차이가 될 수 있습니다.

프로토타이핑 및 검증. 2주 안에 실제 사용자와 아이디어를 테스트해야 할 때 Firebase는 클라이언트 애플리케이션에 전적으로 집중할 수 있게 해줍니다. 아이디어가 검증되면 나중에 더 전통적인 백엔드로 마이그레이션할 수 있습니다.

Firebase의 한계

Firestore의 쿼리 제약. 인덱싱되지 않은 필드에 대해 쿼리할 수 없습니다. 여러 필드에 대해 부등식 필터를 수행할 수 없습니다. 전체 텍스트 검색을 수행할 수 없습니다. 이러한 제약으로 인해 데이터를 적극적으로 비정규화하고 때로는 컬렉션 간에 데이터를 중복해야 합니다.

// Firestore: You CAN'T do this
db.collection('products')
  .where('price', '>', 10)
  .where('rating', '>', 4)
  .orderBy('name')  // Error: needs composite index on price + rating + name

// Firestore: You CAN do this (with a composite index)
db.collection('products')
  .where('price', '>', 10)
  .where('rating', '>', 4)
  .orderBy('price')  // Must order by a field used in inequality

벤더 종속성. 데이터 모델, 보안 규칙 및 쿼리 패턴은 모두 Firebase에 특화되어 있습니다. Firebase에서 벗어나는 것은 마이그레이션이 아니라 재작성입니다.

예측 불가능한 비용. Firebase는 문서 읽기/쓰기당 요금을 청구합니다. 제대로 최적화되지 않은 쿼리나 대규모 컬렉션의 실시간 리스너는 예상치 못한 요금을 발생시킬 수 있습니다. 저는 단일 잘못 구성된 리스너가 전용 PostgreSQL 서버 1년 비용보다 더 많이 드는 프로젝트를 본 적이 있습니다.

제한된 서버 측 로직. Cloud Functions는 일부 서버 측 처리를 처리할 수 있지만, 복잡한 비즈니스 로직(다단계 트랜잭션, 데이터 집계, 백그라운드 처리)은 창의적인 해결 방법이나 어쨌든 별도의 백엔드를 필요로 합니다.

결정: Firebase vs 전통적인 백엔드

Factor Firebase PostgreSQL + API
Time to MVP Days Weeks
Operational overhead Near zero Moderate
Query flexibility Limited Full SQL
Cost at scale Unpredictable Predictable
Vendor lock-in High Low
Offline support Built-in Must build
Complex business logic Difficult Natural
Team required Frontend only Frontend + Backend

Redis: 캐싱, 큐 등

Redis는 대부분의 애플리케이션에서 기본 데이터베이스가 아닙니다(지속성 모듈이 있는 Redis는 그 역할을 할 수 있지만). Redis는 특정 문제에 탁월한 고성능 데이터 구조 저장소입니다.

캐싱

가장 일반적인 Redis 사용 사례. 비용이 많이 드는 데이터베이스 쿼리, API 응답 또는 계산된 결과를 캐시합니다.

import redis
import json

r = redis.Redis(host='localhost', port=6379, db=0)

def get_user_profile(user_id: str):
    # Check cache first
    cached = r.get(f"user:{user_id}")
    if cached:
        return json.loads(cached)

    # Cache miss — query database
    profile = db.query("SELECT * FROM users WHERE id = %s", user_id)

    # Cache for 5 minutes
    r.setex(f"user:{user_id}", 300, json.dumps(profile))
    return profile

세션 저장소

TTL(time-to-live) 지원을 포함하는 Redis의 키-값 모델은 세션 데이터에 자연스럽게 적합합니다. 데이터베이스 기반 세션보다 빠르고, 상태 저장 애플리케이션을 위한 JWT 기반 솔루션보다 간단합니다.

속도 제한

def is_rate_limited(user_id: str, limit: int = 100, window: int = 60) -> bool:
    key = f"rate:{user_id}"
    current = r.incr(key)
    if current == 1:
        r.expire(key, window)
    return current > limit

작업 큐

Redis 목록과 스트림은 훌륭한 작업 큐를 만듭니다. Bull(Node.js), Celery(Python), Sidekiq(Ruby)와 같은 라이브러리는 Redis를 메시지 브로커로 사용합니다.

Redis를 사용하지 말아야 할 때

  • 유일한 데이터베이스로 사용하는 경우. Redis는 기본적으로 인메모리입니다. 지속성(RDB 스냅샷 또는 AOF 로그)을 사용하더라도 잃을 수 없는 데이터를 위해 설계되지 않았습니다.
  • 복잡한 쿼리를 위해. Redis 데이터 구조(문자열, 목록, 세트, 정렬된 세트, 해시)는 강력하지만 데이터베이스처럼 쿼리할 수 없습니다. 임의의 조건이 아닌 키로 데이터에 액세스합니다.
  • 캐싱 문제가 없는 경우. 캐싱이 필요 없는 스택에 Redis를 추가하는 것은 이점 없이 운영 복잡성만 증가시킵니다. 5ms가 걸리는 PostgreSQL 쿼리는 캐시할 필요가 없습니다.

여러 데이터베이스 사용

중간 복잡도의 대부분의 프로덕션 애플리케이션은 하나 이상의 데이터베이스를 사용합니다. 핵심은 각 데이터베이스가 가장 잘하는 일에 사용하고, 하나의 데이터베이스가 모든 것을 처리하도록 강요하지 않는 것입니다.

웹 애플리케이션의 일반적인 패턴:

  • 핵심 비즈니스 데이터(사용자, 주문, 제품)를 위한 PostgreSQL.
  • 캐싱, 세션 및 속도 제한을 위한 Redis.
  • 파일 저장소(이미지, 문서, 백업)를 위한 S3 (또는 이에 상응하는 서비스).

실시간 애플리케이션의 경우:

  • 영구 데이터 및 복잡한 쿼리를 위한 PostgreSQL.
  • pub/sub 및 캐싱을 위한 Redis.
  • 모바일 클라이언트에 실시간 동기화를 위한 Firebase/Supabase.

통합 패턴

여러 데이터베이스를 사용할 때는 명확한 소유권을 설정하십시오. 각 데이터 조각은 하나의 권위 있는 소스를 가지며, 다른 시스템은 해당 데이터의 캐시 또는 뷰입니다.

PostgreSQL (source of truth)
    ├── Redis (read cache, expires after TTL)
    ├── Elasticsearch (search index, synced via change data capture)
    └── Firebase (mobile sync, updated via webhooks)

동일한 데이터의 소유자라고 생각하는 두 개의 데이터베이스를 동시에 사용하지 마십시오. 그 길은 디버깅하기 거의 불가능한 일관성 악몽으로 이어집니다.

마이그레이션 고려사항

잘못된 데이터베이스를 선택했음을 깨달았다면 마이그레이션은 가능하지만 비용이 많이 듭니다. 다음은 실용적인 고려사항입니다.

MongoDB에서 PostgreSQL로의 마이그레이션은 제가 본 가장 흔한 마이그레이션입니다. 일반적인 이유는 애플리케이션이 문서 쿼리를 넘어 복잡한 조인, 트랜잭션 또는 집계가 필요했기 때문입니다. 마이그레이션에는 관계형 스키마 설계, 변환 스크립트 작성, 애플리케이션의 모든 데이터베이스 쿼리 업데이트가 포함됩니다. 중간 복잡도의 애플리케이션의 경우 2~4주를 예상하십시오.

PostgreSQL에서 MongoDB로의 마이그레이션은 더 드물지만, 애플리케이션의 데이터 모델이 주로 문서 지향적으로 변할 때 발생합니다. 마이그레이션은 기계적으로 더 간단하지만(테이블을 문서로 평탄화), 모든 쿼리를 다시 생각하고 트랜잭션 보장을 잃어야 합니다.

Firebase에서 PostgreSQL로의 마이그레이션은 가장 어려운 마이그레이션입니다. 단순히 데이터베이스 변경이 아니라 아키텍처 변경이기 때문입니다. API 계층을 구축하고, 인증을 구현하고, 실시간 리스너를 WebSocket 또는 폴링으로 교체하고, 오프라인 동기화를 처리해야 합니다. 이는 마이그레이션이라기보다는 재작성에 가깝습니다.

최고의 마이그레이션은 피하는 마이그레이션입니다. 데이터 모델에 대해 미리 하루 더 생각하십시오. 유사한 애플리케이션을 구축한 사람과 이야기하십시오. 처음에 올바른 데이터베이스를 선택하는 비용은 나중에 마이그레이션하는 비용보다 항상 적습니다.

비용 분석

대규모 데이터베이스 비용은 특히 관리형 서비스의 경우 놀라울 수 있습니다.

Service Free Tier Small Production Medium Production
Supabase (PostgreSQL) 500MB, 2 projects ~$25/mo (8GB, 2 cores) ~$100/mo (32GB, 4 cores)
Neon (PostgreSQL) 0.5GB storage ~$19/mo ~$69/mo
MongoDB Atlas 512MB shared ~$57/mo (M10 dedicated) ~$200/mo (M30)
Firebase Firestore 1GB storage, 50k reads/day ~$25-100/mo (varies wildly) $100-1000/mo (query dependent)
Redis Cloud 30MB ~$7/mo (250MB) ~$60/mo (1GB)
PlanetScale (MySQL) 5GB, 1B row reads/mo ~$39/mo ~$99/mo

Firebase의 예측 불가능한 비용은 강조할 가치가 있습니다. 저는 몇 달 동안 월 30달러 미만으로 유지되던 프로젝트 비용이 문서 읽기 증가를 유발하는 기능 출시 후 300달러로 급증하는 것을 본 적이 있습니다. PostgreSQL 또는 MongoDB의 경우 비용은 머신 크기와 관련이 있어 예측 가능합니다.

나의 실제 의사결정 과정

새로운 프로젝트를 시작할 때

DU

Danil Ulmashev

Full Stack Developer

함께 일하는 데 관심이 있으신가요?