MongoDB란? SQL과 NoSQL의 차이
MongoDB는 2009년 출시된 오픈소스 도큐먼트 데이터베이스(Document DB)다. 데이터를 JSON 형식의 BSON(Binary JSON) 도큐먼트로 저장하며, 미리 정해진 스키마 없이도 유연하게 데이터 구조를 변경할 수 있다는 점이 가장 큰 특징다. 관계형 데이터베이스(RDBMS)처럼 테이블과 행이 아닌, 컬렉션(Collection)과 도큐먼트(Document)라는 개념을 사용한다.
유연한 스키마
필드 추가·삭제·변경이 자유로워 빠른 프로토타이핑에 최적
JSON 네이티브
BSON 포맷으로 저장, JavaScript 객체 그대로 저장 가능
수평 확장
샤딩(Sharding)으로 데이터 분산, 대용량 트래픽 처리 가능
Atlas 클라우드
MongoDB 공식 클라우드, 무료 티어 제공, AWS/GCP/Azure 지원
SQL vs NoSQL 핵심 비교
| 항목 | SQL (PostgreSQL, MySQL) | NoSQL (MongoDB) |
|---|---|---|
| 데이터 구조 | 테이블 (행, 열) | 컬렉션 (도큐먼트, JSON) |
| 스키마 | 고정 (변경 시 마이그레이션) | 유연 (필드 자유 추가) |
| 확장 방식 | 수직 확장 (스펙 업그레이드) | 수평 확장 (샤딩 분산) |
| 트랜잭션 | 완전한 ACID 보장 | 멀티 도큐먼트 트랜잭션 지원 (v4.0+) |
| 쿼리 언어 | SQL (표준화) | MQL (MongoDB Query Language) |
| JOIN | 네이티브 JOIN 지원 | $lookup (Aggregation) |
| 주요 용도 | 금융, ERP, 복잡한 관계형 데이터 | 콘텐츠, 카탈로그, 로그, IoT |
언제 MongoDB를 선택하나요?
- 스키마가 자주 변경되는 초기 개발: MVP 단계에서 데이터 구조가 확정되지 않은 경우 마이그레이션 없이 자유롭게 필드를 추가·수정할 수 있다.
- 계층형·중첩 데이터: 상품 카탈로그, 블로그 포스트(댓글 포함), 사용자 프로필처럼 관련 데이터를 한 도큐먼트에 임베딩하면 JOIN 없이 빠르게 조회할 수 있다.
- 대용량 비정형 데이터: 로그, 이벤트 스트림, IoT 센서 데이터처럼 정형화하기 어려운 데이터를 효율적으로 저장한다.
- 빠른 읽기·쓰기가 필요한 서비스: 게임 리더보드, 실시간 분석 대시보드, 콘텐츠 관리 시스템(CMS)에 적합한다.
설치 및 초기 설정 (Docker / 직접 설치)
Docker Compose로 빠르게 시작 (권장)
로컬 개발 환경에서는 Docker를 사용하는 것이 가장 간편한다. 아래 compose 파일을 프로젝트 루트에 저장하자.
services:
mongodb:
image: mongo:7.0
container_name: my-mongodb
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: password
MONGO_INITDB_DATABASE: mydb
ports:
- "27017:27017"
volumes:
- mongodb_data:/data/db
volumes:
mongodb_data:docker-compose up -d
# mongosh 접속
docker exec -it my-mongodb mongosh -u admin -p passwordmongosh 기본 명령어
show dbs // DB 목록
use mydb // DB 선택/생성
show collections // 컬렉션 목록
db.stats() // DB 통계brew tap mongodb/brew && brew install mongodb-community@7.0, Windows는 공식 다운로드 페이지에서 MSI 인스톨러를 사용하자. 운영 환경(서버)에서는 MongoDB Atlas 또는 패키지 매니저(apt, yum)를 통한 설치를 권장한다.
컬렉션·도큐먼트 개념과 데이터 모델 설계
MongoDB의 컬렉션(Collection)은 SQL의 테이블에 해당하고, 도큐먼트(Document)는 SQL의 행(row)에 해당한다. 도큐먼트는 JSON 형식으로 표현되며, 중첩 객체와 배열을 자유롭게 포함할 수 있다. 각 도큐먼트는 고유한 _id 필드(기본값: ObjectId)를 가집니다.
블로그 포스트 도큐먼트 예시
{
"_id": "ObjectId('..')",
"title": "MongoDB 가이드",
"author": {
"id": "user_123",
"name": "junetapa"
},
"tags": ["MongoDB", "NoSQL", "DB"],
"meta": {
"views": 1500,
"likes": 42
},
"comments": [],
"published": true,
"createdAt": "ISODate('2026-02-19')"
}임베딩(Embedding) vs 참조(Reference) 선택 가이드
| 항목 | 임베딩 (Embedding) | 참조 (Reference) |
|---|---|---|
| 데이터 위치 | 상위 도큐먼트 내부에 중첩 저장 | 별도 컬렉션에 저장 후 ID 참조 |
| 조회 성능 | 빠름 (단일 쿼리) | $lookup 필요 (추가 쿼리) |
| 데이터 중복 | 중복 가능 (비정규화) | 중복 없음 (정규화) |
| 업데이트 | 여러 도큐먼트 업데이트 필요 | 참조 도큐먼트 1회 업데이트 |
| 적합한 관계 | 1:1, 1:N (N이 적고 변경 적은 경우) | N:M, 1:N (N이 크거나 자주 변경) |
| 예시 | 포스트 안에 작성자 이름 포함 | 포스트에 author_id 저장, users 컬렉션 참조 |
CRUD 완전 정복 — insertOne, find, updateOne, deleteOne
MongoDB의 CRUD 연산은 SQL의 INSERT, SELECT, UPDATE, DELETE에 대응한다. 모든 연산은 컬렉션 객체(db.컬렉션명)를 통해 수행된다.
Create — 도큐먼트 삽입
// Create
db.posts.insertOne({
title: "MongoDB 완전 가이드",
author: { id: "user_1", name: "junetapa" },
tags: ["MongoDB", "NoSQL"],
views: 0,
published: true,
createdAt: new Date()
});
db.posts.insertMany([
{ title: "첫 번째 포스트", tags: ["intro"], published: true },
{ title: "두 번째 포스트", tags: ["advanced"], published: false }
]);Read — 도큐먼트 조회
// Read
db.posts.find({ published: true })
.sort({ createdAt: -1 })
.limit(20)
.skip(0);
// 특정 필드만 반환 (projection)
db.posts.find(
{ published: true },
{ title: 1, tags: 1, createdAt: 1, _id: 0 }
);Update — 도큐먼트 수정
// Update
db.posts.updateOne(
{ _id: ObjectId("..") },
{
$set: { title: "수정된 제목", updatedAt: new Date() },
$inc: { "meta.views": 1 }
}
);
// 배열에 항목 추가
db.posts.updateOne(
{ _id: ObjectId("..") },
{ $push: { tags: "newTag" } }
);
// Upsert
db.users.updateOne(
{ email: "hong@example.com" },
{ $set: { name: "홍길동", updatedAt: new Date() } },
{ upsert: true }
);Delete — 도큐먼트 삭제
// Delete
db.posts.deleteOne({ _id: ObjectId("..") });
db.posts.deleteMany({ published: false, createdAt: { $lt: new Date("2025-01-01") } });쿼리 연산자 — 조건 검색, 배열, 정규식
MongoDB MQL은 다양한 연산자를 제공한다. SQL의 WHERE 절에 해당하는 조건을 JSON 형식으로 표현한다. 연산자는 달러 기호($)로 시작한다.
// 비교 연산자
db.posts.find({ views: { $gte: 100, $lt: 1000 } });
db.posts.find({ title: { $ne: "삭제됨" } });
db.posts.find({ status: { $in: ["draft", "published"] } });
// 논리 연산자
db.posts.find({
$and: [
{ published: true },
{ views: { $gte: 50 } }
]
});
// 배열 연산자
db.posts.find({ tags: "MongoDB" }); // 배열에 포함
db.posts.find({ tags: { $all: ["DB", "NoSQL"] } }); // 모두 포함
db.posts.find({ "tags.0": "MongoDB" }); // 첫 번째 원소
// 정규식 (대소문자 무관 검색)
db.posts.find({ title: { $regex: /mongodb/i } });
// null/존재 확인
db.posts.find({ deletedAt: null });
db.posts.find({ bio: { $exists: true } });주요 연산자 빠른 참조
| 연산자 | 의미 | SQL 대응 |
|---|---|---|
$eq | 같음 | = value |
$ne | 다름 | <> value |
$gt / $gte | 초과 / 이상 | > / >= |
$lt / $lte | 미만 / 이하 | < / <= |
$in | 배열 중 하나 | IN (..) |
$nin | 배열 외 모두 | NOT IN (..) |
$and / $or / $nor | 논리 AND/OR/NOR | AND / OR |
$exists | 필드 존재 여부 | IS NOT NULL |
$regex | 정규식 매칭 | LIKE / ILIKE |
$all | 배열 모든 요소 포함 | (배열 전용) |
인덱스 설계 — 단일, 복합, 텍스트, TTL
인덱스는 쿼리 성능을 획기적으로 개선한다. 인덱스 없이 MongoDB는 컬렉션 전체를 스캔(Collection Scan)하지만, 인덱스가 있으면 해당 필드만 빠르게 탐색(Index Scan)한다. 단, 인덱스는 쓰기 성능과 저장 공간을 소모하므로 꼭 필요한 쿼리 패턴에만 생성해야 한다.
// 단일 인덱스
db.posts.createIndex({ createdAt: -1 });
// 복합 인덱스 (자주 함께 쓰는 필드)
db.posts.createIndex({ published: 1, createdAt: -1 });
// 고유 인덱스
db.users.createIndex({ email: 1 }, { unique: true });
// 텍스트 인덱스 (전문 검색)
db.posts.createIndex({ title: "text", content: "text" });
db.posts.find({ $text: { $search: "MongoDB 가이드" } });
// TTL 인덱스 (자동 만료)
// 생성 후 7일 뒤 자동 삭제
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 604800 });
// 인덱스 목록
db.posts.getIndexes();
// 실행 계획 확인 (SQL의 EXPLAIN ANALYZE)
db.posts.find({ published: true }).sort({ createdAt: -1 }).explain("executionStats");인덱스 설계 원칙
- ESR 규칙: 복합 인덱스는 Equality(등호) → Sort(정렬) → Range(범위) 순서로 필드를 배치하는 것이 최적다.
- 선택도(Selectivity): 값의 종류가 많은 필드(email, _id 등)에 인덱스를 생성하면 효과가 큽니다. published(true/false)처럼 카디널리티가 낮은 필드 단독 인덱스는 효과가 적다.
- 과도한 인덱스 주의: 인덱스가 많을수록 INSERT/UPDATE/DELETE 성능이 저하된다. 실제 쿼리 패턴을 분석 후 필요한 것만 생성하자.
.explain("executionStats") 결과에서 COLLSCAN이 보이면 인덱스를 타지 않는 것다. IXSCAN이 나와야 인덱스를 사용하는 최적화된 쿼리다. totalDocsExamined 값이 nReturned와 가까울수록 효율적인 쿼리다.
Aggregation Pipeline — $match, $group, $lookup
Aggregation Pipeline은 MongoDB의 가장 강력한 기능 중 하나다. 여러 단계(Stage)를 파이프라인으로 연결해 데이터를 필터링, 그룹화, 조인, 변환하는 복잡한 분석 쿼리를 수행할 수 있다. SQL의 GROUP BY, JOIN, HAVING에 해당하는 작업을 모두 처리한다.
$match + $group + $sort — 그룹 집계
db.posts.aggregate([
{ $match: { published: true } }, // 필터
{ $group: {
_id: { $arrayElemAt: ["$tags", 0] }, // 첫 번째 태그로 그룹
postCount: { $sum: 1 },
totalViews: { $sum: "$meta.views" },
avgViews: { $avg: "$meta.views" }
}},
{ $sort: { totalViews: -1 } },
{ $limit: 10 }
]);$lookup — SQL JOIN과 유사한 컬렉션 조인
db.posts.aggregate([
{ $match: { published: true } },
{ $lookup: {
from: "users",
localField: "author.id",
foreignField: "_id",
as: "authorInfo"
}},
{ $unwind: "$authorInfo" },
{ $project: {
title: 1,
"authorInfo.name": 1,
"authorInfo.email": 1,
views: "$meta.views",
createdAt: 1
}},
{ $sort: { createdAt: -1 } },
{ $limit: 20 }
]);$facet — 여러 집계를 한 번에
db.posts.aggregate([
{ $match: { published: true } },
{ $facet: {
"byMonth": [
{ $group: { _id: { $month: "$createdAt" }, count: { $sum: 1 } } },
{ $sort: { "_id": 1 } }
],
"topPosts": [
{ $sort: { "meta.views": -1 } },
{ $limit: 5 },
{ $project: { title: 1, views: "$meta.views" } }
]
}}
]);$match(필터) → $group(집계) → $sort(정렬) → $limit/$skip(페이징) → $project(필드 선택) → $lookup(JOIN) → $unwind(배열 펼치기) → $addFields(필드 추가) → $facet(다중 집계) 순서로 이해하면 복잡한 파이프라인도 쉽게 구성할 수 있다.
Mongoose로 Node.js 연동 — 스키마, 모델, 쿼리
Mongoose는 Node.js에서 MongoDB를 사용할 때 가장 많이 쓰이는 ODM(Object Document Mapper) 라이브러리다. 스키마 정의, 유효성 검사, 미들웨어, populate(참조 조인) 등 강력한 기능을 제공한다.
npm install mongooseDB 연결 모듈
import mongoose from 'mongoose';
export async function connectDB() {
await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/mydb');
console.log('MongoDB 연결 성공');
}스키마 및 모델 정의
// models/Post.js
const postSchema = new mongoose.Schema({
title: { type: String, required: true, maxlength: 500 },
content: { type: String, required: true },
author: {
id: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
name: { type: String, required: true }
},
tags: [{ type: String }],
meta: {
views: { type: Number, default: 0 },
likes: { type: Number, default: 0 }
},
published: { type: Boolean, default: false },
deletedAt: Date
}, {
timestamps: true // createdAt, updatedAt 자동 생성
});
// 인덱스 정의
postSchema.index({ published: 1, createdAt: -1 });
postSchema.index({ tags: 1 });
postSchema.index({ title: 'text', content: 'text' });
// 미들웨어 (소프트 삭제)
postSchema.pre('find', function() {
this.where({ deletedAt: null });
});
export const Post = mongoose.model('Post', postSchema);CRUD 사용 예시
// 생성
const post = await Post.create({ title: '제목', content: '내용', author: { id, name } });
// 조회 (페이징)
const posts = await Post.find({ published: true })
.sort({ createdAt: -1 })
.limit(20)
.skip((page - 1) * 20)
.lean(); // 일반 JS 객체로 반환 (성능 향상)
// populate — 참조 도큐먼트 조인
const post = await Post.findById(id).populate('author.id', 'name email avatar');
// 업데이트
await Post.findByIdAndUpdate(id,
{ $set: { title: '수정 제목' }, $inc: { 'meta.views': 1 } },
{ new: true } // 업데이트된 도큐먼트 반환
);.lean()을 추가하면 일반 JavaScript 객체로 반환되어 메모리 사용량과 처리 시간을 크게 줄일 수 있다.
Python PyMongo 연동
Python에서 MongoDB를 사용할 때는 공식 드라이버인 PyMongo를 사용한다. 비동기 환경에서는 Motor 라이브러리를 사용할 수 있다.
pip install pymongo python-dotenvfrom pymongo import MongoClient, DESCENDING
from bson import ObjectId
from datetime import datetime
import os
# 연결
client = MongoClient(os.getenv("MONGODB_URI", "mongodb://localhost:27017/"))
db = client["mydb"]
posts = db["posts"]
# 삽입
result = posts.insert_one({
"title": "MongoDB Python 가이드",
"tags": ["Python", "MongoDB"],
"meta": {"views": 0},
"published": True,
"createdAt": datetime.utcnow()
})
print(f"삽입된 ID: {result.inserted_id}")
# 조회 (정렬 + 페이징)
cursor = posts.find(
{"published": True},
{"title": 1, "tags": 1, "createdAt": 1}
).sort("createdAt", DESCENDING).limit(20)
for post in cursor:
print(post["title"])
# 업데이트
posts.update_one(
{"_id": ObjectId("..")},
{"$inc": {"meta.views": 1}, "$set": {"updatedAt": datetime.utcnow()}}
)
# Aggregation
pipeline = [
{"$match": {"published": True}},
{"$group": {"_id": None, "total": {"$sum": 1}, "avgViews": {"$avg": "$meta.views"}}}
]
result = list(posts.aggregate(pipeline))pip install motor로 비동기 드라이버를 사용하자. API는 PyMongo와 거의 동일하며 await만 추가하면 된다: result = await collection.find_one({"_id": ObjectId(id)})
운영 팁 — 성능 최적화와 Atlas 클라우드
MongoDB Atlas — 무료 클라우드 시작하기
MongoDB Atlas는 공식 클라우드 데이터베이스 서비스로, 무료 티어(M0, 512MB)를 제공한다. AWS, GCP, Azure 중 원하는 리전을 선택해 배포할 수 있으며, 자동 백업, 모니터링, 보안 설정이 기본 제공된다. 개인 프로젝트나 스타트업의 초기 서비스에 적합한다.
# Atlas 연결 문자열 (환경변수로 관리)
MONGODB_URI=mongodb+srv://<username>:<password>@cluster0.xxxxx.mongodb.net/mydb?retryWrites=true&w=majority성능 최적화 체크리스트
explain()으로 느린 쿼리 찾기
프로파일러(db.setProfilingLevel(1))를 활성화하거나 .explain("executionStats")로 쿼리 플랜을 분석한다. COLLSCAN이 나오면 인덱스 추가를 검토하자.
.lean() 사용으로 Mongoose 오버헤드 제거
읽기 전용 조회에 .lean()을 추가하면 Mongoose Document 변환을 건너뛰어 쿼리 성능이 최대 30% 향상된다.
projection으로 필요한 필드만 반환
{ title: 1, tags: 1, _id: 0 }처럼 필요한 필드만 지정하면 네트워크 전송량과 메모리 사용량을 줄다. 특히 content처럼 큰 필드는 목록 조회에서 제외하자.
배치 작업에 insertMany/bulkWrite 활용
대량 데이터 처리 시 insertOne을 반복 호출하는 대신 insertMany나 bulkWrite를 사용하면 네트워크 왕복 횟수가 줄어 성능이 크게 향상된다.
TTL 인덱스로 세션/로그 자동 정리
sessions, logs, temp_data 컬렉션에 TTL 인덱스를 설정하면 만료된 데이터가 자동 삭제되어 별도 정리 스크립트가 필요 없다.
백업 및 복원
# 백업 (전체 DB)
mongodump --uri="mongodb://localhost:27017" --out=./backup/
# 특정 컬렉션만 백업
mongodump --uri="mongodb://localhost:27017/mydb" --collection=posts --out=./backup/
# 복원
mongorestore --uri="mongodb://localhost:27017" ./backup/new ObjectId(idString) 또는 Mongoose에서는 Types.ObjectId(idString)으로 변환 후 쿼리하자. 문자열 그대로 비교하면 항상 결과가 없다.