개발자라면 한 번쯤 고민해봤을 질문입니다. MongoDB 같은 NoSQL 데이터베이스가 각광받으면서, 마치 모든 프로젝트에 NoSQL을 써야 할 것 같은 분위기도 있죠.
하지만 NoSQL이 무조건 좋은 건 아닙니다.
이 글에서는 실제 사례를 통해 언제 NoSQL(MongoDB)을 쓰고, 언제 SQL(PostgreSQL/MySQL)을 써야 하는지 명확히 알려드리겠습니다.
용어 정리부터 하고 가죠
SQL vs NoSQL - 기본 용어 비교
SQL (PostgreSQL/MySQL)NoSQL (MongoDB)
| Database | Database |
| Table | Collection |
| Row | Document |
| Column | Field |
SQL의 "테이블"은 NoSQL에서 "컬렉션"이라고 부릅니다. 용어만 다를 뿐, 개념은 비슷합니다.
결론
NoSQL(MongoDB)을 선택하세요:
- ✅ 스키마가 자주 바뀌는 프로젝트
- ✅ 계층 구조 데이터 (댓글, 카테고리)
- ✅ 다양한 콘텐츠 타입을 다루는 CMS
- ✅ 대량의 로그/분석 데이터
- ✅ 빠른 프로토타이핑
SQL(PostgreSQL/MySQL)을 선택하세요:
- ✅ 명확하고 복잡한 관계
- ✅ 트랜잭션이 중요한 경우
- ✅ 데이터 무결성이 중요한 경우
- ✅ 복잡한 분석 쿼리가 많은 경우
❌ NoSQL을 쓰면 안 되는 경우
1. 관계가 명확하고 복잡한 경우
예시: JWT 인증 시스템
User ─┬─ RefreshToken (1:N)
├─ PasswordResetToken (1:N)
└─ EmailVerificationToken (1:N)
이런 구조에서는 사용자와 토큰의 관계가 명확합니다.
MongoDB로 구현하면?
// 1. 사용자 조회
const user = await User.findById(userId);
// 2. 토큰들 따로 조회 (별도 쿼리 필요)
const tokens = await RefreshToken.find({ userId: user._id });
const resetTokens = await PasswordResetToken.find({ userId: user._id });
// 👎 여러 번 쿼리해야 하고, 코드가 복잡함
PostgreSQL로 구현하면?
-- 한 번의 쿼리로 모든 데이터 가져오기
SELECT u.*,
rt.token as refresh_token,
prt.token as reset_token
FROM users u
LEFT JOIN refresh_tokens rt ON u.id = rt.user_id
LEFT JOIN password_reset_tokens prt ON u.id = prt.user_id
WHERE u.id = ?;
-- ✅ 간단하고 명확함!
결론: 테이블 간 관계가 명확하고 JOIN이 자주 필요하면 PostgreSQL
2. 트랜잭션이 중요한 경우
예시: 결제 시스템
결제할 때는 여러 작업이 동시에 일어납니다:
- 사용자 포인트 차감
- 주문 생성
- 재고 감소
이 중 하나라도 실패하면 전부 취소되어야 합니다. (All or Nothing)
PostgreSQL의 강력한 트랜잭션
BEGIN;
-- 포인트 차감
UPDATE users SET points = points - 10000 WHERE id = ?;
-- 주문 생성
INSERT INTO orders (user_id, amount) VALUES (?, 10000);
-- 재고 감소
UPDATE products SET stock = stock - 1 WHERE id = ?;
COMMIT;
-- 중간에 에러 발생하면 자동으로 ROLLBACK!
-- ✅ 데이터 일관성 보장
MongoDB는?
// MongoDB도 트랜잭션 지원하지만 복잡함
const session = await mongoose.startSession();
session.startTransaction();
try {
await User.updateOne({...}, {$inc: {points: -10000}}, {session});
await Order.create([{...}], {session});
await Product.updateOne({...}, {$inc: {stock: -1}}, {session});
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
} finally {
session.endSession();
}
// 👎 코드가 복잡하고 성능도 PostgreSQL만큼 안 나옴
결론: 트랜잭션이 중요하면 PostgreSQL
3. 데이터 무결성이 중요한 경우
예시: 외래키 제약조건
-- PostgreSQL은 외래키로 데이터 무결성 강제
ALTER TABLE refresh_tokens
ADD CONSTRAINT fk_user
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
-- 사용자 삭제 → 관련 토큰도 자동 삭제!
DELETE FROM users WHERE id = ?;
-- ✅ 고아 데이터(orphan data) 발생 안 함
MongoDB는 외래키 개념이 없어서 직접 관리해야 합니다.
// MongoDB는 수동으로 처리
await User.deleteOne({ _id: userId });
await RefreshToken.deleteMany({ userId: userId });
await PasswordResetToken.deleteMany({ userId: userId });
// 👎 개발자가 깜빡하면 고아 데이터 발생!
결론: 데이터 무결성이 중요하면 PostgreSQL
4. 복잡한 쿼리가 많은 경우
예시: 통계 쿼리
-- 이런 쿼리는 SQL이 압도적으로 쉬움
SELECT
u.name,
COUNT(o.id) as order_count,
SUM(o.amount) as total_amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at > '2024-01-01'
GROUP BY u.id
HAVING total_amount > 1000
ORDER BY total_amount DESC
LIMIT 10;
-- ✅ 읽기 쉽고 명확함
MongoDB로 같은 쿼리를 작성하면:
// Aggregation Pipeline... 😰
db.users.aggregate([
{
$lookup: {
from: "orders",
localField: "_id",
foreignField: "userId",
as: "orders"
}
},
{
$match: {
createdAt: { $gt: new Date("2024-01-01") }
}
},
{
$project: {
name: 1,
order_count: { $size: "$orders" },
total_amount: { $sum: "$orders.amount" }
}
},
{
$match: {
total_amount: { $gt: 1000 }
}
},
{
$sort: { total_amount: -1 }
},
{
$limit: 10
}
]);
// 👎 복잡하고 가독성도 떨어짐
결론: 복잡한 집계/분석 쿼리가 많으면 PostgreSQL
✅ NoSQL이 빛을 발하는 경우
1. 스키마가 자주 바뀌는 경우
예시: 블로그/CMS 시스템
블로그를 만들다 보면 기능이 계속 추가됩니다:
// 1주차: 기본 블로그
{
title: "제목",
content: "내용",
author: "작성자"
}
// 2주차: 태그 추가
{
title: "제목",
content: "내용",
author: "작성자",
tags: ["javascript", "nodejs"] // 🆕
}
// 3주차: SEO 메타데이터 추가
{
title: "제목",
content: "내용",
author: "작성자",
tags: ["javascript", "nodejs"],
seo: { // 🆕
metaTitle: "...",
metaDescription: "...",
keywords: [...]
}
}
// 4주차: 조회수, 좋아요 추가
{
...,
views: 1500, // 🆕
likes: 42 // 🆕
}
MongoDB로 구현하면?
// 그냥 바로 추가하면 끝!
await Post.updateOne(
{ _id: postId },
{ $set: { views: 1500, likes: 42 } }
);
// ✅ 스키마 변경 없음! 바로 반영!
PostgreSQL로 구현하면?
-- 매번 테이블 수정 필요 😰
ALTER TABLE posts ADD COLUMN views INTEGER DEFAULT 0;
ALTER TABLE posts ADD COLUMN likes INTEGER DEFAULT 0;
-- 마이그레이션 파일 작성, 배포, 롤백 준비...
-- 👎 번거롭고 시간 소요
결론: 스키마가 자주 바뀌는 프로토타입이나 CMS는 MongoDB
2. 계층 구조 데이터
예시: 댓글 시스템 (무한 대댓글)
// MongoDB: 한 문서에 전부 저장
{
_id: 1,
text: "부모 댓글",
author: "유저1",
replies: [
{
text: "대댓글 1",
author: "유저2",
replies: [
{
text: "대대댓글",
author: "유저3",
replies: [
{
text: "대대대댓글",
author: "유저4"
}
]
}
]
},
{
text: "대댓글 2",
author: "유저5"
}
]
}
// 한 번의 쿼리로 전체 댓글 트리 조회!
const comment = await Comment.findById(commentId);
// ✅ 모든 대댓글이 이미 포함되어 있음!
PostgreSQL로 구현하면?
-- 재귀 쿼리 필요 (복잡함) 😰
WITH RECURSIVE comment_tree AS (
-- 부모 댓글
SELECT id, parent_id, text, author, 0 as depth
FROM comments
WHERE id = ?
UNION ALL
-- 자식 댓글들 재귀적으로
SELECT c.id, c.parent_id, c.text, c.author, ct.depth + 1
FROM comments c
INNER JOIN comment_tree ct ON c.parent_id = ct.id
)
SELECT * FROM comment_tree ORDER BY depth;
-- 또는 여러 번 쿼리
-- 1. 부모 댓글 조회
-- 2. 자식 댓글 조회
-- 3. 손자 댓글 조회
-- 4. 증손자 댓글 조회...
-- 👎 복잡하고 성능도 안 좋음
결론: 트리/계층 구조 데이터는 MongoDB
3. 다양한 콘텐츠 타입
예시: Headless CMS
CMS에서는 페이지마다 완전히 다른 구조를 가질 수 있습니다:
// 홈페이지
{
type: "homepage",
title: "메인",
hero: {
image: "hero.jpg",
title: "환영합니다",
subtitle: "최고의 서비스"
},
sections: [
{ type: "featured", posts: [...] },
{ type: "newsletter", placeholder: "이메일 입력" }
]
}
// 제품 페이지
{
type: "product",
name: "MacBook Pro",
price: 2490000,
specs: {
cpu: "M3 Pro",
ram: "32GB",
storage: "1TB"
},
variants: [
{ color: "silver", stock: 10 },
{ color: "space gray", stock: 5 }
]
}
// 블로그 포스트
{
type: "blog",
title: "NoSQL 선택 가이드",
content: "...",
author: {
name: "개발자",
avatar: "profile.jpg"
},
tags: ["database", "mongodb"]
}
// ✅ 모두 같은 'pages' 컬렉션에 저장!
// 타입마다 다른 필드를 가져도 OK!
PostgreSQL로 구현하면?
-- 방법 1: 각 타입마다 별도 테이블 😰
CREATE TABLE homepage_pages (...);
CREATE TABLE product_pages (...);
CREATE TABLE blog_pages (...);
-- 타입이 100개면 테이블 100개?
-- 방법 2: EAV 패턴 (Entity-Attribute-Value) - 지옥
CREATE TABLE pages (
id UUID PRIMARY KEY,
type VARCHAR(50)
);
CREATE TABLE page_attributes (
page_id UUID,
attribute_name VARCHAR(100),
attribute_value TEXT
);
-- 👎 쿼리가 악몽이 됨...
SELECT p.id,
MAX(CASE WHEN pa.attribute_name = 'title' THEN pa.attribute_value END) as title,
MAX(CASE WHEN pa.attribute_name = 'price' THEN pa.attribute_value END) as price,
...
FROM pages p
LEFT JOIN page_attributes pa ON p.id = pa.page_id
GROUP BY p.id;
결론: 다양한 콘텐츠 타입을 다루는 CMS는 MongoDB
4. 실시간 로그/분석 데이터
예시: 웹 애플리케이션 로그
// 로그마다 다른 구조
{
timestamp: ISODate("2025-10-06T10:30:45Z"),
level: "info",
action: "page_view",
userId: "user123",
page: "/products/123",
responseTime: 45,
metadata: {
referrer: "google.com",
device: "mobile"
}
}
{
timestamp: ISODate("2025-10-06T10:31:10Z"),
level: "error",
action: "payment_failed",
userId: "user456",
errorCode: "CARD_DECLINED",
amount: 50000,
metadata: {
paymentMethod: "card",
cardLast4: "1234"
}
}
MongoDB의 강점:
1. 초고속 쓰기 성능
// 비동기 대량 삽입
await Log.insertMany(thousandsOfLogs, { ordered: false });
// ✅ 초당 10,000+ 건 처리 가능
PostgreSQL은 트랜잭션 처리로 인해 초당 1,000건 정도가 한계입니다.
2. Capped Collection (자동 정리)
// 크기 제한 컬렉션 - 오래된 로그 자동 삭제
db.createCollection("logs", {
capped: true,
size: 10737418240, // 10GB
max: 10000000 // 최대 1천만 건
});
// ✅ 오래된 로그는 자동으로 삭제됨!
// 별도 크론잡 필요 없음!
PostgreSQL은 주기적으로 삭제 쿼리를 실행해야 하고, 수백만 건 삭제 시 DB 락이 발생할 수 있습니다.
3. 빠른 시계열 조회
// 시간 범위 쿼리가 매우 빠름
db.logs.find({
timestamp: {
$gte: ISODate("2025-10-06T00:00:00Z"),
$lt: ISODate("2025-10-07T00:00:00Z")
},
userId: "user123"
}).sort({ timestamp: -1 });
// ✅ 수백만 건에서도 밀리초 내 조회
4. 실시간 분석
// Aggregation Pipeline으로 실시간 대시보드
db.logs.aggregate([
{
$match: {
timestamp: { $gte: today },
action: "page_view"
}
},
{
$group: {
_id: {
hour: { $hour: "$timestamp" },
page: "$page"
},
count: { $sum: 1 },
avgResponseTime: { $avg: "$responseTime" }
}
},
{
$sort: { count: -1 }
},
{
$limit: 10
}
]);
// ✅ 시간대별 페이지 트래픽 분석
결론: 대량의 로그/분석 데이터는 MongoDB
실전 비교: 블로그 시스템
같은 기능을 두 DB로 구현해봅시다.
MongoDB 버전
// 포스트 생성 (한 번에)
const post = await Post.create({
title: "MongoDB vs PostgreSQL",
content: "내용...",
tags: ["database", "comparison"],
author: {
name: "개발자",
avatar: "profile.jpg"
},
comments: [],
views: 0,
likes: 0
});
// 댓글 추가 (한 번의 쿼리)
await Post.updateOne(
{ _id: postId },
{
$push: {
comments: {
author: "댓글러",
text: "좋은 글!",
createdAt: new Date(),
replies: []
}
}
}
);
// 전체 데이터 조회 (한 번의 쿼리)
const fullPost = await Post.findById(postId);
// ✅ 포스트, 태그, 댓글 모두 포함!
PostgreSQL 버전
-- 포스트 생성
INSERT INTO posts (title, content, author_name, author_avatar)
VALUES ('MongoDB vs PostgreSQL', '내용...', '개발자', 'profile.jpg');
-- 태그 저장 (별도 테이블, 별도 쿼리)
INSERT INTO post_tags (post_id, tag)
VALUES
(?, 'database'),
(?, 'comparison');
-- 댓글 저장 (별도 테이블, 별도 쿼리)
INSERT INTO comments (post_id, author, text, created_at)
VALUES (?, '댓글러', '좋은 글!', NOW());
-- 전체 데이터 조회 (3개 테이블 JOIN)
SELECT
p.*,
p.author_name,
p.author_avatar,
array_agg(DISTINCT t.tag) as tags,
json_agg(
json_build_object(
'author', c.author,
'text', c.text,
'createdAt', c.created_at
)
) as comments
FROM posts p
LEFT JOIN post_tags t ON p.id = t.post_id
LEFT JOIN comments c ON p.id = c.post_id
WHERE p.id = ?
GROUP BY p.id;
MongoDB는 간단하지만, PostgreSQL은 여러 테이블과 JOIN이 필요합니다.
그래서 뭘 써야 하나요?
선택 가이드 플로우차트
당신의 프로젝트가...
📊 관계가 명확하고 복잡함?
📊 트랜잭션이 중요함?
📊 데이터 무결성이 중요함?
📊 복잡한 분석 쿼리가 많음?
→ PostgreSQL / MySQL 선택!
📁 스키마가 자주 바뀜?
📁 계층/트리 구조 데이터?
📁 다양한 콘텐츠 타입?
📁 대량의 로그/분석 데이터?
📁 빠른 프로토타입?
→ MongoDB 선택!
프로젝트별 추천
프로젝트 타입추천 DB이유
| 전자상거래 | PostgreSQL | 결제 트랜잭션, 재고 관리 |
| 인증 시스템 | PostgreSQL | 명확한 관계, 데이터 무결성 |
| 소셜 네트워크 | PostgreSQL | 복잡한 관계 (친구, 팔로우) |
| 블로그/CMS | MongoDB | 유연한 스키마, 계층 구조 |
| 실시간 채팅 | MongoDB | 빠른 쓰기, 유연한 메시지 구조 |
| 로그/모니터링 | MongoDB | 대량 쓰기, 시계열 데이터 |
| 게임 리더보드 | MongoDB | 빠른 읽기/쓰기, 단순 구조 |
| IoT 데이터 | MongoDB | 대량 센서 데이터, 유연한 구조 |
실무 팁: 둘 다 쓰세요!
실제 프로덕션에서는 하이브리드 접근이 흔합니다:
[예시: 전자상거래 플랫폼]
PostgreSQL:
- 사용자 계정
- 주문/결제
- 재고 관리
MongoDB:
- 상품 리뷰/댓글
- 활동 로그
- 검색 히스토리
각 데이터베이스를 강점이 있는 부분에만 사용하는 것이 가장 현명합니다.
Database Abstraction Layer의 중요성
두 DB를 모두 지원하려면 추상화 레이어가 필요합니다:
// Repository Pattern
const userRepository = createRepository(User);
// DB에 관계없이 동일한 코드
const user = await userRepository.findOne({ email });
const users = await userRepository.findAll({ role: 'admin' });
// .env만 변경하면 DB 전환
// DB_TYPE=mongodb → DB_TYPE=postgresql
이렇게 하면 프로젝트마다 상황에 맞는 DB를 선택할 수 있습니다.
핵심 메시지:
"NoSQL이 좋다/나쁘다"가 아니라
"이 프로젝트에 맞다/안 맞다"로 생각하세요!
트렌드에 휩쓸리지 말고, 프로젝트의 특성을 분석해서 현명하게 선택하시기 바랍니다. 🎯
참고 자료
- MongoDB 공식 문서: https://docs.mongodb.com
- PostgreSQL 공식 문서: https://www.postgresql.org/docs
- Database Abstraction Pattern: Repository Pattern
다음 글 예고: "Database Abstraction Layer 구축하기 - 한 코드로 MongoDB와 PostgreSQL 모두 지원하기"
궁금한 점이 있으시면 댓글로 남겨주세요! 👋</parameter> </invoke>
블로그 형식으로 정리했습니다! 📝
주요 구성:
- 들어가며 - 문제 제기
- ❌ NoSQL 쓰면 안 되는 4가지 경우
- 관계가 복잡한 경우 (JWT 인증)
- 트랜잭션이 중요한 경우 (결제)
- 데이터 무결성이 중요한 경우 (외래키)
- 복잡한 쿼리가 많은 경우 (통계)
- ✅ NoSQL이 좋은 4가지 경우
- 스키마가 자주 바뀌는 경우 (블로그/CMS)
- 계층 구조 데이터 (댓글)
- 다양한 콘텐츠 타입 (Headless CMS)
- 실시간 로그/분석 데이터
- 실전 비교 - 블로그 시스템 예제
- 선택 가이드 - 플로우차트 + 프로젝트별 추천
- 실무 팁 - 하이브리드 접근
- 결론 - 명확한 가이드라인
코드 예제와 비교를 풍부하게 넣어서 실전에서 바로 참고할 수 있도록 작성했습니다!
수정하거나 추가하고 싶은 부분 있으면 말씀해주세요! 🚀
'Tech Notes' 카테고리의 다른 글
| 런타임(Runtime)이란? 개발자라면 꼭 알아야 할 핵심 개념 (0) | 2025.10.07 |
|---|---|
| PostgreSQL 기반 클라우드 DB 완벽 가이드 (0) | 2025.10.06 |
| 소셜 로그인 OAuth 2.0 흐름 (A안: DB 없이 인증만) (0) | 2025.10.04 |
| 보일러 플레이트란? (0) | 2025.10.03 |
| 개발자라면 꼭 알아야 할 개행 문자 이야기 (0) | 2025.09.29 |