C++ MongoDB 실전 완벽 가이드 | mongocxx CRUD·집계·인덱싱·레플리카셋·프로덕션

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. 환경 설정 및 설치
  2. 기본 연결 및 Hello World
  3. 완전한 CRUD 예제
  4. 집계 파이프라인
  5. 인덱싱 전략
  6. 레플리카셋 연결
  7. 자주 발생하는 에러와 해결법
  8. 베스트 프랙티스
  9. 프로덕션 패턴
  10. 구현 체크리스트
  11. 정리

1. 환경 설정 및 설치

필수 의존성

항목버전비고
C++C++17 이상C++11 최소, C++17 권장
mongocxx4.xMongoDB C++ 드라이버
bsoncxx4.xBSON 문서 라이브러리
CMake3.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 생성. 중복 _idbulk_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_manymongocxx::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_manyinserted_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
CRUDinsert_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, 설정 외부화, 재시도, 로깅

핵심 원칙:

  1. mongocxx::instance를 반드시 먼저 생성
  2. document::value 수명에 주의, view dangling 방지
  3. client 재사용, 매 요청마다 새로 만들지 않기
  4. 대용량 조회는 커서로 스트리밍
  5. 집계는 $match로 먼저 데이터 축소, allow_disk_use 활용
  6. 레플리카셋은 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)


참고 자료


관련 글

  • C++ MongoDB 완벽 가이드 | mongocxx·CRUD·연결·문제 해결·성능 최적화 [#52-3]
  • C++ MongoDB 드라이버 고급 | 집계 파이프라인·인덱싱·레플리카셋 완벽 가이드 [#52-4]
  • C++ 데이터베이스 연동 완벽 가이드 | SQLite·PostgreSQL·연결 풀·트랜잭션 [#31-3]
  • C++ PostgreSQL 클라이언트 완벽 가이드 | libpq·libpqxx
... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3