Modern C++ SQLite 예제, SmartSpace KDM의 메모리 관리 가이드 적용
이 프로젝트는 전통적인 C 스타일 라이브러리인 SQLite3를 Modern C++ (C++14 이상) 스타일로 안전하고 효율적으로 사용하는 방법을 보여주는 예제입니다.
스마트 포인터(std::unique_ptr)와 RAII(Resource Acquisition Is Initialization) 패턴을 적용하여 메모리 및 리소스 누수를 방지하는 방법을 중점적으로 다룹니다.
소스 구성
sqlite_modern_example.cpp: 메인 소스 코드입니다.- Custom Deleters:
sqlite3_close와sqlite3_finalize를 자동으로 호출하는 구조체 정의. - Database Class: 데이터베이스 열기, 쿼리 실행, 문(Statement) 준비를 담당하는 래퍼 클래스.
- Main Logic: DB 연결, 테이블 생성, 데이터 삽입, 조회 예제.
- Custom Deleters:
전체 소스 코드 (Full Source Code)
#include < iostream>
#include < memory>
#include < string>
#include < sqlite3.h>
/*
Modern C++ Memory Management Strategy applied to SQLite3
1. Resource Management (Heap/External): Use std::unique_ptr with Custom Deleter.
- SQLite resources (sqlite3*, sqlite3_stmt*) are C-style pointers that require manual cleanup.
- adhering to "Rank 2: std::unique_ptr" from the guide.
2. Scope Management: Use Stack allocation for the unique_ptr itself.
- adhering to "Rank 1: Stack Allocation".
3. Access: Use Raw Pointers (get()) only for passing to C API functions (Observer).
- adhering to "Rank 3: Raw Pointer (Observer)".
*/
// Custom Deleter for sqlite3 connection
struct Sqlite3Deleter {
void operator()(sqlite3* db) const {
if (db) {
std::cout << "[Resource] Closing database connection...n";
sqlite3_close(db);
}
}
};
// Custom Deleter for sqlite3 statement
struct Sqlite3StmtDeleter {
void operator()(sqlite3_stmt* stmt) const {
if (stmt) {
std::cout << "[Resource] Finalizing statement...n";
sqlite3_finalize(stmt);
}
}
};
// Alias types for convenience
using Sqlite3Ptr = std::unique_ptr;
using Sqlite3StmtPtr = std::unique_ptr;
class Database {
public:
// Factory method to open database and return a unique_ptr
static Sqlite3Ptr open(const std::string& filename) {
sqlite3* dbRaw = nullptr;
// Rank 3: Using raw pointer &dbRaw just to interact with C API
int rc = sqlite3_open(filename.c_str(), &dbRaw);
// Wrap immediately in unique_ptr (Rank 2)
Sqlite3Ptr db(dbRaw);
if (rc != SQLITE_OK) {
std::cerr << "Can't open database: " << sqlite3_errmsg(db.get()) << "n";
return nullptr; // db will be closed automatically if it was opened
}
std::cout << "Opened database successfully: " << filename << "n";
return db;
}
static void execute(sqlite3* db, const std::string& sql) {
char* errMsg = nullptr;
// Rank 3: Passing raw pointer 'db' as an observer
int rc = sqlite3_exec(db, sql.c_str(), nullptr, nullptr, &errMsg);
if (rc != SQLITE_OK) {
std::string error = errMsg;
sqlite3_free(errMsg);
throw std::runtime_error("SQL error: " + error);
}
}
static Sqlite3StmtPtr prepare(sqlite3* db, const std::string& sql) {
sqlite3_stmt* stmtRaw = nullptr;
// Rank 3: Passing raw pointer 'db' as observer
int rc = sqlite3_prepare_v2(db, sql.c_str(), -1, &stmtRaw, nullptr);
// Wrap immediately (Rank 2)
Sqlite3StmtPtr stmt(stmtRaw);
if (rc != SQLITE_OK) {
std::cerr << "Failed to prepare statement: " << sqlite3_errmsg(db) << "n";
return nullptr;
}
return stmt;
}
};
int main() {
try {
// 1. Open Database
// 'db' is on the Stack (Rank 1), managing a Heap resource (Rank 2)
Sqlite3Ptr db = Database::open("test.db");
if (!db) return 1;
// 2. Create Table
std::string createTable =
"CREATE TABLE IF NOT EXISTS Users ("
"ID INTEGER PRIMARY KEY AUTOINCREMENT,"
"Name TEXT NOT NULL,"
"Age INTEGER);";
Database::execute(db.get(), createTable); // .get() returns raw pointer (Rank 3)
std::cout << "Table created.n";
// 3. Insert Data using Prepared Statement
std::string insertSql = "INSERT INTO Users (Name, Age) VALUES (?, ?);";
Sqlite3StmtPtr stmt = Database::prepare(db.get(), insertSql);
if (stmt) {
// Bind parameters
sqlite3_bind_text(stmt.get(), 1, "Alice", -1, SQLITE_STATIC);
sqlite3_bind_int(stmt.get(), 2, 30);
// Execute
if (sqlite3_step(stmt.get()) == SQLITE_DONE) {
std::cout << "User Alice inserted.n";
}
}
// stmt goes out of scope here -> sqlite3_finalize called automatically!
// 4. Query Data
std::string selectSql = "SELECT ID, Name, Age FROM Users;";
Sqlite3StmtPtr queryStmt = Database::prepare(db.get(), selectSql);
if (queryStmt) {
std::cout << "n--- User List ---n";
while (sqlite3_step(queryStmt.get()) == SQLITE_ROW) {
int id = sqlite3_column_int(queryStmt.get(), 0);
const unsigned char* name = sqlite3_column_text(queryStmt.get(), 1);
int age = sqlite3_column_int(queryStmt.get(), 2);
std::cout << "ID: " << id << ", Name: " << name << ", Age: " << age << "n";
}
std::cout << "-----------------n";
}
// queryStmt dies here -> finalized automatically.
} catch (const std::exception& e) {
std::cerr << "Exception: " << e.what() << "n";
}
std::cout << "End of main. Database connection will close now.n";
return 0;
// db dies here -> sqlite3_close called automatically.
}
이 코드가 어떻게 동작하는지 아주 쉽게 설명합니다.
### 1️⃣ "뒤처리는 제가 할게요" (Custom Deleter)
C/C++ 프로그래밍에서 가장 귀찮고 위험한 것이 바로 **메모리 정리**입니다. sqlite3_open으로 DB를 열었으면 반드시 sqlite3_close로 닫아줘야 하는데, 깜빡하면 메모리가 줄줄 샙니다(Memory Leak).
이 코드에서는 Sqlite3Deleter와 Sqlite3StmtDeleter라는 **도우미**들을 미리 만들어 두었습니다.
* **역할**: "주인님(unique_ptr)이 사라질 때, 제가 알아서 문 닫고 청소(close/finalize)해 놓겠습니다!"
### 2️⃣ "안전한 금고" (std::unique_ptr)
sqlite3*는 날것의 위험한 포인터입니다. 이걸 그대로 쓰지 않고 Sqlite3Ptr라는 **금고**(std::unique_ptr) 안에 넣어둡니다.
* **특징**: 이 금고는 **단 하나**만 존재할 수 있습니다. (복사 금지)
* **자동 정리**: 금고가 있는 방(함수 {})을 나가는 순간, 금고가 자동으로 열리면서 위의 도우미(Deleter)가 튀어나와 정리를 시작합니다. **개발자가 close를 호출할 필요가 아예 없습니다!**
### 3️⃣ 프로그램의 흐름 (main 함수)
1. **DB 열기 (Database::open)**
* test.db 파일을 엽니다.
* 성공하면 db라는 스마트 포인터(금고)에 담아서 돌려줍니다.
* 이제 db 변수가 사라지면 파일도 자동으로 닫힙니다.
2. **테이블 만들기 (Database::execute)**
* CREATE TABLE... SQL 명령어를 실행하여 표(Table)를 만듭니다.
* 이때는 금고 안의 내용물(db.get())만 살짝 꺼내서 사용합니다. (Observer 패턴)
3. **데이터 넣기 (Database::prepare -> bind -> step)**
* INSERT INTO... 명령을 준비합니다. 이것도 stmt라는 스마트 포인터에 담습니다.
* stmt도 마찬가지로, 다 쓰고 나면 알아서 청소됩니다.
* **정리**: Alice(30세) 정보를 안전하게 DB에 넣습니다.
4. **데이터 조회하기 (Database::prepare -> step 반복)**
* SELECT... 명령으로 저장된 데이터를 한 줄씩 꺼내와서 출력합니다.
**결론**: main 함수가 끝나는 순간, stmt(명령)들이 먼저 정리되고, 마지막으로 db(연결)가 정리되면서 프로그램이 아주 깔끔하게 종료됩니다.
코드 핵심 설명 (메모리 관리 전략)
이 예제는 SmartSpace KDM의 메모리 관리 가이드를 따릅니다.
-
리소스 관리 (2순위:
std::unique_ptr)sqlite3*(DB 연결)와sqlite3_stmt*(쿼리 문)은 힙(Heap) 또는 외부 리소스입니다.- 이를 직접
new/delete하거나malloc/free하지 않고,std::unique_ptr에 커스텀 삭제자(Custom Deleter)를 붙여 관리합니다. - 스코프를 벗어나면 자동으로
sqlite3_close()나sqlite3_finalize()가 호출됩니다.
-
스코프 관리 (1순위: 스택 할당)
std::unique_ptr객체 자체(db,stmt)는 스택(Stack)에 선언됩니다.- 함수가 종료되거나 블록이 끝나면 스택이 정리되면서, 소유하고 있던 힙 리소스도 자동으로 정리됩니다.
-
API 연동 (3순위: Raw Pointer)
- SQLite C API(
sqlite3_exec,sqlite3_step등)는 여전히 Raw Pointer를 요구합니다. - 이때는
unique_ptr::get()을 사용하여 소유권 이전 없이(Observer 패턴) 주소값만 전달합니다.
- SQLite C API(
빌드 (Build)
터미널에서 아래 명령어를 실행하여 컴파일합니다. (sqlite3 라이브러리가 설치되어 있어야 합니다.)
macOS / Linux
clang++ -std=c++14 sqlite_modern_example.cpp -lsqlite3 -o sqlite_example
또는
g++ -std=c++14 sqlite_modern_example.cpp -lsqlite3 -o sqlite_example
실행 (Run)
빌드된 실행 파일을 실행합니다.
./sqlite_example
실행 결과 예시:
Opened database successfully: test.db
Table created.
User Alice inserted.
User List
ID: 1, Name: Alice, Age: 30
[Resource] Finalizing statement...
[Resource] Finalizing statement...
[Resource] Closing database connection...
End of main. Database connection will close now.