feat: DB 커넥션 풀 기능 추가

TODO: 동시에 여러 DB 연결 확인해야함. 현재 기능만 확인 완료
This commit is contained in:
bumpsoo 2026-02-09 14:00:15 +00:00
parent 31b4763bc3
commit 4d60403adf
2 changed files with 133 additions and 93 deletions

View file

@ -39,18 +39,38 @@ public:
boost::asio::awaitable<RankingResult> GetRanking(int type,
std::string nickname);
// 내부용 객체
class PooledConnection {
public:
PooledConnection(DatabaseManager &manager,
std::unique_ptr<boost::mysql::tcp_ssl_connection> conn);
~PooledConnection();
boost::mysql::tcp_ssl_connection &Get();
boost::mysql::tcp_ssl_connection *operator->();
private:
DatabaseManager &manager_;
std::unique_ptr<boost::mysql::tcp_ssl_connection> conn_;
};
boost::asio::awaitable<std::unique_ptr<PooledConnection>> GetConnection();
private:
DatabaseManager() = default;
void ReturnConnection(std::unique_ptr<boost::mysql::tcp_ssl_connection> conn);
// 비동기 Mutex 역할을 할 함수들
boost::asio::awaitable<void> Lock();
void Unlock();
// DB 접속 정보 저장용 (새로운 커넥션 생성 시 필요)
std::string host_, user_, password_, db_;
uint16_t port_;
std::unique_ptr<boost::mysql::tcp_ssl_connection> conn_;
std::unique_ptr<boost::asio::strand<boost::asio::io_context::executor_type>>
strand_;
// 비동기 락 시스템을 위한 상태 변수
bool is_locked_ = false;
// 풀 상태 관리
std::vector<std::unique_ptr<boost::mysql::tcp_ssl_connection>>
available_conns_;
std::queue<std::shared_ptr<boost::asio::steady_timer>> waiting_queue_;
const uint16_t max_pool_size_ = 10;
int16_t current_total_conns_ = 0;
};

View file

@ -1,8 +1,27 @@
#include "DatabaseManager.h"
#include "Logger.h"
#include <boost/asio/bind_executor.hpp>
#include <boost/asio/post.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/mysql.hpp>
DatabaseManager::PooledConnection::PooledConnection(
DatabaseManager &manager,
std::unique_ptr<boost::mysql::tcp_ssl_connection> conn)
: manager_(manager), conn_(std::move(conn)) {}
DatabaseManager::PooledConnection::~PooledConnection() {
if (conn_)
manager_.ReturnConnection(std::move(conn_));
}
boost::mysql::tcp_ssl_connection &DatabaseManager::PooledConnection::Get() {
return *conn_;
}
boost::mysql::tcp_ssl_connection *
DatabaseManager::PooledConnection::operator->() {
return conn_.get();
}
DatabaseManager &DatabaseManager::GetInstance() {
static DatabaseManager instance;
@ -13,43 +32,58 @@ bool DatabaseManager::Init(boost::asio::io_context &io_context,
const std::string &host, uint16_t port,
const std::string &user, const std::string &password,
const std::string &db) {
try {
host_ = host;
port_ = port;
user_ = user;
password_ = password;
db_ = db;
strand_ = std::make_unique<
boost::asio::strand<boost::asio::io_context::executor_type>>(
io_context.get_executor());
boost::asio::ip::tcp::resolver resolver(io_context.get_executor());
auto endpoints = resolver.resolve(host, std::to_string(port));
boost::mysql::handshake_params params(user, password, db);
static boost::asio::ssl::context ssl_ctx(
boost::asio::ssl::context::tlsv12_client);
conn_ = std::make_unique<boost::mysql::tcp_ssl_connection>(
io_context.get_executor(), ssl_ctx);
conn_->connect(*endpoints.begin(), params);
Logger::Log("Database 연결 성공 (AsyncLock 적용): ", host, ":", port);
Logger::Log("DatabaseManager Init 성공 (Connection Pool 준비됨): ", host, ":",
port);
return true;
} catch (const std::exception &e) {
Logger::Log("Database 연결 실패: ", e.what());
return false;
}
}
// 비동기 락 획득 (이미 락이 걸려있으면 대기열에 들어감)
boost::asio::awaitable<void> DatabaseManager::Lock() {
boost::asio::awaitable<std::unique_ptr<DatabaseManager::PooledConnection>>
DatabaseManager::GetConnection() {
auto executor = co_await boost::asio::this_coro::executor;
co_await boost::asio::post(
boost::asio::bind_executor(*strand_, boost::asio::use_awaitable));
if (!is_locked_) {
is_locked_ = true;
co_return;
}
std::unique_ptr<boost::mysql::tcp_ssl_connection> conn;
// 이미 잠겨있으면 타이머를 만들어서 대기열에 추가 (비동기 대기)
if (!available_conns_.empty()) {
// 가져오기
conn = std::move(available_conns_.back());
available_conns_.pop_back();
} else if (current_total_conns_ < max_pool_size_) {
// 생성
current_total_conns_++;
try {
static boost::asio::ssl::context ssl_ctx(
boost::asio::ssl::context::tlsv12_client);
conn =
std::make_unique<boost::mysql::tcp_ssl_connection>(executor, ssl_ctx);
boost::asio::ip::tcp::resolver resolver(executor);
auto endpoints = resolver.resolve(host_, std::to_string(port_));
boost::mysql::handshake_params params(user_, password_, db_);
co_await conn->async_connect(*endpoints.begin(), params,
boost::asio::use_awaitable);
Logger::Log("새로운 DB 커넥션 생성됨. 현재 총 개수: ",
current_total_conns_);
} catch (const std::exception &e) {
current_total_conns_--;
Logger::Log("커넥션 생성 실패: ", e.what());
throw;
}
} else {
// 대기
auto timer = std::make_shared<boost::asio::steady_timer>(
executor, std::chrono::steady_clock::time_point::max());
waiting_queue_.push(timer);
@ -57,53 +91,53 @@ boost::asio::awaitable<void> DatabaseManager::Lock() {
try {
co_await timer->async_wait(boost::asio::use_awaitable);
} catch (...) {
// 타이머 취소(Unlock에서 호출함)되면 성공적으로 락을 획득한 것으로 간주
}
co_return;
co_return co_await GetConnection();
}
co_return std::make_unique<PooledConnection>(*this, std::move(conn));
}
// 비동기 락 해제 (대기열에 다음 사람이 있으면 깨워줌)
void DatabaseManager::Unlock() {
boost::asio::dispatch(*strand_, [this]() {
if (waiting_queue_.empty()) {
is_locked_ = false;
} else {
void DatabaseManager::ReturnConnection(
std::unique_ptr<boost::mysql::tcp_ssl_connection> conn) {
boost::asio::dispatch(*strand_, [this, conn = std::move(conn)]() mutable {
if (!waiting_queue_.empty()) {
// 대기 중이면 넣어주기
available_conns_.push_back(std::move(conn));
auto next = waiting_queue_.front();
waiting_queue_.pop();
next->cancel();
} else {
// 없으면 보관
available_conns_.push_back(std::move(conn));
}
});
}
boost::asio::awaitable<DatabaseManager::UserData>
DatabaseManager::LoadUser(std::string nickname) {
co_await Lock();
try {
if (!conn_) {
Unlock();
co_return UserData{nickname, 10000, 0};
}
auto conn_ptr = co_await GetConnection();
auto &conn = conn_ptr->Get();
boost::mysql::results result;
boost::mysql::statement stmt = co_await conn_->async_prepare_statement(
boost::mysql::statement stmt = co_await conn.async_prepare_statement(
"SELECT username, gold, sword_level, max_sword_level FROM users WHERE "
"username = ?",
boost::asio::use_awaitable);
co_await conn_->async_execute(stmt.bind(nickname), result,
co_await conn.async_execute(stmt.bind(nickname), result,
boost::asio::use_awaitable);
if (result.rows().empty()) {
boost::mysql::statement ins_stmt =
co_await conn_->async_prepare_statement(
"INSERT INTO users (username, gold, sword_level) VALUES (?, "
"10000, 0)",
boost::mysql::statement ins_stmt = co_await conn.async_prepare_statement(
"INSERT INTO users (username, gold, sword_level) VALUES (?, 10000, "
"0)",
boost::asio::use_awaitable);
co_await conn_->async_execute(ins_stmt.bind(nickname), result,
co_await conn.async_execute(ins_stmt.bind(nickname), result,
boost::asio::use_awaitable);
Logger::Log("새로운 유저 탄생 (DB): ", nickname);
Unlock();
co_return UserData{nickname, 10000, 0};
co_return UserData{nickname, 10000, 0, 0};
} else {
auto row = result.rows().at(0);
UserData data;
@ -111,56 +145,44 @@ DatabaseManager::LoadUser(std::string nickname) {
data.gold = row.at(1).as_uint64();
data.swordLevel = (uint32_t)row.at(2).as_int64();
data.maxSwordLevel = (uint32_t)row.at(3).as_int64();
Unlock();
co_return data;
}
} catch (const std::exception &e) {
Logger::Log("LoadUser 에러: ", e.what());
Unlock();
co_return UserData{nickname, 10000, 0};
co_return UserData{nickname, 10000, 0, 0};
}
}
boost::asio::awaitable<void> DatabaseManager::SaveUser(std::string nickname,
uint64_t gold,
uint32_t swordLevel) {
co_await Lock();
try {
if (!conn_) {
Unlock();
co_return;
}
auto conn_ptr = co_await GetConnection();
auto &conn = conn_ptr->Get();
boost::mysql::results result;
boost::mysql::statement stmt = co_await conn_->async_prepare_statement(
boost::mysql::statement stmt = co_await conn.async_prepare_statement(
"UPDATE users SET gold = ?, sword_level = ?, max_sword_level = "
"GREATEST(max_sword_level, ?) WHERE username = ?",
boost::asio::use_awaitable);
co_await conn_->async_execute(
co_await conn.async_execute(
stmt.bind(gold, (int64_t)swordLevel, (int64_t)swordLevel, nickname),
result, boost::asio::use_awaitable);
} catch (const std::exception &e) {
Logger::Log("SaveUser 에러: ", e.what());
}
Unlock();
co_return;
}
boost::asio::awaitable<DatabaseManager::RankingResult>
DatabaseManager::GetRanking(int type, std::string nickname) {
co_await Lock();
RankingResult res;
res.myRank = -1;
res.myValue = 0;
try {
if (!conn_) {
Unlock();
co_return res;
}
auto conn_ptr = co_await GetConnection();
auto &conn = conn_ptr->Get();
std::string columnName = (type == 0) ? "max_sword_level" : "gold";
@ -168,7 +190,7 @@ DatabaseManager::GetRanking(int type, std::string nickname) {
std::string topQuery = "SELECT username, " + columnName +
" FROM users ORDER BY " + columnName +
" DESC LIMIT 10";
co_await conn_->async_execute(topQuery, topResult,
co_await conn.async_execute(topQuery, topResult,
boost::asio::use_awaitable);
uint32_t rank = 1;
@ -176,7 +198,6 @@ DatabaseManager::GetRanking(int type, std::string nickname) {
RankingResult::Entry entry;
entry.rank = rank++;
entry.nickname = row.at(0).as_string();
// DB 타입이 달라 분기처리..
if (type == 0) {
entry.value = (uint64_t)row.at(1).as_int64();
} else {
@ -190,9 +211,9 @@ DatabaseManager::GetRanking(int type, std::string nickname) {
"SELECT " + columnName + ", (SELECT COUNT(*) + 1 FROM users WHERE " +
columnName + " > u." + columnName + ") FROM users u WHERE username = ?";
boost::mysql::statement stmt = co_await conn_->async_prepare_statement(
boost::mysql::statement stmt = co_await conn.async_prepare_statement(
myQuery, boost::asio::use_awaitable);
co_await conn_->async_execute(stmt.bind(nickname), myResult,
co_await conn.async_execute(stmt.bind(nickname), myResult,
boost::asio::use_awaitable);
if (!myResult.rows().empty()) {
@ -208,6 +229,5 @@ DatabaseManager::GetRanking(int type, std::string nickname) {
Logger::Log("GetRanking 에러: ", e.what());
}
Unlock();
co_return res;
}