블로그 (Blog)/개발로그 (Devlogs)

LMDB를 이용한 Tree DB

티클러 2026. 1. 8. 03:06
#include <iostream>
#include <string>
#include <vector>
#include <cstdint>
#include <lmdb.h>

class TreeDB {
private:
	MDB_env *env = nullptr;
	MDB_dbi dbi;
	std::string dbPath;
	size_t currentMapSize;

	// DB 초기화 및 오픈 (공통 로직)
	void initDB(size_t newSize) {
		if (env) mdb_env_close(env); // 기존 환경 닫기

		mdb_env_create(&env);
		mdb_env_set_mapsize(env, newSize);
		// MDB_WRITEMAP: 쓰기 시 성능 향상 및 확장 시 유리
		int rc = mdb_env_open(env, dbPath.c_str(), MDB_NOSUBDIR, 0664);

		if (rc != 0) {
			throw std::runtime_error("DB Open Failed!");
		}

		MDB_txn *txn;
		mdb_txn_begin(env, NULL, 0, &txn);
		mdb_dbi_open(txn, NULL, 0, &dbi);
		mdb_txn_commit(txn);

		currentMapSize = newSize;
		std::cout << "[System] DB Capacity Updated: " << currentMapSize / 1024 / 1024 << "MB" << std::endl;
	}

public:
	TreeDB(const std::string& path, size_t initialSize = 1048576) : dbPath(path) { // 기본 1MB
		initDB(initialSize);
	}

	~TreeDB() {
		mdb_dbi_close(env, dbi);
		mdb_env_close(env);
	}

	// 용량 부족 시 자동 재시도 로직이 포함된 Insert
	// 노드 추가 (부모 경로 + 현재 이름)
	bool insertNode(const std::string& parentPath, const std::string& nodeName, const std::string& value) {
		std::string fullPath = parentPath.empty() ? nodeName : parentPath + "/" + nodeName;

		MDB_val key, data;
		key.mv_size = fullPath.size();
		key.mv_data = (void*)fullPath.c_str();
		data.mv_size = value.size();
		data.mv_data = (void*)value.c_str();

		while (true) {
			MDB_txn *txn;
			mdb_txn_begin(env, NULL, 0, &txn);

			int rc = mdb_put(txn, dbi, &key, &data, 0);

			if (rc == MDB_MAP_FULL) {
				// 용량이 꽉 찼을 경우 현재 트랜잭션 취소
				mdb_txn_abort(txn);

				// 용량을 2배로 키워서 DB 재오픈
				initDB(currentMapSize * 2);

				// 루프를 통해 다시 시도 (while true)
				continue; 
			} else if (rc != 0) {
				mdb_txn_abort(txn);
				return false;
			}

			mdb_txn_commit(txn);
			return true;
		}
	}

	// 바이너리 데이터 저장을 위한 오버로딩 또는 수정
	bool insertNode(const std::string& parentPath, const std::string& nodeName, const std::vector<uint8_t>& binaryData) {
		std::string fullPath = parentPath.empty() ? nodeName : parentPath + "/" + nodeName;

		MDB_val key, data;
		// 1. 키(경로) 설정
		key.mv_size = fullPath.size();
		key.mv_data = (void*)fullPath.c_str();

		// 2. 바이너리 데이터 설정
		data.mv_size = binaryData.size();
		data.mv_data = (void*)binaryData.data(); // 바이트 배열의 시작 주소

		while (true) {
			MDB_txn *txn;
			mdb_txn_begin(env, NULL, 0, &txn);

			int rc = mdb_put(txn, dbi, &key, &data, 0);

			if (rc == MDB_MAP_FULL) {
				mdb_txn_abort(txn);
				initDB(currentMapSize * 2);
				continue; 
			} else if (rc != 0) {
				mdb_txn_abort(txn);
				return false;
			}

			mdb_txn_commit(txn);
			return true;
		}
	}

	// 특정 노드 및 자식 노드 검색 
	void listChildren(const std::string& path) {
		MDB_txn *txn;
		MDB_cursor *cursor;
		mdb_txn_begin(env, NULL, MDB_RDONLY, &txn);
		mdb_cursor_open(txn, dbi, &cursor);

		MDB_val key, data;
		key.mv_size = path.size();
		key.mv_data = (void*)path.c_str();

		// 경로로 이동
		int rc = mdb_cursor_get(cursor, &key, &data, MDB_SET_RANGE);

		while (rc == 0) {
			std::string foundKey((char*)key.mv_data, key.mv_size);

			// 현재 경로로 시작하는 데이터만 출력 (자식들)
			if (foundKey.find(path) != 0) break;

			std::cout << "Path: " << foundKey << " | Value: " << (char*)data.mv_data << std::endl;
			rc = mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
		}

		mdb_cursor_close(cursor);
		mdb_txn_abort(txn);
	}

	// 특정 노드와 그 하위의 모든 자식 노드를 삭제하는 함수
	void deleteNodeRecursive(const std::string& path) {
		if (path.empty()) return;

		MDB_txn *txn;
		MDB_cursor *cursor;

		// 쓰기 트랜잭션 시작
		int rc = mdb_txn_begin(env, NULL, 0, &txn);
		if (rc != 0) return;

		rc = mdb_cursor_open(txn, dbi, &cursor);
		if (rc != 0) {
			mdb_txn_abort(txn);
			return;
		}

		MDB_val key, data;
		key.mv_size = path.size();
		key.mv_data = (void*)path.c_str();

		// 삭제를 시작할 위치(해당 경로)를 찾음
		rc = mdb_cursor_get(cursor, &key, &data, MDB_SET_RANGE);

		while (rc == 0) {
			std::string foundKey((char*)key.mv_data, key.mv_size);

			// 현재 찾은 키가 삭제하려는 path로 시작하는지 확인 (Prefix Check)
			// 예: path가 "Users/Alice"일 때 "Users/Alice/ProjectA"는 삭제 대상임
			if (foundKey.find(path) == 0) {
				// 해당 노드 삭제
				rc = mdb_cursor_del(cursor, 0);
				if (rc != 0) break;

				// 다음 노드로 이동 (삭제 후에는 현재 커서 위치가 다음 노드를 가리킴)
				rc = mdb_cursor_get(cursor, &key, &data, MDB_GET_CURRENT);
			} else {
				// 더 이상 해당 경로로 시작하는 키가 없으면 중단
				break;
			}
		}

		// 커서 닫기 및 트랜잭션 커밋 (실제 파일에 반영)
		mdb_cursor_close(cursor);
		mdb_txn_commit(txn);

		std::cout << "Successfully deleted path and its children: " << path << std::endl;
	}

	// ASCII 문자열로 가져오기 (std::string 반환)
	std::string getNodeValueAscii(const std::string& fullPath) {
		MDB_txn *txn;
		// 읽기 전용 트랜잭션은 가볍고 빠릅니다.
		int rc = mdb_txn_begin(env, NULL, MDB_RDONLY, &txn);
		if (rc != 0) return "";

		MDB_val key, data;
		key.mv_size = fullPath.size();
		key.mv_data = (void*)fullPath.c_str();

		rc = mdb_get(txn, dbi, &key, &data);

		std::string result = "";
		if (rc == 0) {
			// LMDB 내부 메모리 영역을 std::string으로 복사
			result.assign(reinterpret_cast<char*>(data.mv_data), data.mv_size);
		}

		mdb_txn_abort(txn); // 읽기 트랜잭션은 abort로 종료 (변경사항 없음)
		return result;
	}

	// Binary 데이터로 가져오기 (std::vector<uint8_t> 반환)
	std::vector<uint8_t> getNodeValueBinary(const std::string& fullPath) {
		MDB_txn *txn;
		int rc = mdb_txn_begin(env, NULL, MDB_RDONLY, &txn);
		if (rc != 0) return {};

		MDB_val key, data;
		key.mv_size = fullPath.size();
		key.mv_data = (void*)fullPath.c_str();

		rc = mdb_get(txn, dbi, &key, &data);

		std::vector<uint8_t> result;
		if (rc == 0) {
			// 바이너리 데이터는 안전하게 vector로 복사하여 전달
			uint8_t* ptr = reinterpret_cast<uint8_t*>(data.mv_data);
			result.assign(ptr, ptr + data.mv_size);
		}

		mdb_txn_abort(txn);
		return result;
	}
};

int main() 
{
	TreeDB db("mytree.db");

	// 아스키 데이타 삽입
	db.insertNode("", "Users", "Root for users");
	db.insertNode("Users", "Alice", "Developer");
	db.insertNode("Users", "Bob", "Designer");
	db.insertNode("Users/Alice", "ProjectA", "Lead");

	// 바이너리 데이타 삽입
	std::vector<uint8_t> data;
	for(int i=0; i<10; i++) data.push_back(i);
	db.insertNode("Users/Alice", "Data", data);

	// 트리 구조 출력
	std::cout << "--- Tree Structure under 'Users' ---" << std::endl;
	db.listChildren("Users");
	std::cout << "--- Tree Structure under 'Users/Alice' ---" << std::endl;
	db.listChildren("Users/Alice");

	// ASCII 데이터 읽기
	std::string value = db.getNodeValueAscii("Users/Bob");
	std::cout << "Value: " << value << std::endl;

	// Binary 데이터 읽기
	std::vector<uint8_t> data2 = db.getNodeValueBinary("Users/Alice/Data");
	if (!data2.empty()) {
		for(int i=0; i<data2.size(); i++) {
			printf("0x%x ", data2[i]);
		}
	}
	printf("\n");

	return 0;
}

테스트..

[MIN@DESKTOP-RSH0QT3 temp]$ g++ treedb.cpp  -llmdb

[MIN@DESKTOP-RSH0QT3 temp]$ ./a
[System] DB Capacity Updated: 1MB
--- Tree Structure under 'Users' ---
Path: Users | Value: Root for users
Path: Users/Alice | Value: Developer
Path: Users/Alice/Data | Value:
Path: Users/Alice/ProjectA | Value: Lead
Path: Users/Bob | Value: Designer
--- Tree Structure under 'Users/Alice' ---
Path: Users/Alice | Value: Developer
Path: Users/Alice/Data | Value:
Path: Users/Alice/ProjectA | Value: Lead
Value: Designer
0x0 0x1 0x2 0x3 0x4 0x5 0x6 0x7 0x8 0x9