C++에서 SQLite를 사용한 로컬 데이터베이스 구축 방법

C++에서 SQLite를 활용하면 가벼운 로컬 데이터베이스를 쉽게 구축할 수 있습니다. SQLite는 파일 기반의 관계형 데이터베이스 관리 시스템(RDBMS)으로, 서버 없이도 간단한 데이터 저장 및 관리를 수행할 수 있습니다.

본 기사에서는 C++에서 SQLite 라이브러리를 사용하여 로컬 데이터베이스를 생성하고, 데이터를 저장하고, 조회하는 방법을 다룹니다. 기본적인 설정부터 CRUD(Create, Read, Update, Delete) 연산, 예외 처리, 성능 최적화까지 단계별로 설명합니다. 이를 통해 C++ 프로젝트에서 데이터 저장소를 효과적으로 관리하는 방법을 배울 수 있습니다.

목차

SQLite 개요 및 C++과의 연동 개념

SQLite는 가볍고 독립적인 관계형 데이터베이스 관리 시스템(RDBMS)으로, 서버가 필요 없는 파일 기반 데이터베이스입니다. 다음과 같은 특징을 가지고 있습니다.

SQLite의 주요 특징

  • 서버리스(Serverless): 별도의 데이터베이스 서버 없이 파일 하나로 데이터 저장 및 관리 가능
  • 제로 설정(Zero Configuration): 복잡한 설치 과정 없이 즉시 사용 가능
  • 경량(Lightweight): 작은 크기(약 500KB)로 임베디드 시스템에서도 활용 가능
  • 트랜잭션 지원: ACID(Atomicity, Consistency, Isolation, Durability) 보장
  • C/C++ 기반의 API 제공: 다양한 프로그래밍 언어에서 활용 가능

C++에서 SQLite 연동 개념


C++에서 SQLite를 활용하려면 SQLite C API를 사용합니다. SQLite는 기본적으로 C 언어로 작성되었기 때문에, C++ 코드에서 sqlite3.h 헤더 파일을 포함하여 직접 API를 호출해야 합니다.

C++ 프로그램이 SQLite를 활용하는 기본 흐름은 다음과 같습니다.

  1. SQLite 라이브러리 포함sqlite3.h 헤더 파일을 포함하여 SQLite 함수를 사용할 수 있도록 설정
  2. 데이터베이스 열기 및 연결sqlite3_open() 함수를 사용하여 데이터베이스 파일을 생성 또는 연결
  3. SQL 실행sqlite3_exec() 또는 sqlite3_prepare_v2()를 통해 SQL 문 실행
  4. 결과 처리 – 조회된 데이터를 가져와 C++에서 처리
  5. 데이터베이스 닫기sqlite3_close()를 사용하여 연결 해제

SQLite는 간단한 데이터 저장소가 필요한 C++ 프로젝트에 유용하며, 다양한 애플리케이션에서 활용될 수 있습니다. 다음 단계에서는 SQLite를 C++ 환경에서 설정하는 방법을 알아보겠습니다.

SQLite 라이브러리 다운로드 및 환경 설정

SQLite를 C++ 프로젝트에서 사용하려면 먼저 라이브러리를 다운로드하고 프로젝트에 포함해야 합니다. 다음은 Windows 및 Linux 환경에서 SQLite를 설정하는 방법입니다.

1. SQLite 라이브러리 다운로드


SQLite는 공식 웹사이트에서 다운로드할 수 있습니다.

필요한 파일:

  • sqlite-amalgamation-XXXXXX.zip (C 소스 코드 포함)
  • sqlite-dll-win64-x64-XXXXXX.zip (Windows DLL 및 실행 파일)

Windows 사용자는 sqlite3.dll, sqlite3.lib, sqlite3.h 파일을 다운로드하여 프로젝트에서 사용하면 됩니다. Linux 사용자는 패키지 매니저를 통해 설치할 수 있습니다.

# Ubuntu/Debian
sudo apt update
sudo apt install sqlite3 libsqlite3-dev

# Fedora
sudo dnf install sqlite sqlite-devel

# Arch Linux
sudo pacman -S sqlite

2. C++ 프로젝트에서 SQLite 설정


SQLite를 프로젝트에서 사용하려면 sqlite3.h 헤더 파일을 포함하고, SQLite 라이브러리를 링크해야 합니다.

Windows 환경에서는 Visual Studio 또는 MinGW를 사용하는 경우 sqlite3.lib를 링크해야 합니다.
Linux 환경에서는 -lsqlite3 옵션을 추가하여 컴파일합니다.

Windows (MinGW) 컴파일 예제

g++ main.cpp -o app -I. -L. -lsqlite3

Linux 컴파일 예제

g++ main.cpp -o app -lsqlite3

3. 간단한 SQLite 연결 테스트


설정이 완료되었는지 확인하려면 아래 코드를 실행해 보세요.

#include <iostream>
#include <sqlite3.h>

int main() {
    sqlite3* db;
    int exit = sqlite3_open("test.db", &db);

    if (exit) {
        std::cerr << "SQLite 연결 실패: " << sqlite3_errmsg(db) << std::endl;
        return exit;
    } else {
        std::cout << "SQLite 연결 성공!" << std::endl;
    }

    sqlite3_close(db);
    return 0;
}

이제 SQLite 라이브러리가 C++ 프로젝트에서 정상적으로 설정되었으며, 데이터베이스를 사용할 준비가 되었습니다. 다음 단계에서는 데이터베이스를 생성하고 연결하는 방법을 알아보겠습니다.

SQLite 데이터베이스 생성 및 연결

C++에서 SQLite를 활용하려면 먼저 데이터베이스 파일을 생성하고 프로그램에서 연결하는 과정이 필요합니다. SQLite는 파일 기반의 데이터베이스이므로, 데이터베이스 파일이 존재하지 않으면 자동으로 생성됩니다.

1. 데이터베이스 생성 및 연결

SQLite에서 데이터베이스를 생성하고 연결하려면 sqlite3_open() 함수를 사용합니다. 다음 코드에서는 test.db라는 데이터베이스 파일을 생성하고 연결하는 예제를 보여줍니다.

#include <iostream>
#include <sqlite3.h>

int main() {
    sqlite3* db;
    int exit = sqlite3_open("test.db", &db);

    if (exit) {
        std::cerr << "데이터베이스 연결 실패: " << sqlite3_errmsg(db) << std::endl;
        return exit;
    } else {
        std::cout << "데이터베이스 연결 성공!" << std::endl;
    }

    sqlite3_close(db);
    return 0;
}

이 코드가 실행되면 test.db 파일이 현재 디렉터리에 생성되고, 데이터베이스 연결이 성공하면 "데이터베이스 연결 성공!"이라는 메시지가 출력됩니다.

2. 데이터베이스 연결 함수화

반복적인 데이터베이스 연결 코드를 줄이기 위해 connectToDatabase() 함수를 만들어 관리할 수 있습니다.

sqlite3* connectToDatabase(const char* filename) {
    sqlite3* db;
    if (sqlite3_open(filename, &db) == SQLITE_OK) {
        std::cout << "데이터베이스 연결 성공: " << filename << std::endl;
    } else {
        std::cerr << "데이터베이스 연결 실패: " << sqlite3_errmsg(db) << std::endl;
    }
    return db;
}

int main() {
    sqlite3* db = connectToDatabase("example.db");
    sqlite3_close(db);
    return 0;
}

이제 connectToDatabase("example.db") 함수를 호출하면 자동으로 데이터베이스에 연결됩니다.

3. 메모리 내(In-Memory) 데이터베이스 사용

파일이 아닌 메모리에 데이터베이스를 생성하려면 "test.db" 대신 ":memory:"를 사용하면 됩니다.

sqlite3* db;
sqlite3_open(":memory:", &db);

이 방법은 일시적인 데이터 저장소가 필요할 때 유용합니다. 프로그램이 종료되면 데이터는 사라집니다.

SQLite 데이터베이스를 생성하고 연결하는 방법을 익혔다면, 다음으로 테이블을 생성하고 데이터를 삽입하는 방법을 알아보겠습니다.

테이블 생성 및 데이터 삽입

SQLite를 활용하여 데이터 저장소를 구축하려면 먼저 테이블을 생성하고 데이터를 삽입해야 합니다. SQLite에서는 SQL 명령어를 실행하여 테이블을 만들고 데이터를 추가할 수 있습니다.

1. 테이블 생성

SQLite에서 테이블을 생성하려면 CREATE TABLE SQL 명령어를 사용합니다. 다음 C++ 코드는 users라는 테이블을 생성합니다.

#include <iostream>
#include <sqlite3.h>

const char* createTableSQL = "CREATE TABLE IF NOT EXISTS users ("
                             "id INTEGER PRIMARY KEY AUTOINCREMENT, "
                             "name TEXT NOT NULL, "
                             "age INTEGER NOT NULL);";

void executeSQL(sqlite3* db, const char* sql) {
    char* errorMessage;
    int exit = sqlite3_exec(db, sql, nullptr, nullptr, &errorMessage);
    if (exit != SQLITE_OK) {
        std::cerr << "SQL 실행 오류: " << errorMessage << std::endl;
        sqlite3_free(errorMessage);
    } else {
        std::cout << "SQL 실행 성공!" << std::endl;
    }
}

int main() {
    sqlite3* db;
    int exit = sqlite3_open("test.db", &db);

    if (exit) {
        std::cerr << "데이터베이스 연결 실패: " << sqlite3_errmsg(db) << std::endl;
        return exit;
    }

    // 테이블 생성 실행
    executeSQL(db, createTableSQL);

    sqlite3_close(db);
    return 0;
}

위 코드는 test.db 파일에 users 테이블을 생성합니다. 만약 테이블이 이미 존재한다면 새로운 테이블을 만들지 않습니다.

2. 데이터 삽입

데이터를 삽입하려면 INSERT INTO SQL 명령어를 사용해야 합니다. 다음 코드에서는 users 테이블에 데이터를 추가하는 기능을 구현합니다.

const char* insertSQL = "INSERT INTO users (name, age) VALUES ('Alice', 25), ('Bob', 30);";

int main() {
    sqlite3* db;
    int exit = sqlite3_open("test.db", &db);

    if (exit) {
        std::cerr << "데이터베이스 연결 실패: " << sqlite3_errmsg(db) << std::endl;
        return exit;
    }

    // 데이터 삽입 실행
    executeSQL(db, insertSQL);

    sqlite3_close(db);
    return 0;
}

위 코드가 실행되면 users 테이블에 "Alice", 25"Bob", 30 두 개의 레코드가 삽입됩니다.

3. 파라미터 바인딩을 활용한 안전한 데이터 삽입

위의 방식은 SQL 인젝션 공격에 취약할 수 있으므로, 보다 안전한 방법으로 sqlite3_prepare_v2()sqlite3_bind_text()를 사용하여 값을 동적으로 삽입할 수 있습니다.

void insertUser(sqlite3* db, const std::string& name, int age) {
    const char* sql = "INSERT INTO users (name, age) VALUES (?, ?);";
    sqlite3_stmt* stmt;

    if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
        std::cerr << "SQL 준비 오류: " << sqlite3_errmsg(db) << std::endl;
        return;
    }

    sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_STATIC);
    sqlite3_bind_int(stmt, 2, age);

    if (sqlite3_step(stmt) != SQLITE_DONE) {
        std::cerr << "데이터 삽입 실패: " << sqlite3_errmsg(db) << std::endl;
    } else {
        std::cout << "데이터 삽입 성공: " << name << ", " << age << std::endl;
    }

    sqlite3_finalize(stmt);
}

int main() {
    sqlite3* db;
    sqlite3_open("test.db", &db);

    insertUser(db, "Charlie", 27);
    insertUser(db, "Diana", 22);

    sqlite3_close(db);
    return 0;
}

위 코드를 실행하면 users 테이블에 "Charlie", 27"Diana", 22 데이터가 안전하게 삽입됩니다.

이제 SQLite에서 데이터를 저장하는 방법을 익혔으므로, 다음으로 데이터를 조회하고 결과를 처리하는 방법을 살펴보겠습니다.

데이터 조회 및 결과 처리

SQLite 데이터베이스에서 저장된 데이터를 가져오려면 SELECT SQL 명령어를 사용해야 합니다. C++에서는 sqlite3_exec() 또는 sqlite3_prepare_v2()를 사용하여 SQL 쿼리를 실행하고 결과를 처리할 수 있습니다.

1. 간단한 데이터 조회

SQLite에서 데이터를 조회하는 기본적인 방법은 sqlite3_exec()를 사용하여 SELECT 문을 실행하는 것입니다. 다음 예제는 users 테이블에서 모든 데이터를 조회하는 코드입니다.

#include <iostream>
#include <sqlite3.h>

int callback(void* NotUsed, int argc, char** argv, char** azColName) {
    for (int i = 0; i < argc; i++) {
        std::cout << azColName[i] << ": " << (argv[i] ? argv[i] : "NULL") << " | ";
    }
    std::cout << std::endl;
    return 0;
}

int main() {
    sqlite3* db;
    char* errorMessage;

    int exit = sqlite3_open("test.db", &db);
    if (exit) {
        std::cerr << "데이터베이스 연결 실패: " << sqlite3_errmsg(db) << std::endl;
        return exit;
    }

    const char* selectSQL = "SELECT * FROM users;";

    std::cout << "=== 사용자 목록 ===" << std::endl;
    exit = sqlite3_exec(db, selectSQL, callback, 0, &errorMessage);

    if (exit != SQLITE_OK) {
        std::cerr << "쿼리 실행 오류: " << errorMessage << std::endl;
        sqlite3_free(errorMessage);
    }

    sqlite3_close(db);
    return 0;
}

위 코드에서는 callback() 함수를 사용하여 sqlite3_exec() 실행 시 각 행을 출력하도록 했습니다.

예제 실행 결과:

=== 사용자 목록 ===  
id: 1 | name: Alice | age: 25 |  
id: 2 | name: Bob | age: 30 |  
id: 3 | name: Charlie | age: 27 |  
id: 4 | name: Diana | age: 22 |  

2. `sqlite3_prepare_v2()`를 이용한 데이터 조회

sqlite3_exec() 방식은 간단하지만, 보다 정교한 데이터 처리를 위해 sqlite3_prepare_v2()를 사용할 수 있습니다.

void fetchUsers(sqlite3* db) {
    const char* sql = "SELECT id, name, age FROM users;";
    sqlite3_stmt* stmt;

    if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
        std::cerr << "SQL 준비 오류: " << sqlite3_errmsg(db) << std::endl;
        return;
    }

    std::cout << "=== 사용자 목록 ===" << std::endl;
    while (sqlite3_step(stmt) == SQLITE_ROW) {
        int id = sqlite3_column_int(stmt, 0);
        const unsigned char* name = sqlite3_column_text(stmt, 1);
        int age = sqlite3_column_int(stmt, 2);

        std::cout << "ID: " << id << " | 이름: " << name << " | 나이: " << age << std::endl;
    }

    sqlite3_finalize(stmt);
}

int main() {
    sqlite3* db;
    sqlite3_open("test.db", &db);

    fetchUsers(db);

    sqlite3_close(db);
    return 0;
}

위 코드는 sqlite3_prepare_v2()sqlite3_step()을 사용하여 데이터를 한 줄씩 가져오는 방식으로 동작합니다.

3. 특정 조건으로 데이터 조회

특정 사용자를 검색하려면 WHERE 절을 활용할 수 있습니다.

void fetchUserByName(sqlite3* db, const std::string& name) {
    const char* sql = "SELECT id, name, age FROM users WHERE name = ?;";
    sqlite3_stmt* stmt;

    if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
        std::cerr << "SQL 준비 오류: " << sqlite3_errmsg(db) << std::endl;
        return;
    }

    sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_STATIC);

    std::cout << "=== 검색 결과 ===" << std::endl;
    while (sqlite3_step(stmt) == SQLITE_ROW) {
        int id = sqlite3_column_int(stmt, 0);
        const unsigned char* fetchedName = sqlite3_column_text(stmt, 1);
        int age = sqlite3_column_int(stmt, 2);

        std::cout << "ID: " << id << " | 이름: " << fetchedName << " | 나이: " << age << std::endl;
    }

    sqlite3_finalize(stmt);
}

int main() {
    sqlite3* db;
    sqlite3_open("test.db", &db);

    fetchUserByName(db, "Alice");

    sqlite3_close(db);
    return 0;
}

위 코드 실행 시 "Alice"라는 이름을 가진 사용자의 데이터만 조회됩니다.

4. 조회한 데이터 가공 및 활용

조회한 데이터를 JSON이나 CSV 형식으로 변환할 수도 있습니다.

#include <sstream>
std::string fetchUsersAsJSON(sqlite3* db) {
    const char* sql = "SELECT id, name, age FROM users;";
    sqlite3_stmt* stmt;
    std::ostringstream json;

    if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
        std::cerr << "SQL 준비 오류: " << sqlite3_errmsg(db) << std::endl;
        return "{}";
    }

    json << "[";
    bool first = true;
    while (sqlite3_step(stmt) == SQLITE_ROW) {
        if (!first) json << ",";
        first = false;

        int id = sqlite3_column_int(stmt, 0);
        const unsigned char* name = sqlite3_column_text(stmt, 1);
        int age = sqlite3_column_int(stmt, 2);

        json << "{ \"id\": " << id << ", \"name\": \"" << name << "\", \"age\": " << age << " }";
    }
    json << "]";

    sqlite3_finalize(stmt);
    return json.str();
}

int main() {
    sqlite3* db;
    sqlite3_open("test.db", &db);

    std::cout << fetchUsersAsJSON(db) << std::endl;

    sqlite3_close(db);
    return 0;
}

위 코드는 users 테이블 데이터를 JSON 형식으로 변환하여 출력하는 예제입니다.

이제 SQLite에서 데이터를 조회하고 결과를 처리하는 방법을 익혔습니다. 다음으로 데이터 수정 및 삭제하는 방법을 알아보겠습니다.

데이터 수정 및 삭제

SQLite에서는 UPDATE 문을 사용하여 데이터를 수정하고, DELETE 문을 사용하여 데이터를 삭제할 수 있습니다. C++에서 SQLite를 활용해 데이터를 안전하게 변경하는 방법을 알아보겠습니다.


1. 데이터 수정 (UPDATE)

SQLite에서 데이터를 수정하려면 UPDATE SQL 문을 실행해야 합니다. 아래 코드에서는 특정 사용자의 나이를 변경하는 기능을 구현합니다.

#include <iostream>
#include <sqlite3.h>

void updateUserAge(sqlite3* db, const std::string& name, int newAge) {
    const char* sql = "UPDATE users SET age = ? WHERE name = ?;";
    sqlite3_stmt* stmt;

    if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
        std::cerr << "SQL 준비 오류: " << sqlite3_errmsg(db) << std::endl;
        return;
    }

    sqlite3_bind_int(stmt, 1, newAge);
    sqlite3_bind_text(stmt, 2, name.c_str(), -1, SQLITE_STATIC);

    if (sqlite3_step(stmt) != SQLITE_DONE) {
        std::cerr << "데이터 수정 실패: " << sqlite3_errmsg(db) << std::endl;
    } else {
        std::cout << "사용자 " << name << "의 나이를 " << newAge << "(으)로 변경했습니다." << std::endl;
    }

    sqlite3_finalize(stmt);
}

int main() {
    sqlite3* db;
    sqlite3_open("test.db", &db);

    updateUserAge(db, "Alice", 28);

    sqlite3_close(db);
    return 0;
}

위 코드를 실행하면 Alice의 나이가 28로 업데이트됩니다.


2. 데이터 삭제 (DELETE)

사용자를 삭제하려면 DELETE SQL 문을 실행해야 합니다. 특정 사용자의 데이터를 삭제하는 코드를 작성해 보겠습니다.

void deleteUser(sqlite3* db, const std::string& name) {
    const char* sql = "DELETE FROM users WHERE name = ?;";
    sqlite3_stmt* stmt;

    if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
        std::cerr << "SQL 준비 오류: " << sqlite3_errmsg(db) << std::endl;
        return;
    }

    sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_STATIC);

    if (sqlite3_step(stmt) != SQLITE_DONE) {
        std::cerr << "데이터 삭제 실패: " << sqlite3_errmsg(db) << std::endl;
    } else {
        std::cout << "사용자 " << name << "를 삭제했습니다." << std::endl;
    }

    sqlite3_finalize(stmt);
}

int main() {
    sqlite3* db;
    sqlite3_open("test.db", &db);

    deleteUser(db, "Bob");

    sqlite3_close(db);
    return 0;
}

이제 Bob의 데이터가 삭제됩니다.


3. 전체 데이터 삭제

테이블의 모든 데이터를 삭제하려면 DELETE FROM 테이블명;을 사용할 수 있습니다.

void deleteAllUsers(sqlite3* db) {
    const char* sql = "DELETE FROM users;";
    char* errorMessage;

    int exit = sqlite3_exec(db, sql, nullptr, nullptr, &errorMessage);
    if (exit != SQLITE_OK) {
        std::cerr << "전체 데이터 삭제 실패: " << errorMessage << std::endl;
        sqlite3_free(errorMessage);
    } else {
        std::cout << "모든 사용자 데이터를 삭제했습니다." << std::endl;
    }
}

int main() {
    sqlite3* db;
    sqlite3_open("test.db", &db);

    deleteAllUsers(db);

    sqlite3_close(db);
    return 0;
}

위 코드를 실행하면 users 테이블의 모든 데이터가 삭제됩니다.


4. 데이터 삭제 시 주의점

  • DELETE 문을 실행할 때, WHERE 절이 없으면 테이블의 모든 데이터가 삭제됩니다.
  • 특정 데이터를 삭제할 때, WHERE 조건을 명확하게 지정해야 의도하지 않은 데이터 삭제를 방지할 수 있습니다.
  • 데이터 삭제 후 VACUUM; 명령을 실행하면 삭제된 공간을 정리할 수 있습니다.
void optimizeDatabase(sqlite3* db) {
    const char* sql = "VACUUM;";
    char* errorMessage;

    int exit = sqlite3_exec(db, sql, nullptr, nullptr, &errorMessage);
    if (exit != SQLITE_OK) {
        std::cerr << "데이터베이스 최적화 실패: " << errorMessage << std::endl;
        sqlite3_free(errorMessage);
    } else {
        std::cout << "데이터베이스 최적화 완료!" << std::endl;
    }
}

데이터 수정 및 삭제 기능을 마스터하면 데이터베이스를 더욱 효과적으로 관리할 수 있습니다. 다음으로는 SQLite에서 발생할 수 있는 오류를 처리하는 방법을 알아보겠습니다.

예외 처리 및 에러 핸들링

SQLite를 C++에서 사용할 때 다양한 오류가 발생할 수 있습니다. 데이터베이스 연결 오류, SQL 구문 오류, 데이터 무결성 위반 등의 문제가 있을 수 있으며, 이를 적절히 처리해야 안정적인 애플리케이션을 개발할 수 있습니다.


1. SQLite 에러 코드와 메시지

SQLite는 각종 오류에 대해 다양한 반환 값을 제공합니다. 주요 오류 코드는 다음과 같습니다.

오류 코드설명
SQLITE_OK정상 실행
SQLITE_ERROR일반적인 SQL 오류
SQLITE_BUSY데이터베이스가 잠겨 있음
SQLITE_LOCKED테이블이 다른 프로세스에 의해 잠김
SQLITE_NOMEM메모리 부족
SQLITE_READONLY읽기 전용 데이터베이스 접근 오류
SQLITE_CONSTRAINT제약 조건 위반 (예: 중복 키 삽입)
SQLITE_MISUSESQLite API를 잘못 사용

SQLite 오류를 처리하는 가장 기본적인 방법은 sqlite3_errmsg() 함수를 사용하여 오류 메시지를 출력하는 것입니다.


2. 데이터베이스 연결 오류 처리

SQLite 데이터베이스에 연결하는 과정에서 파일이 존재하지 않거나, 잘못된 파일을 열려고 하면 오류가 발생할 수 있습니다.

#include <iostream>
#include <sqlite3.h>

sqlite3* connectToDatabase(const char* filename) {
    sqlite3* db;
    int exit = sqlite3_open(filename, &db);

    if (exit != SQLITE_OK) {
        std::cerr << "데이터베이스 연결 실패: " << sqlite3_errmsg(db) << std::endl;
        return nullptr;
    }

    std::cout << "데이터베이스 연결 성공: " << filename << std::endl;
    return db;
}

int main() {
    sqlite3* db = connectToDatabase("test.db");

    if (db) {
        sqlite3_close(db);
    }

    return 0;
}

만약 sqlite3_open()이 실패하면, sqlite3_errmsg(db)를 호출하여 오류 원인을 출력할 수 있습니다.


3. SQL 실행 오류 처리

SQL 실행 중 구문 오류나 테이블이 존재하지 않는 경우 오류가 발생할 수 있습니다. 이를 처리하려면 sqlite3_exec()의 반환 값을 확인해야 합니다.

void executeSQL(sqlite3* db, const char* sql) {
    char* errorMessage;
    int exit = sqlite3_exec(db, sql, nullptr, nullptr, &errorMessage);

    if (exit != SQLITE_OK) {
        std::cerr << "SQL 실행 오류: " << errorMessage << std::endl;
        sqlite3_free(errorMessage);
    } else {
        std::cout << "SQL 실행 성공!" << std::endl;
    }
}

int main() {
    sqlite3* db = connectToDatabase("test.db");

    if (db) {
        executeSQL(db, "CREATE TABLE invalid_syntax;");
        sqlite3_close(db);
    }

    return 0;
}

위 코드를 실행하면 invalid_syntax 테이블을 생성하는 SQL 문이 잘못되었기 때문에 SQL 실행 오류 메시지가 출력됩니다.


4. 트랜잭션을 활용한 오류 처리

데이터 삽입, 수정, 삭제 등의 작업에서 오류가 발생하면 데이터의 일관성을 유지하기 위해 트랜잭션을 사용할 수 있습니다.

void executeTransaction(sqlite3* db) {
    char* errorMessage;

    // 트랜잭션 시작
    sqlite3_exec(db, "BEGIN TRANSACTION;", nullptr, nullptr, nullptr);

    int exit = sqlite3_exec(db, "INSERT INTO users (name, age) VALUES ('Eve', 29);", nullptr, nullptr, &errorMessage);
    if (exit != SQLITE_OK) {
        std::cerr << "삽입 오류: " << errorMessage << std::endl;
        sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr);
        sqlite3_free(errorMessage);
        return;
    }

    exit = sqlite3_exec(db, "INSERT INTO users (name, age) VALUES ('Eve', 'INVALID_AGE');", nullptr, nullptr, &errorMessage);
    if (exit != SQLITE_OK) {
        std::cerr << "삽입 오류: " << errorMessage << std::endl;
        sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr);
        sqlite3_free(errorMessage);
        return;
    }

    // 트랜잭션 커밋
    sqlite3_exec(db, "COMMIT;", nullptr, nullptr, nullptr);
    std::cout << "트랜잭션 완료!" << std::endl;
}

int main() {
    sqlite3* db = connectToDatabase("test.db");

    if (db) {
        executeTransaction(db);
        sqlite3_close(db);
    }

    return 0;
}

위 코드에서는 두 개의 INSERT 문이 실행됩니다. 첫 번째 INSERT 문은 정상적으로 실행되지만, 두 번째 INSERT 문에서 데이터 형식 오류가 발생하면 ROLLBACK이 수행되어 데이터가 저장되지 않습니다.


5. 예외 발생 시 프로그램 종료 방지

SQLite를 사용할 때 예외가 발생하면 프로그램이 강제 종료되지 않도록 try-catch 구문을 사용할 수 있습니다.

void safeExecute(sqlite3* db, const char* sql) {
    try {
        executeSQL(db, sql);
    } catch (const std::exception& e) {
        std::cerr << "예외 발생: " << e.what() << std::endl;
    }
}

int main() {
    sqlite3* db = connectToDatabase("test.db");

    if (db) {
        safeExecute(db, "INSERT INTO users (name, age) VALUES ('John', 35);");
        safeExecute(db, "INSERT INTO invalid_table (name) VALUES ('Jane');"); // 오류 발생
        sqlite3_close(db);
    }

    return 0;
}

위 코드에서 존재하지 않는 invalid_table에 데이터를 삽입하려 하면 예외가 발생하지만, try-catch를 사용하면 프로그램이 강제 종료되지 않습니다.


6. SQLite 예외 처리 시 고려할 점

  • sqlite3_exec()의 반환 값 확인: 모든 SQL 실행 후 반환 값을 확인해야 오류 발생 여부를 알 수 있습니다.
  • sqlite3_errmsg()를 활용한 디버깅: 발생한 오류의 원인을 쉽게 파악할 수 있도록 에러 메시지를 출력해야 합니다.
  • 트랜잭션을 사용하여 데이터 무결성 유지: 여러 개의 SQL 문이 실행될 경우, 오류 발생 시 ROLLBACK을 수행하여 데이터 정합성을 유지해야 합니다.
  • 예외 처리로 프로그램 종료 방지: try-catch 구문을 활용하여 예외 발생 시 프로그램이 강제 종료되지 않도록 해야 합니다.

이제 SQLite의 오류를 효과적으로 처리하는 방법을 익혔으므로, 다음으로는 실제 프로젝트에서 SQLite를 적용하는 방법과 최적화 팁을 알아보겠습니다.

프로젝트에 SQLite 적용 및 최적화 팁

SQLite를 C++ 프로젝트에 적용할 때 성능과 안정성을 높이기 위한 최적화 방법을 살펴보겠습니다. 데이터베이스가 커지거나 빈번한 읽기/쓰기 작업이 발생하는 경우 최적화는 필수적입니다.


1. 효율적인 데이터베이스 연결 관리

SQLite 데이터베이스 연결(sqlite3_open(), sqlite3_close())은 비용이 높은 작업입니다. 따라서 다음과 같은 방법으로 최적화할 수 있습니다.

  • 연결 풀링(Connection Pooling): 여러 개의 SQLite 연결을 미리 생성해 두고 필요할 때 재사용하는 방식
  • 싱글톤 패턴 적용: 하나의 sqlite3 인스턴스를 애플리케이션에서 공유
class Database {
private:
    sqlite3* db;
    Database() {
        if (sqlite3_open("test.db", &db) != SQLITE_OK) {
            std::cerr << "데이터베이스 연결 실패: " << sqlite3_errmsg(db) << std::endl;
        }
    }

public:
    static Database& getInstance() {
        static Database instance;
        return instance;
    }

    sqlite3* getDB() { return db; }

    ~Database() {
        sqlite3_close(db);
    }
};

위 코드처럼 싱글톤 패턴을 사용하면 한 번 열린 데이터베이스 연결을 여러 곳에서 재사용할 수 있습니다.


2. 인덱스를 활용한 성능 최적화

데이터가 많아지면 SELECT 성능이 저하될 수 있습니다. INDEX를 추가하면 검색 속도를 대폭 향상할 수 있습니다.

const char* createIndexSQL = "CREATE INDEX IF NOT EXISTS idx_users_name ON users(name);";

void createIndex(sqlite3* db) {
    char* errorMessage;
    int exit = sqlite3_exec(db, createIndexSQL, nullptr, nullptr, &errorMessage);

    if (exit != SQLITE_OK) {
        std::cerr << "인덱스 생성 오류: " << errorMessage << std::endl;
        sqlite3_free(errorMessage);
    } else {
        std::cout << "인덱스 생성 완료!" << std::endl;
    }
}

int main() {
    sqlite3* db;
    sqlite3_open("test.db", &db);

    createIndex(db);

    sqlite3_close(db);
    return 0;
}

위 코드를 실행하면 users 테이블의 name 컬럼에 대한 인덱스가 생성되어 검색 성능이 향상됩니다.


3. `PRAGMA`를 활용한 성능 최적화

SQLite는 PRAGMA 명령어를 사용하여 설정을 조정할 수 있습니다.

PRAGMA 옵션설명
PRAGMA synchronous = OFF;디스크 I/O를 줄여 성능 향상 (안전성 감소)
PRAGMA journal_mode = WAL;Write-Ahead Logging 모드 활성화 (동시성 향상)
PRAGMA cache_size = 10000;메모리 캐시 크기 설정 (단위: 페이지)
PRAGMA temp_store = MEMORY;임시 데이터 저장소를 메모리로 설정
void optimizeDatabase(sqlite3* db) {
    sqlite3_exec(db, "PRAGMA synchronous = OFF;", nullptr, nullptr, nullptr);
    sqlite3_exec(db, "PRAGMA journal_mode = WAL;", nullptr, nullptr, nullptr);
    sqlite3_exec(db, "PRAGMA cache_size = 10000;", nullptr, nullptr, nullptr);
    sqlite3_exec(db, "PRAGMA temp_store = MEMORY;", nullptr, nullptr, nullptr);
    std::cout << "데이터베이스 최적화 설정 완료!" << std::endl;
}

int main() {
    sqlite3* db;
    sqlite3_open("test.db", &db);

    optimizeDatabase(db);

    sqlite3_close(db);
    return 0;
}

4. 일괄 처리(Batch Processing) 적용

대량의 데이터를 삽입할 때 INSERT 문을 반복 실행하면 성능이 저하됩니다. 이를 방지하기 위해 트랜잭션을 활용한 일괄 처리를 적용할 수 있습니다.

void batchInsert(sqlite3* db) {
    sqlite3_exec(db, "BEGIN TRANSACTION;", nullptr, nullptr, nullptr);

    const char* sql = "INSERT INTO users (name, age) VALUES (?, ?);";
    sqlite3_stmt* stmt;
    sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr);

    for (int i = 0; i < 1000; i++) {
        std::string name = "User" + std::to_string(i);
        sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_STATIC);
        sqlite3_bind_int(stmt, 2, 20 + (i % 30));

        sqlite3_step(stmt);
        sqlite3_reset(stmt); // 다음 실행을 위해 준비 상태로 초기화
    }

    sqlite3_finalize(stmt);
    sqlite3_exec(db, "COMMIT;", nullptr, nullptr, nullptr);

    std::cout << "일괄 삽입 완료!" << std::endl;
}

int main() {
    sqlite3* db;
    sqlite3_open("test.db", &db);

    batchInsert(db);

    sqlite3_close(db);
    return 0;
}

위 코드를 실행하면 1000개의 데이터를 빠르게 삽입할 수 있습니다.


5. 데이터베이스 크기 최적화

삭제된 데이터가 많아지면 파일 크기가 줄어들지 않습니다. VACUUM 명령을 실행하면 최적화할 수 있습니다.

void vacuumDatabase(sqlite3* db) {
    sqlite3_exec(db, "VACUUM;", nullptr, nullptr, nullptr);
    std::cout << "데이터베이스 최적화 완료!" << std::endl;
}

int main() {
    sqlite3* db;
    sqlite3_open("test.db", &db);

    vacuumDatabase(db);

    sqlite3_close(db);
    return 0;
}

위 명령어를 실행하면 불필요한 공간이 정리되면서 파일 크기가 줄어듭니다.


6. 동시성 문제 해결 (WAL 모드 활성화)

SQLite는 기본적으로 단일 프로세스에서 사용하도록 설계되었지만, WAL (Write-Ahead Logging) 모드를 활성화하면 다중 프로세스에서도 빠른 읽기/쓰기가 가능합니다.

void enableWALMode(sqlite3* db) {
    sqlite3_exec(db, "PRAGMA journal_mode = WAL;", nullptr, nullptr, nullptr);
    std::cout << "WAL 모드 활성화 완료!" << std::endl;
}

int main() {
    sqlite3* db;
    sqlite3_open("test.db", &db);

    enableWALMode(db);

    sqlite3_close(db);
    return 0;
}

WAL 모드는 데이터 읽기/쓰기를 비동기적으로 수행하므로 성능이 향상됩니다.


7. SQLite 적용 시 고려할 점

  • 데이터 읽기 성능 최적화: 인덱스를 적절히 사용하여 검색 속도를 높인다.
  • 쓰기 성능 향상: 트랜잭션을 사용하여 일괄 삽입을 수행한다.
  • 파일 크기 관리: VACUUM 명령을 주기적으로 실행한다.
  • 동시성 개선: WAL 모드를 활성화하여 다중 프로세스 환경을 지원한다.
  • 최적화된 PRAGMA 설정 적용: 캐시 크기, 동기화 옵션 등을 적절히 조정한다.

이제 C++ 프로젝트에서 SQLite를 최적화하는 방법을 익혔습니다. 마지막으로 전체 내용을 정리하는 요약을 살펴보겠습니다.

요약

본 기사에서는 C++에서 SQLite를 활용하여 로컬 데이터베이스를 구축하는 방법을 단계별로 설명했습니다.

SQLite의 개념과 특징을 이해하고, C++ 프로젝트에서 SQLite를 설정하는 방법을 배웠습니다. 이후 데이터베이스 생성, 테이블 생성 및 데이터 삽입, 조회, 수정, 삭제를 수행하는 방법을 코드 예제와 함께 설명했습니다. 또한, SQLite를 보다 효율적으로 사용하기 위한 예외 처리 및 트랜잭션 활용법, 성능 최적화 전략(인덱스 활용, PRAGMA 설정, WAL 모드 활성화, 일괄 삽입 기법 등)을 다뤘습니다.

SQLite는 가볍고 사용하기 쉬우며, 서버 없이 로컬에서 빠르게 데이터베이스를 활용할 수 있는 강력한 도구입니다. 적절한 최적화 기법을 적용하면 성능을 더욱 향상시킬 수 있습니다. 이를 통해 C++ 애플리케이션에서 안정적이고 효율적인 데이터 저장소를 구축할 수 있습니다.

목차