Guider/Database/DatabaseDevGuide0005
Database#05· 40분 읽기

DatabaseDevGuide0005

NoSQL 완전 정복

list목차(41)
DATABASE DEV GUIDE

🍃 DatabaseDevGuide0005

NoSQL 완전 정복 A-Z

MongoDB · Redis · 언제 SQL? 언제 NoSQL?

현업에서 바로 쓰는 NoSQL의 모든 것을 A-Z로 배워봅시다

🍃 MongoDB ⚡ Redis 🔵 Elasticsearch 🔶 Cassandra ⚖️ SQL vs NoSQL

📋 학습 목차

챕터 주제 핵심 키워드
01 NoSQL이란? SQL vs NoSQL, 4가지 NoSQL 종류, CAP 이론
02 MongoDB 기초 Document, Collection, CRUD, mongosh
03 MongoDB 심화 Aggregation, 인덱스, 스키마 설계, Mongoose
04 Redis 완전 정복 자료구조, 캐싱, 세션, Pub/Sub, TTL
05 Elasticsearch 입문 전문 검색, 역색인, 한국어 분석기
06 NoSQL 데이터 모델링 임베딩 vs 참조, 패턴, 스키마 전략
07 언제 SQL? 언제 NoSQL? 선택 기준, 혼합 사용 전략, 실전 아키텍처
08 MongoDB Atlas (클라우드) 클라우드 DB, 무료 Tier, 연결 설정
09 실전 프로젝트 블로그 서비스 MongoDB + Redis 구현
10 현업 면접 Q&A TOP 10 NoSQL 관련 면접 질문 완전 정리
11 학습 로드맵 & 체크리스트 단계별 학습 경로, 추천 도구

📌 01. NoSQL이란 무엇인가?

Not Only SQL — 표(테이블) 형식이 아닌 방법으로 데이터를 저장하는 DB!

🏗️ SQL vs NoSQL 한눈에 비교

비교 항목 🔵 SQL (관계형 DB) 🍃 NoSQL
데이터 형식 표(테이블) 형식 문서, 키-값, 그래프 등
스키마 고정 스키마 (미리 정의) 유연한 스키마 (동적)
확장 방식 수직 확장 (서버 업그레이드) 수평 확장 (서버 추가)
JOIN ✅ 강력한 JOIN 지원 ❌ 보통 JOIN 없음
ACID ✅ 완전한 ACID 보장 ⚠️ 보통 최종 일관성
대표 DB MySQL, PostgreSQL, Oracle MongoDB, Redis, Cassandra
적합한 상황 금융, 결제, 정형 데이터 SNS, 로그, 실시간, 대용량

🗂️ NoSQL의 4가지 종류

📄

문서형 (Document)

JSON/BSON 형태의 문서로 저장.
각 문서마다 다른 구조 가능

대표: MongoDB, CouchDB

SNS 프로필, 상품 카탈로그

🔑

키-값형 (Key-Value)

키(key)로 값(value)을 저장.
극도로 빠른 읽기/쓰기

대표: Redis, DynamoDB

캐싱, 세션, 장바구니

🏛️

컬럼형 (Column-Family)

컬럼 단위로 데이터 저장.
대용량 쓰기에 최적화

대표: Cassandra, HBase

IoT 센서, 로그, 시계열 데이터

🕸️

그래프형 (Graph)

노드와 엣지(관계)로 저장.
복잡한 관계 탐색에 최적화

대표: Neo4j, Amazon Neptune

SNS 친구관계, 추천 엔진

⚖️ CAP 이론 (분산 시스템의 핵심)

CAP 이론: 분산 시스템에서 아래 3가지를 동시에 모두 만족할 수 없다는 이론

항목 설명 예시
C (Consistency) 모든 노드가 같은 데이터를 읽음 은행 잔액: 어디서 조회해도 같아야 함
A (Availability) 모든 요청에 응답 가능 (다운 없음) 24/7 서비스 유지
P (Partition tolerance) 네트워크 단절돼도 동작 서버 간 연결 끊겨도 작동

현실에서 P는 포기 불가 → CP(일관성+분산) 또는 AP(가용성+분산) 중 선택!
MongoDB = CP, Cassandra = AP, Redis = CP (Sentinel/Cluster 구성 시)

💡 비유로 이해하는 NoSQL

SQL = 정돈된 엑셀 표. 모든 행이 같은 컬럼 구조. 찾기 쉽고 관계 표현 탁월.
MongoDB = 메모장 파일 더미. 각 파일(문서)이 다른 내용을 가질 수 있음. 유연하고 빠름.
Redis = 초고속 사물함. 열쇠(키)로 물건(값)을 꺼내는 방식. 속도가 생명!
Cassandra = 거대한 스프레드시트 클러스터. 수십억 행도 거뜬히 처리.

📌 02. MongoDB 기초 — 문서형 NoSQL의 대표주자

JSON과 비슷한 BSON 형식으로 데이터를 저장하는 세계 1위 NoSQL DB!

🔑 MongoDB 핵심 용어 비교

MySQL (SQL) MongoDB (NoSQL) 설명
Database Database 동일: 데이터의 집합
Table Collection 데이터를 묶는 그룹
Row (행) Document 하나의 데이터 단위 (JSON 형태)
Column (열) Field 문서 안의 속성
Primary Key _id 자동 생성 ObjectId (12바이트)
JOIN $lookup (Aggregation) 컬렉션 간 결합 (권장하지 않음)
WHERE 절 Query Filter 조건 필터링

📄 Document란?

// MongoDB Document 예시 (BSON = Binary JSON)
{
  "_id": ObjectId("64f2a1b3c4d5e6f789012345"),  // 자동 생성 고유 ID
  "name": "김철수",
  "email": "chulsoo@example.com",
  "age": 28,
  "address": {                    // 중첩 문서 (Embedded Document)
    "city": "서울",
    "district": "강남구"
  },
  "hobbies": ["독서", "코딩", "여행"],  // 배열 가능!
  "orders": [                     // 배열 안에 문서도 가능!
    { "product": "노트북", "price": 1200000 },
    { "product": "마우스", "price": 30000 }
  ],
  "createdAt": ISODate("2026-03-27T09:00:00Z"),
  "isActive": true
}

// ✅ SQL과 달리 각 문서가 다른 필드를 가져도 됩니다!
{
  "_id": ObjectId("..."),
  "name": "이영희",
  "phone": "010-1234-5678"   // address 없어도 OK!
  // hobbies 없어도 OK!
}

⚡ MongoDB CRUD 완전 정복

📥 Create (데이터 삽입)

// 단일 문서 삽입
db.users.insertOne({
  name: "김철수",
  email: "chulsoo@example.com",
  age: 28,
  role: "user"
});

// 여러 문서 한꺼번에 삽입
db.users.insertMany([
  { name: "이영희", email: "younghee@example.com", age: 25 },
  { name: "박민준", email: "minjun@example.com", age: 32 },
  { name: "최수진", email: "soojin@example.com", age: 29 }
]);

// 결과:
// { acknowledged: true, insertedId: ObjectId("...") }
// { acknowledged: true, insertedIds: { '0': ..., '1': ..., '2': ... } }

🔍 Read (데이터 조회)

// 전체 조회
db.users.find();

// 조건 조회 (SQL의 WHERE)
db.users.find({ age: 28 });                     // age가 28인 문서
db.users.find({ role: "user", age: { $gte: 25 } }); // role=user AND age>=25

// 단일 문서 조회
db.users.findOne({ email: "chulsoo@example.com" });

// 특정 필드만 조회 (SQL의 SELECT)
db.users.find(
  { role: "user" },           // 조건
  { name: 1, email: 1, _id: 0 }  // 프로젝션: name, email만 가져오기 (_id 제외)
);

// 정렬, 개수 제한
db.users.find().sort({ age: 1 });     // age 오름차순 (1=오름, -1=내림)
db.users.find().sort({ age: -1 }).limit(10).skip(20); // 페이징!

// 개수 세기
db.users.countDocuments({ role: "user" });

// 비교 연산자
// $eq: 같음, $ne: 다름, $gt: 초과, $gte: 이상, $lt: 미만, $lte: 이하
db.users.find({ age: { $gte: 25, $lte: 30 } });  // 25~30세

// 논리 연산자
db.users.find({ $or: [{ age: 25 }, { age: 30 }] }); // 25세 OR 30세
db.users.find({ $and: [{ age: { $gte: 20 } }, { role: "admin" }] });

// 배열 조회
db.users.find({ hobbies: "코딩" });              // hobbies 배열에 "코딩" 포함
db.users.find({ hobbies: { $in: ["코딩", "독서"] } }); // "코딩" 또는 "독서" 포함

// 중첩 문서 조회 (dot notation)
db.users.find({ "address.city": "서울" });       // 중첩 필드 접근

// 정규식 (LIKE와 유사)
db.users.find({ name: { $regex: "김", $options: "i" } }); // 이름에 "김" 포함 (대소문자 무시)

✏️ Update (데이터 수정)

// 단일 문서 수정
db.users.updateOne(
  { email: "chulsoo@example.com" },  // 조건
  { $set: { age: 29, city: "부산" } }  // 수정 내용
);

// 여러 문서 수정
db.users.updateMany(
  { role: "user" },
  { $set: { isActive: true } }
);

// ⚠️ $set 없이 쓰면 문서 전체가 교체됩니다!
db.users.updateOne({ email: "..." }, { age: 30 }); // ❌ 다른 필드 모두 사라짐!
db.users.updateOne({ email: "..." }, { $set: { age: 30 } }); // ✅ age만 변경

// 자주 쓰는 Update 연산자
// $set: 필드 값 설정
// $unset: 필드 삭제
// $inc: 숫자 증가/감소
// $push: 배열에 값 추가
// $pull: 배열에서 값 제거
// $addToSet: 배열에 중복 없이 추가

db.users.updateOne(
  { email: "chulsoo@example.com" },
  {
    $inc: { loginCount: 1 },           // loginCount +1
    $push: { hobbies: "게임" },         // hobbies 배열에 "게임" 추가
    $set: { lastLogin: new Date() }    // lastLogin 업데이트
  }
);

// upsert: 없으면 삽입, 있으면 수정
db.users.updateOne(
  { email: "new@example.com" },
  { $set: { name: "신규유저", role: "user" } },
  { upsert: true }  // 없으면 새로 만들어!
);

🗑️ Delete (데이터 삭제)

// 단일 문서 삭제
db.users.deleteOne({ email: "chulsoo@example.com" });

// 여러 문서 삭제
db.users.deleteMany({ isActive: false });

// 컬렉션의 모든 문서 삭제 (컬렉션 자체는 유지)
db.users.deleteMany({});

// ⚠️ 실수 방지: 삭제 전에 find()로 먼저 확인!
db.users.find({ isActive: false });  // 먼저 확인
db.users.deleteMany({ isActive: false });  // 그 다음 삭제

📌 03. MongoDB 심화 — Aggregation, 인덱스, Mongoose

현업에서 꼭 필요한 MongoDB 중급 기술들!

🔧 Aggregation Pipeline (집계 파이프라인)

Aggregation Pipeline은 SQL의 GROUP BY + JOIN + HAVING을 한꺼번에 할 수 있는 강력한 기능입니다.
데이터가 여러 단계(Stage)를 파이프라인처럼 통과하면서 변환됩니다.

// 주요 Aggregation 스테이지
// $match: 조건 필터 (SQL의 WHERE)
// $group: 그룹화 (SQL의 GROUP BY)
// $sort:  정렬
// $limit: 개수 제한
// $skip:  건너뛰기
// $project: 필드 선택/변환 (SQL의 SELECT)
// $lookup: 다른 컬렉션과 조인
// $unwind: 배열을 풀어서 개별 문서로 변환

// ✅ 예시: 역할별 사용자 수와 평균 나이
db.users.aggregate([
  { $match: { isActive: true } },           // 활성 사용자만
  { $group: {
      _id: "$role",                         // role별 그룹화
      count: { $sum: 1 },                   // 각 그룹 수
      avgAge: { $avg: "$age" },             // 평균 나이
      names: { $push: "$name" }            // 이름 목록 배열로
  }},
  { $sort: { count: -1 } },                // 많은 순으로 정렬
  { $project: {
      _id: 0,
      role: "$_id",
      count: 1,
      avgAge: { $round: ["$avgAge", 1] }   // 소수점 1자리로 반올림
  }}
]);

// ✅ 예시: 주문 컬렉션에서 월별 매출 집계
db.orders.aggregate([
  { $match: { status: "완료" } },
  { $group: {
      _id: {
        year:  { $year: "$createdAt" },
        month: { $month: "$createdAt" }
      },
      totalRevenue: { $sum: "$finalPrice" },
      orderCount:   { $sum: 1 }
  }},
  { $sort: { "_id.year": -1, "_id.month": -1 } },
  { $limit: 12 }   // 최근 12개월
]);

// ✅ $lookup: 컬렉션 JOIN (users + orders)
db.orders.aggregate([
  { $lookup: {
      from: "users",               // 조인할 컬렉션
      localField: "userId",        // orders의 필드
      foreignField: "_id",         // users의 필드
      as: "userInfo"               // 결과 배열 이름
  }},
  { $unwind: "$userInfo" },        // 배열을 단일 문서로
  { $project: {
      orderId: "$_id",
      userName: "$userInfo.name",
      totalPrice: 1,
      status: 1
  }}
]);

📑 MongoDB 인덱스

// 인덱스 생성
db.users.createIndex({ email: 1 });              // 오름차순 단일 인덱스
db.users.createIndex({ email: 1 }, { unique: true }); // 유니크 인덱스
db.users.createIndex({ age: 1, role: 1 });       // 복합 인덱스
db.users.createIndex({ name: "text" });          // 텍스트 검색 인덱스
db.users.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 }); // TTL 인덱스

// 인덱스 목록 확인
db.users.getIndexes();

// 인덱스 삭제
db.users.dropIndex("email_1");

// 쿼리 성능 분석 (SQL의 EXPLAIN)
db.users.find({ email: "test@example.com" }).explain("executionStats");
// "IXSCAN" = 인덱스 사용 ✅  "COLLSCAN" = 풀스캔 ❌

🟢 Mongoose — Node.js에서 MongoDB 사용하기

Mongoose는 Node.js에서 MongoDB를 쉽게 사용하기 위한 ODM(Object Document Mapper) 라이브러리입니다.
스키마를 정의하고, 유효성 검사, 미들웨어 기능을 제공합니다.

// npm install mongoose

const mongoose = require('mongoose');

// 1. DB 연결
mongoose.connect('mongodb://localhost:27017/shopdb')
  .then(() => console.log('MongoDB 연결 성공!'))
  .catch(err => console.error('연결 실패:', err));

// 2. 스키마 정의
const userSchema = new mongoose.Schema({
  name:      { type: String, required: true, trim: true },
  email:     { type: String, required: true, unique: true, lowercase: true },
  age:       { type: Number, min: 0, max: 150 },
  role:      { type: String, enum: ['user', 'admin'], default: 'user' },
  isActive:  { type: Boolean, default: true },
  createdAt: { type: Date, default: Date.now }
});

// 3. 모델 생성
const User = mongoose.model('User', userSchema);

// 4. CRUD 사용 (async/await)
// Create
const newUser = await User.create({ name: '김철수', email: 'cs@example.com', age: 28 });

// Read
const users = await User.find({ role: 'user' }).select('name email').limit(10);
const user  = await User.findById('64f2a1b3c4d5e6f789012345');
const one   = await User.findOne({ email: 'cs@example.com' });

// Update
await User.findByIdAndUpdate(id, { $set: { age: 29 } }, { new: true });

// Delete
await User.findByIdAndDelete(id);

// 5. Mongoose 가상 필드 (Virtual)
userSchema.virtual('fullInfo').get(function() {
  return `${this.name} (${this.email})`;
});

// 6. pre/post 미들웨어 (Hooks)
userSchema.pre('save', async function(next) {
  if (this.isModified('password')) {
    this.password = await bcrypt.hash(this.password, 10); // 저장 전 암호화
  }
  next();
});

📌 04. Redis 완전 정복 ⚡

인메모리(RAM) 기반의 초고속 키-값 저장소. 캐싱, 세션, 실시간 처리의 핵심!

⚡ Redis가 빠른 이유: 데이터를 디스크가 아닌 메모리(RAM)에 저장합니다.
메모리 접근 속도 = 약 100ns (나노초) vs 디스크 = 약 10ms (밀리초) → 10만 배 차이!
초당 100만 건 이상의 읽기/쓰기가 가능합니다.

🗂️ Redis 5가지 핵심 자료구조

① String — 가장 기본적인 자료형

# SET, GET
SET username "김철수"
GET username        # "김철수"

# TTL (Time To Live) 설정 - 자동 만료!
SET session:user123 "data..." EX 3600   # 1시간 후 자동 삭제
TTL session:user123                      # 남은 초 확인
PERSIST session:user123                  # TTL 제거 (영구 저장)

# 숫자 증가/감소 (카운터)
SET pageView 0
INCR pageView       # 1
INCR pageView       # 2
INCRBY pageView 10  # 12
DECR pageView       # 11

② Hash — 객체 저장 (사용자 정보 등)

# 사용자 정보를 Hash로 저장
HSET user:1001 name "김철수" email "cs@example.com" age 28
HGET user:1001 name      # "김철수"
HGETALL user:1001        # 모든 필드와 값

# 특정 필드 수정
HSET user:1001 age 29

# 필드 삭제
HDEL user:1001 age

# 존재 여부 확인
HEXISTS user:1001 email  # 1 (존재) or 0 (없음)

# 모든 키 / 값 목록
HKEYS user:1001          # ["name", "email", "age"]
HVALS user:1001          # ["김철수", "cs@example.com", "28"]

③ List — 순서 있는 목록 (큐, 스택)

# 왼쪽/오른쪽에 추가
LPUSH recentView:user1 "상품A"
LPUSH recentView:user1 "상품B"
RPUSH recentView:user1 "상품C"

# 조회 (0부터 시작, -1은 끝까지)
LRANGE recentView:user1 0 -1   # ["상품B", "상품A", "상품C"]
LRANGE recentView:user1 0 4    # 최근 5개

# 꺼내기 (큐: LPUSH + RPOP, 스택: LPUSH + LPOP)
RPOP recentView:user1  # "상품C" (오른쪽에서 꺼냄)
LPOP recentView:user1  # "상품B" (왼쪽에서 꺼냄)

# 크기
LLEN recentView:user1  # 리스트 길이

④ Set — 중복 없는 집합 (좋아요, 팔로워)

# 팔로워 관리 예시
SADD followers:user1 user2 user3 user4
SADD followers:user1 user2  # 중복! 무시됨

SMEMBERS followers:user1    # { user2, user3, user4 }
SCARD followers:user1       # 3 (개수)
SISMEMBER followers:user1 user2  # 1 (있음)

# 집합 연산 (SNS 공통 친구 찾기!)
SINTERSTORE commonFriends followers:user1 followers:user2  # 교집합
SUNIONSTORE allFriends followers:user1 followers:user2     # 합집합
SDIFFSTORE onlyUser1 followers:user1 followers:user2       # 차집합

⑤ Sorted Set (ZSet) — 점수 기반 정렬 집합 (랭킹!)

# 실시간 랭킹 구현!
ZADD gameRanking 98500 "김철수"
ZADD gameRanking 87200 "이영희"
ZADD gameRanking 99100 "박민준"

# 높은 점수 순으로 상위 10명 조회 (점수도 같이)
ZREVRANGE gameRanking 0 9 WITHSCORES
# 1위: 박민준 99100, 2위: 김철수 98500, ...

# 내 순위 조회 (0부터 시작)
ZREVRANK gameRanking "김철수"  # 1 (2위)

# 점수 갱신
ZINCRBY gameRanking 1500 "이영희"  # 이영희 점수 +1500

🚀 Redis 실전 활용 패턴

① 캐싱 (Caching) — DB 부하 줄이기

// Node.js + Redis 캐싱 패턴 (Cache-Aside Pattern)
const redis = require('redis');
const client = redis.createClient({ url: 'redis://localhost:6379' });

async function getUserById(userId) {
  const cacheKey = `user:${userId}`;
  
  // 1. Redis에서 먼저 확인
  const cached = await client.get(cacheKey);
  if (cached) {
    console.log('캐시 히트! Redis에서 반환');
    return JSON.parse(cached);
  }
  
  // 2. 캐시 없으면 DB에서 조회
  const user = await User.findById(userId);
  
  // 3. Redis에 저장 (1시간 TTL)
  await client.setEx(cacheKey, 3600, JSON.stringify(user));
  
  console.log('캐시 미스. DB에서 조회 후 캐싱');
  return user;
}

② 세션 관리 (Session)

// 로그인 세션 저장
async function saveSession(sessionId, userData) {
  const key = `session:${sessionId}`;
  await client.hSet(key, {
    userId: userData.id,
    email:  userData.email,
    role:   userData.role
  });
  await client.expire(key, 86400);  // 24시간 후 자동 만료
}

// 세션 확인
async function getSession(sessionId) {
  return await client.hGetAll(`session:${sessionId}`);
}

// 로그아웃 (세션 삭제)
async function destroySession(sessionId) {
  await client.del(`session:${sessionId}`);
}

③ Pub/Sub — 실시간 메시지 전달

// Publisher (메시지 발행)
const publisher = redis.createClient();
await publisher.publish('notifications', JSON.stringify({
  type: 'NEW_ORDER',
  orderId: 12345,
  userId: 678
}));

// Subscriber (메시지 구독)
const subscriber = redis.createClient();
await subscriber.subscribe('notifications', (message) => {
  const data = JSON.parse(message);
  console.log('새 알림:', data);
  // 알림 처리 로직...
});

📊 Redis 주요 사용 사례 정리

사용 사례 자료구조 설명
API 응답 캐싱 String DB 쿼리 결과를 JSON으로 캐싱, TTL 설정
로그인 세션 Hash 세션ID를 키로 사용자 정보 저장
실시간 랭킹 Sorted Set 게임 점수, 인기 상품, 좋아요 순위
최근 본 상품 List LPUSH + LTRIM으로 최근 N개 유지
중복 방지 Set 좋아요 중복 클릭 방지, 유니크 방문자
채팅/알림 Pub/Sub 실시간 메시지 전달
분산 락 String + NX 동시성 제어 (재고 차감 등)

📌 05. Elasticsearch 입문 🔵

전문 검색(Full-Text Search)의 최강자! 로그 분석, 검색 서비스에 필수

Elasticsearch는 Apache Lucene 기반의 분산 검색 엔진입니다.
"쿠팡에서 '노트북' 검색 시 관련 상품이 즉시 나오는 것", "유튜브 자막 검색", "로그 모니터링"에 사용됩니다.
역색인(Inverted Index) 구조 덕분에 수억 건의 텍스트도 밀리초 단위로 검색합니다!

🔍 역색인(Inverted Index)이란?

📚 일반 DB 저장 방식

ID 내용
1 노트북 삼성 갤럭시
2 노트북 LG 그램
3 삼성 갤럭시 폰

❌ "삼성" 검색 시 모든 행을 스캔!

⚡ 역색인 방식

단어 문서 ID
노트북 [1, 2]
삼성 [1, 3]
갤럭시 [1, 3]
LG [2]

✅ "삼성" → 바로 [1,3] 조회! 초고속

💻 Elasticsearch 기본 사용법 (REST API)

# 인덱스 생성 (= 테이블 생성)
PUT /products
{
  "mappings": {
    "properties": {
      "name":        { "type": "text", "analyzer": "korean" },
      "price":       { "type": "integer" },
      "category":    { "type": "keyword" },
      "createdAt":   { "type": "date" }
    }
  }
}

# 문서 삽입
POST /products/_doc
{
  "name": "삼성 갤럭시 노트북 15인치",
  "price": 1200000,
  "category": "전자제품"
}

# 전문 검색
GET /products/_search
{
  "query": {
    "match": { "name": "갤럭시 노트북" }
  }
}

# 복합 검색 (AND/OR/필터)
GET /products/_search
{
  "query": {
    "bool": {
      "must":   [{ "match":  { "name": "노트북" } }],
      "filter": [{ "range":  { "price": { "gte": 500000, "lte": 2000000 } } }]
    }
  },
  "sort": [{ "price": "asc" }],
  "from": 0, "size": 10
}

📊 Elasticsearch vs MySQL 비교

항목 MySQL LIKE Elasticsearch
검색 방식 %검색어% 패턴 매칭 역색인 기반 전문 검색
속도 (1억 건) 수초~수분 밀리초(ms)
형태소 분석 ✅ (한국어 nori 플러그인)
적합한 용도 정형 데이터 조회 전문 검색, 로그 분석

📌 06. NoSQL 데이터 모델링

NoSQL에서는 SQL과 다른 방식으로 데이터를 설계해야 합니다!

🔗 임베딩 vs 참조 — 가장 중요한 설계 결정

📦 임베딩 (Embedding)

{
  "_id": 1,
  "name": "김철수",
  "address": {        // 임베딩!
    "city": "서울",
    "zip": "12345"
  },
  "orders": [...]     // 임베딩!
}
  • ✅ 한 번 읽기로 모든 데이터 조회
  • ✅ 조인 불필요, 빠른 읽기
  • ❌ 데이터 중복 발생
  • ❌ 문서 크기 제한 (16MB)

적합: 1:1, 소규모 1:N, 자주 함께 조회

🔗 참조 (Reference)

{
  "_id": 1,
  "name": "김철수",
  "addressId": ObjectId("..."),  // 참조!
  "orderIds": [               // 참조!
    ObjectId("..."),
    ObjectId("...")
  ]
}
  • ✅ 데이터 중복 없음
  • ✅ 독립적 업데이트 가능
  • ❌ 추가 쿼리($lookup) 필요
  • ❌ 성능 저하 가능

적합: 대규모 1:N, M:N, 독립 관리 필요

🎯 MongoDB 스키마 설계 Best Practices

원칙 설명
쿼리 패턴 우선 가장 자주 쓰는 쿼리에 맞게 스키마 설계 (SQL과 반대)
함께 읽으면 함께 저장 항상 같이 조회되는 데이터는 임베딩
독립 업데이트면 참조 자주 독립적으로 수정되는 데이터는 참조
배열 크기 제한 임베딩 배열이 무한정 커지면 문서 크기 초과 (16MB)
Bucket 패턴 시계열 데이터를 시간대별로 묶어서 저장 (IoT, 로그)

📌 07. 언제 SQL? 언제 NoSQL? — 현업 선택 가이드

둘 중 하나가 더 낫다는 건 없습니다. 상황에 맞는 선택이 중요!

🔵 SQL이 더 적합한 경우

  • 금융/결제 시스템: 트랜잭션 무결성(ACID)이 절대적으로 필요한 경우
  • 복잡한 JOIN이 필요: 여러 테이블의 관계가 복잡하고 JOIN 쿼리가 핵심인 경우
  • 정형화된 구조: 데이터 구조가 고정되어 있고 변경이 거의 없는 경우
  • 데이터 일관성 중요: 어느 서버에서 읽어도 같은 데이터를 보장해야 하는 경우
  • 팀 역량: 팀원들이 SQL에 익숙하고 학습 비용을 줄여야 하는 경우

대표 사례: 은행 계좌 시스템, 쇼핑몰 결제, ERP, 인사/급여 시스템

🍃 NoSQL이 더 적합한 경우

  • 빠른 확장이 필요: 트래픽이 급증할 때 서버를 수평 확장해야 하는 경우
  • 다양한 데이터 구조: 상품마다 속성이 달라서 고정 스키마가 불편한 경우
  • 대용량 쓰기: 초당 수십만 건의 로그, IoT 센서 데이터 저장
  • 실시간 처리: 캐싱, 실시간 랭킹, 채팅 등 속도가 생명인 경우
  • 빠른 프로토타이핑: 스키마를 자주 변경해야 하는 초기 개발 단계

대표 사례: SNS 피드(MongoDB), 캐시(Redis), 검색(Elasticsearch), IoT(Cassandra)

🏗️ 현업 실전 아키텍처 — SQL + NoSQL 함께 쓰기


┌─────────────────────────────────────────────────────────────┐
│                    쇼핑몰 서비스 아키텍처                      │
├────────────────┬────────────────┬────────────────────────────┤
│   MySQL        │   MongoDB      │   Redis                    │
│  (핵심 데이터)  │  (유연한 데이터) │  (캐시/세션)               │
├────────────────┼────────────────┼────────────────────────────┤
│ 회원 정보       │ 상품 상세 정보  │ 로그인 세션                  │
│ 주문/결제       │ 리뷰/평점       │ 상품 목록 캐시               │
│ 재고 관리       │ 검색 로그       │ 장바구니 임시 저장            │
│ 정산 데이터     │ 사용자 활동 로그 │ 실시간 인기 상품 랭킹         │
└────────────────┴────────────────┴────────────────────────────┘
                              +
┌─────────────────────────────────────────────────────────────┐
│                  Elasticsearch                              │
│  상품 검색 (한국어 형태소 분석, 유사 검색, 필터 검색)           │
└─────────────────────────────────────────────────────────────┘
💡 실제 대기업 사례
  • 카카오/라인: MySQL(메인) + Redis(캐시, 메시지큐) + HBase(로그)
  • Netflix: Cassandra(사용자 시청 기록) + MySQL(결제) + Redis(캐시)
  • Airbnb: MySQL + Elasticsearch(숙소 검색) + Redis(캐시)
  • Uber: MySQL + Cassandra(위치 데이터) + Redis(실시간 드라이버 위치)

📌 08. MongoDB Atlas — 클라우드 MongoDB 완벽 가이드

설치 없이 바로 쓸 수 있는 MongoDB 클라우드 서비스! 무료 Tier 제공

🚀 MongoDB Atlas 시작하기

1

회원가입: cloud.mongodb.com 접속 → Google 계정으로 간편 가입

2

클러스터 생성: "Free" 선택 → AWS/Google Cloud 선택 → 서울(ap-northeast-2) 리전 선택

3

보안 설정: DB 사용자 생성 (아이디/비밀번호) → IP 허용 (0.0.0.0/0 = 모든 IP 허용)

4

연결: "Connect" 클릭 → "Connect using MongoDB Driver" → Connection String 복사

// MongoDB Atlas 연결 (Node.js)
const mongoose = require('mongoose');

const MONGODB_URI = 'mongodb+srv://username:password@cluster0.xxxxx.mongodb.net/shopdb?retryWrites=true&w=majority';

mongoose.connect(MONGODB_URI)
  .then(() => console.log('✅ MongoDB Atlas 연결 성공!'))
  .catch(err => console.error('❌ 연결 실패:', err));

// .env 파일에 저장하여 보안 관리
// MONGODB_URI=mongodb+srv://...
// require('dotenv').config();
// mongoose.connect(process.env.MONGODB_URI)

📊 MongoDB Atlas Free Tier 스펙

항목 Free Tier (M0)
저장 공간 512MB (무료)
연결 수 최대 500개 연결
가용성 3개 노드 Replica Set
백업 미포함 (유료 플랜부터)
적합한 용도 학습, 포트폴리오, 소규모 프로젝트

📌 09. 실전 프로젝트 — 블로그 서비스 MongoDB + Redis 구현

실제 블로그 서비스의 NoSQL 구조를 A-Z로 설계하고 구현합니다!

📋 요구사항

  • 사용자가 블로그 포스트를 작성/수정/삭제할 수 있다 → MongoDB posts 컬렉션
  • 포스트에 댓글을 달 수 있다 → MongoDB 임베딩 or 별도 컬렉션
  • 포스트에 태그를 달 수 있다 → MongoDB Array 필드
  • 로그인 세션 관리 → Redis Hash + TTL
  • 포스트 목록은 캐싱 → Redis String + TTL
  • 인기 포스트 랭킹 → Redis Sorted Set

🏗️ MongoDB 컬렉션 설계

// ✅ users 컬렉션
{
  "_id": ObjectId("..."),
  "username": "김철수",
  "email": "cs@example.com",
  "passwordHash": "...",
  "bio": "안녕하세요! 개발자입니다.",
  "avatar": "https://...",
  "followersCount": 142,
  "followingCount": 87,
  "createdAt": ISODate("2026-01-01")
}

// ✅ posts 컬렉션 (댓글은 별도 컬렉션으로 참조)
{
  "_id": ObjectId("..."),
  "title": "MongoDB 완전 정복 가이드",
  "slug": "mongodb-complete-guide",    // URL 친화적 ID
  "content": "# MongoDB란?...",        // Markdown 형식
  "excerpt": "MongoDB 입문부터 심화까지...", // 미리보기 텍스트
  "authorId": ObjectId("..."),         // users 참조
  "tags": ["MongoDB", "NoSQL", "Database"],  // 배열로 태그
  "thumbnail": "https://...",
  "status": "published",               // draft, published, archived
  "viewCount": 1523,
  "likeCount": 89,
  "commentCount": 23,                  // 반정규화: 매번 COUNT() 안 해도 됨
  "publishedAt": ISODate("2026-03-27"),
  "createdAt": ISODate("2026-03-27"),
  "updatedAt": ISODate("2026-03-27")
}

// ✅ comments 컬렉션 (많은 경우 별도 컬렉션 권장)
{
  "_id": ObjectId("..."),
  "postId": ObjectId("..."),    // posts 참조
  "authorId": ObjectId("..."),  // users 참조
  "parentId": null,             // 대댓글: 부모 댓글 ID (자기 참조)
  "content": "좋은 글이네요!",
  "likeCount": 5,
  "createdAt": ISODate("...")
}

⚡ Redis 활용 코드 (Node.js)

const redis = require('redis');
const client = redis.createClient();

// ① 포스트 목록 캐싱
async function getPostList(page = 1) {
  const cacheKey = `posts:list:page:${page}`;
  
  const cached = await client.get(cacheKey);
  if (cached) return JSON.parse(cached);
  
  const posts = await Post.find({ status: 'published' })
    .sort({ publishedAt: -1 })
    .skip((page - 1) * 10)
    .limit(10)
    .select('title slug excerpt tags viewCount likeCount publishedAt');
  
  // 5분 캐싱
  await client.setEx(cacheKey, 300, JSON.stringify(posts));
  return posts;
}

// ② 조회수 증가 + 인기 포스트 랭킹 업데이트
async function incrementViewCount(postId) {
  // MongoDB 조회수 업데이트
  await Post.findByIdAndUpdate(postId, { $inc: { viewCount: 1 } });
  
  // Redis Sorted Set으로 실시간 인기 랭킹
  await client.zIncrBy('posts:popular:today', 1, postId.toString());
}

// ③ 인기 포스트 TOP 10
async function getPopularPosts() {
  const postIds = await client.zRevRange('posts:popular:today', 0, 9, { withScores: true });
  
  // ID 목록으로 MongoDB 조회
  const ids = postIds.filter((_, i) => i % 2 === 0); // 짝수 인덱스가 ID
  return await Post.find({ _id: { $in: ids } }).select('title slug');
}

// ④ 글 발행 시 캐시 무효화
async function publishPost(postData) {
  const post = await Post.create(postData);
  
  // 캐시 무효화 (모든 페이지 캐시 삭제)
  const keys = await client.keys('posts:list:*');
  if (keys.length > 0) await client.del(keys);
  
  return post;
}

📊 인덱스 설정

// posts 컬렉션 인덱스
db.posts.createIndex({ slug: 1 }, { unique: true });    // URL 중복 방지
db.posts.createIndex({ authorId: 1, status: 1 });       // 특정 작가 글 조회
db.posts.createIndex({ tags: 1 });                       // 태그별 검색
db.posts.createIndex({ publishedAt: -1 });               // 최신순 정렬
db.posts.createIndex({ title: "text", content: "text" }); // 텍스트 검색

📌 10. 현업 면접 Q&A TOP 10

NoSQL 관련 실제 면접에서 자주 나오는 질문들!

Q1. NoSQL과 SQL의 차이를 설명해주세요.

SQL은 테이블 형태의 정형 데이터를 ACID 트랜잭션으로 관리합니다. 복잡한 JOIN과 강한 일관성이 장점이나 수평 확장이 어렵습니다. NoSQL은 문서/키-값/그래프 등 다양한 형태로 저장하며, 스키마가 유연하고 수평 확장에 적합합니다. 트레이드오프는 ACID 보장이 약하고 JOIN이 제한적이라는 점입니다.

Q2. MongoDB에서 _id는 무엇이고, 왜 ObjectId를 사용하나요?

_id는 MongoDB의 기본키로 모든 문서에 자동 생성됩니다. ObjectId는 12바이트 구조로 앞 4바이트는 타임스탬프, 5바이트는 랜덤값, 3바이트는 카운터로 구성됩니다. 분산 환경에서 서버 간 충돌 없이 전역 유일한 ID를 생성할 수 있어 AUTO_INCREMENT 방식보다 분산 시스템에 적합합니다.

Q3. Redis를 캐시로 사용할 때 Cache Invalidation 전략은?

TTL 만료: 일정 시간 후 자동 삭제 (간단하지만 데이터 불일치 가능). ② Write-Through: DB 쓸 때 캐시도 동시에 업데이트. ③ Write-Behind: 캐시에 먼저 쓰고 나중에 DB에 반영. ④ 캐시 직접 삭제: 데이터 변경 시 관련 캐시 키를 즉시 DEL. 실무에서는 주로 TTL + 데이터 변경 시 캐시 직접 삭제를 혼용합니다.

Q4. MongoDB에서 임베딩과 참조를 어떻게 선택하나요?

데이터 크기: 무한정 커질 수 있으면 참조 (16MB 문서 한도). ② 접근 패턴: 항상 함께 조회되면 임베딩, 독립적으로 조회되면 참조. ③ 업데이트 빈도: 임베딩된 데이터가 자주 변경되면 여러 곳을 수정해야 하므로 참조가 유리. ④ 1:N 비율: N이 매우 크면 참조가 안전합니다.

Q5. Redis의 데이터 영속성(Persistence)은 어떻게 보장하나요?

Redis는 두 가지 방법으로 영속성을 제공합니다. ① RDB(스냅샷): 일정 시간마다 전체 데이터를 디스크에 덤프. 빠르지만 마지막 스냅샷 이후 데이터 유실 가능. ② AOF(Append Only File): 모든 쓰기 명령을 로그로 저장. 데이터 유실 최소화지만 파일 크기 증가. 현업에서는 둘을 혼용(RDB + AOF)하여 사용합니다.

Q6. CAP 이론이 무엇인지 MongoDB를 예시로 설명해주세요.

CAP = Consistency(일관성), Availability(가용성), Partition tolerance(분산 내성). 세 가지를 동시에 만족할 수 없습니다. MongoDB는 기본적으로 CP(일관성 + 분산 내성) 시스템입니다. Replica Set에서 Primary 장애 시 Secondary가 선출되는 동안 잠시 쓰기 불가(가용성 희생), 하지만 항상 최신 데이터를 보장합니다.

Q7. MongoDB Aggregation Pipeline의 $match는 왜 앞에 두는 게 좋나요?

$match를 파이프라인 앞에 두면 필터링 후에 다음 스테이지가 실행되어 처리할 문서 수가 줄어듭니다. 뒤에 두면 전체 데이터를 $group 등으로 처리한 후 필터링하므로 매우 비효율적입니다. 특히 $match가 인덱스를 활용할 수 있도록 인덱스된 필드로 필터링하는 것이 중요합니다.

Q8. Redis Sorted Set으로 실시간 랭킹을 구현할 때 고려사항은?

기간별 랭킹: 일별/주별 키를 따로 관리 (예: ranking:2026-03-27). ② TTL 설정: 오래된 랭킹 키는 EXPIRE로 자동 삭제. ③ 동점 처리: Sorted Set은 점수가 같으면 사전순으로 정렬되므로 별도 처리 필요. ④ 정합성: Redis가 재시작되면 데이터 유실 가능하므로 주기적으로 DB에 동기화.

Q9. Elasticsearch를 MySQL과 함께 쓸 때 데이터 동기화를 어떻게 하나요?

이중 쓰기(Dual Write): 서비스 레이어에서 MySQL 저장 후 Elasticsearch에도 저장. 간단하지만 실패 처리 필요. ② CDC(Change Data Capture): MySQL 바이너리 로그(binlog)를 Debezium 등으로 읽어 ES에 자동 반영. ③ 배치 동기화: 주기적으로 MySQL에서 변경 데이터를 읽어 ES를 갱신. 현업에서는 CDC 방식이 가장 안정적입니다.

Q10. NoSQL을 선택할 때 가장 중요하게 고려하는 것은?

쿼리 패턴: 어떤 방식으로 데이터를 자주 읽고 쓰는가. ② 일관성 요구사항: 금융처럼 강한 일관성이 필요한가. ③ 확장성: 얼마나 많은 데이터와 트래픽이 예상되는가. ④ 팀 역량: 팀원들의 기술 스택과 학습 비용. ⑤ 운영 비용: 클라우드 매니지드 서비스 비용. 기술적 우열보다 서비스 특성에 맞는 선택이 중요합니다.

📌 11. 학습 로드맵 & 체크리스트

📖

1주차

NoSQL 개념 이해
SQL vs NoSQL 비교
CAP 이론

🍃

2주차

MongoDB CRUD
Atlas 계정 생성
Mongoose 연결

3주차

Redis 자료구조
캐싱 패턴
세션 관리

🏗️

4주차

MongoDB Aggregation
인덱스 최적화
데이터 모델링

🛍️

5주차

실전 프로젝트
블로그/쇼핑몰 구현
면접 준비

🛠️ 추천 학습 도구 & 환경

도구 용도 비용
MongoDB Atlas 클라우드 MongoDB (설치 불필요) 무료 512MB
MongoDB Compass GUI 관리 도구 (쿼리 테스트) 무료
Redis Stack (Docker) 로컬 Redis 설치 (RedisInsight 포함) 무료
Upstash Redis 클라우드 Redis (Vercel 연동 최적) 무료 Tier
mongosh MongoDB 공식 CLI 셸 무료

🎉 DatabaseDevGuide0005 완료!

NoSQL의 세계를 완전 정복했습니다!
MongoDB, Redis, Elasticsearch — 이제 현업에서 상황에 맞게 선택하고 활용하세요!

📌 다음 단계: DatabaseDevGuide0006 - 실전 활용
(백엔드 연결, 트랜잭션, 백업 & 보안)

✅ MongoDB CRUD ✅ Aggregation ✅ Redis 자료구조 ✅ 캐싱 패턴 ✅ SQL vs NoSQL