본문 바로가기
Tech Notes

NoSQL이 좋다던데 우리 프로젝트에도 써야 할까요?

by miracle-tech 2025. 10. 6.
728x90
반응형

개발자라면 한 번쯤 고민해봤을 질문입니다. 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로 구현하면?

 
 
javascript
// 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로 구현하면?

 
 
sql
-- 한 번의 쿼리로 모든 데이터 가져오기
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. 트랜잭션이 중요한 경우

예시: 결제 시스템

결제할 때는 여러 작업이 동시에 일어납니다:

  1. 사용자 포인트 차감
  2. 주문 생성
  3. 재고 감소

이 중 하나라도 실패하면 전부 취소되어야 합니다. (All or Nothing)

PostgreSQL의 강력한 트랜잭션

 
 
sql
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는?

 
 
javascript
// 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. 데이터 무결성이 중요한 경우

예시: 외래키 제약조건

 
 
sql
-- 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는 외래키 개념이 없어서 직접 관리해야 합니다.

 
 
javascript
// MongoDB는 수동으로 처리
await User.deleteOne({ _id: userId });
await RefreshToken.deleteMany({ userId: userId });
await PasswordResetToken.deleteMany({ userId: userId });

// 👎 개발자가 깜빡하면 고아 데이터 발생!

결론: 데이터 무결성이 중요하면 PostgreSQL


4. 복잡한 쿼리가 많은 경우

예시: 통계 쿼리

 
 
sql
-- 이런 쿼리는 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로 같은 쿼리를 작성하면:

 
 
javascript
// 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 시스템

블로그를 만들다 보면 기능이 계속 추가됩니다:

 
 
javascript
// 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로 구현하면?

 
 
javascript
// 그냥 바로 추가하면 끝!
await Post.updateOne(
  { _id: postId },
  { $set: { views: 1500, likes: 42 } }
);

// ✅ 스키마 변경 없음! 바로 반영!

PostgreSQL로 구현하면?

 
 
sql
-- 매번 테이블 수정 필요 😰
ALTER TABLE posts ADD COLUMN views INTEGER DEFAULT 0;
ALTER TABLE posts ADD COLUMN likes INTEGER DEFAULT 0;

-- 마이그레이션 파일 작성, 배포, 롤백 준비...
-- 👎 번거롭고 시간 소요

결론: 스키마가 자주 바뀌는 프로토타입이나 CMS는 MongoDB


2. 계층 구조 데이터

예시: 댓글 시스템 (무한 대댓글)

 
 
javascript
// 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로 구현하면?

 
 
sql
-- 재귀 쿼리 필요 (복잡함) 😰
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에서는 페이지마다 완전히 다른 구조를 가질 수 있습니다:

 
 
javascript
// 홈페이지
{
  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로 구현하면?

 
 
sql
-- 방법 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. 실시간 로그/분석 데이터

예시: 웹 애플리케이션 로그

 
 
javascript
// 로그마다 다른 구조
{
  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. 초고속 쓰기 성능

 
 
javascript
// 비동기 대량 삽입
await Log.insertMany(thousandsOfLogs, { ordered: false });
// ✅ 초당 10,000+ 건 처리 가능

PostgreSQL은 트랜잭션 처리로 인해 초당 1,000건 정도가 한계입니다.

2. Capped Collection (자동 정리)

 
 
javascript
// 크기 제한 컬렉션 - 오래된 로그 자동 삭제
db.createCollection("logs", {
  capped: true,
  size: 10737418240,  // 10GB
  max: 10000000       // 최대 1천만 건
});

// ✅ 오래된 로그는 자동으로 삭제됨!
// 별도 크론잡 필요 없음!

PostgreSQL은 주기적으로 삭제 쿼리를 실행해야 하고, 수백만 건 삭제 시 DB 락이 발생할 수 있습니다.

3. 빠른 시계열 조회

 
 
javascript
// 시간 범위 쿼리가 매우 빠름
db.logs.find({
  timestamp: {
    $gte: ISODate("2025-10-06T00:00:00Z"),
    $lt: ISODate("2025-10-07T00:00:00Z")
  },
  userId: "user123"
}).sort({ timestamp: -1 });

// ✅ 수백만 건에서도 밀리초 내 조회

4. 실시간 분석

 
 
javascript
// 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 버전

 
 
javascript
// 포스트 생성 (한 번에)
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 버전

 
 
sql
-- 포스트 생성
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를 모두 지원하려면 추상화 레이어가 필요합니다:

 
 
javascript
// 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이 좋다/나쁘다"가 아니라
"이 프로젝트에 맞다/안 맞다"로 생각하세요!

트렌드에 휩쓸리지 말고, 프로젝트의 특성을 분석해서 현명하게 선택하시기 바랍니다. 🎯


참고 자료


다음 글 예고: "Database Abstraction Layer 구축하기 - 한 코드로 MongoDB와 PostgreSQL 모두 지원하기"

궁금한 점이 있으시면 댓글로 남겨주세요! 👋</parameter> </invoke>

블로그 형식으로 정리했습니다! 📝

주요 구성:

  1. 들어가며 - 문제 제기
  2. ❌ NoSQL 쓰면 안 되는 4가지 경우
    • 관계가 복잡한 경우 (JWT 인증)
    • 트랜잭션이 중요한 경우 (결제)
    • 데이터 무결성이 중요한 경우 (외래키)
    • 복잡한 쿼리가 많은 경우 (통계)
  3. ✅ NoSQL이 좋은 4가지 경우
    • 스키마가 자주 바뀌는 경우 (블로그/CMS)
    • 계층 구조 데이터 (댓글)
    • 다양한 콘텐츠 타입 (Headless CMS)
    • 실시간 로그/분석 데이터
  4. 실전 비교 - 블로그 시스템 예제
  5. 선택 가이드 - 플로우차트 + 프로젝트별 추천
  6. 실무 팁 - 하이브리드 접근
  7. 결론 - 명확한 가이드라인

코드 예제와 비교를 풍부하게 넣어서 실전에서 바로 참고할 수 있도록 작성했습니다!

수정하거나 추가하고 싶은 부분 있으면 말씀해주세요! 🚀

728x90