C++ MongoDB 실전 완벽 가이드 | mongocxx CRUD·집계·인덱싱·레플리카셋·프로덕션
이 글의 핵심
C++에서 MongoDB 실무 연동: mongocxx 설치부터 CRUD·집계 파이프라인·인덱싱·레플리카셋까지. Connection timeout·bulk_write_exception·메모리 초과 등 흔한 에러 해결, 베스트 프랙티스, 프로덕션 패턴을 900줄 분량으로 다룹니다.
들어가며: “C++에서 MongoDB 연동이 막막해요”
실제 겪는 문제 시나리오
시나리오 1: 로그 스키마가 매주 바뀜
상황: 시스템 로그에 새 필드가 계속 추가됨 (traceId, spanId, metadata 등)
문제: 관계형 DB는 ALTER TABLE·마이그레이션 부담, JSON 컬럼은 쿼리 한계
결과: MongoDB 문서 단위 유연한 스키마로 필드 추가/삭제 자유로움
시나리오 2: Connection refused·타임아웃 폭주
상황: mongocxx로 연결 시도 시 서버 다운·URI 오류로 전체 요청 실패
문제: 재시도·타임아웃 설정 방법을 모름
결과: URI 옵션(connectTimeoutMS, socketTimeoutMS) + 지수 백오프 재시도
시나리오 3: Duplicate key로 insert 실패
상황: _id 중복 또는 유니크 인덱스 위반 시 bulk_write_exception 발생
문제: 에러 코드 파싱·upsert 전환 방법 모름
결과: 에러 코드 11000 처리, upsert 패턴 적용
시나리오 4: find() 결과가 수십만 건이라 OOM
상황: 전체 문서를 vector에 담다 메모리 폭증
문제: 커서 스트리밍·프로젝션 패턴을 모름
결과: mongocxx::cursor로 스트리밍 조회, projection으로 필드 제한
시나리오 5: 일별·시간별 집계가 필요함
상황: 수백만 건 이벤트에서 "일별 활성 사용자", "시간대별 평균 응답시간" 계산
문제: find()만으로는 GROUP BY·SUM·AVG 불가
결과: 집계 파이프라인($match, $group, $lookup) 활용
시나리오 6: find()가 느려서 타임아웃
상황: userId·createdAt 조회가 수십만 건에서 수 초 소요
문제: 인덱스 없이 COLLSCAN, 복합 인덱스 순서 모름
결과: (userId, createdAt) 복합 인덱스, explain으로 실행 계획 확인
시나리오 7: 읽기 부하로 DB 과부하
상황: 쓰기는 적고 읽기가 많은 서비스, 단일 인스턴스 병목
문제: 레플리카셋 Secondary로 읽기 분산 방법 모름
결과: read_preference::k_secondary_preferred, URI에 replicaSet 지정
시나리오 8: 집계 파이프라인 메모리 초과
상황: $group·$sort에서 "Exceeded memory limit of 100MB" 에러
문제: allow_disk_use·파이프라인 분할 방법 모름
결과: opts.allow_disk_use(true), $match로 먼저 데이터 축소
flowchart TB
subgraph 문제[실무 문제]
P1[스키마 변경] --> S1[문서 스키마]
P2[연결 실패] --> S2[URI·재시도]
P3[Duplicate key] --> S3[upsert]
P4[대용량 OOM] --> S4[커서·프로젝션]
P5[집계 필요] --> S5[파이프라인]
P6[느린 쿼리] --> S6[인덱싱]
P7[읽기 과부하] --> S7[레플리카셋]
end
이 글에서 다루는 것:
- mongocxx·bsoncxx 설치 및 환경 설정
- 완전한 CRUD 예제 (insert, find, update, delete)
- 집계 파이프라인 ($match, $group, $lookup, $sort)
- 인덱싱 전략 (단일·복합·TTL)
- 레플리카셋 연결·Read Preference·Write Concern
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스·프로덕션 패턴 요구 환경: C++17 이상, mongocxx 4.x, MongoDB 5.0+
실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
1. 환경 설정 및 설치
필수 의존성
| 항목 | 버전 | 비고 |
|---|---|---|
| C++ | C++17 이상 | C++11 최소, C++17 권장 |
| mongocxx | 4.x | MongoDB C++ 드라이버 |
| bsoncxx | 4.x | BSON 문서 라이브러리 |
| CMake | 3.15+ | find_package 지원 |
MongoDB 서버 실행
# Docker로 MongoDB 실행 (권장)
docker run -d -p 27017:27017 --name mongo mongo:7
# 연결 확인
mongosh --eval "db.adminCommand('ping')"
vcpkg로 설치 (권장)
vcpkg install mongo-cxx-driver
CMakeLists.txt 기본 설정
cmake_minimum_required(VERSION 3.15)
project(mongodb_demo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
find_package(bsoncxx REQUIRED)
find_package(mongocxx REQUIRED)
add_executable(mongodb_demo main.cpp)
target_link_libraries(mongodb_demo PRIVATE
mongo::mongocxx_shared
mongo::bsoncxx_shared
)
pkg-config로 컴파일
c++ -std=c++17 main.cpp $(pkg-config --cflags --libs libmongocxx) -o mongodb_demo
macOS dyld 에러 시:
c++ -std=c++17 main.cpp -Wl,-rpath,/usr/local/lib \
$(pkg-config --cflags --libs libmongocxx) -o mongodb_demo
2. 기본 연결 및 Hello World
mongocxx::instance (필수)
mongocxx::client 사용 전 반드시 mongocxx::instance를 한 번 생성합니다.
#include <mongocxx/client.hpp>
#include <mongocxx/instance.hpp>
#include <mongocxx/uri.hpp>
#include <iostream>
int main() {
// 1. 인스턴스 생성 (프로세스당 1회)
mongocxx::instance inst{};
// 2. URI로 클라이언트 생성
mongocxx::uri uri("mongodb://localhost:27017");
mongocxx::client client(uri);
// 3. DB와 컬렉션 접근
auto db = client[mydb];
auto collection = db[test];
// 4. Ping으로 연결 확인
auto admin = client[admin];
auto cmd = bsoncxx::builder::basic::make_document(
bsoncxx::builder::basic::kvp("ping", 1));
auto result = admin.run_command(cmd.view());
std::cout << "Ping: " << bsoncxx::to_json(result) << std::endl;
return 0;
}
코드 설명:
mongocxx::instance: 드라이버 초기화, 소멸 시 정리mongocxx::uri: 연결 문자열client[dbname]: 데이터베이스 핸들db[collname]: 컬렉션 핸들
URI 옵션 예시
// 로컬 기본
mongocxx::uri uri("mongodb://localhost:27017");
// 인증 포함
mongocxx::uri uri("mongodb://user:password@localhost:27017/mydb?authSource=admin");
// 타임아웃 설정
mongocxx::uri uri("mongodb://localhost:27017/?connectTimeoutMS=5000&socketTimeoutMS=10000");
// 레플리카셋
mongocxx::uri uri("mongodb://host1:27017,host2:27017/?replicaSet=rs0");
Hello World: insert_one + find_one
#include <bsoncxx/builder/basic/document.hpp>
#include <bsoncxx/json.hpp>
#include <mongocxx/client.hpp>
#include <mongocxx/instance.hpp>
#include <mongocxx/uri.hpp>
#include <iostream>
using bsoncxx::builder::basic::kvp;
using bsoncxx::builder::basic::make_document;
int main() {
mongocxx::instance inst{};
mongocxx::client client{mongocxx::uri{"mongodb://localhost:27017"}};
auto collection = client[mydb][greetings];
// insert_one
auto doc = make_document(kvp("message", "Hello, MongoDB!"));
auto insert_result = collection.insert_one(doc.view());
if (insert_result) {
std::cout << "Inserted id: "
<< insert_result->inserted_id().get_oid().value.to_string()
<< std::endl;
}
// find_one
auto filter = make_document(kvp("message", "Hello, MongoDB!"));
auto result = collection.find_one(filter.view());
if (result) {
std::cout << "Found: " << bsoncxx::to_json(*result) << std::endl;
} else {
std::cout << "No document found" << std::endl;
}
return 0;
}
3. 완전한 CRUD 예제
3.1 Insert (insert_one, insert_many)
#include <bsoncxx/builder/basic/array.hpp>
#include <bsoncxx/builder/basic/document.hpp>
#include <bsoncxx/json.hpp>
#include <mongocxx/client.hpp>
#include <mongocxx/instance.hpp>
#include <mongocxx/uri.hpp>
#include <iostream>
#include <vector>
using bsoncxx::builder::basic::kvp;
using bsoncxx::builder::basic::make_document;
using bsoncxx::builder::basic::make_array;
void insert_examples(mongocxx::collection& collection) {
// insert_one: 단일 문서
auto doc = make_document(
kvp("name", "Mongo's Burgers"),
kvp("cuisine", "American"),
kvp("rating", 4.5));
auto result = collection.insert_one(doc.view());
if (result && result->inserted_id()) {
std::cout << "Inserted: "
<< result->inserted_id().get_oid().value.to_string() << "\n";
}
// insert_many: 여러 문서 (1회 왕복)
std::vector<bsoncxx::document::value> docs;
docs.push_back(make_document(kvp("name", "Mongo's Pizza")));
docs.push_back(make_document(kvp("name", "Mongo's Tacos")));
auto many_result = collection.insert_many(docs);
if (many_result) {
std::cout << "Inserted count: " << many_result->inserted_count() << "\n";
}
}
주의: _id 미지정 시 드라이버가 자동 ObjectId 생성. 중복 _id는 bulk_write_exception 발생.
3.2 Find (find_one, find + cursor)
void find_examples(mongocxx::collection& collection) {
using bsoncxx::builder::basic::kvp;
using bsoncxx::builder::basic::make_document;
// find_one: 단일 문서
auto filter = make_document(kvp("name", "Mongo's Burgers"));
auto result = collection.find_one(filter.view());
if (result) {
std::cout << "Found: " << bsoncxx::to_json(*result) << "\n";
}
// find: 여러 문서 (커서로 스트리밍)
auto cursor = collection.find(make_document(kvp("cuisine", "American")));
for (auto&& doc : cursor) {
std::cout << bsoncxx::to_json(doc) << "\n";
}
// find + limit, sort, projection
mongocxx::options::find opts{};
opts.limit(10);
opts.sort(make_document(kvp("rating", -1)).view());
opts.projection(make_document(kvp("name", 1), kvp("rating", 1)).view());
auto cursor2 = collection.find({}, opts);
for (auto&& doc : cursor2) {
std::cout << bsoncxx::to_json(doc) << "\n";
}
}
커서 주의: mongocxx::cursor는 범위 기반 for로 순회. 한 번 순회하면 끝이므로 재사용 시 find() 재호출.
3.3 Update (update_one, update_many, upsert)
void update_examples(mongocxx::collection& collection) {
using bsoncxx::builder::basic::kvp;
using bsoncxx::builder::basic::make_document;
// update_one: $set으로 필드 수정
auto filter = make_document(kvp("name", "Mongo's Burgers"));
auto update = make_document(kvp("$set", make_document(kvp("rating", 4.8))));
auto result = collection.update_one(filter.view(), update.view());
if (result) {
std::cout << "Modified: " << result->modified_count() << "\n";
}
// upsert: 없으면 삽입
mongocxx::options::update opts{};
opts.upsert(true);
auto filter3 = make_document(kvp("name", "New Restaurant"));
auto update3 = make_document(kvp("$set", make_document(kvp("cuisine", "Korean"))));
collection.update_one(filter3.view(), update3.view(), opts);
}
3.4 Delete (delete_one, delete_many)
void delete_examples(mongocxx::collection& collection) {
using bsoncxx::builder::basic::kvp;
using bsoncxx::builder::basic::make_document;
// delete_one: 첫 번째 매칭 문서 삭제
auto filter = make_document(kvp("name", "Mongo's Tacos"));
auto result = collection.delete_one(filter.view());
if (result) {
std::cout << "Deleted: " << result->deleted_count() << "\n";
}
// delete_many: 조건에 맞는 모든 문서 삭제
auto filter2 = make_document(kvp("rating", make_document(kvp("$lt", 3.0))));
collection.delete_many(filter2.view());
}
3.5 실전 예제: 사용자 프로필 저장소
class UserProfileStore {
public:
UserProfileStore(mongocxx::database db) : db_(std::move(db)) {
collection_ = db_[users];
}
bool upsertProfile(const std::string& userId,
const std::string& name,
const std::string& email,
int age) {
auto filter = make_document(kvp("_id", userId));
auto update = make_document(kvp("$set", make_document(
kvp("name", name), kvp("email", email), kvp("age", age),
kvp("updatedAt", bsoncxx::types::b_date{std::chrono::system_clock::now()}))));
mongocxx::options::update opts{};
opts.upsert(true);
auto result = collection_.update_one(filter.view(), update.view(), opts);
return result && (result->modified_count() > 0 || result->upserted_id());
}
std::optional<bsoncxx::document::value> getProfile(const std::string& userId) {
return collection_.find_one(make_document(kvp("_id", userId)).view());
}
bool deleteProfile(const std::string& userId) {
auto result = collection_.delete_one(make_document(kvp("_id", userId)).view());
return result && result->deleted_count() > 0;
}
private:
mongocxx::database db_;
mongocxx::collection collection_;
};
3.6 트랜잭션 예제
void transaction_example(mongocxx::client& client) {
auto db = client[mydb];
auto accounts = db[accounts];
mongocxx::client_session session = client.start_session();
session.start_transaction();
try {
// 계좌 A에서 100 차감
auto filter_a = make_document(kvp("_id", "account_a"));
auto update_a =
make_document(kvp("$inc", make_document(kvp("balance", -100))));
accounts.update_one(session, filter_a.view(), update_a.view());
// 계좌 B에 100 추가
auto filter_b = make_document(kvp("_id", "account_b"));
auto update_b =
make_document(kvp("$inc", make_document(kvp("balance", 100))));
accounts.update_one(session, filter_b.view(), update_b.view());
session.commit_transaction();
std::cout << "Transaction committed\n";
} catch (const mongocxx::exception& e) {
session.abort_transaction();
std::cerr << "Transaction aborted: " << e.what() << "\n";
}
}
주의: 트랜잭션은 MongoDB 4.0+ 레플리카셋 또는 샤드 클러스터에서 지원.
4. 집계 파이프라인
핵심 개념
집계 파이프라인은 문서가 여러 단계(stage)를 거치며 변환되는 구조입니다. $match, $group, $sort 등으로 SQL의 WHERE, GROUP BY, ORDER BY에 대응합니다.
flowchart TB
subgraph Pipeline[집계 파이프라인]
S1[$match\n필터링] --> S2[$group\n그룹핑]
S2 --> S3[$sort\n정렬]
S3 --> S4["$project\n필드 선택"]
end
Docs[문서들] --> S1
S4 --> Result[결과]
4.1 $match + $group 기본 집계
#include <bsoncxx/builder/basic/document.hpp>
#include <bsoncxx/json.hpp>
#include <mongocxx/client.hpp>
#include <mongocxx/instance.hpp>
#include <mongocxx/uri.hpp>
#include <iostream>
using bsoncxx::builder::basic::kvp;
using bsoncxx::builder::basic::make_document;
void aggregation_basic(mongocxx::collection& collection) {
// 일별 활성 사용자 수 집계
mongocxx::pipeline stages;
stages
.match(make_document(
kvp("createdAt",
make_document(kvp(
"$gte",
bsoncxx::types::b_date{
std::chrono::system_clock::now() -
std::chrono::hours(24 * 7)})))))
.group(make_document(
kvp("_id",
make_document(kvp(
"$dateToString",
make_document(kvp("format", "%Y-%m-%d"),
kvp("date", "$createdAt")))),
kvp("activeUsers", make_document(kvp("$addToSet", "$userId"))),
kvp("count", make_document(kvp("$sum", 1)))));
auto cursor = collection.aggregate(stages);
for (auto&& doc : cursor) {
std::cout << bsoncxx::to_json(doc) << std::endl;
}
}
4.2 $lookup으로 컬렉션 조인
void lookup_example(mongocxx::collection& orders) {
mongocxx::pipeline stages;
stages.lookup(make_document(
kvp("from", "users"),
kvp("localField", "userId"),
kvp("foreignField", "_id"),
kvp("as", "userInfo")));
stages.project(make_document(
kvp("orderId", "$_id"),
kvp("amount", 1),
kvp("userName",
make_document(kvp("$arrayElemAt",
make_array("$userInfo.name", 0)))));
auto cursor = orders.aggregate(stages);
for (auto&& doc : cursor) {
std::cout << bsoncxx::to_json(doc) << std::endl;
}
}
4.3 $match + $group + $sort + $limit + allow_disk_use
void aggregation_full(mongocxx::collection& collection) {
mongocxx::pipeline stages;
stages
.match(make_document(kvp("rating", make_document(kvp("$exists", true)))))
.group(make_document(
kvp("_id", "$cuisine"),
kvp("avgRating", make_document(kvp("$avg", "$rating"))),
kvp("count", make_document(kvp("$sum", 1)))))
.sort(make_document(kvp("avgRating", -1)))
.limit(5);
// 대용량 시 메모리 초과 방지
mongocxx::options::aggregate opts{};
opts.allow_disk_use(true);
auto cursor = collection.aggregate(stages, opts);
for (auto&& doc : cursor) {
std::cout << bsoncxx::to_json(doc) << std::endl;
}
}
5. 인덱싱 전략
핵심 개념
인덱스 없으면 COLLSCAN(전체 스캔). 인덱스 있으면 IXSCAN으로 빠르게 조회.
flowchart LR
subgraph NoIdx[인덱스 없음]
N1[쿼리] --> N2[전체 스캔]
N2 --> N3[느림]
end
subgraph WithIdx[인덱스 있음]
W1[쿼리] --> W2[인덱스 조회]
W2 --> W3[빠름]
end
5.1 단일·복합 인덱스
void create_indexes(mongocxx::collection& collection) {
using bsoncxx::builder::basic::kvp;
using bsoncxx::builder::basic::make_document;
// 단일 인덱스
auto idx1 = make_document(kvp("userId", 1));
collection.create_index(idx1.view());
// 복합 인덱스 (조회 패턴에 맞춰 순서 중요)
auto idx2 = make_document(kvp("userId", 1), kvp("createdAt", -1));
collection.create_index(idx2.view());
// 유니크 인덱스
mongocxx::options::index opts{};
opts.unique(true);
auto idx3 = make_document(kvp("email", 1));
collection.create_index(idx3.view(), opts);
}
5.2 TTL 인덱스 (자동 만료)
void create_ttl_index(mongocxx::collection& collection) {
auto keys = make_document(kvp("createdAt", 1));
mongocxx::options::index opts{};
opts.expire_after(std::chrono::seconds(7 * 24 * 60 * 60)); // 7일
collection.create_index(keys.view(), opts);
}
5.3 인덱스 목록·explain
collection.list_indexes()로 인덱스 목록, opts.explain(true)로 실행 계획 확인.
6. 레플리카셋 연결
핵심 개념
레플리카셋은 Primary(쓰기)와 Secondary(읽기) 노드로 구성. read_preference로 읽기 요청을 제어합니다.
flowchart TB
subgraph RS[레플리카셋]
P[Primary\n쓰기]
S1[Secondary 1]
S2[Secondary 2]
end
App[C++ 앱] -->|쓰기| P
App -->|읽기: primary| P
App -->|읽기: secondary| S1
App -->|읽기: secondary| S2
6.1 레플리카셋 URI
// 복수 호스트 + replicaSet 이름 필수
mongocxx::uri uri(
"mongodb://host1:27017,host2:27017,host3:27017/?replicaSet=rs0");
// MongoDB Atlas (srv)
mongocxx::uri uri(
"mongodb+srv://cluster0.xxxxx.mongodb.net/?retryWrites=true&w=majority");
6.2 Read Preference 설정
mongocxx::read_preference rp{};
rp.mode(mongocxx::read_preference::read_mode::k_secondary_preferred);
mongocxx::options::find opts{};
opts.read_preference(rp);
auto cursor = collection.find(filter.view(), opts);
6.3 Write Concern 설정
mongocxx::write_concern wc;
wc.acknowledge_level(mongocxx::write_concern::level::k_majority);
wc.timeout(std::chrono::milliseconds{5000});
mongocxx::options::insert opts{};
opts.write_concern(wc);
collection.insert_one(doc.view(), opts);
7. 자주 발생하는 에러와 해결법
에러 1: Connection timeout / Connection refused
증상: client 생성 또는 첫 작업 시 “Connection refused”
원인: 서버 미실행, 잘못된 호스트/포트, 방화벽, URI 문법 오류
해결법:
# MongoDB 서버 확인
mongosh --eval "db.adminCommand('ping')"
docker ps | grep mongo
// ❌ 잘못된 URI
mongocxx::uri uri("mongodb://wronghost:27017");
// ✅ 타임아웃 포함 URI
mongocxx::uri uri(
"mongodb://localhost:27017/?connectTimeoutMS=5000&socketTimeoutMS=10000");
// ✅ 연결 확인 후 사용
try {
mongocxx::client client(uri);
auto admin = client[admin];
auto cmd = bsoncxx::builder::basic::make_document(
bsoncxx::builder::basic::kvp("ping", 1));
admin.run_command(cmd.view());
} catch (const mongocxx::exception& e) {
std::cerr << "MongoDB 연결 실패: " << e.what() << "\n";
}
에러 2: bulk_write_exception (Duplicate key)
증상: insert_one/insert_many 시 mongocxx::bulk_write_exception
원인: _id 중복 또는 유니크 인덱스 위반
해결법:
// ✅ upsert 사용
auto filter = make_document(kvp("_id", "fixed_id"));
auto update = make_document(kvp("$set", make_document(kvp("name", "test"))));
mongocxx::options::update opts{};
opts.upsert(true);
collection.update_one(filter.view(), update.view(), opts);
// ✅ 에러 코드 확인 후 처리
try {
collection.insert_one(doc.view());
} catch (const mongocxx::bulk_write_exception& e) {
if (e.raw_server_error() && e.raw_server_error()->code() == 11000) {
// Duplicate key → upsert로 전환
}
throw;
}
에러 3: mongocxx::instance가 없음
증상: client 생성 시 크래시
원인: mongocxx::instance 미생성
해결법:
// ❌ 잘못된 코드
mongocxx::client client(mongocxx::uri{"mongodb://localhost:27017"});
// ✅ instance를 client보다 먼저 생성
mongocxx::instance inst{};
mongocxx::client client(mongocxx::uri{"mongodb://localhost:27017"});
에러 4: bsoncxx::document::view 수명 문제 (dangling)
증상: “Using a view of a deleted document” 또는 잘못된 데이터
원인: make_document() 반환값이 스코프를 벗어나 소멸한 뒤 view() 사용
해결법:
// ❌ 위험한 코드
bsoncxx::document::view getFilter() {
auto doc = make_document(kvp("name", "test"));
return doc.view(); // doc 소멸 → view dangling!
}
// ✅ value를 반환
bsoncxx::document::value getFilter() {
return make_document(kvp("name", "test"));
}
auto filter = getFilter();
collection.find_one(filter.view());
에러 5: Exceeded memory limit for $group
증상: 집계 파이프라인 실행 중 “Exceeded memory limit of 100MB”
원인: $group·$sort가 대용량 데이터를 메모리에서 처리
해결법:
// ✅ allow_disk_use 활성화
mongocxx::options::aggregate opts{};
opts.allow_disk_use(true);
auto cursor = collection.aggregate(stages, opts);
에러 6: Not a replica set member
증상: 레플리카셋 클러스터 연결 시 “Not a replica set member”
원인: URI에 replicaSet 이름 없음
해결법:
// ❌ 잘못된 URI
mongocxx::uri uri("mongodb://host1:27017");
// ✅ 레플리카셋 URI
mongocxx::uri uri(
"mongodb://host1:27017,host2:27017,host3:27017/?replicaSet=rs0");
에러 7: macOS dyld / Authentication failed
dyld: -Wl,-rpath,/usr/local/lib 링크 옵션 추가.
authSource: mongodb://user:pass@localhost:27017/mydb?authSource=admin 형식으로 authSource 명시.
8. 베스트 프랙티스
1. 클라이언트 재사용
// ❌ 나쁜 예: 매 요청마다 새 client
void handleRequest() {
mongocxx::client client(mongocxx::uri{"mongodb://localhost:27017"});
// ...
}
// ✅ 좋은 예: 전역 또는 스레드당 1개
mongocxx::instance inst{};
mongocxx::client g_client{mongocxx::uri{"mongodb://localhost:27017"}};
void handleRequest() {
auto collection = g_client[mydb][items];
// ...
}
2. bulk_write로 대량 삽입
// ❌ N번 왕복
for (const auto& doc : documents) {
collection.insert_one(doc.view());
}
// ✅ 1번 왕복
collection.insert_many(documents);
3. 프로젝션으로 필드 제한
mongocxx::options::find opts{};
opts.projection(
make_document(kvp("name", 1), kvp("email", 1)).view());
auto cursor = collection.find(filter.view(), opts);
4. 커서로 대용량 조회 (전체 로드 금지)
// ❌ 메모리 폭증
std::vector<bsoncxx::document::value> all;
auto cursor = collection.find({});
for (auto&& doc : cursor) {
all.push_back(bsoncxx::document::value(doc));
}
// ✅ 스트리밍 처리
auto cursor = collection.find({});
for (auto&& doc : cursor) {
processDocument(doc);
}
5. 집계 시 $match를 앞쪽에 배치
// ✅ $match로 먼저 데이터 축소
stages.match(make_document(kvp("createdAt", make_document(
kvp("$gte", startDate),
kvp("$lte", endDate)))))
.group(...);
6. document::value 수명 관리
// value를 스코프 내에서 유지하고 view만 전달
auto filter = make_document(kvp("name", "test"));
collection.find_one(filter.view());
9. 프로덕션 패턴
패턴 1: Health Check 및 재연결
bool ping(mongocxx::client& client) {
try {
auto admin = client[admin];
auto cmd = bsoncxx::builder::basic::make_document(
bsoncxx::builder::basic::kvp("ping", 1));
admin.run_command(cmd.view());
return true;
} catch (...) {
return false;
}
}
패턴 2: 설정 외부화
struct MongoConfig {
std::string uri = "mongodb://localhost:27017";
int connectTimeoutMs = 5000;
int socketTimeoutMs = 10000;
};
MongoConfig loadFromEnv() {
MongoConfig c;
if (const char* u = std::getenv("MONGODB_URI"))
c.uri = u;
if (const char* t = std::getenv("MONGODB_CONNECT_TIMEOUT_MS")) {
c.connectTimeoutMs = std::stoi(t);
}
return c;
}
패턴 3: 재시도 (지수 백오프)
template <typename Func>
auto withRetry(Func&& f, int maxRetries = 3) -> decltype(f()) {
for (int i = 0; i < maxRetries; ++i) {
try {
return f();
} catch (const mongocxx::exception& e) {
if (i == maxRetries - 1)
throw;
auto delay = std::chrono::milliseconds(100 * (1 << i));
std::this_thread::sleep_for(delay);
}
}
throw std::runtime_error("Unreachable");
}
// 사용
auto result = withRetry([&]() {
return collection.find_one(filter.view());
});
패턴 4: RAII 래퍼 클래스
class MongoClient {
public:
MongoClient(const std::string& uriStr) {
inst_ = std::make_unique<mongocxx::instance>();
client_ = std::make_unique<mongocxx::client>(mongocxx::uri{uriStr});
}
mongocxx::database database(const std::string& name) {
return (*client_)[name];
}
private:
std::unique_ptr<mongocxx::instance> inst_;
std::unique_ptr<mongocxx::client> client_;
};
패턴 5: 인덱스 자동 생성 + Graceful Shutdown
void ensureIndexes(mongocxx::database db) {
auto idx = make_document(kvp("userId", 1), kvp("createdAt", -1));
try { db[events].create_index(idx.view()); }
catch (const mongocxx::operation_exception& e) {
if (e.code().value() != 85) throw;
}
}
// Graceful Shutdown: std::signal(SIGINT, handler)에서 g_running=false 설정 후
// while(g_running) 루프 종료 시 client·instance 자동 정리
10. 구현 체크리스트
환경 설정
- MongoDB 서버 실행 확인 (
mongosh또는docker ps) - mongocxx·bsoncxx 설치 (vcpkg 또는 소스)
- CMake 또는 pkg-config 연동
연결
-
mongocxx::instance를 client보다 먼저 생성 - URI에 타임아웃 설정 (
connectTimeoutMS,socketTimeoutMS) - 인증 시
authSource명시
CRUD
-
insert_one/insert_many후inserted_id확인 -
find결과는 커서로 스트리밍, 전체 로드 금지 -
document::value수명 관리 (view dangling 방지)
집계·인덱싱
-
$match를 파이프라인 앞쪽에 배치 - 대용량 시
allow_disk_use(true)설정 - 쿼리 패턴에 맞는 복합 인덱스 순서
- TTL 인덱스 (로그·세션 등)
레플리카셋
- URI에
replicaSet이름 포함 - 읽기 부하 분산 시
read_preference설정 - 쓰기 내구성 필요 시
write_concern설정
에러 처리
-
bulk_write_exception시 에러 코드 11000 처리 - 연결 실패 시 재시도 또는 폴백
- 트랜잭션 실패 시
abort_transaction호출
프로덕션
- Health Check (ping) 주기적 수행
- 설정 외부화 (환경 변수)
- 재시도 정책 (지수 백오프)
- 로깅·메트릭 (지연 시간, 에러)
11. 정리
| 항목 | 요약 |
|---|---|
| 설치 | vcpkg 또는 소스 빌드, C++17 권장 |
| 연결 | instance → uri → client → database → collection |
| CRUD | insert_one/many, find_one/find, update_one/many, delete_one/many |
| 집계 | $match + $group + $lookup + $sort, allow_disk_use |
| 인덱싱 | 단일·복합·TTL, 쿼리 패턴에 맞는 순서 |
| 레플리카셋 | URI에 replicaSet, read_preference, write_concern |
| 에러 | bulk_write_exception(11000), view 수명, dyld rpath |
| 성능 | client 재사용, insert_many, 프로젝션, 인덱스, 커서 스트리밍 |
| 프로덕션 | Health Check, 설정 외부화, 재시도, 로깅 |
| 핵심 원칙: |
- mongocxx::instance를 반드시 먼저 생성
- document::value 수명에 주의, view dangling 방지
- client 재사용, 매 요청마다 새로 만들지 않기
- 대용량 조회는 커서로 스트리밍
- 집계는 $match로 먼저 데이터 축소, allow_disk_use 활용
- 레플리카셋은 URI에 replicaSet 필수
자주 묻는 질문 (FAQ)
Q. MongoDB Atlas에 연결하려면?
A. Atlas 대시보드에서 연결 문자열을 복사해 mongocxx::uri에 넣습니다. mongodb+srv://... 형식도 지원합니다. IP 화이트리스트와 DB 사용자 설정을 확인하세요.
Q. bsoncxx와 mongocxx 차이는?
A. bsoncxx는 BSON 문서 생성·파싱 라이브러리입니다. mongocxx는 MongoDB 연결·CRUD를 담당하며 bsoncxx에 의존합니다.
Q. 트랜잭션은 언제 필요한가요?
A. 여러 컬렉션에 걸친 원자적 작업(예: 계좌 이체, 주문+재고 차감)이 필요할 때 사용합니다. 단일 문서 수정은 MongoDB가 원자적이므로 트랜잭션이 필요 없습니다.
Q. 레플리카셋 없이 read_preference를 설정하면?
A. 단일 인스턴스에서는 Primary만 있으므로, secondary 모드여도 Primary에서 읽습니다. 에러는 나지 않지만 분산 효과는 없습니다. 한 줄 요약: mongocxx로 C++에서 MongoDB CRUD·집계·인덱싱·레플리카셋을 타입 안전하게 구현하고, 커서·재시도·프로덕션 패턴으로 안정적으로 운영할 수 있습니다. 다음 글: C++ 시리즈 목차에서 다음 주제를 확인하세요. 이전 글: MongoDB 드라이버 고급(#52-4)
참고 자료
- MongoDB C++ Driver 공식 문서
- mongocxx API 레퍼런스
- mongo-cxx-driver GitHub
- MongoDB Aggregation Pipeline
- MongoDB Replica Set
관련 글
- C++ MongoDB 완벽 가이드 | mongocxx·CRUD·연결·문제 해결·성능 최적화 [#52-3]
- C++ MongoDB 드라이버 고급 | 집계 파이프라인·인덱싱·레플리카셋 완벽 가이드 [#52-4]
- C++ 데이터베이스 연동 완벽 가이드 | SQLite·PostgreSQL·연결 풀·트랜잭션 [#31-3]
- C++ PostgreSQL 클라이언트 완벽 가이드 | libpq·libpqxx
심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「C++ MongoDB 실전 완벽 가이드 | mongocxx CRUD·집계·인덱싱·레플리카셋·프로덕션」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.
내부 동작과 핵심 메커니즘
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(런타임·게이트웨이·프로세스) participant D as 의존성(API·DB·큐·파일) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
- 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
- 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.
프로덕션 운영 패턴
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.
확장 예시: 엔드투엔드 미니 시나리오
앞선 본문 주제(「C++ MongoDB 실전 완벽 가이드 | mongocxx CRUD·집계·인덱싱·레플리카셋·프로덕션」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request)
authorize(validated, ctx)
result = domainCore(validated)
persistOrEmit(result, idempotentKey)
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정 불일치 | 프로필·시크릿·기본값, 리전 | 스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ MongoDB 완벽 가이드 | mongocxx·CRUD·연결·문제 해결·성능 최적화 [#52-3]
- C++ MongoDB 드라이버 고급 | 집계 파이프라인·인덱싱·레플리카셋 완벽 가이드 [#52-4]
- C++ Elasticsearch 완벽 가이드 | Elasticlient·REST API
이 글에서 다루는 키워드 (관련 검색어)
C++, MongoDB, mongocxx, NoSQL, CRUD, 집계, 인덱싱 등으로 검색하시면 이 글이 도움이 됩니다.