Indexing 완벽 마스터

Indexing의 핵심 개념과 실전 활용법

Python중급
6시간
3개 항목
학습 진행률0 / 3 (0%)

학습 항목

1. JavaScript
초급
MongoDB|핵심|개념|완벽|정리
퀴즈튜토리얼
2. Python
고급
ElasticSearch|핵심|개념|완벽|정리
퀴즈튜토리얼
3. Python
중급
MySQL 핵심 개념 완벽 정리
퀴즈튜토리얼
1 / 3

이미지 로딩 중...

MongoDB 핵심 개념 완벽 정리 - 슬라이드 1/11

MongoDB 핵심 개념 완벽 정리

MongoDB의 필수 개념들을 초급 개발자도 쉽게 이해할 수 있도록 정리했습니다. Document 구조부터 인덱싱, 집계 파이프라인까지 실무에서 바로 활용할 수 있는 내용을 담았습니다. 실제 코드 예제와 함께 MongoDB의 핵심을 마스터해보세요.


목차

  1. Document와_Collection
  2. CRUD_연산
  3. 쿼리_연산자
  4. 인덱스
  5. Aggregation_Pipeline
  6. 스키마_설계_패턴
  7. 트랜잭션
  8. 레플리케이션

1. Document와_Collection

시작하며

여러분이 사용자 정보를 데이터베이스에 저장하려고 할 때, 관계형 DB에서는 테이블과 행으로 나누어 저장해야 했던 경험이 있으실 겁니다. 사용자 기본 정보 테이블, 주소 테이블, 취미 테이블...

이렇게 여러 테이블을 조인해야 하는 복잡함이 있었죠. MongoDB는 이런 문제를 완전히 다른 방식으로 접근합니다.

관계형 DB의 테이블과 행 대신 Collection과 Document라는 개념을 사용합니다. 바로 이것이 MongoDB가 유연하고 빠른 이유입니다.

하나의 Document에 사용자의 모든 정보를 담을 수 있어서 조인 없이 데이터를 가져올 수 있습니다. 특히 SNS, 전자상거래, 실시간 분석 시스템처럼 빠르게 변화하는 데이터 구조가 필요한 서비스에서 MongoDB의 진가가 발휘됩니다.

개요

간단히 말해서, Document는 JSON 형식의 데이터 한 덩어리이고, Collection은 이런 Document들의 모음입니다. 관계형 DB에서 테이블의 각 행이 정해진 컬럼만 가질 수 있다면, MongoDB의 Document는 각자 다른 필드를 가질 수 있습니다.

예를 들어, 같은 사용자 Collection에서 어떤 Document는 'age' 필드가 있고 어떤 것은 없을 수 있습니다. 기존 SQL DB에서는 스키마를 먼저 정의하고 ALTER TABLE로 변경해야 했다면, MongoDB에서는 Document를 그냥 저장하면 됩니다.

스키마가 유연하기 때문에 애플리케이션 요구사항이 변경되어도 빠르게 대응할 수 있습니다. Document는 BSON(Binary JSON) 형식으로 저장되어 효율적이고, 최대 16MB까지 저장할 수 있습니다.

중첩된 객체나 배열을 포함할 수 있어서 복잡한 데이터 구조도 자연스럽게 표현됩니다. Collection은 동적 스키마를 가지므로, 같은 Collection 안에서도 완전히 다른 구조의 Document를 저장할 수 있습니다.

이것이 MongoDB의 가장 큰 장점이자 주의해야 할 점입니다.

코드 예제

// MongoDB 연결 및 Collection 가져오기
const { MongoClient } = require('mongodb');
const client = new MongoClient('mongodb://localhost:27017');

async function createDocument() {
  await client.connect();
  const db = client.db('myapp');
  const users = db.collection('users');

  // Document 삽입 - 유연한 구조
  const result = await users.insertOne({
    name: '김개발',
    email: 'kim@example.com',
    age: 28,
    skills: ['JavaScript', 'MongoDB', 'React'],
    address: {
      city: '서울',
      district: '강남구'
    },
    createdAt: new Date()
  });

  console.log('Document ID:', result.insertedId);
}

설명

이것이 하는 일: MongoDB에 연결하여 users라는 Collection에 사용자 정보를 Document 형태로 저장합니다. 첫 번째로, MongoClient를 통해 MongoDB 서버에 연결합니다.

'myapp'이라는 데이터베이스를 선택하고, 그 안의 'users' Collection을 가져옵니다. 만약 Collection이 존재하지 않으면 첫 Document 삽입 시 자동으로 생성됩니다.

두 번째로, insertOne 메서드로 Document를 삽입합니다. 여기서 주목할 점은 Document의 구조가 매우 유연하다는 것입니다.

name, email 같은 단순 필드뿐만 아니라 skills처럼 배열도 저장할 수 있고, address처럼 중첩된 객체도 저장할 수 있습니다. 이것이 바로 MongoDB의 강력함입니다.

세 번째로, MongoDB는 각 Document에 자동으로 _id 필드를 추가합니다. 이것은 ObjectId 타입으로 고유한 식별자 역할을 합니다.

result.insertedId로 방금 생성된 Document의 ID를 확인할 수 있습니다. 네 번째로, 중첩된 구조 덕분에 관계형 DB에서 필요했던 조인 연산 없이 한 번의 쿼리로 모든 정보를 가져올 수 있습니다.

address 정보를 별도 테이블에 저장하고 JOIN할 필요가 없는 것이죠. 여러분이 이 코드를 사용하면 빠른 프로토타이핑이 가능하고, 스키마 변경에 유연하게 대응할 수 있으며, 읽기 성능이 뛰어난 애플리케이션을 만들 수 있습니다.

특히 사용자 프로필, 상품 정보, 블로그 포스트처럼 복잡한 중첩 구조를 가진 데이터에 최적입니다.

실전 팁

💡 Document 크기는 16MB 제한이 있으므로, 대용량 파일이나 이미지는 GridFS를 사용하거나 외부 스토리지에 저장하고 URL만 Document에 저장하세요.

💡 Collection 이름은 복수형을 사용하는 것이 관례입니다(user가 아닌 users). 그리고 네임스페이스 충돌을 피하기 위해 system으로 시작하는 이름은 피하세요.

💡 _id 필드는 자동 생성되지만, 직접 지정할 수도 있습니다. 외부 시스템의 ID를 사용해야 한다면 insertOne 시 _id 필드를 명시적으로 지정하세요.

💡 스키마가 유연하다고 해서 아무렇게나 저장하면 안 됩니다. Mongoose 같은 ODM을 사용하거나 애플리케이션 레벨에서 스키마 검증을 하는 것이 좋습니다.

💡 중첩 데이터의 깊이가 너무 깊으면 쿼리와 인덱싱이 복잡해집니다. 일반적으로 2-3단계 이상 중첩은 피하는 것이 좋습니다.


2. CRUD_연산

시작하며

여러분이 실제 애플리케이션을 만들 때 가장 기본이 되는 작업이 무엇일까요? 바로 데이터를 생성하고, 읽고, 수정하고, 삭제하는 것입니다.

이 네 가지 작업을 CRUD(Create, Read, Update, Delete)라고 부릅니다. MongoDB에서는 이 CRUD 연산을 매우 직관적인 메서드로 제공합니다.

SQL의 복잡한 문법 대신 JavaScript 객체를 사용해서 자연스럽게 데이터를 다룰 수 있습니다. 실제 프로젝트에서는 이 CRUD 연산이 전체 코드의 80% 이상을 차지합니다.

제대로 이해하고 효율적으로 사용하는 것이 MongoDB 마스터의 첫걸음입니다. 특히 Node.js와 함께 사용할 때 MongoDB의 CRUD API는 async/await와 완벽하게 어울려서 깔끔한 비동기 코드를 작성할 수 있습니다.

개요

간단히 말해서, MongoDB는 insertOne/insertMany로 생성하고, findOne/find로 조회하고, updateOne/updateMany로 수정하고, deleteOne/deleteMany로 삭제합니다. Create 작업에서는 단일 Document를 삽입하는 insertOne과 여러 Document를 한 번에 삽입하는 insertMany가 있습니다.

대량 삽입이 필요한 경우 insertMany를 사용하면 네트워크 왕복을 줄여 성능이 크게 향상됩니다. Read 작업은 가장 자주 사용되는 연산입니다.

findOne은 조건에 맞는 첫 번째 Document를, find는 조건에 맞는 모든 Document를 반환합니다. find는 커서(cursor)를 반환하므로 toArray()로 배열로 변환하거나 forEach로 순회할 수 있습니다.

Update 작업에서는 $set, $inc, $push 같은 업데이트 연산자를 사용합니다. updateOne은 첫 번째 매칭 Document만, updateMany는 모든 매칭 Document를 수정합니다.

replaceOne을 사용하면 Document 전체를 교체할 수도 있습니다. Delete 작업은 deleteOne과 deleteMany가 있으며, 조건 없이 사용하면 위험하므로 항상 필터 조건을 신중하게 작성해야 합니다.

실무에서는 실제 삭제 대신 'deleted' 플래그를 사용하는 소프트 삭제 패턴을 많이 사용합니다.

코드 예제

const users = db.collection('users');

// Create: 사용자 생성
await users.insertOne({ name: '이몽고', email: 'lee@example.com', points: 100 });

// Read: 이메일로 사용자 찾기
const user = await users.findOne({ email: 'lee@example.com' });
console.log(user);

// Read: 포인트 50 이상인 모든 사용자
const activeUsers = await users.find({ points: { $gte: 50 } }).toArray();

// Update: 포인트 증가
await users.updateOne(
  { email: 'lee@example.com' },
  { $inc: { points: 10 } }
);

// Update: 여러 사용자에게 배지 추가
await users.updateMany(
  { points: { $gte: 100 } },
  { $set: { badge: 'gold' } }
);

// Delete: 비활성 사용자 삭제
await users.deleteMany({ lastLogin: { $lt: new Date('2024-01-01') } });

설명

이것이 하는 일: users Collection에서 데이터를 생성, 조회, 수정, 삭제하는 완전한 CRUD 예제를 보여줍니다. 첫 번째로, insertOne으로 새 사용자를 생성합니다.

JavaScript 객체를 그대로 전달하면 MongoDB가 _id를 자동 생성하여 저장합니다. 반환값의 insertedId로 생성된 Document의 ID를 확인할 수 있습니다.

두 번째로, findOne과 find로 데이터를 조회합니다. findOne은 단일 객체를, find는 커서를 반환합니다.

커서는 메모리 효율적으로 대량의 데이터를 처리할 수 있게 해줍니다. { points: { $gte: 50 } }는 포인트가 50 이상인 조건을 나타냅니다.

세 번째로, updateOne과 updateMany로 데이터를 수정합니다. $inc 연산자는 숫자 필드를 증가시키고, $set은 필드 값을 설정합니다.

updateOne은 첫 번째 매칭 Document만 수정하고, updateMany는 조건에 맞는 모든 Document를 수정합니다. 이는 대량 업데이트 시 매우 유용합니다.

네 번째로, deleteMany로 조건에 맞는 모든 Document를 삭제합니다. 여기서는 2024년 이전에 로그인한 사용자들을 삭제합니다.

$lt는 'less than'을 의미하는 비교 연산자입니다. 여러분이 이 코드를 사용하면 복잡한 SQL 쿼리 없이 직관적으로 데이터를 다룰 수 있고, 업데이트 연산자를 활용해 원자적(atomic) 연산을 보장받을 수 있으며, 커서를 통해 메모리 효율적으로 대량 데이터를 처리할 수 있습니다.

특히 RESTful API나 GraphQL 리졸버를 구현할 때 이런 CRUD 패턴이 그대로 활용됩니다.

실전 팁

💡 find()는 커서를 반환하므로 limit(), skip(), sort() 메서드를 체인해서 페이지네이션을 구현할 수 있습니다. 예: find().skip(20).limit(10).sort({ createdAt: -1 })

💡 updateOne/updateMany 사용 시 upsert: true 옵션을 주면 Document가 없을 때 새로 생성합니다. 이는 "없으면 생성, 있으면 수정" 로직을 간단하게 만들어줍니다.

💡 deleteMany를 사용할 때는 항상 먼저 find로 삭제될 Document를 확인하세요. 실수로 모든 데이터를 삭제하는 것을 방지할 수 있습니다.

💡 대량 작업(bulk operations)이 필요하다면 bulkWrite() 메서드를 사용하세요. 여러 insertOne, updateOne, deleteOne을 한 번의 네트워크 왕복으로 처리해 성능이 대폭 향상됩니다.

💡 updateOne/updateMany의 반환값에는 matchedCount(조건에 맞는 수)와 modifiedCount(실제 수정된 수)가 있습니다. 이를 확인해서 작업이 예상대로 실행되었는지 검증하세요.


3. 쿼리_연산자

시작하며

여러분이 "30세 이상이면서 서울에 거주하고, 프로그래밍 스킬이 있는 사용자"를 찾아야 한다고 상상해보세요. SQL에서는 WHERE 절에 AND, OR, LIKE 같은 조건들을 조합해야 했습니다.

MongoDB에서는 쿼리 연산자를 사용해서 이런 복잡한 조건을 JavaScript 객체로 표현합니다. 훨씬 읽기 쉽고 작성하기도 편합니다.

실제 서비스에서는 단순한 일치 검색보다 범위 검색, 패턴 매칭, 배열 검색이 훨씬 많이 사용됩니다. 쿼리 연산자를 제대로 알아야 효율적인 검색 기능을 구현할 수 있습니다.

MongoDB의 쿼리 연산자는 비교, 논리, 요소, 배열, 정규식 등 다양한 카테고리로 분류되며, 이들을 조합하면 거의 모든 검색 조건을 표현할 수 있습니다.

개요

간단히 말해서, 쿼리 연산자는 $ 기호로 시작하며 복잡한 검색 조건을 표현하는 MongoDB의 특수 키워드입니다. 비교 연산자는 가장 기본적입니다.

$eq(같음), $ne(다름), $gt(보다 큼), $gte(이상), $lt(미만), $lte(이하)를 사용해 숫자나 날짜 범위를 검색할 수 있습니다. 예를 들어, 가격이 1000원에서 5000원 사이인 상품을 찾을 때 사용합니다.

논리 연산자는 여러 조건을 조합합니다. $and는 모든 조건을 만족, $or는 하나라도 만족, $not은 조건의 반대, $nor는 모든 조건을 만족하지 않을 때 사용합니다.

복잡한 비즈니스 로직을 쿼리로 표현할 때 필수적입니다. 배열 연산자는 MongoDB의 강력한 기능입니다.

$in은 배열에 포함된 값 중 하나와 일치, $all은 배열의 모든 값을 포함, $elemMatch는 배열 요소가 여러 조건을 동시에 만족할 때 사용합니다. SNS의 해시태그 검색, 전자상거래의 카테고리 필터링 등에 활용됩니다.

정규식을 사용하면 텍스트 패턴 매칭이 가능합니다. $regex 연산자나 JavaScript 정규식 객체를 사용해서 "이름이 '김'으로 시작하는 사용자" 같은 검색을 구현할 수 있습니다.

다만 전문 검색(full-text search)이 필요하다면 텍스트 인덱스를 사용하는 것이 더 효율적입니다.

코드 예제

const products = db.collection('products');

// 비교 연산자: 가격이 10000 이상 50000 이하인 상품
const affordableProducts = await products.find({
  price: { $gte: 10000, $lte: 50000 }
}).toArray();

// 논리 연산자: 할인 중이거나 베스트셀러인 상품
const promotedProducts = await products.find({
  $or: [
    { onSale: true },
    { bestseller: true }
  ]
}).toArray();

// 배열 연산자: 특정 태그를 모두 포함하는 상품
const taggedProducts = await products.find({
  tags: { $all: ['electronics', 'smartphone'] }
}).toArray();

// 정규식: 이름에 'iPhone'이 포함된 상품 (대소문자 무시)
const iphones = await products.find({
  name: { $regex: /iphone/i }
}).toArray();

설명

이것이 하는 일: 다양한 쿼리 연산자를 사용해서 상품 데이터베이스에서 복잡한 조건으로 데이터를 검색합니다. 첫 번째로, $gte와 $lte 비교 연산자를 사용해서 가격 범위를 지정합니다.

{ price: { $gte: 10000, $lte: 50000 } }는 "price 필드가 10000 이상이면서 동시에 50000 이하"를 의미합니다. 같은 필드에 여러 조건을 적용할 때 이렇게 객체 안에 여러 연산자를 넣습니다.

두 번째로, $or 논리 연산자를 사용합니다. 배열 안의 조건 중 하나라도 만족하면 Document가 선택됩니다.

여기서는 onSale이 true이거나 bestseller가 true인 상품을 찾습니다. $and는 암묵적으로 적용되지만, $or는 명시적으로 사용해야 합니다.

세 번째로, $all 배열 연산자를 사용합니다. tags 필드가 배열이고, 그 배열이 'electronics'와 'smartphone'을 모두 포함하는 Document를 찾습니다.

$in과 혼동하기 쉬운데, $in은 하나만 포함해도 되고, $all은 모두 포함해야 합니다. 네 번째로, $regex로 정규식 검색을 수행합니다.

/iphone/i는 대소문자를 구분하지 않고 'iphone'이 포함된 문자열을 찾습니다. 정규식은 강력하지만 인덱스를 제대로 활용하지 못하면 성능이 느릴 수 있습니다.

특히 ^(시작)로 시작하지 않는 패턴은 전체 스캔이 발생합니다. 여러분이 이 코드를 사용하면 전자상거래 사이트의 상품 필터링, 사용자 검색, 콘텐츠 추천 등 다양한 검색 기능을 구현할 수 있습니다.

쿼리 연산자를 적절히 조합하면 복잡한 비즈니스 로직을 데이터베이스 레벨에서 처리할 수 있어 애플리케이션 코드가 단순해지고 성능도 향상됩니다.

실전 팁

💡 $in 연산자 사용 시 배열의 크기가 크면 성능이 저하됩니다. 가능하면 100개 이하로 제한하고, 그 이상이면 쿼리를 나누거나 데이터 모델을 재검토하세요.

💡 정규식 검색은 인덱스를 활용하려면 ^ (시작 앵커)를 사용해야 합니다. { name: { $regex: /^iPhone/ } }는 인덱스를 사용하지만, { name: { $regex: /iPhone/ } }는 전체 스캔을 합니다.

💡 $or 연산자보다는 가능하면 $in을 사용하세요. { status: { $in: ['active', 'pending'] } }가 { $or: [{ status: 'active' }, { status: 'pending' }] }보다 효율적입니다.

💡 null 체크는 주의가 필요합니다. { field: null }은 필드가 null이거나 필드가 없는 경우 모두 매칭됩니다. 필드 존재 여부를 확인하려면 { field: { $exists: true } }를 사용하세요.

💡 배열 필드에서 특정 조건의 요소를 찾을 때 $elemMatch를 사용하세요. { scores: { $elemMatch: { $gte: 80, $lt: 90 } } }는 80 이상 90 미만인 점수를 포함하는 Document를 찾습니다.


4. 인덱스

시작하며

여러분의 서비스에 사용자가 1만 명이 있고, 특정 이메일로 사용자를 찾아야 한다고 가정해봅시다. 인덱스가 없다면 MongoDB는 1만 개의 Document를 모두 확인해야 합니다.

1만 개면 괜찮지만, 100만 개라면 어떨까요? 이것이 바로 인덱스가 필요한 이유입니다.

책의 색인처럼 인덱스는 특정 필드 값으로 Document를 빠르게 찾을 수 있게 해줍니다. 실제 프로덕션 환경에서 인덱스 없이 운영하는 것은 상상할 수 없습니다.

쿼리 속도가 수백 배에서 수천 배까지 차이가 날 수 있습니다. 하지만 인덱스는 공짜가 아닙니다.

쓰기 성능을 희생하고 저장 공간을 사용합니다. 어떤 필드에 인덱스를 만들지 신중하게 결정해야 합니다.

개요

간단히 말해서, 인덱스는 특정 필드의 값을 빠르게 찾기 위한 정렬된 데이터 구조입니다. MongoDB는 기본적으로 _id 필드에 자동으로 인덱스를 생성합니다.

이것이 _id로 Document를 찾는 것이 매우 빠른 이유입니다. 하지만 다른 필드로 검색한다면 직접 인덱스를 생성해야 합니다.

단일 필드 인덱스는 가장 기본적인 형태입니다. createIndex({ email: 1 })은 email 필드에 오름차순 인덱스를 만듭니다.

1은 오름차순, -1은 내림차순을 의미하며, 단일 필드 인덱스에서는 방향이 크게 중요하지 않습니다. 복합 인덱스(compound index)는 여러 필드를 조합한 인덱스입니다.

createIndex({ status: 1, createdAt: -1 })은 status로 먼저 정렬하고, 같은 status 내에서 createdAt으로 정렬합니다. 쿼리에서 이 두 필드를 함께 사용한다면 복합 인덱스가 매우 효율적입니다.

텍스트 인덱스는 전문 검색(full-text search)을 위한 특별한 인덱스입니다. createIndex({ content: 'text' })는 content 필드의 단어들을 인덱싱해서 $text 연산자로 검색할 수 있게 합니다.

블로그 검색, 상품 검색 등에 유용합니다. 유니크 인덱스는 필드 값의 중복을 방지합니다.

createIndex({ email: 1 }, { unique: true })는 이메일 중복 가입을 데이터베이스 레벨에서 막아줍니다.

코드 예제

const users = db.collection('users');

// 단일 필드 인덱스 생성 - 이메일 검색 최적화
await users.createIndex({ email: 1 }, { unique: true });

// 복합 인덱스 - 상태와 생성일로 자주 검색하는 경우
await users.createIndex({ status: 1, createdAt: -1 });

// 텍스트 인덱스 - 프로필 전문 검색
await users.createIndex({ bio: 'text', interests: 'text' });

// 인덱스 목록 확인
const indexes = await users.indexes();
console.log('현재 인덱스:', indexes);

// explain으로 쿼리 실행 계획 확인
const explainResult = await users.find({ email: 'test@example.com' })
  .explain('executionStats');
console.log('검사한 Document 수:', explainResult.executionStats.totalDocsExamined);
console.log('반환한 Document 수:', explainResult.executionStats.nReturned);

설명

이것이 하는 일: users Collection에 다양한 타입의 인덱스를 생성하고, explain 메서드로 쿼리 성능을 분석합니다. 첫 번째로, email 필드에 유니크 인덱스를 생성합니다.

unique: true 옵션은 같은 이메일로 여러 사용자가 가입하는 것을 방지합니다. 이제 email로 검색할 때 전체 스캔 대신 B-tree 인덱스를 사용해 O(log n) 시간에 찾을 수 있습니다.

두 번째로, status와 createdAt의 복합 인덱스를 생성합니다. "활성 사용자를 최신순으로" 같은 쿼리를 자주 실행한다면 이 인덱스가 매우 효율적입니다.

복합 인덱스의 순서가 중요한데, 첫 번째 필드(status)로 필터링하고 두 번째 필드(createdAt)로 정렬하는 쿼리에 최적화됩니다. 세 번째로, bio와 interests 필드에 텍스트 인덱스를 생성합니다.

이제 $text 연산자로 "프로그래밍을 좋아하는 사용자"처럼 자연어 검색이 가능합니다. 텍스트 인덱스는 Collection당 하나만 만들 수 있지만 여러 필드를 포함할 수 있습니다.

네 번째로, indexes() 메서드로 현재 Collection의 모든 인덱스를 확인합니다. 프로덕션에서는 정기적으로 인덱스를 검토해서 사용하지 않는 인덱스를 제거해야 합니다.

마지막으로, explain 메서드로 쿼리 실행 계획을 분석합니다. totalDocsExamined(검사한 Document 수)와 nReturned(반환한 수)를 비교해서 인덱스가 잘 작동하는지 확인합니다.

이상적으로는 이 두 값이 같아야 합니다. 여러분이 이 코드를 사용하면 쿼리 성능을 극적으로 향상시킬 수 있고, 데이터 무결성을 데이터베이스 레벨에서 보장받을 수 있으며, 전문 검색 기능을 쉽게 구현할 수 있습니다.

특히 대용량 데이터를 다루는 서비스에서 인덱스 최적화는 필수입니다.

실전 팁

💡 인덱스는 읽기 성능을 향상시키지만 쓰기 성능을 저하시킵니다. 인덱스가 많을수록 insert/update/delete가 느려지므로, 실제로 사용하는 쿼리에만 인덱스를 만드세요.

💡 복합 인덱스의 순서는 매우 중요합니다. ESR 규칙(Equality, Sort, Range)을 따르세요. 동등 비교(=) 필드를 먼저, 정렬 필드를 중간에, 범위 필드를 마지막에 배치하세요.

💡 explain('executionStats')를 사용해서 쿼리가 인덱스를 사용하는지 확인하세요. 'IXSCAN' stage가 보이면 인덱스를 사용하는 것이고, 'COLLSCAN'이 보이면 전체 스캔을 하는 것입니다.

💡 부분 인덱스(partial index)를 사용하면 조건에 맞는 Document만 인덱싱해서 공간을 절약할 수 있습니다. 예: createIndex({ status: 1 }, { partialFilterExpression: { status: 'active' } })

💡 백그라운드에서 인덱스를 생성하려면 background: true 옵션을 사용하세요. 큰 Collection에서 인덱스를 만들 때 서비스 중단을 최소화할 수 있습니다.


5. Aggregation_Pipeline

시작하며

여러분이 전자상거래 사이트의 월별 매출 통계를 내야 한다고 가정해봅시다. 카테고리별 판매량, 평균 주문 금액, 상위 10개 상품...

이런 복잡한 분석을 어떻게 하시겠습니까? SQL에서는 GROUP BY, JOIN, HAVING 같은 절들을 조합해야 했습니다.

MongoDB의 Aggregation Pipeline은 이런 복잡한 데이터 처리를 단계별로 체인처럼 연결해서 수행합니다. Aggregation은 MongoDB의 가장 강력한 기능 중 하나입니다.

단순 CRUD를 넘어서 데이터 분석, 보고서 생성, 데이터 변환 등 거의 모든 것을 할 수 있습니다. 실제 서비스에서는 대시보드, 분석 리포트, 데이터 마이그레이션 등에 Aggregation Pipeline을 활용합니다.

제대로 이해하면 복잡한 비즈니스 로직을 데이터베이스 레벨에서 효율적으로 처리할 수 있습니다.

개요

간단히 말해서, Aggregation Pipeline은 Document들이 여러 단계(stage)를 거쳐 변환되고 집계되는 파이프라인입니다. 각 단계는 $ 기호로 시작하는 연산자로 표현됩니다.

$match는 필터링, $group은 그룹화, $sort는 정렬, $project는 필드 선택/변환, $lookup은 조인 같은 작업을 수행합니다. 이 단계들을 배열로 나열하면 순차적으로 실행됩니다.

$match 단계는 가능한 한 파이프라인 초반에 배치해야 합니다. 일찍 필터링할수록 다음 단계에서 처리할 데이터가 줄어들어 성능이 향상됩니다.

인덱스도 $match 단계에서만 활용되므로 성능에 매우 중요합니다. $group은 SQL의 GROUP BY와 유사합니다.

_id 필드로 그룹화 기준을 지정하고, $sum, $avg, $max, $min, $push 같은 누산기(accumulator)로 집계합니다. 예를 들어, 카테고리별 평균 가격을 계산하거나 사용자별 주문 목록을 만들 수 있습니다.

$lookup은 관계형 DB의 JOIN과 비슷한 기능을 제공합니다. 다른 Collection의 Document를 참조해서 가져올 수 있습니다.

MongoDB는 비정규화를 선호하지만, 때로는 정규화된 데이터를 조인해야 할 때가 있고 이때 $lookup을 사용합니다. $project는 결과의 형태를 결정합니다.

필요한 필드만 선택하거나, 계산된 필드를 추가하거나, 필드 이름을 변경할 수 있습니다. 네트워크 전송량을 줄이고 클라이언트에 필요한 형식으로 데이터를 제공할 때 유용합니다.

코드 예제

const orders = db.collection('orders');

// 월별 카테고리별 매출 분석
const salesReport = await orders.aggregate([
  // 1단계: 2024년 주문만 필터링
  { $match: {
    orderDate: { $gte: new Date('2024-01-01'), $lt: new Date('2025-01-01') },
    status: 'completed'
  }},

  // 2단계: 월과 카테고리로 그룹화
  { $group: {
    _id: {
      month: { $month: '$orderDate' },
      category: '$category'
    },
    totalSales: { $sum: '$amount' },
    orderCount: { $sum: 1 },
    avgOrderValue: { $avg: '$amount' }
  }},

  // 3단계: 매출 기준 내림차순 정렬
  { $sort: { totalSales: -1 }},

  // 4단계: 필드 재구성
  { $project: {
    _id: 0,
    month: '$_id.month',
    category: '$_id.category',
    totalSales: { $round: ['$totalSales', 2] },
    orderCount: 1,
    avgOrderValue: { $round: ['$avgOrderValue', 2] }
  }}
]).toArray();

console.log('매출 리포트:', salesReport);

설명

이것이 하는 일: 주문 데이터를 분석해서 2024년의 월별/카테고리별 매출 통계를 생성합니다. 첫 번째 단계($match)에서는 2024년에 완료된 주문만 필터링합니다.

이 단계를 초반에 배치해서 처리할 데이터양을 줄입니다. orderDate 필드에 인덱스가 있다면 이 단계에서 활용됩니다.

두 번째 단계($group)에서는 월과 카테고리의 조합으로 그룹화합니다. $month 연산자로 orderDate에서 월을 추출합니다.

각 그룹에 대해 $sum으로 총 매출과 주문 수를, $avg로 평균 주문 금액을 계산합니다. _id는 그룹화 키이고, 나머지는 집계 결과입니다.

세 번째 단계($sort)에서는 totalSales 기준으로 내림차순 정렬합니다. -1은 내림차순, 1은 오름차순을 의미합니다.

이렇게 하면 매출이 높은 카테고리가 먼저 나옵니다. 네 번째 단계($project)에서는 결과의 형태를 재구성합니다.

_id는 제거하고, 그룹화 키였던 month와 category를 최상위 필드로 올립니다. $round 연산자로 소수점 둘째 자리까지만 표시합니다.

이렇게 하면 클라이언트에서 사용하기 편한 형태가 됩니다. 여러분이 이 코드를 사용하면 복잡한 비즈니스 리포트를 생성하고, 대시보드용 통계 데이터를 만들고, 데이터 분석 작업을 데이터베이스에서 직접 수행할 수 있습니다.

특히 대량 데이터 처리 시 애플리케이션 메모리로 가져와서 처리하는 것보다 훨씬 효율적입니다.

실전 팁

💡 $match와 $sort를 파이프라인 초반에 배치하면 인덱스를 활용할 수 있습니다. $match를 첫 단계로, $sort를 두 번째 단계로 놓으면 최적화됩니다.

💡 allowDiskUse: true 옵션을 사용하면 메모리 제한(100MB)을 초과하는 Aggregation을 실행할 수 있습니다. 대량 데이터 처리 시 필수입니다.

💡 $lookup은 성능 비용이 큽니다. 가능하면 데이터를 비정규화해서 $lookup을 피하세요. 불가피하다면 $match로 먼저 Document 수를 줄인 후 $lookup을 수행하세요.

💡 explain() 메서드를 Aggregation에도 사용할 수 있습니다. aggregate([...], { explain: true })로 실행 계획을 확인하세요.

💡 $facet을 사용하면 하나의 파이프라인에서 여러 집계를 동시에 수행할 수 있습니다. 대시보드에서 여러 통계를 한 번에 가져올 때 유용합니다.


6. 스키마_설계_패턴

시작하며

여러분이 블로그 시스템을 설계한다고 가정해봅시다. 게시글과 댓글을 어떻게 저장하시겠습니까?

관계형 DB처럼 별도 테이블로 나눌까요, 아니면 게시글 Document 안에 댓글을 포함시킬까요? MongoDB에서는 스키마 설계가 성능에 엄청난 영향을 미칩니다.

잘못 설계하면 조인이 많아지거나 Document 크기가 너무 커져서 성능이 급격히 저하됩니다. 관계형 DB의 정규화 원칙과 달리, MongoDB는 상황에 따라 비정규화(denormalization)를 적극 활용합니다.

"읽기 패턴에 맞춰 설계하라"가 MongoDB 스키마 설계의 핵심입니다. 실제 프로젝트에서는 임베딩(embedding)과 참조(referencing)를 적절히 조합해야 합니다.

일대다, 다대다 관계를 어떻게 표현할지 이해하는 것이 MongoDB 마스터의 핵심입니다.

개요

간단히 말해서, MongoDB 스키마 설계는 데이터를 Document 안에 포함시킬지(임베딩), 별도 Document로 분리하고 참조할지(참조)를 결정하는 것입니다. 임베딩 패턴은 관련 데이터를 하나의 Document 안에 중첩해서 저장합니다.

일대소수(one-to-few) 관계에 적합합니다. 예를 들어, 사용자와 주소(최대 2-3개)는 임베딩하는 것이 좋습니다.

장점은 한 번의 쿼리로 모든 데이터를 가져올 수 있다는 것이고, 단점은 Document 크기가 커질 수 있다는 것입니다. 참조 패턴은 관계형 DB의 외래 키처럼 다른 Document의 _id를 저장합니다.

일대다(one-to-many)나 다대다(many-to-many) 관계에 적합합니다. 예를 들어, 블로그 게시글과 댓글(수백 개 이상)은 분리하는 것이 좋습니다.

장점은 Document 크기가 제한되고 데이터 중복이 없다는 것이고, 단점은 여러 쿼리가 필요하거나 $lookup이 필요하다는 것입니다. 확장 참조 패턴(Extended Reference)은 자주 함께 조회되는 필드만 임베딩하고 나머지는 참조합니다.

예를 들어, 댓글에 작성자의 _id와 name만 포함시키고, 전체 프로필은 별도로 저장합니다. 이렇게 하면 조인 없이 기본 정보를 표시하고, 필요할 때만 추가 정보를 가져올 수 있습니다.

버킷 패턴은 시계열 데이터나 로그처럼 계속 증가하는 데이터를 다룰 때 유용합니다. 시간 단위로 데이터를 묶어서 저장합니다.

예를 들어, 센서 데이터를 시간별로 묶으면 Document 수가 줄어들고 쿼리 성능이 향상됩니다.

코드 예제

// 임베딩 패턴: 블로그 포스트 (일대소수 관계)
const blogPost = {
  _id: ObjectId('...'),
  title: 'MongoDB 스키마 설계',
  content: '...',
  author: {
    _id: ObjectId('...'),
    name: '김개발',
    email: 'kim@example.com'
  },
  tags: ['MongoDB', 'Database', 'NoSQL'],
  comments: [  // 댓글이 적을 때는 임베딩
    { user: '이몽고', text: '좋은 글이네요!', date: new Date() },
    { user: '박디비', text: '도움이 되었습니다', date: new Date() }
  ],
  createdAt: new Date()
};

// 참조 패턴: 댓글이 많은 경우 (일대다 관계)
const post = {
  _id: ObjectId('post123'),
  title: 'MongoDB 스키마 설계',
  authorId: ObjectId('user456'),  // 참조
  commentCount: 523  // 카운터 필드로 성능 향상
};

const comment = {
  _id: ObjectId('...'),
  postId: ObjectId('post123'),  // 부모 참조
  userId: ObjectId('user789'),
  userName: '이몽고',  // 확장 참조 패턴
  text: '좋은 글이네요!',
  createdAt: new Date()
};

설명

이것이 하는 일: 블로그 시스템에서 포스트와 댓글의 관계를 임베딩과 참조 패턴으로 각각 표현합니다. 첫 번째 예제(임베딩 패턴)에서는 author, tags, comments를 모두 포스트 Document 안에 포함시킵니다.

이 방식은 댓글이 적을 때(10개 미만) 적합합니다. 한 번의 findOne 쿼리로 포스트와 모든 댓글을 가져올 수 있어 읽기 성능이 뛰어납니다.

하지만 댓글이 계속 증가하면 Document 크기가 16MB 제한에 가까워질 수 있습니다. 두 번째 예제(참조 패턴)에서는 댓글을 별도 Collection으로 분리합니다.

postId로 어떤 포스트의 댓글인지 참조하고, userId로 작성자를 참조합니다. 이 방식은 댓글이 많을 때 적합합니다.

포스트와 댓글을 독립적으로 수정할 수 있고, Document 크기 제한 걱정이 없습니다. 확장 참조 패턴을 주목하세요.

comment Document에 userId뿐만 아니라 userName도 저장했습니다. 이것은 데이터 중복이지만, 댓글 목록을 표시할 때 users Collection을 조회하지 않아도 됩니다.

사용자가 이름을 변경하면 데이터 불일치가 발생할 수 있지만, 이름 변경이 드물다면 읽기 성능 향상이 더 중요합니다. commentCount 필드도 주목하세요.

댓글 수를 표시할 때마다 comments Collection을 COUNT하면 느립니다. 대신 포스트 Document에 카운터를 유지하고, 댓글 추가/삭제 시 $inc로 업데이트하면 훨씬 빠릅니다.

여러분이 이 패턴들을 사용하면 읽기/쓰기 성능을 최적화하고, 데이터 관계를 명확히 표현하고, 확장 가능한 스키마를 설계할 수 있습니다. 실제 프로젝트에서는 데이터 크기, 관계의 카디널리티, 읽기/쓰기 비율을 고려해서 패턴을 선택해야 합니다.

실전 팁

💡 "일대소수는 임베딩, 일대다는 참조, 다대다는 양방향 참조"라는 기본 규칙을 기억하세요. 하지만 항상 읽기 패턴을 먼저 고려하세요.

💡 Document 크기가 커지면 업데이트 성능이 저하됩니다. 자주 변경되는 필드와 드물게 변경되는 필드를 분리하는 것을 고려하세요.

💡 임베딩된 배열의 크기가 수백 개를 넘어가면 참조 패턴으로 전환하세요. $slice 연산자로 배열 일부만 가져올 수 있지만 근본적인 해결책은 아닙니다.

💡 양방향 참조를 사용하면 쿼리가 유연해집니다. 예를 들어, 포스트에서 댓글 목록(postId 인덱스)과 사용자별 댓글 목록(userId 인덱스)을 모두 효율적으로 조회할 수 있습니다.

💡 스키마를 설계할 때 "이 데이터를 어떻게 읽을 것인가?"를 먼저 질문하세요. MongoDB는 쓰기가 아닌 읽기 패턴에 최적화되어야 합니다.


7. 트랜잭션

시작하며

여러분이 은행 계좌 이체 기능을 개발한다고 상상해보세요. A 계좌에서 돈을 빼고, B 계좌에 돈을 넣어야 합니다.

만약 첫 번째 작업은 성공하고 두 번째 작업은 실패한다면? 돈이 사라지는 끔찍한 상황이 벌어집니다.

이런 문제를 해결하는 것이 트랜잭션입니다. 여러 작업을 하나의 원자적(atomic) 단위로 묶어서, 모두 성공하거나 모두 실패하도록 보장합니다.

MongoDB는 4.0 버전부터 다중 Document 트랜잭션을 지원합니다. 그 이전에는 단일 Document 작업만 원자적이었지만, 이제는 여러 Document, 심지어 여러 Collection에 걸친 작업도 트랜잭션으로 보호할 수 있습니다.

하지만 트랜잭션은 성능 비용이 있습니다. 가능하면 스키마 설계로 트랜잭션을 피하는 것이 좋지만, 데이터 무결성이 중요한 경우에는 반드시 사용해야 합니다.

개요

간단히 말해서, 트랜잭션은 여러 데이터베이스 작업을 하나의 단위로 묶어서 ACID 속성을 보장하는 메커니즘입니다. ACID는 Atomicity(원자성), Consistency(일관성), Isolation(격리성), Durability(지속성)를 의미합니다.

원자성은 모두 성공하거나 모두 실패를, 일관성은 데이터 규칙 유지를, 격리성은 동시 트랜잭션 간 간섭 방지를, 지속성은 커밋된 변경의 영구 저장을 보장합니다. MongoDB에서 트랜잭션을 사용하려면 레플리카 셋(replica set)이나 샤드 클러스터가 필요합니다.

단일 MongoDB 인스턴스에서는 트랜잭션을 사용할 수 없습니다. 개발 환경에서는 단일 노드 레플리카 셋을 구성하면 됩니다.

트랜잭션 사용 패턴은 세션을 시작하고, startTransaction으로 트랜잭션을 시작하고, 작업들을 수행하고, 성공하면 commitTransaction, 실패하면 abortTransaction을 호출하는 것입니다. try-catch 블록으로 에러를 처리하는 것이 중요합니다.

단일 Document 작업은 자동으로 원자적입니다. 예를 들어, updateOne에서 $inc로 두 필드를 동시에 업데이트하면 트랜잭션 없이도 원자성이 보장됩니다.

트랜잭션은 여러 Document나 Collection에 걸친 작업에만 필요합니다.

코드 예제

const { MongoClient } = require('mongodb');
const client = new MongoClient('mongodb://localhost:27017/?replicaSet=rs0');

async function transferMoney(fromAccount, toAccount, amount) {
  const session = client.startSession();

  try {
    // 트랜잭션 시작
    session.startTransaction();

    const accounts = client.db('bank').collection('accounts');

    // 출금 계좌에서 금액 차감
    const debitResult = await accounts.updateOne(
      { _id: fromAccount, balance: { $gte: amount } },
      { $inc: { balance: -amount } },
      { session }
    );

    if (debitResult.matchedCount === 0) {
      throw new Error('잔액 부족');
    }

    // 입금 계좌에 금액 추가
    await accounts.updateOne(
      { _id: toAccount },
      { $inc: { balance: amount } },
      { session }
    );

    // 트랜잭션 커밋
    await session.commitTransaction();
    console.log('이체 성공');

  } catch (error) {
    // 에러 발생 시 롤백
    await session.abortTransaction();
    console.error('이체 실패:', error.message);
    throw error;

  } finally {
    // 세션 종료
    await session.endSession();
  }
}

설명

이것이 하는 일: 두 계좌 간 금액 이체를 트랜잭션으로 보호하여, 출금과 입금이 모두 성공하거나 모두 실패하도록 보장합니다. 첫 번째로, startSession()으로 세션을 생성합니다.

MongoDB에서 트랜잭션은 세션 단위로 동작합니다. 이 세션 객체를 모든 데이터베이스 작업에 전달해야 합니다.

두 번째로, startTransaction()으로 트랜잭션을 시작합니다. 이제부터 이 세션에서 수행되는 모든 작업은 트랜잭션에 포함됩니다.

다른 세션에서는 커밋되기 전까지 변경 사항을 볼 수 없습니다. 세 번째로, 출금 작업을 수행합니다.

중요한 점은 { balance: { $gte: amount } } 조건으로 잔액이 충분한지 확인한다는 것입니다. matchedCount가 0이면 잔액 부족이므로 에러를 던집니다.

{ session } 옵션으로 이 작업이 트랜잭션에 포함되도록 합니다. 네 번째로, 입금 작업을 수행합니다.

이것도 같은 세션을 사용해서 트랜잭션에 포함시킵니다. 만약 이 단계에서 에러가 발생하면 catch 블록으로 이동합니다.

다섯 번째로, 모든 작업이 성공하면 commitTransaction()으로 트랜잭션을 커밋합니다. 이제 변경 사항이 영구적으로 저장되고 다른 세션에서도 볼 수 있습니다.

에러가 발생하면 abortTransaction()으로 모든 변경을 롤백합니다. 여러분이 이 코드를 사용하면 금융 거래, 재고 관리, 예약 시스템처럼 데이터 일관성이 중요한 애플리케이션을 안전하게 구현할 수 있습니다.

트랜잭션은 복잡한 비즈니스 로직의 무결성을 보장하는 강력한 도구입니다.

실전 팁

💡 트랜잭션은 60초 타임아웃이 기본입니다. 장시간 작업은 피하고, 필요하면 transactionLifetimeLimitSeconds를 조정하세요.

💡 가능하면 스키마를 설계해서 트랜잭션을 피하세요. 임베딩 패턴으로 관련 데이터를 하나의 Document에 넣으면 단일 Document 작업만으로 원자성이 보장됩니다.

💡 트랜잭션 중에는 DDL 작업(Collection 생성 등)을 할 수 없습니다. 필요한 Collection은 트랜잭션 밖에서 미리 생성하세요.

💡 읽기 전용 작업에는 트랜잭션이 불필요합니다. 트랜잭션은 쓰기 작업의 일관성을 보장하는 것이므로, 읽기만 하는 경우 오버헤드만 추가됩니다.

💡 재시도 로직을 구현하세요. 동시성 충돌로 트랜잭션이 실패할 수 있으므로, 일시적 에러(TransientTransactionError)는 재시도해야 합니다.


8. 레플리케이션

시작하며

여러분의 서비스가 성장해서 하루에 수백만 명이 접속한다고 가정해봅시다. 단일 MongoDB 서버로는 감당할 수 없고, 서버가 다운되면 서비스 전체가 멈춥니다.

어떻게 해야 할까요? 레플리케이션(Replication)은 MongoDB의 고가용성(High Availability)과 읽기 확장성(Read Scalability)을 제공하는 핵심 기능입니다.

데이터를 여러 서버에 복제해서 장애 상황에서도 서비스를 유지합니다. MongoDB의 레플리카 셋(Replica Set)은 Primary 노드 하나와 여러 Secondary 노드들로 구성됩니다.

Primary는 모든 쓰기를 처리하고, Secondary는 Primary의 데이터를 복제합니다. 실제 프로덕션 환경에서는 레플리카 셋 없이 MongoDB를 운영하는 것은 상상할 수 없습니다.

데이터 안전성과 서비스 가용성을 위해 필수적입니다.

개요

간단히 말해서, 레플리케이션은 같은 데이터를 여러 MongoDB 서버에 복제해서 저장하는 것입니다. 레플리카 셋은 최소 3개의 노드로 구성하는 것이 권장됩니다.

Primary 1개, Secondary 2개 구성이 일반적입니다. 홀수 개의 노드를 사용하는 이유는 Primary 선출 시 과반수 투표가 필요하기 때문입니다.

Primary 노드는 모든 쓰기 작업을 처리하고, oplog(operation log)에 기록합니다. Secondary 노드들은 이 oplog를 복제해서 동일한 작업을 수행합니다.

이 과정을 비동기 복제라고 하며, 약간의 지연(lag)이 발생할 수 있습니다. Primary가 다운되면 Secondary들이 자동으로 선거를 진행해서 새로운 Primary를 선출합니다.

이 과정은 보통 10-30초 정도 걸리며, 이 시간 동안 쓰기가 불가능합니다. 하지만 애플리케이션은 자동으로 새 Primary에 연결됩니다.

Read Preference를 설정하면 읽기 작업을 Secondary로 분산시킬 수 있습니다. primary(기본값)는 모든 읽기를 Primary에서, secondary는 Secondary에서만, primaryPreferred는 Primary 우선이지만 불가능하면 Secondary에서 읽습니다.

이를 통해 읽기 성능을 향상시킬 수 있습니다. Write Concern과 Read Concern으로 데이터 일관성 수준을 제어할 수 있습니다.

{ w: 'majority' }는 과반수 노드에 복제될 때까지 기다리고, { w: 1 }은 Primary에만 쓰면 즉시 반환합니다. 일관성과 성능 사이의 트레이드오프입니다.

코드 예제

const { MongoClient } = require('mongodb');

// 레플리카 셋 연결 문자열
const uri = 'mongodb://node1:27017,node2:27017,node3:27017/?replicaSet=rs0';
const client = new MongoClient(uri);

async function useReplicaSet() {
  await client.connect();
  const db = client.db('myapp');
  const users = db.collection('users');

  // Write Concern: 과반수 노드에 복제되면 완료
  await users.insertOne(
    { name: '김개발', email: 'kim@example.com' },
    { writeConcern: { w: 'majority', wtimeout: 5000 } }
  );

  // Read Preference: Secondary에서 읽기 (읽기 확장)
  const secondaryUsers = await users.find()
    .readPref('secondary')
    .toArray();

  // Primary에서 읽기 (강한 일관성)
  const primaryUser = await users.findOne(
    { email: 'kim@example.com' },
    { readPreference: 'primary' }
  );

  // Read Concern: 과반수 노드에서 확인된 데이터만 읽기
  const confirmedUsers = await users.find()
    .readConcern('majority')
    .toArray();
}

설명

이것이 하는 일: 레플리카 셋에 연결하고, Write Concern과 Read Preference를 설정해서 데이터 일관성과 성능을 제어합니다. 첫 번째로, 연결 문자열에 여러 노드 주소를 나열하고 replicaSet 파라미터를 지정합니다.

MongoClient는 자동으로 현재 Primary를 찾아서 연결하고, Primary가 변경되면 자동으로 새 Primary로 재연결합니다. 두 번째로, insertOne에 writeConcern 옵션을 지정합니다.

{ w: 'majority' }는 과반수(3개 중 2개) 노드에 복제될 때까지 기다립니다. wtimeout은 타임아웃 시간으로, 5초 안에 복제가 완료되지 않으면 에러를 반환합니다.

이렇게 하면 데이터 손실 가능성이 크게 줄어들지만 쓰기 지연이 증가합니다. 세 번째로, readPref('secondary')로 Secondary 노드에서 읽기를 수행합니다.

이렇게 하면 Primary의 부하를 줄이고 읽기 성능을 향상시킬 수 있습니다. 하지만 복제 지연으로 인해 최신 데이터가 아닐 수 있습니다.

통계나 분석 쿼리처럼 약간의 지연이 허용되는 경우에 적합합니다. 네 번째로, readPreference: 'primary'로 Primary에서 직접 읽습니다.

이것은 기본값이며, 항상 최신 데이터를 보장합니다. 금융 거래처럼 강한 일관성이 필요한 경우 사용합니다.

다섯 번째로, readConcern('majority')로 과반수 노드에서 확인된 데이터만 읽습니다. 이렇게 하면 롤백될 가능성이 있는 데이터를 읽지 않게 됩니다.

최고 수준의 일관성을 원할 때 writeConcern과 readConcern을 모두 'majority'로 설정합니다. 여러분이 이 코드를 사용하면 서비스의 가용성을 크게 높이고, 읽기 성능을 확장하고, 데이터 손실을 방지하고, 일관성 수준을 세밀하게 제어할 수 있습니다.

프로덕션 환경에서는 반드시 레플리카 셋을 구성하고 적절한 Write Concern과 Read Preference를 설정해야 합니다.

실전 팁

💡 개발 환경에서도 레플리카 셋을 사용하세요. Docker Compose로 단일 노드 레플리카 셋을 쉽게 구성할 수 있고, 트랜잭션도 테스트할 수 있습니다.

💡 레플리카 셋은 홀수 개(3, 5, 7)의 노드로 구성하세요. 짝수 개는 split-brain 문제를 일으킬 수 있습니다. 비용 때문에 노드를 줄이려면 Arbiter를 사용하세요.

💡 Secondary의 복제 지연(replication lag)을 모니터링하세요. rs.printReplicationInfo()와 rs.printSlaveReplicationInfo()로 확인할 수 있습니다. 지연이 크면 Secondary 읽기가 부정확해집니다.

💡 Write Concern { w: 'majority' }와 Read Concern 'majority'를 함께 사용하면 인과적 일관성(causal consistency)을 보장받을 수 있습니다. 중요한 데이터는 이 조합을 사용하세요.

💡 지역별로 노드를 분산 배치하면 재해 복구(disaster recovery)가 가능합니다. 하지만 네트워크 지연으로 복제 지연이 증가할 수 있으므로 트레이드오프를 고려하세요.


#MongoDB#Database#NoSQL#CRUD#Indexing#JavaScript