본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 11. 27. · 0 Views
MongoDB CRUD 기본 연산 완벽 가이드
MongoDB에서 데이터를 생성, 조회, 수정, 삭제하는 기본 연산을 배웁니다. 실무에서 바로 활용할 수 있는 CRUD 패턴과 대량 처리 기법까지 다룹니다.
목차
- insertOne과 insertMany
- find와 findOne 조회
- updateOne과 updateMany
- deleteOne과 deleteMany
- replaceOne 사용법
- bulkWrite로 대량 처리
1. insertOne과 insertMany
김개발 씨가 처음으로 MongoDB 프로젝트에 투입되었습니다. 첫 번째 미션은 회원 가입 기능을 만드는 것이었습니다.
"일단 데이터를 넣어야 하는데, 어떻게 시작하지?" MySQL만 써봤던 김개발 씨는 잠시 멈칫했습니다.
insertOne은 하나의 문서를 컬렉션에 추가하고, insertMany는 여러 문서를 한 번에 추가합니다. 마치 도서관에 책을 한 권씩 등록하거나, 새로 들어온 책 더미를 한꺼번에 등록하는 것과 같습니다.
이 두 메서드를 이해하면 데이터 입력의 기초를 완전히 다질 수 있습니다.
다음 코드를 살펴봅시다.
// 단일 문서 삽입 - 회원 한 명 등록
const result = await db.collection('users').insertOne({
name: '김개발',
email: 'kim@example.com',
age: 28,
createdAt: new Date()
});
console.log('삽입된 문서 ID:', result.insertedId);
// 여러 문서 한 번에 삽입 - 초기 데이터 세팅
const bulkResult = await db.collection('users').insertMany([
{ name: '이디자인', email: 'lee@example.com', age: 25 },
{ name: '박기획', email: 'park@example.com', age: 32 },
{ name: '최마케팅', email: 'choi@example.com', age: 29 }
]);
console.log('삽입된 문서 수:', bulkResult.insertedCount);
김개발 씨는 입사 첫 주에 MongoDB를 처음 접했습니다. 그동안 MySQL에서 INSERT INTO 문법만 써왔던 터라, NoSQL의 문서 기반 구조가 낯설게 느껴졌습니다.
선배 개발자 박시니어 씨가 옆자리에서 말했습니다. "MongoDB는 JSON 형태로 데이터를 저장해.
테이블 대신 컬렉션, 행 대신 문서라고 부르지. 일단 데이터 넣는 것부터 해볼까?" 그렇다면 MongoDB에서 데이터를 삽입한다는 것은 정확히 무엇일까요?
쉽게 비유하자면, insertOne은 마치 우체통에 편지 한 통을 넣는 것과 같습니다. 편지를 넣으면 우체통이 "잘 받았어요"라고 영수증을 주듯이, MongoDB도 삽입된 문서의 고유 ID를 돌려줍니다.
반면 insertMany는 편지 묶음을 한꺼번에 넣는 것과 같습니다. MongoDB 이전에는 어땠을까요?
관계형 데이터베이스에서는 먼저 테이블 스키마를 정의해야 했습니다. 컬럼 이름, 데이터 타입, 제약 조건을 미리 설계하고 나서야 데이터를 넣을 수 있었습니다.
하지만 MongoDB는 다릅니다. 스키마를 미리 정의하지 않아도 됩니다.
문서 형태로 원하는 데이터를 바로 넣을 수 있습니다. 이것이 바로 유연한 스키마의 장점입니다.
위의 코드를 살펴보겠습니다. 먼저 insertOne 호출 시 객체 하나를 전달합니다.
name, email, age 같은 필드를 원하는 대로 정의했습니다. 반환되는 result 객체에서 insertedId를 통해 방금 생성된 문서의 고유 식별자를 확인할 수 있습니다.
insertMany는 배열을 인자로 받습니다. 배열 안에 여러 객체를 담아 전달하면, MongoDB가 한 번의 네트워크 요청으로 모두 처리합니다.
반환되는 insertedCount로 실제 삽입된 문서 수를 확인할 수 있습니다. 실제 현업에서는 어떻게 활용할까요?
회원 가입 기능에서는 insertOne을 사용합니다. 반면 서비스 초기 데이터 마이그레이션이나 CSV 파일 임포트 같은 작업에서는 insertMany가 훨씬 효율적입니다.
주의할 점도 있습니다. insertMany 사용 시 중간에 하나의 문서가 실패하면 어떻게 될까요?
기본적으로 실패한 지점에서 멈추고, 이미 삽입된 문서는 롤백되지 않습니다. 이를 제어하려면 ordered: false 옵션을 사용하면 됩니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 바로 회원 가입 API를 완성했습니다.
"생각보다 간단하네요!" 데이터 삽입은 CRUD의 첫 관문입니다. 이 기초를 탄탄히 다져두면 나머지 연산도 수월하게 익힐 수 있습니다.
실전 팁
💡 - _id 필드를 직접 지정하지 않으면 MongoDB가 자동으로 ObjectId를 생성합니다
- 대량 삽입 시 ordered: false 옵션으로 일부 실패해도 나머지를 계속 처리할 수 있습니다
- 삽입 전 데이터 유효성 검사는 애플리케이션 레벨에서 처리하는 것이 좋습니다
2. find와 findOne 조회
회원 가입 기능을 완성한 김개발 씨에게 새로운 미션이 주어졌습니다. 이번에는 회원 목록 조회와 상세 정보 페이지를 만드는 것이었습니다.
"데이터를 넣었으니, 이제 꺼내봐야겠지?" 김개발 씨는 조회 메서드를 찾아보기 시작했습니다.
find는 조건에 맞는 여러 문서를 조회하고, findOne은 조건에 맞는 첫 번째 문서 하나만 반환합니다. 마치 도서관에서 "컴퓨터 관련 책 전부 보여주세요"라고 하면 여러 권을 가져오고, "가장 최신 컴퓨터 책 한 권만 주세요"라고 하면 한 권만 가져오는 것과 같습니다.
다음 코드를 살펴봅시다.
// 단일 문서 조회 - 특정 사용자 찾기
const user = await db.collection('users').findOne({
email: 'kim@example.com'
});
console.log('찾은 사용자:', user.name);
// 여러 문서 조회 - 조건에 맞는 사용자들
const youngUsers = await db.collection('users').find({
age: { $lt: 30 } // 30세 미만
}).toArray();
console.log('30세 미만 사용자 수:', youngUsers.length);
// 프로젝션으로 필요한 필드만 선택
const userNames = await db.collection('users').find(
{}, // 모든 문서
{ projection: { name: 1, email: 1, _id: 0 } }
).toArray();
김개발 씨는 관리자 페이지의 회원 목록을 구현해야 했습니다. 전체 회원을 보여주는 테이블과, 클릭하면 상세 정보를 보여주는 모달이 필요했습니다.
박시니어 씨가 힌트를 주었습니다. "목록은 find, 상세는 findOne을 쓰면 돼.
그런데 find의 결과는 배열이 아니라 커서라는 걸 기억해." 그렇다면 커서란 무엇일까요? 쉽게 비유하자면, 커서는 마치 뷔페 레스토랑의 줄과 같습니다.
음식이 한꺼번에 테이블에 올라오는 것이 아니라, 줄을 서서 하나씩 접시에 담아가는 것입니다. 데이터가 많을 때 메모리를 효율적으로 사용할 수 있게 해줍니다.
만약 커서 없이 모든 데이터를 한 번에 가져온다면 어떻게 될까요? 100만 건의 문서가 있다면, 서버 메모리가 터질 수 있습니다.
커서를 사용하면 필요한 만큼만 조금씩 가져올 수 있습니다. 코드를 자세히 살펴보겠습니다.
findOne은 조건 객체를 받아 일치하는 첫 번째 문서를 바로 반환합니다. 결과가 없으면 null을 반환하므로, 반드시 null 체크를 해야 합니다.
find 메서드는 커서를 반환합니다. 작은 데이터셋에서는 **toArray()**로 배열로 변환해서 사용하면 편리합니다.
하지만 대용량 데이터에서는 forEach나 next 메서드로 하나씩 처리하는 것이 좋습니다. $lt는 MongoDB의 비교 연산자입니다.
less than의 약자로, 해당 값보다 작은 문서를 찾습니다. 이외에도 $gt(초과), $lte(이하), $gte(이상), $eq(같음), $ne(다름) 등이 있습니다.
프로젝션은 SQL의 SELECT 절과 비슷합니다. 필요한 필드만 선택하면 네트워크 전송량과 메모리 사용량을 줄일 수 있습니다.
1은 포함, 0은 제외를 의미합니다. 실제 서비스에서는 페이지네이션이 필수입니다.
skip과 limit 메서드를 체이닝해서 사용합니다. 예를 들어 find().skip(20).limit(10)은 21번째부터 10개를 가져옵니다.
주의할 점이 있습니다. 빈 객체 {}를 조건으로 전달하면 모든 문서를 조회합니다.
프로덕션 환경에서 실수로 전체 조회를 하면 서버에 큰 부담이 될 수 있습니다. 김개발 씨는 회원 목록에 find를, 상세 모달에 findOne을 적용했습니다.
그리고 프로젝션으로 목록에서는 이름과 이메일만, 상세에서는 모든 정보를 보여주도록 구현했습니다.
실전 팁
💡 - findOne 결과는 반드시 null 체크를 하세요
- 대량 데이터 조회 시 toArray() 대신 커서를 직접 순회하세요
- 인덱스가 없는 필드로 조회하면 성능이 크게 저하될 수 있습니다
3. updateOne과 updateMany
어느 날 기획팀에서 긴급 요청이 들어왔습니다. "일부 회원의 등급을 VIP로 변경해야 해요." 김개발 씨는 데이터를 수정하는 방법을 찾아야 했습니다.
삽입과 조회는 해봤지만, 수정은 처음이었습니다.
updateOne은 조건에 맞는 첫 번째 문서를 수정하고, updateMany는 조건에 맞는 모든 문서를 수정합니다. 마치 주소록에서 한 사람의 전화번호를 고치거나, 특정 지역 사람들의 지역번호를 일괄 변경하는 것과 같습니다.
$set 연산자로 필드를 업데이트합니다.
다음 코드를 살펴봅시다.
// 단일 문서 수정 - 특정 사용자 정보 변경
const updateResult = await db.collection('users').updateOne(
{ email: 'kim@example.com' }, // 조건
{
$set: { grade: 'VIP', updatedAt: new Date() },
$inc: { loginCount: 1 } // 숫자 1 증가
}
);
console.log('수정된 문서 수:', updateResult.modifiedCount);
// 여러 문서 일괄 수정 - 조건에 맞는 모든 사용자
const bulkUpdate = await db.collection('users').updateMany(
{ grade: { $exists: false } }, // grade 필드가 없는 문서
{ $set: { grade: 'NORMAL' } } // 기본 등급 설정
);
console.log('일괄 수정된 문서 수:', bulkUpdate.modifiedCount);
김개발 씨는 기획팀의 요청을 받고 고민에 빠졌습니다. 단순히 문서를 통째로 덮어쓰면 될까요?
아니면 특정 필드만 바꿀 수 있을까요? 박시니어 씨가 조언했습니다.
"MongoDB의 업데이트는 연산자를 사용해. 그냥 객체를 전달하면 문서 전체가 교체되어 버리니까 조심해야 해." 그렇다면 업데이트 연산자란 무엇일까요?
쉽게 비유하자면, 집을 리모델링한다고 생각해보세요. 화장실만 고치고 싶은데 집 전체를 허물고 새로 짓는 것은 비효율적입니다.
$set 연산자는 원하는 부분만 정확히 수정해주는 정밀한 도구입니다. 업데이트 연산자 없이 그냥 객체를 전달하면 어떻게 될까요?
기존 문서의 모든 필드가 사라지고, 새로 전달한 객체로 완전히 교체됩니다. 이것은 의도치 않은 데이터 손실로 이어질 수 있습니다.
코드를 분석해보겠습니다. updateOne의 첫 번째 인자는 조건입니다.
find와 동일한 문법을 사용합니다. 두 번째 인자에서 $set 연산자로 grade 필드를 'VIP'로, updatedAt을 현재 시간으로 설정합니다.
$inc 연산자는 숫자를 증감합니다. loginCount: 1은 기존 값에 1을 더합니다.
음수를 전달하면 감소합니다. 조회수, 좋아요 수 등을 업데이트할 때 유용합니다.
$exists 연산자는 필드의 존재 여부를 검사합니다. 새로운 grade 필드를 추가했는데 기존 문서에는 없다면, 이 연산자로 일괄 초기화할 수 있습니다.
반환 객체의 matchedCount와 modifiedCount의 차이도 알아두세요. matchedCount는 조건에 맞는 문서 수, modifiedCount는 실제로 값이 변경된 문서 수입니다.
이미 같은 값이면 수정되지 않습니다. 실무에서는 $set 외에도 다양한 연산자를 사용합니다.
$unset은 필드를 삭제하고, $push는 배열에 요소를 추가하고, $pull은 배열에서 요소를 제거합니다. 주의할 점이 있습니다.
updateMany로 대량 수정 시, 조건을 잘못 설정하면 예상치 못한 문서까지 수정될 수 있습니다. 먼저 find로 대상을 확인한 후 updateMany를 실행하는 것이 안전합니다.
김개발 씨는 updateMany로 등급이 없는 모든 회원에게 기본 등급을 부여한 후, 특정 조건의 회원들에게 VIP 등급을 적용했습니다. "연산자 개념을 알고 나니 어렵지 않네요!"
실전 팁
💡 - 업데이트 전 find로 대상 문서를 먼저 확인하세요
- $set 없이 객체만 전달하면 문서 전체가 교체됩니다
- upsert: true 옵션으로 문서가 없으면 생성할 수 있습니다
4. deleteOne과 deleteMany
서비스 운영 중 GDPR 규정에 따른 회원 탈퇴 요청이 들어왔습니다. 개인정보를 완전히 삭제해야 하는 상황이었습니다.
김개발 씨는 삭제 기능을 구현해야 했지만, 실수로 다른 데이터까지 지워버릴까 봐 긴장되었습니다.
deleteOne은 조건에 맞는 첫 번째 문서를 삭제하고, deleteMany는 조건에 맞는 모든 문서를 삭제합니다. 마치 서류함에서 특정 서류 한 장을 빼거나, 오래된 서류 전체를 폐기하는 것과 같습니다.
삭제는 되돌릴 수 없으므로 신중하게 사용해야 합니다.
다음 코드를 살펴봅시다.
// 단일 문서 삭제 - 특정 사용자 탈퇴 처리
const deleteResult = await db.collection('users').deleteOne({
email: 'kim@example.com'
});
console.log('삭제된 문서 수:', deleteResult.deletedCount);
// 여러 문서 일괄 삭제 - 비활성 사용자 정리
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const bulkDelete = await db.collection('sessions').deleteMany({
lastActivity: { $lt: thirtyDaysAgo } // 30일 이상 비활성
});
console.log('만료 세션 삭제:', bulkDelete.deletedCount);
// 삭제 전 확인 패턴
const toDelete = await db.collection('users').find({ status: 'deleted' }).toArray();
console.log('삭제 대상:', toDelete.length, '건');
김개발 씨는 삭제 기능 앞에서 손이 떨렸습니다. 실수로 잘못된 조건을 넣으면 중요한 데이터가 영원히 사라질 수 있기 때문입니다.
박시니어 씨가 말했습니다. "삭제는 정말 조심해야 해.
프로덕션 DB에서 deleteMany를 빈 조건으로 실행하면 컬렉션 전체가 날아가. 항상 먼저 find로 확인하는 습관을 들여." 왜 삭제가 이토록 위험할까요?
쉽게 비유하자면, 삭제는 마치 문서 분쇄기와 같습니다. 한번 분쇄된 서류는 복원할 수 없습니다.
INSERT나 UPDATE는 실수해도 다시 고칠 수 있지만, DELETE는 되돌리기가 극히 어렵습니다. 실무에서는 이 위험을 줄이기 위한 패턴들이 있습니다.
첫 번째는 소프트 삭제입니다. 실제로 삭제하지 않고 deleted: true 같은 플래그를 설정하는 방식입니다.
나중에 복구가 필요하면 플래그만 바꾸면 됩니다. 두 번째는 삭제 전 확인 패턴입니다.
위 코드의 마지막 예제처럼, 동일한 조건으로 먼저 find를 실행해서 대상을 확인합니다. 예상한 문서가 맞는지 검토한 후에 delete를 실행합니다.
코드를 살펴보겠습니다. deleteOne은 조건에 맞는 첫 번째 문서만 삭제합니다.
만약 동일한 이메일을 가진 문서가 여러 개 있어도 하나만 지워집니다. deletedCount로 실제 삭제된 문서 수를 확인할 수 있습니다.
deleteMany는 조건에 맞는 모든 문서를 삭제합니다. 위 예제에서는 30일 이상 활동이 없는 세션을 정리합니다.
이런 정기적인 데이터 정리 작업에 deleteMany가 적합합니다. 날짜 비교 시 주의점이 있습니다.
JavaScript의 Date 객체와 MongoDB의 날짜 타입은 호환됩니다. 하지만 문자열로 저장된 날짜와는 직접 비교할 수 없습니다.
데이터 설계 시 날짜는 반드시 Date 타입으로 저장하세요. 빈 조건 {} 금지는 필수 규칙입니다.
deleteMany({})는 컬렉션의 모든 문서를 삭제합니다. 이런 실수를 방지하기 위해 삭제 함수에 빈 조건 체크 로직을 추가하는 것이 좋습니다.
백업의 중요성도 강조해야 합니다. 아무리 조심해도 실수는 발생합니다.
정기적인 백업과 복구 테스트를 해두면 최악의 상황에서도 데이터를 살릴 수 있습니다. 김개발 씨는 삭제 API에 이중 확인 로직을 추가했습니다.
먼저 find로 대상을 조회하고, 프론트엔드에서 한번 더 확인을 받은 후에야 실제 삭제를 실행하도록 했습니다.
실전 팁
💡 - 프로덕션 환경에서는 소프트 삭제 패턴을 고려하세요
- 삭제 전 반드시 find로 대상을 확인하는 습관을 들이세요
- 빈 조건 {}으로 deleteMany를 호출하면 전체 문서가 삭제됩니다
5. replaceOne 사용법
김개발 씨가 회원 정보 수정 API를 만들던 중, 이상한 현상을 발견했습니다. updateOne으로 수정했더니 기존 필드는 그대로인데, 새 필드만 추가되었습니다.
"문서 전체를 새로운 내용으로 바꾸고 싶은데..." 이럴 때 필요한 것이 바로 replaceOne입니다.
replaceOne은 조건에 맞는 문서를 새로운 문서로 완전히 교체합니다. updateOne이 부분 수정이라면, replaceOne은 문서 전체를 덮어씁니다.
마치 낡은 가구를 수리하는 것과 새 가구로 교체하는 것의 차이와 같습니다. _id 필드만 유지되고 나머지는 모두 새 내용으로 바뀝니다.
다음 코드를 살펴봅시다.
// 기존 문서 조회
const existingUser = await db.collection('users').findOne({
_id: new ObjectId('507f1f77bcf86cd799439011')
});
// 문서 전체 교체 - 새로운 구조로 완전히 변경
const replaceResult = await db.collection('users').replaceOne(
{ _id: existingUser._id },
{
// _id는 자동 유지됨
fullName: '김개발', // name에서 fullName으로 변경
contact: { // 구조 변경
email: 'kim@example.com',
phone: '010-1234-5678'
},
profile: { age: 28, department: '개발팀' },
migratedAt: new Date()
}
);
console.log('교체 성공:', replaceResult.modifiedCount === 1);
김개발 씨는 서비스가 성장하면서 회원 데이터 구조를 바꿔야 하는 상황에 처했습니다. 기존에는 name과 email이 평면적으로 저장되어 있었는데, 이제는 중첩된 구조로 변경해야 했습니다.
박시니어 씨가 설명했습니다. "updateOne의 $set으로는 기존 필드를 유지하면서 새 필드를 추가해.
하지만 replaceOne은 문서 자체를 통째로 바꿔버려. 용도가 다르다고 보면 돼." 그렇다면 언제 replaceOne을 사용해야 할까요?
쉽게 비유하자면, updateOne은 자동차 부품을 하나씩 교체하는 정비와 같고, replaceOne은 자동차 전체를 새 차로 바꾸는 것과 같습니다. 부품 몇 개만 고칠 때는 정비가 효율적이지만, 모델 자체가 바뀔 때는 차를 바꾸는 게 낫습니다.
코드를 살펴보겠습니다. replaceOne의 첫 번째 인자는 조건입니다.
두 번째 인자가 중요한데, 여기에는 업데이트 연산자 없이 순수한 객체를 전달합니다. 이 객체가 기존 문서를 완전히 대체합니다.
주목할 점은 _id 필드입니다. 새 객체에 _id를 포함하지 않아도 기존 _id가 유지됩니다.
만약 다른 _id를 지정하면 에러가 발생합니다. 문서의 정체성은 _id로 결정되기 때문입니다.
실무에서 replaceOne은 주로 스키마 마이그레이션에 사용됩니다. 데이터 구조가 크게 변경될 때, 기존 문서를 새 구조로 완전히 교체해야 합니다.
위 예제처럼 name을 fullName으로, email을 contact.email로 재구조화하는 경우입니다. 또 다른 활용 사례는 전체 수정 API입니다.
클라이언트에서 수정된 전체 객체를 보내오면, 서버에서는 이를 replaceOne으로 저장합니다. 부분 수정이 아닌 전체 교체가 비즈니스 로직인 경우에 적합합니다.
주의할 점이 있습니다. replaceOne은 문서 전체를 바꾸므로, 실수로 필드를 누락하면 데이터가 손실됩니다.
예를 들어 createdAt 필드를 새 객체에 포함하지 않으면 사라집니다. 기존 문서를 먼저 조회해서 필요한 필드를 유지하는 것이 좋습니다.
upsert 옵션도 replaceOne에서 유용합니다. 조건에 맞는 문서가 없으면 새로 생성합니다.
마이그레이션 스크립트에서 "있으면 교체, 없으면 생성" 로직에 활용됩니다. 김개발 씨는 데이터 마이그레이션 스크립트를 작성했습니다.
기존 문서를 조회하고, 새 구조로 변환한 후, replaceOne으로 교체하는 방식이었습니다. 배치 작업으로 전체 회원 데이터를 성공적으로 마이그레이션했습니다.
실전 팁
💡 - replaceOne의 두 번째 인자에는 $set 같은 연산자를 사용하지 마세요
- 기존 문서를 먼저 조회해서 유지할 필드를 확인하세요
- 대량 마이그레이션 시 bulkWrite와 함께 사용하면 효율적입니다
6. bulkWrite로 대량 처리
서비스가 급성장하면서 매일 밤 수십만 건의 데이터를 처리해야 하는 배치 작업이 생겼습니다. 김개발 씨가 for 문으로 하나씩 처리하니 몇 시간이 걸렸습니다.
"이렇게 느려서야..." 효율적인 대량 처리 방법이 절실했습니다.
bulkWrite는 여러 쓰기 작업을 하나의 요청으로 묶어서 실행합니다. insertOne, updateOne, deleteOne 등을 배열로 전달하면 MongoDB가 최적화된 방식으로 처리합니다.
마치 택배를 하나씩 보내는 것보다 한 트럭에 모아서 보내는 것이 효율적인 것과 같습니다.
다음 코드를 살펴봅시다.
// 다양한 작업을 한 번에 처리
const operations = [
// 새 문서 삽입
{ insertOne: { document: { name: '신입사원', email: 'new@example.com' } } },
// 기존 문서 수정
{ updateOne: {
filter: { email: 'kim@example.com' },
update: { $set: { lastLogin: new Date() } }
}},
// 여러 문서 수정
{ updateMany: {
filter: { status: 'pending' },
update: { $set: { status: 'processed' } }
}},
// 문서 삭제
{ deleteOne: { filter: { status: 'deleted' } } }
];
const bulkResult = await db.collection('users').bulkWrite(operations, {
ordered: false // 순서 무관하게 병렬 처리
});
console.log('삽입:', bulkResult.insertedCount);
console.log('수정:', bulkResult.modifiedCount);
console.log('삭제:', bulkResult.deletedCount);
김개발 씨는 매일 밤 실행되는 배치 작업 때문에 골머리를 앓았습니다. 10만 건의 데이터를 처리하는데 3시간이 넘게 걸렸고, 새벽에 끝나지 않으면 서비스에 영향을 주었습니다.
박시니어 씨가 코드를 보더니 말했습니다. "아, 이게 문제구나.
건별로 DB에 요청을 보내고 있잖아. 10만 번의 네트워크 왕복이 일어나는 거야.
bulkWrite를 쓰면 획기적으로 줄어들 거야." 왜 네트워크 왕복이 문제가 될까요? 쉽게 비유하자면, 편의점에서 물건을 10개 사야 하는데 하나 사고 집에 갔다가, 또 하나 사러 오고, 또 집에 갔다가...
이러면 시간이 엄청 걸리겠죠? 한 번에 10개를 담아서 계산하는 것이 훨씬 효율적입니다.
bulkWrite가 바로 그 역할을 합니다. 코드를 분석해보겠습니다.
operations 배열에 다양한 작업을 담습니다. 각 작업은 객체 형태로, 작업 유형(insertOne, updateOne, deleteOne 등)을 키로, 세부 내용을 값으로 가집니다.
insertOne 작업은 document 속성에 삽입할 문서를 담습니다. updateOne과 updateMany는 filter와 update 속성을 가집니다.
deleteOne과 deleteMany는 filter 속성만 있으면 됩니다. ordered: false 옵션이 중요합니다.
기본값은 true로, 배열 순서대로 실행하다가 하나라도 실패하면 멈춥니다. false로 설정하면 순서에 상관없이 병렬로 처리하고, 실패한 작업이 있어도 나머지를 계속 진행합니다.
반환 객체에서 각 작업 유형별 결과를 확인할 수 있습니다. insertedCount, matchedCount, modifiedCount, deletedCount, upsertedCount 등으로 세부 결과를 파악합니다.
실무에서 bulkWrite는 다양한 상황에 활용됩니다. 첫 번째는 배치 처리입니다.
로그 분석, 통계 집계, 데이터 마이그레이션 등 대량 데이터를 다루는 작업에 필수입니다. 두 번째는 복합 트랜잭션입니다.
여러 문서를 한 번에 수정해야 하는 비즈니스 로직에서, 부분 실패 시 전체 롤백이 필요하다면 ordered: true와 함께 사용합니다. 성능 향상 폭은 상황에 따라 다르지만, 일반적으로 10배 이상 빨라집니다.
네트워크 레이턴시가 클수록 효과가 큽니다. 원격 DB에 연결하는 경우 특히 체감됩니다.
주의할 점도 있습니다. 너무 많은 작업을 한 번에 담으면 메모리 문제가 생길 수 있습니다.
일반적으로 1000~5000개 단위로 나눠서 처리하는 것이 좋습니다. 김개발 씨는 배치 작업을 bulkWrite로 리팩토링했습니다.
3시간 걸리던 작업이 15분으로 줄었습니다. "이렇게 차이가 날 줄이야!" 팀원들도 놀라워했습니다.
실전 팁
💡 - ordered: false로 병렬 처리하면 성능이 더 좋아집니다
- 1000~5000개 단위로 배치를 나눠서 메모리를 관리하세요
- 에러 발생 시 result.getWriteErrors()로 실패한 작업을 확인할 수 있습니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
MongoDB 집계 파이프라인 고급 완벽 가이드
MongoDB의 고급 집계 파이프라인 기능을 초급 개발자도 쉽게 이해할 수 있도록 설명합니다. $lookup, $unwind, $facet, $bucket, $graphLookup 등 실무에서 자주 사용하는 연산자들을 실제 예제와 함께 다룹니다.
MongoDB 집계 파이프라인 기초
MongoDB의 집계 파이프라인을 처음 접하는 개발자를 위한 가이드입니다. 데이터를 필터링하고, 그룹화하고, 변환하는 방법을 실무 예제와 함께 차근차근 알아봅니다.
MongoDB 인덱스 기초 완벽 가이드
MongoDB에서 쿼리 성능을 획기적으로 개선하는 인덱스의 모든 것을 다룹니다. 단일 필드 인덱스부터 복합 인덱스, 그리고 실무에서 반드시 알아야 할 인덱스 관리 방법까지 초급 개발자도 쉽게 이해할 수 있도록 설명합니다.
MongoDB 업데이트 연산자 완벽 가이드
MongoDB에서 문서를 수정할 때 사용하는 다양한 업데이트 연산자를 학습합니다. $set부터 arrayFilters까지 실무에서 자주 쓰이는 연산자들을 예제와 함께 알아봅니다.
MongoDB 쿼리 연산자 완벽 가이드
MongoDB에서 데이터를 효율적으로 검색하고 필터링하는 쿼리 연산자를 알아봅니다. 비교, 논리, 배열 연산자부터 정규표현식, 프로젝션, 정렬까지 실무에서 바로 활용할 수 있는 내용을 다룹니다.