Modern C++ SQLite 예제, SmartSpace KDM의 메모리 관리 가이드 적용

Modern C++ SQLite 예제, SmartSpace KDM의 메모리 관리 가이드 적용

이 프로젝트는 전통적인 C 스타일 라이브러리인 SQLite3Modern C++ (C++14 이상) 스타일로 안전하고 효율적으로 사용하는 방법을 보여주는 예제입니다.

스마트 포인터(std::unique_ptr)와 RAII(Resource Acquisition Is Initialization) 패턴을 적용하여 메모리 및 리소스 누수를 방지하는 방법을 중점적으로 다룹니다.

소스 구성

  • sqlite_modern_example.cpp: 메인 소스 코드입니다.
    • Custom Deleters: sqlite3_closesqlite3_finalize를 자동으로 호출하는 구조체 정의.
    • Database Class: 데이터베이스 열기, 쿼리 실행, 문(Statement) 준비를 담당하는 래퍼 클래스.
    • Main Logic: DB 연결, 테이블 생성, 데이터 삽입, 조회 예제.

전체 소스 코드 (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).

이 코드에서는 Sqlite3DeleterSqlite3StmtDeleter라는 **도우미**들을 미리 만들어 두었습니다.
* **역할**: "주인님(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의 메모리 관리 가이드를 따릅니다.

  1. 리소스 관리 (2순위: std::unique_ptr)

    • sqlite3* (DB 연결)와 sqlite3_stmt* (쿼리 문)은 힙(Heap) 또는 외부 리소스입니다.
    • 이를 직접 new/delete 하거나 malloc/free 하지 않고, std::unique_ptr커스텀 삭제자(Custom Deleter)를 붙여 관리합니다.
    • 스코프를 벗어나면 자동으로 sqlite3_close()sqlite3_finalize()가 호출됩니다.
  2. 스코프 관리 (1순위: 스택 할당)

    • std::unique_ptr 객체 자체(db, stmt)는 스택(Stack)에 선언됩니다.
    • 함수가 종료되거나 블록이 끝나면 스택이 정리되면서, 소유하고 있던 힙 리소스도 자동으로 정리됩니다.
  3. API 연동 (3순위: Raw Pointer)

    • SQLite C API(sqlite3_exec, sqlite3_step 등)는 여전히 Raw Pointer를 요구합니다.
    • 이때는 unique_ptr::get()을 사용하여 소유권 이전 없이(Observer 패턴) 주소값만 전달합니다.

빌드 (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.

댓글 남기기