MongoDB 완벽 가이드: NoSQL 문서 데이터베이스
이 글의 핵심
MongoDB는 유연한 스키마와 수평 확장이 가능한 NoSQL 문서 데이터베이스입니다. JSON 형태의 BSON 문서로 데이터를 저장하며, 강력한 쿼리, 집계 파이프라인, 샤딩으로 대규모 애플리케이션에 적합합니다.
MongoDB란?
MongoDB는 문서 지향(Document-Oriented) NoSQL 데이터베이스로, JSON과 유사한 BSON(Binary JSON) 형식으로 데이터를 저장합니다. 2009년 출시 이후 가장 인기있는 NoSQL 데이터베이스로 자리잡았습니다.
핵심 특징
-
문서 기반
- JSON 형태 저장
- 유연한 스키마
- 중첩 구조 지원
-
확장성
- 수평 확장 (샤딩)
- 레플리카셋
- 자동 페일오버
-
강력한 쿼리
- 풍부한 쿼리 연산자
- 집계 파이프라인
- 텍스트 검색
-
고성능
- 인덱싱
- 메모리 맵 파일
- WiredTiger 스토리지 엔진
SQL vs NoSQL 비교
| 항목 | SQL (MySQL) | NoSQL (MongoDB) |
|---|---|---|
| 데이터 모델 | 테이블 (행/열) | 문서 (JSON) |
| 스키마 | 고정 | 유연 |
| 확장 | 수직 (Scale-up) | 수평 (Scale-out) |
| JOIN | 강력 | 제한적 (Lookup) |
| 트랜잭션 | 완벽 지원 | 4.0부터 지원 |
| 사용 사례 | 금융, 재고 관리 | 소셜 미디어, IoT |
| 학습 곡선 | 중간 | 낮음 |
설치
MongoDB를 시작하는 방법은 여러 가지가 있습니다. 로컬 개발 환경에서는 Docker를 사용하는 것이 가장 간편하며, macOS와 Ubuntu에서는 패키지 매니저를 통해 설치할 수 있습니다.
MongoDB 설치
Docker를 사용하면 몇 초 만에 MongoDB를 시작할 수 있어 개발 환경 구축이 매우 빠릅니다. 별도의 설정 없이 기본 포트(27017)로 접속할 수 있으며, 컨테이너 삭제로 깔끔하게 정리할 수 있습니다.
macOS 사용자라면 Homebrew로 설치하는 것이 편리합니다. 설치 후 백그라운드 서비스로 실행되어 시스템 부팅 시 자동으로 시작됩니다. Ubuntu에서는 공식 APT 저장소를 추가한 후 설치할 수 있습니다.
# macOS (Homebrew)
brew tap mongodb/brew
brew install mongodb-community
brew services start mongodb-community
# Ubuntu
wget -qO - https://www.mongodb.org/static/pgp/server-7.0.asc | sudo apt-key add -
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list
sudo apt-get update
sudo apt-get install -y mongodb-org
sudo systemctl start mongod
# Docker
docker run -d -p 27017:27017 --name mongodb mongo:7
Node.js 드라이버 설치
Node.js에서 MongoDB를 사용하려면 공식 드라이버나 Mongoose ODM을 설치해야 합니다. 공식 드라이버는 저수준 API를 제공하고, Mongoose는 스키마 검증과 편리한 기능을 추가로 제공합니다. 실무에서는 Mongoose를 사용하는 것이 일반적입니다.
# MongoDB 드라이버
npm install mongodb
# Mongoose ODM (권장)
npm install mongoose
기본 CRUD
연결
// MongoDB 드라이버
import { MongoClient } from 'mongodb';
const client = new MongoClient('mongodb://localhost:27017');
await client.connect();
const db = client.db('myapp');
const users = db.collection('users');
// Mongoose
import mongoose from 'mongoose';
await mongoose.connect('mongodb://localhost:27017/myapp');
Create (삽입)
// 단일 문서
const result = await users.insertOne({
name: '홍길동',
email: '[email protected]',
age: 25,
tags: ['developer', 'nodejs']
});
console.log(result.insertedId);
// 여러 문서
const result = await users.insertMany([
{ name: '김철수', email: '[email protected]' },
{ name: '이영희', email: '[email protected]' }
]);
console.log(result.insertedIds);
Read (조회)
// 모든 문서
const allUsers = await users.find().toArray();
// 조건 조회
const result = await users.find({ age: { $gte: 20 } }).toArray();
// 단일 문서
const user = await users.findOne({ email: '[email protected]' });
// Projection (필드 선택)
const users = await users.find(
{ age: { $gte: 20 } },
{ projection: { name: 1, email: 1, _id: 0 } }
).toArray();
// 정렬
const users = await users.find().sort({ age: -1 }).toArray();
// 페이지네이션
const page = 1;
const limit = 10;
const users = await users.find()
.skip((page - 1) * limit)
.limit(limit)
.toArray();
Update (수정)
// 단일 문서
await users.updateOne(
{ email: '[email protected]' },
{ $set: { age: 26 } }
);
// 여러 문서
await users.updateMany(
{ age: { $lt: 20 } },
{ $set: { category: 'minor' } }
);
// Upsert (없으면 생성)
await users.updateOne(
{ email: '[email protected]' },
{ $set: { name: '신규' } },
{ upsert: true }
);
// 증가/감소
await users.updateOne(
{ _id: userId },
{ $inc: { views: 1 } }
);
// 배열 추가
await users.updateOne(
{ _id: userId },
{ $push: { tags: 'react' } }
);
Delete (삭제)
// 단일 문서
await users.deleteOne({ email: '[email protected]' });
// 여러 문서
await users.deleteMany({ age: { $lt: 18 } });
Mongoose ODM
스키마 정의
import mongoose from 'mongoose';
const userSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true,
minlength: 2,
maxlength: 50
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
match: /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/
},
age: {
type: Number,
min: 0,
max: 150
},
tags: [String],
profile: {
bio: String,
avatar: String,
social: {
twitter: String,
github: String
}
},
isActive: {
type: Boolean,
default: true
},
createdAt: {
type: Date,
default: Date.now
}
}, {
timestamps: true // createdAt, updatedAt 자동 생성
});
// 인덱스
userSchema.index({ email: 1 });
userSchema.index({ name: 'text' });
// 가상 필드
userSchema.virtual('fullName').get(function() {
return `${this.firstName} ${this.lastName}`;
});
// 메서드
userSchema.methods.comparePassword = async function(password) {
return bcrypt.compare(password, this.password);
};
// 스태틱 메서드
userSchema.statics.findByEmail = function(email) {
return this.findOne({ email });
};
const User = mongoose.model('User', userSchema);
Mongoose CRUD
// Create
const user = new User({
name: '홍길동',
email: '[email protected]'
});
await user.save();
// 또는
const user = await User.create({
name: '홍길동',
email: '[email protected]'
});
// Read
const users = await User.find({ age: { $gte: 20 } });
const user = await User.findById(userId);
const user = await User.findOne({ email: '[email protected]' });
// Update
const user = await User.findByIdAndUpdate(
userId,
{ age: 26 },
{ new: true } // 업데이트된 문서 반환
);
// Delete
await User.findByIdAndDelete(userId);
집계 파이프라인
// 사용자 연령대별 통계
const stats = await users.aggregate([
// 필터링
{ $match: { isActive: true } },
// 그룹화
{
$group: {
_id: {
$floor: { $divide: ['$age', 10] }
},
count: { $sum: 1 },
avgAge: { $avg: '$age' }
}
},
// 정렬
{ $sort: { _id: 1 } },
// 프로젝션
{
$project: {
ageGroup: { $multiply: ['$_id', 10] },
count: 1,
avgAge: { $round: ['$avgAge', 1] }
}
}
]);
인덱싱
인덱스는 쿼리 성능을 크게 향상시킵니다. 하지만 인덱스를 너무 많이 만들면 쓰기 성능이 저하되고 저장 공간이 증가하므로 신중하게 선택해야 합니다.
가장 자주 조회하는 필드에 인덱스를 생성하세요. 예를 들어 이메일로 사용자를 찾는 경우가 많다면 email 필드에 인덱스를 만들어야 합니다. 복합 인덱스는 여러 필드를 조합해 조회할 때 유용합니다.
// 단일 필드 인덱스
await users.createIndex({ email: 1 });
// 복합 인덱스
await users.createIndex({ lastName: 1, firstName: 1 });
// 텍스트 인덱스
await users.createIndex({ bio: 'text', tags: 'text' });
// 지리 공간 인덱스
await locations.createIndex({ location: '2dsphere' });
// 인덱스 확인
const indexes = await users.indexes();
텍스트 인덱스는 전문 검색(full-text search)에 사용되며, 여러 필드를 하나의 텍스트 인덱스로 묶을 수 있습니다. 지리 공간 인덱스는 위치 기반 쿼리에 필수입니다.
실전 사례: 전자상거래 시스템
MongoDB의 유연한 스키마는 전자상거래 시스템처럼 다양한 종류의 상품을 다룰 때 특히 유용합니다. 의류는 사이즈와 색상 정보가 필요하고, 전자제품은 스펙 정보가 필요한데 이를 하나의 컬렉션에 자연스럽게 저장할 수 있습니다.
임베디드 문서로 주문 시스템 설계
주문(Order)과 주문 항목(OrderItem)을 별도 컬렉션으로 분리하는 대신 임베디드 문서로 저장하면 한 번의 쿼리로 모든 정보를 가져올 수 있습니다. 이는 관계형 DB에서 JOIN을 사용하는 것보다 훨씬 빠릅니다.
// 주문 생성
const order = await orders.insertOne({
userId: new ObjectId(userId),
orderNumber: 'ORD-2026-0001',
status: 'pending',
items: [
{
productId: new ObjectId(productId1),
productName: '노트북',
quantity: 1,
price: 1500000,
options: {
color: 'silver',
storage: '512GB'
}
},
{
productId: new ObjectId(productId2),
productName: '마우스',
quantity: 2,
price: 30000
}
],
totalAmount: 1560000,
shippingAddress: {
name: '홍길동',
phone: '010-1234-5678',
address: '서울시 강남구',
zipCode: '12345'
},
payment: {
method: 'card',
cardNumber: '1234-****-****-5678',
paidAt: new Date()
},
createdAt: new Date(),
updatedAt: new Date()
});
// 주문 조회 (한 번의 쿼리로 모든 정보 획득)
const order = await orders.findOne({
orderNumber: 'ORD-2026-0001'
});
이렇게 설계하면 주문 상세 페이지를 렌더링할 때 하나의 쿼리만 필요합니다. 관계형 DB라면 orders, order_items, products, shipping_addresses 테이블을 JOIN해야 합니다.
집계 파이프라인으로 매출 분석
MongoDB의 집계 파이프라인은 복잡한 데이터 분석을 쉽게 만듭니다. 일별 매출, 인기 상품, 사용자별 구매 패턴 등을 효율적으로 계산할 수 있습니다.
// 월별 매출 통계
const monthlySales = await orders.aggregate([
{
$match: {
status: 'completed',
createdAt: {
$gte: new Date('2026-01-01'),
$lt: new Date('2027-01-01')
}
}
},
{
$group: {
_id: {
year: { $year: '$createdAt' },
month: { $month: '$createdAt' }
},
totalSales: { $sum: '$totalAmount' },
orderCount: { $sum: 1 },
avgOrderValue: { $avg: '$totalAmount' }
}
},
{
$sort: { '_id.year': 1, '_id.month': 1 }
},
{
$project: {
_id: 0,
period: {
$concat: [
{ $toString: '$_id.year' },
'-',
{ $toString: '$_id.month' }
]
},
totalSales: { $round: ['$totalSales', 0] },
orderCount: 1,
avgOrderValue: { $round: ['$avgOrderValue', 0] }
}
}
]).toArray();
이 파이프라인은 날짜 필터링, 그룹화, 집계, 정렬, 포맷팅을 한 번에 수행합니다. SQL로 작성하면 훨씬 복잡한 쿼리가 됩니다.
재고 관리와 동시성 제어
여러 사용자가 동시에 같은 상품을 주문할 때 재고가 음수가 되는 문제를 방지해야 합니다. MongoDB의 원자적 연산을 사용하면 안전하게 처리할 수 있습니다.
// 재고 감소 (동시성 안전)
async function decreaseStock(productId, quantity) {
const result = await products.updateOne(
{
_id: new ObjectId(productId),
stock: { $gte: quantity } // 재고가 충분한 경우만 업데이트
},
{
$inc: { stock: -quantity }
}
);
if (result.modifiedCount === 0) {
throw new Error('재고가 부족합니다');
}
return result;
}
이 패턴은 race condition 없이 재고를 안전하게 관리합니다. 조건을 만족하는 경우에만 업데이트가 실행되어 재고가 음수가 되지 않습니다.
성능 최적화 팁
1. 인덱스 전략
쿼리에서 자주 사용하는 필드에 인덱스를 만들되, 쓰기가 많은 컬렉션에는 최소한의 인덱스만 유지하세요. explain() 메서드로 쿼리 성능을 분석할 수 있습니다.
2. 프로젝션 활용
필요한 필드만 조회하면 네트워크 전송량과 메모리 사용량을 줄일 수 있습니다. 특히 큰 문서나 배열 필드가 있는 경우 효과가 큽니다.
3. 커넥션 풀 설정
Node.js 앱에서는 MongoClient를 싱글톤으로 관리하세요. 요청마다 새로운 연결을 만들면 성능이 크게 저하됩니다.
MongoDB는 유연하고 확장 가능한 NoSQL 데이터베이스입니다. 문서 기반 모델과 강력한 쿼리로 현대적인 애플리케이션 개발에 적합합니다. 특히 스키마가 자주 변경되거나, 계층적 데이터를 다루거나, 수평 확장이 필요한 경우 MongoDB가 탁월한 선택입니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. MongoDB를 활용한 NoSQL 데이터베이스 완벽 가이드. MySQL 대비 장점, 설치부터 CRUD, 인덱싱, 집계 파이프라인, 샤딩, 레플리카셋, Mongoose ODM, 성능 최적화까지 실전 예제로 학습합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
이 글에서 다루는 키워드 (관련 검색어)
MongoDB, NoSQL, Database, Node.js, Mongoose, Backend 등으로 검색하시면 이 글이 도움이 됩니다.