[2026] C++ PostgreSQL 클라이언트 완벽 가이드 | libpq·libpqxx
이 글의 핵심
C++에서 PostgreSQL 연동: libpq·libpqxx 설치·연결, CRUD·트랜잭션·Prepared Statement 실전 코드. 주문·결제 데이터를 안전하게 저장해야 하는데, 어떻게 하죠? 사용자 입력을 그대로 쿼리에 넣으면 SQL injection 위험이 있어요.
들어가며: C++에서 PostgreSQL을 왜 쓰나요?
핵심 질문
"주문·결제 데이터를 안전하게 저장해야 하는데, 어떻게 하죠?"
"사용자 입력을 그대로 쿼리에 넣으면 SQL injection 위험이 있어요."
"DB 연결이 끊어지면 재연결·재시도 로직을 어떻게 구현하나요?"
PostgreSQL은 ACID를 보장하는 관계형 DB로, C++ 서버에서 주문·결제·재고, 복잡한 JOIN·트랜잭션, Prepared Statement를 통해 안전하고 효율적인 데이터 처리를 가능하게 합니다. 이 글은 libpq(C 기반, PostgreSQL 공식)와 libpqxx(Modern C++ 래퍼)를 사용해 PostgreSQL을 C++에서 연동하는 완전한 가이드입니다. 이 글을 읽으면:
- libpq·libpqxx 설치 및 기본 연결을 할 수 있습니다.
- CRUD, 트랜잭션, Prepared Statement 등 실전 패턴을 구현할 수 있습니다.
- Connection refused, SQL injection, 메모리 누수 등 흔한 에러를 해결할 수 있습니다.
- 성능 최적화와 프로덕션 배포 패턴을 적용할 수 있습니다. 요구 환경: C++17 이상, PostgreSQL 11 이상 권장
실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
문제 시나리오
시나리오 1: 주문·결제 시 데이터 일관성
"주문 생성 후 결제 실패 시, 주문 상태를 어떻게 롤백하죠?"
"재고 차감과 주문 생성이 동시에 실패하면 안 돼요."
상황: 주문 생성과 결제 처리, 재고 차감이 여러 테이블에 걸쳐 있습니다. 중간에 실패하면 부분 커밋으로 데이터 불일치가 발생합니다.
해결 포인트: PostgreSQL 트랜잭션(BEGIN/COMMIT/ROLLBACK)으로 원자적 처리. libpqxx의 pqxx::work로 RAII 기반 자동 롤백 처리.
시나리오 2: SQL Injection 방지
"사용자 입력을 그대로 쿼리에 넣으면 위험하다고 들었어요."
"문자열 이스케이프를 어떻게 하죠?"
상황: "SELECT * FROM users WHERE name = '" + userInput + "'" 같은 문자열 연결은 SQL injection에 취약합니다.
해결 포인트: Prepared Statement로 파라미터 바인딩. $1, $2 플레이스홀더에 값을 바인딩하면 이스케이프가 자동 처리됩니다.
시나리오 3: 연결 풀 부족
"동시 요청이 많아지면 'too many connections' 에러가 나요."
"매 요청마다 새 연결을 만들면 느려요."
상황: 웹 서버가 요청마다 새 DB 연결을 생성하면, PostgreSQL max_connections 한도에 도달하고 연결 오버헤드로 지연이 발생합니다.
해결 포인트: 연결 풀로 연결 재사용. libpqxx 또는 PgBouncer와 함께 사용.
시나리오 4: 대용량 결과 처리
"10만 건 조회 시 메모리가 폭발해요."
상황: SELECT * FROM logs로 대량 조회 시 전체 결과를 메모리에 로드하면 OOM이 발생합니다.
해결 포인트: 커서(Cursor) 또는 스트리밍으로 청크 단위 처리. libpqxx의 stream_from 사용.
시나리오 5: 연결 끊김·재연결
"DB 서버 재시작 후 앱이 계속 에러를 내요."
상황: 장시간 유지 연결이 DB 재시작이나 네트워크로 끊기면, 이후 쿼리가 실패합니다.
해결 포인트: Health Check 및 재연결 로직. PQstatus() 체크 후 PQreset() 또는 새 연결 생성.
시나리오별 권장 패턴
| 시나리오 | 해결책 | C++ 라이브러리 |
|---|---|---|
| 트랜잭션 | BEGIN/COMMIT/ROLLBACK | libpqxx::work |
| SQL injection | Prepared Statement | libpqxx::prepare |
| 연결 풀 | Connection Pool | libpqxx, PgBouncer |
| 대용량 조회 | Cursor/스트리밍 | libpqxx::stream_from |
| 재연결 | PQstatus + PQreset | libpq |
목차
- 환경 설정 및 설치
- libpq 기본 연결 및 CRUD
- libpqxx Modern C++ 클라이언트
- 완전한 PostgreSQL C++ 예제
- 자주 발생하는 에러와 해결법
- 성능 최적화 팁
- 프로덕션 패턴
- 구현 체크리스트
1. 환경 설정 및 설치
PostgreSQL 서버 실행
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# Docker로 PostgreSQL 실행 (권장)
docker run -d -p 5432:5432 \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=mydb \
postgres:16-alpine
# 또는 로컬 설치 후
pg_ctl -D /usr/local/var/postgres start
libpq 설치
libpq는 PostgreSQL 공식 C 클라이언트 라이브러리입니다. 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# Ubuntu/Debian
sudo apt-get install libpq-dev
# macOS (Homebrew)
brew install libpq
# vcpkg
vcpkg install libpq
libpqxx 설치
libpqxx는 libpq 위에 구축된 공식 C++ 래퍼입니다. 다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# vcpkg (권장)
vcpkg install libpqxx
# Ubuntu/Debian
sudo apt-get install libpqxx-dev
# macOS (Homebrew)
brew install libpqxx
# 또는 소스 빌드
git clone https://github.com/jtv/libpqxx.git
cd libpqxx
mkdir build && cd build
cmake ...-DCMAKE_BUILD_TYPE=Release
make && sudo make install
CMake 연동 예시
아래 코드는 cmake를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# CMakeLists.txt - libpq 사용
cmake_minimum_required(VERSION 3.16)
project(postgres_demo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
find_package(PostgreSQL REQUIRED)
add_executable(pq_demo main.cpp)
target_link_libraries(pq_demo PRIVATE PostgreSQL::PostgreSQL)
아래 코드는 cmake를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# CMakeLists.txt - libpqxx 사용
cmake_minimum_required(VERSION 3.16)
project(postgres_demo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
find_package(libpqxx REQUIRED)
add_executable(pqxx_demo main.cpp)
target_link_libraries(pqxx_demo PRIVATE libpqxx::pqxx)
2. libpq 기본 연결 및 CRUD
아키텍처 다이어그램
다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
flowchart TB
subgraph App[C++ 애플리케이션]
Main[main]
Client[PgClient]
end
subgraph Libpq[libpq]
Conn[PGconn]
Result[PGresult]
Exec[PQexec]
end
subgraph PG[PostgreSQL 서버]
DB[(데이터베이스)]
end
Main --> Client
Client --> Conn
Client --> Exec
Exec --> Result
Conn -->|TCP 5432| DB
연결 문자열 (Connection String)
postgresql://user:password@host:port/dbname
user: DB 사용자password: 비밀번호host: 호스트 (127.0.0.1 또는 로컬)port: 포트 (기본 5432)dbname: 데이터베이스 이름
기본 연결 (RAII)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// libpq_basic.cpp
// 컴파일: g++ -std=c++17 -o pq_basic libpq_basic.cpp -lpq
#include <libpq-fe.h>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
struct PgConnection {
PGconn* conn = nullptr;
PgConnection(const char* conninfo) {
conn = PQconnectdb(conninfo);
if (conn == nullptr) {
throw std::runtime_error("PostgreSQL 연결 할당 실패");
}
if (PQstatus(conn) != CONNECTION_OK) {
std::string err = PQerrorMessage(conn);
PQfinish(conn);
throw std::runtime_error("PostgreSQL 연결 실패: " + err);
}
}
~PgConnection() {
if (conn) PQfinish(conn);
}
PgConnection(const PgConnection&) = delete;
PgConnection& operator=(const PgConnection&) = delete;
};
int main() {
try {
PgConnection conn("host=127.0.0.1 port=5432 dbname=mydb user=postgres password=postgres");
// 테이블 생성
PGresult* res = PQexec(conn.conn, "CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name VARCHAR(100), email VARCHAR(100))");
if (PQresultStatus(res) != PGRES_COMMAND_OK) {
std::cerr << "CREATE TABLE 에러: " << PQerrorMessage(conn.conn) << "\n";
PQclear(res);
return 1;
}
PQclear(res);
// INSERT
res = PQexecParams(conn.conn,
"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id",
2, nullptr, (const char*[]){"홍길동", "hong@example.com"},
nullptr, nullptr, 0);
if (PQresultStatus(res) != PGRES_TUPLES_OK) {
std::cerr << "INSERT 에러: " << PQerrorMessage(conn.conn) << "\n";
PQclear(res);
return 1;
}
std::cout << "INSERT 성공, id: " << PQgetvalue(res, 0, 0) << "\n";
PQclear(res);
// SELECT
res = PQexec(conn.conn, "SELECT id, name, email FROM users");
if (PQresultStatus(res) != PGRES_TUPLES_OK) {
std::cerr << "SELECT 에러: " << PQerrorMessage(conn.conn) << "\n";
PQclear(res);
return 1;
}
int rows = PQntuples(res);
for (int i = 0; i < rows; ++i) {
std::cout << PQgetvalue(res, i, 0) << " | "
<< PQgetvalue(res, i, 1) << " | "
<< PQgetvalue(res, i, 2) << "\n";
}
PQclear(res);
} catch (const std::exception& e) {
std::cerr << "에러: " << e.what() << "\n";
return 1;
}
return 0;
}
PQexecParams로 파라미터 바인딩 (SQL Injection 방지)
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// PQexecParams: $1, $2 플레이스홀더에 값을 바인딩
// - SQL injection 방지
// - 타입 안전성
// - 쿼리 플랜 재사용
const char* paramValues[] = {userName.c_str(), userEmail.c_str()};
const int paramLengths[] = {static_cast<int>(userName.size()), static_cast<int>(userEmail.size())};
const int paramFormats[] = {0, 0}; // 0 = 텍스트
PGresult* res = PQexecParams(conn,
"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id",
2, nullptr, paramValues, paramLengths, paramFormats, 0);
PGresult RAII 래퍼
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// pg_result_guard.hpp
#pragma once
#include <libpq-fe.h>
#include <utility>
struct PgResultGuard {
PGresult* res = nullptr;
explicit PgResultGuard(PGresult* r) : res(r) {}
~PgResultGuard() { if (res) PQclear(res); }
PgResultGuard(const PgResultGuard&) = delete;
PgResultGuard& operator=(const PgResultGuard&) = delete;
PgResultGuard(PgResultGuard&& other) noexcept : res(std::exchange(other.res, nullptr)) {}
PGresult* get() const { return res; }
PGresult* operator->() const { return res; }
};
3. libpqxx Modern C++ 클라이언트
연결 및 기본 사용
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// libpqxx_basic.cpp
// vcpkg install libpqxx 후 컴파일
#include <pqxx/pqxx>
#include <iostream>
#include <string>
int main() {
try {
pqxx::connection conn("host=127.0.0.1 port=5432 dbname=mydb user=postgres password=postgres");
// 테이블 생성
pqxx::work w(conn);
w.exec("CREATE TABLE IF NOT EXISTS products (id SERIAL PRIMARY KEY, name VARCHAR(100), price INTEGER)");
w.commit();
// INSERT
pqxx::work w2(conn);
w2.exec_params("INSERT INTO products (name, price) VALUES ($1, $2) RETURNING id",
"상품A", 9900);
w2.commit();
// SELECT
pqxx::read_transaction r(conn);
pqxx::result res = r.exec("SELECT id, name, price FROM products");
for (auto row : res) {
std::cout << row[0].as<int>() << " | "
<< row[1].as<std::string>() << " | "
<< row[2].as<int>() << "\n";
}
} catch (const pqxx::sql_error& e) {
std::cerr << "SQL 에러: " << e.what() << "\n쿼리: " << e.query() << "\n";
return 1;
} catch (const std::exception& e) {
std::cerr << "에러: " << e.what() << "\n";
return 1;
}
return 0;
}
트랜잭션 (RAII 자동 롤백)
아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// pqxx::work: 트랜잭션. commit() 호출 시 커밋, 예외 시 자동 롤백
pqxx::work w(conn);
try {
w.exec_params("INSERT INTO orders (user_id, amount) VALUES ($1, $2)", 1, 10000);
w.exec_params("UPDATE inventory SET stock = stock - 1 WHERE product_id = $1", 1);
w.commit(); // 성공 시 커밋
} catch (...) {
// w 소멸 시 자동 ROLLBACK
throw;
}
Prepared Statement
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// Prepared Statement: 쿼리 플랜 재사용, SQL injection 방지
conn.prepare("get_user", "SELECT id, name FROM users WHERE id = $1");
conn.prepare("insert_order", "INSERT INTO orders (user_id, amount) VALUES ($1, $2) RETURNING id");
pqxx::work w(conn);
pqxx::result r = w.prepared("get_user")(user_id).exec();
pqxx::result r2 = w.prepared("insert_order")(user_id)(amount).exec();
libpq vs libpqxx 비교
| 항목 | libpq | libpqxx |
|---|---|---|
| 언어 | C | C++11/14/17 |
| 의존성 | 없음 (libpq만) | libpq |
| 트랜잭션 | 수동 BEGIN/COMMIT | pqxx::work RAII |
| 결과 처리 | PQgetvalue, 수동 | row.as |
| Prepared | PQprepare | conn.prepare() |
| 예외 | 없음 (return code) | 예외 기반 |
4. 완전한 PostgreSQL C++ 예제
예제 1: CRUD 래퍼 클래스 (libpqxx)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// user_repository.hpp
#pragma once
#include <pqxx/pqxx>
#include <optional>
#include <string>
#include <vector>
struct User {
int id;
std::string name;
std::string email;
};
class UserRepository {
public:
explicit UserRepository(pqxx::connection& conn) : conn_(conn) {}
std::optional<User> findById(int id) {
pqxx::read_transaction r(conn_);
conn_.prepare("find_user", "SELECT id, name, email FROM users WHERE id = $1");
auto res = r.prepared("find_user")(id).exec();
if (res.empty()) return std::nullopt;
auto row = res[0];
return User{
row[0].as<int>(),
row[1].as<std::string>(),
row[2].as<std::string>()
};
}
int insert(const std::string& name, const std::string& email) {
pqxx::work w(conn_);
conn_.prepare("insert_user", "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id");
auto res = w.prepared("insert_user")(name)(email).exec();
int id = res[0][0].as<int>();
w.commit();
return id;
}
bool update(int id, const std::string& name, const std::string& email) {
pqxx::work w(conn_);
conn_.prepare("update_user", "UPDATE users SET name = $1, email = $2 WHERE id = $3");
auto res = w.prepared("update_user")(name)(email)(id).exec();
bool ok = res.affected_rows() > 0;
w.commit();
return ok;
}
bool remove(int id) {
pqxx::work w(conn_);
conn_.prepare("delete_user", "DELETE FROM users WHERE id = $1");
auto res = w.prepared("delete_user")(id).exec();
bool ok = res.affected_rows() > 0;
w.commit();
return ok;
}
private:
pqxx::connection& conn_;
};
예제 2: 주문·결제 트랜잭션 (원자적 처리)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// order_service.cpp
#include <pqxx/pqxx>
#include <stdexcept>
#include <string>
struct OrderResult {
int order_id;
bool success;
std::string error_message;
};
OrderResult createOrderWithPayment(pqxx::connection& conn,
int user_id,
int product_id,
int quantity,
int amount) {
pqxx::work w(conn);
try {
conn.prepare("insert_order", "INSERT INTO orders (user_id, product_id, quantity, amount) VALUES ($1, $2, $3, $4) RETURNING id");
conn.prepare("update_inventory", "UPDATE inventory SET stock = stock - $1 WHERE product_id = $2 AND stock >= $1");
conn.prepare("insert_payment", "INSERT INTO payments (order_id, amount) VALUES ($1, $2)");
auto order_res = w.prepared("insert_order")(user_id)(product_id)(quantity)(amount).exec();
int order_id = order_res[0][0].as<int>();
auto inv_res = w.prepared("update_inventory")(quantity)(product_id).exec();
if (inv_res.affected_rows() == 0) {
throw std::runtime_error("재고 부족");
}
w.prepared("insert_payment")(order_id)(amount).exec();
w.commit();
return {order_id, true, ""};
} catch (const std::exception& e) {
// w 소멸 시 자동 ROLLBACK
return {0, false, e.what()};
}
}
예제 3: 대용량 데이터 스트리밍 (stream_from)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// bulk_insert.cpp
// libpqxx stream_from: 청크 단위로 대량 INSERT
#include <pqxx/pqxx>
#include <vector>
#include <string>
void bulkInsertProducts(pqxx::connection& conn,
const std::vector<std::pair<std::string, int>>& products) {
pqxx::work w(conn);
w.exec("CREATE TABLE IF NOT EXISTS products (id SERIAL PRIMARY KEY, name VARCHAR(100), price INTEGER)");
w.commit();
pqxx::stream_from stream(w, "products", std::vector<std::string>{"name", "price"});
for (const auto& [name, price] : products) {
stream << name << price;
}
stream.complete();
w.commit();
}
// stream_to: SELECT 결과를 스트리밍으로 읽기
void streamLargeResult(pqxx::connection& conn) {
pqxx::read_transaction r(conn);
for (auto [id, name, price] : r.stream<int, std::string, int>(
"SELECT id, name, price FROM products")) {
// 10만 건이어도 한 번에 메모리에 로드하지 않음
std::cout << id << " " << name << " " << price << "\n";
}
}
예제 4: 연결 풀 (간단한 구현)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// connection_pool.hpp
#pragma once
#include <pqxx/pqxx>
#include <condition_variable>
#include <mutex>
#include <queue>
#include <string>
class ConnectionPool {
public:
ConnectionPool(const std::string& conninfo, size_t pool_size = 10)
: conninfo_(conninfo) {
for (size_t i = 0; i < pool_size; ++i) {
pool_.push(std::make_unique<pqxx::connection>(conninfo_));
}
}
std::unique_ptr<pqxx::connection> acquire() {
std::unique_lock lock(mutex_);
cv_.wait(lock, [this] { return !pool_.empty(); });
auto conn = std::move(pool_.front());
pool_.pop();
return conn;
}
void release(std::unique_ptr<pqxx::connection> conn) {
if (!conn) return;
std::lock_guard lock(mutex_);
pool_.push(std::move(conn));
cv_.notify_one();
}
private:
std::string conninfo_;
std::queue<std::unique_ptr<pqxx::connection>> pool_;
std::mutex mutex_;
std::condition_variable cv_;
};
예제 5: 재연결 로직 (libpq)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// reconnect.cpp
#include <libpq-fe.h>
#include <chrono>
#include <iostream>
#include <thread>
PGconn* ensureConnection(PGconn* conn, const char* conninfo) {
if (conn && PQstatus(conn) == CONNECTION_OK) {
return conn;
}
if (conn) {
PQfinish(conn);
}
conn = PQconnectdb(conninfo);
if (PQstatus(conn) != CONNECTION_OK) {
std::cerr << "재연결 실패: " << PQerrorMessage(conn) << "\n";
PQfinish(conn);
return nullptr;
}
return conn;
}
// 또는 PQreset: 기존 연결 리소스 재사용
bool resetConnection(PGconn* conn) {
if (PQstatus(conn) != CONNECTION_OK) {
PQreset(conn);
return PQstatus(conn) == CONNECTION_OK;
}
return true;
}
5. 자주 발생하는 에러와 해결법
에러 1: Connection refused / Connection timed out
증상: PQconnectdb 실패, PQerrorMessage에 “Connection refused” 또는 “Connection timed out”
원인:
- PostgreSQL 서버가 실행 중이 아님
- 잘못된 호스트/포트
- 방화벽 차단
pg_hba.conf에서 클라이언트 IP 미허용 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 잘못된 설정
PgConnection conn("host=wronghost port=5432 dbname=mydb"); // 잘못된 호스트
// ✅ 연결 문자열 검증
const char* conninfo = "host=127.0.0.1 port=5432 dbname=mydb user=postgres password=postgres connect_timeout=5";
PGconn* conn = PQconnectdb(conninfo);
if (PQstatus(conn) != CONNECTION_OK) {
std::cerr << "연결 실패: " << PQerrorMessage(conn) << "\n";
PQfinish(conn);
}
# PostgreSQL 서버 확인
pg_isready -h 127.0.0.1 -p 5432
# exit 0이면 정상
에러 2: PQclear 누락으로 메모리 누수
증상: 장시간 실행 시 메모리 사용량이 계속 증가
원인: PQexec/PQexecParams가 반환하는 PGresult*를 PQclear로 해제하지 않음
다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 메모리 누수
PGresult* res = PQexec(conn, "SELECT * FROM users");
// ....사용 ...
// PQclear(res) 누락!
해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ RAII 래퍼 사용
PgResultGuard guard(PQexec(conn, "SELECT * FROM users"));
PGresult* res = guard.get();
if (PQresultStatus(res) != PGRES_TUPLES_OK) {
return;
}
// guard 소멸 시 자동 PQclear
에러 3: SQL Injection
증상: 악의적 사용자 입력으로 인해 데이터 유출 또는 삭제 원인: 사용자 입력을 문자열 연결로 쿼리에 직접 삽입 다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ SQL injection 취약
std::string query = "SELECT * FROM users WHERE name = '" + userInput + "'";
PQexec(conn, query.c_str());
// userInput = "'; DROP TABLE users; --" 이면 테이블 삭제됨
해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ PQexecParams 또는 Prepared Statement 사용
const char* paramValues[] = {userInput.c_str()};
PQexecParams(conn, "SELECT * FROM users WHERE name = $1", 1, nullptr, paramValues, nullptr, nullptr, 0);
// libpqxx
conn.prepare("get_user", "SELECT * FROM users WHERE name = $1");
r.prepared("get_user")(userInput).exec();
에러 4: too many connections
증상: (error) FATAL: sorry, too many clients already
원인: PostgreSQL max_connections 한도 초과 (기본 100)
해결법:
- 연결 풀 사용: 연결 재사용
- PgBouncer 도입: 연결 풀링 프록시
max_connections증가 (PostgreSQL 설정) 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ 연결 풀 사용
ConnectionPool pool("host=127.0.0.1 dbname=mydb user=postgres password=postgres", 10);
auto conn = pool.acquire();
// ....사용 ...
pool.release(std::move(conn));
에러 5: 트랜잭션 중 연결 끊김
증상: PQexec 실패, “connection lost” 또는 “server closed the connection”
원인: 트랜잭션 진행 중 DB 재시작이나 네트워크 끊김
해결법:
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ✅ 재시도 전 PQreset 또는 새 연결
PGresult* res = PQexec(conn, "SELECT ...");
if (res == nullptr || PQresultStatus(res) == PGRES_FATAL_ERROR) {
if (PQstatus(conn) != CONNECTION_OK) {
PQreset(conn);
if (PQstatus(conn) != CONNECTION_OK) {
// 새 연결 생성 또는 에러 반환
}
}
// 재시도
}
에러 6: NULL 값 처리
증상: PQgetvalue가 NULL 반환 시 std::stoi 등에서 크래시
원인: DB 컬럼이 NULL일 수 있는데 NULL 체크 없이 사용
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ NULL 미처리
int id = std::stoi(PQgetvalue(res, 0, 0)); // NULL이면 "NULL" 문자열이 아님, PQgetisnull 확인 필요
// ✅ NULL 체크
if (PQgetisnull(res, 0, 0)) {
// NULL 처리
} else {
int id = std::stoi(PQgetvalue(res, 0, 0));
}
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// libpqxx: row[0].is_null() 체크
if (row[0].is_null()) {
// NULL 처리
} else {
int id = row[0].as<int>();
}
에러 7: 동일 연결을 멀티스레드에서 공유
증상: 간헐적 크래시, 잘못된 결과
원인: libpq PGconn은 스레드 안전하지 않음
다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 위험
PGconn* conn = PQconnectdb(conninfo);
std::thread t1([&]() { PQexec(conn, "SELECT 1"); });
std::thread t2([&]() { PQexec(conn, "SELECT 2"); });
해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ 스레드당 연결 또는 연결 풀
void worker() {
thread_local pqxx::connection conn(conninfo);
pqxx::work w(conn);
w.exec("SELECT ...");
}
6. 성능 최적화 팁
팁 1: Prepared Statement 사용
동일 쿼리를 반복 실행할 때 Prepared Statement로 쿼리 플랜 재사용. 파싱·플랜 최적화 비용을 줄입니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 매번 파싱
for (int i = 0; i < 1000; ++i) {
w.exec_params("SELECT * FROM users WHERE id = $1", i);
}
// ✅ Prepared Statement
conn.prepare("get_user", "SELECT * FROM users WHERE id = $1");
for (int i = 0; i < 1000; ++i) {
w.prepared("get_user")(i).exec();
}
팁 2: COPY로 대량 INSERT
단일 INSERT 대신 COPY로 대량 배치 삽입 시 10~100배 빠름. 아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// libpqxx stream_to
pqxx::work w(conn);
pqxx::stream_to stream(w, "products", std::vector<std::string>{"name", "price"});
for (const auto& [name, price] : products) {
stream << name << price;
}
stream.complete();
w.commit();
팁 3: 연결 풀 사용
매 요청마다 새 연결을 만들면 TCP 핸드셰이크·인증 비용이 큽니다. 연결 풀로 재사용하세요.
// libpqxx: ConnectionPool 사용 또는 PgBouncer
팁 4: 대용량 결과는 스트리밍
10만 건 이상 조회 시 pqxx::stream 또는 stream_from 사용
아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 전체 메모리 로드
pqxx::result res = r.exec("SELECT * FROM large_table");
// 10만 건 * 1KB = 100MB 이상
// ✅ 스트리밍
for (auto [id, name] : r.stream<int, std::string>("SELECT id, name FROM large_table")) {
process(id, name);
}
팁 5: 인덱스 활용
-- 자주 조회하는 컬럼에 인덱스
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_orders_user_id ON orders(user_id);
팁 6: 배치 커밋
대량 INSERT 시 INSERT ....VALUES (...), (...), (...) 여러 행을 한 번에 처리
아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 1000건을 100건씩 묶어서 INSERT
std::string values;
for (size_t i = 0; i < batch.size(); i += 100) {
values.clear();
for (size_t j = i; j < std::min(i + 100, batch.size()); ++j) {
if (j > i) values += ",";
values += "('" + escape(batch[j].name) + "'," + std::to_string(batch[j].price) + ")";
}
w.exec("INSERT INTO products (name, price) VALUES " + values);
}
w.commit();
7. 프로덕션 패턴
패턴 1: Health Check 및 재연결
다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
bool isConnectionHealthy(pqxx::connection& conn) {
try {
pqxx::nontransaction n(conn);
n.exec("SELECT 1");
return true;
} catch (...) {
return false;
}
}
void ensureConnection(pqxx::connection& conn, const std::string& conninfo) {
if (!isConnectionHealthy(conn)) {
conn.close();
conn = pqxx::connection(conninfo);
}
}
패턴 2: 설정 외부화
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
struct PgConfig {
std::string host = "127.0.0.1";
int port = 5432;
std::string dbname = "mydb";
std::string user = "postgres";
std::string password;
};
PgConfig loadFromEnv() {
PgConfig c;
if (const char* h = std::getenv("PGHOST")) c.host = h;
if (const char* p = std::getenv("PGPORT")) c.port = std::stoi(p);
if (const char* d = std::getenv("PGDATABASE")) c.dbname = d;
if (const char* u = std::getenv("PGUSER")) c.user = u;
if (const char* pw = std::getenv("PGPASSWORD")) c.password = pw;
return c;
}
std::string toConnectionString(const PgConfig& c) {
return "host=" + c.host + " port=" + std::to_string(c.port) +
" dbname=" + c.dbname + " user=" + c.user +
" password=" + c.password;
}
패턴 3: 재시도 로직 (지수 백오프)
다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
template <typename Func>
auto retryWithBackoff(Func&& f, int max_retries = 3) {
for (int i = 0; i < max_retries; ++i) {
try {
return f();
} catch (const pqxx::sql_error& e) {
if (i == max_retries - 1) throw;
std::this_thread::sleep_for(std::chrono::milliseconds(100 * (1 << i)));
}
}
throw std::runtime_error("재시도 실패");
}
// 사용
retryWithBackoff([&]() {
pqxx::work w(conn);
w.exec("INSERT INTO ...");
w.commit();
});
패턴 4: 로깅 및 모니터링
아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class LoggingConnection {
public:
pqxx::result exec(const std::string& query) {
auto start = std::chrono::steady_clock::now();
auto res = conn_.exec(query);
auto elapsed = std::chrono::steady_clock::now() - start;
log("Query: " + query + " elapsed: " + std::to_string(elapsed.count()) + "ms");
return res;
}
private:
pqxx::connection conn_;
};
패턴 5: 트랜잭션 격리 수준
다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// READ COMMITTED (기본) | REPEATABLE READ | SERIALIZABLE
pqxx::work w(conn);
w.exec("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ");
w.commit();
8. 구현 체크리스트
환경 설정
- PostgreSQL 서버 실행 확인 (
pg_isready) - libpq 또는 libpqxx 설치
- CMake/vcpkg 연동
연결 및 기본 사용
-
PQconnectdb또는pqxx::connection으로 연결 - RAII로
PGconn/PGresult관리 -
PQclear누락 없이 호출 (libpq)
에러 처리
-
PQstatus(conn) != CONNECTION_OK체크 -
PQresultStatus(res)체크 -
pqxx::sql_error예외 처리
보안
- Prepared Statement 또는
PQexecParams사용 (SQL injection 방지) - 비밀번호 환경 변수 사용
성능
- 연결 풀 또는 스레드당 연결
- Prepared Statement로 반복 쿼리 최적화
- 대용량 조회 시 스트리밍
프로덕션
- Health Check 주기적 수행, 재연결·재시도 정책
정리
| 항목 | libpq | libpqxx |
|---|---|---|
| 용도 | C 호환, 경량, 임베디드 | Modern C++, 풍부한 API |
| 연결 | PGconn 직접 관리 | pqxx::connection |
| 트랜잭션 | 수동 BEGIN/COMMIT | pqxx::work RAII |
| 에러 | return code | 예외 기반 |
| 권장 | 레거시, 최소 의존성 | 신규 프로젝트 |
| 핵심 원칙: |
- RAII로 연결·결과 관리
- Prepared Statement로 SQL injection 방지
- 멀티스레드에서는 연결 풀 또는 스레드당 연결
- 트랜잭션은
pqxx::work로 자동 롤백 보장 다음 글 Redis C++(#52-2)에서는 캐싱, 세션, 분산락을 다룹니다.