C++ 게임 서버에서 MariaDB를 연동해 순위 시스템 구현하기

도입 문구


C++로 게임 서버를 개발할 때, 실시간 순위 시스템을 구축하는 것은 매우 중요한 요소입니다. 본 기사에서는 C++와 MariaDB를 사용하여 효율적인 순위 시스템을 구현하는 방법을 다룹니다.

C++ 게임 서버와 MariaDB 연동 개요


게임 서버는 사용자 데이터, 게임 진행 상황, 순위 등을 실시간으로 처리해야 합니다. 이러한 데이터를 효율적으로 관리하기 위해 MariaDB와 같은 관계형 데이터베이스를 사용하면 많은 장점을 얻을 수 있습니다. MariaDB는 높은 성능과 안정성을 제공하며, SQL 쿼리를 통해 빠르게 데이터를 조회하고 수정할 수 있습니다.

C++는 높은 성능을 요구하는 게임 서버 개발에 적합한 언어로, 데이터베이스와의 연동을 통해 다양한 게임 기능을 구현할 수 있습니다. 본 기사에서는 C++에서 MariaDB를 어떻게 연동하고, 게임의 순위 시스템을 어떻게 구성할 수 있는지에 대해 설명합니다.

MariaDB 설치 및 설정 방법

MariaDB를 게임 서버와 연동하기 위해 먼저 데이터베이스를 설치하고 설정하는 과정이 필요합니다. 여기서는 리눅스 환경(Ubuntu 기준)과 Windows 환경에서의 설치 방법을 설명합니다.

Ubuntu에서 MariaDB 설치

  1. 패키지 리스트를 업데이트합니다.
   sudo apt update
  1. MariaDB를 설치합니다.
   sudo apt install mariadb-server
  1. 설치 완료 후, MariaDB 서비스가 실행 중인지 확인합니다.
   sudo systemctl status mariadb
  1. 보안 설정을 실행하여 기본적인 보안 조치를 적용합니다.
   sudo mysql_secure_installation
  • 관리자(root) 비밀번호 설정
  • 원격 접속 차단 여부 설정
  • 불필요한 익명 사용자 및 테스트 데이터베이스 삭제

Windows에서 MariaDB 설치

  1. MariaDB 공식 웹사이트에서 최신 버전의 설치 파일을 다운로드합니다.
  2. 설치 프로그램을 실행하고, 기본 설정을 유지하며 설치를 진행합니다.
  3. 설치 과정에서 root 비밀번호를 설정합니다.
  4. 설치 완료 후, MariaDB를 실행하고 다음 명령어로 정상 작동 여부를 확인합니다.
   mysql -u root -p

MariaDB 사용자 및 데이터베이스 설정


게임 서버에서 사용할 데이터베이스와 계정을 생성합니다.

  1. MariaDB에 접속합니다.
   mysql -u root -p
  1. 게임 데이터베이스 생성
   CREATE DATABASE game_ranking;
  1. 게임 서버에서 사용할 사용자 계정 생성 및 권한 부여
   CREATE USER 'game_user'@'%' IDENTIFIED BY 'password';
   GRANT ALL PRIVILEGES ON game_ranking.* TO 'game_user'@'%';
   FLUSH PRIVILEGES;

이제 게임 서버에서 MariaDB를 사용할 준비가 완료되었습니다. 다음 단계에서는 C++에서 MariaDB에 연결하는 방법을 다룹니다.

C++에서 MariaDB 라이브러리 사용하기

C++에서 MariaDB와의 연결을 위해 MariaDB Connector/C 라이브러리를 사용합니다. 이 라이브러리는 MySQL과도 호환되며, MariaDB에 직접 연결할 수 있도록 지원합니다.

MariaDB Connector/C 설치

Ubuntu에서 설치

sudo apt update
sudo apt install libmariadb-dev

Windows에서 설치

  1. MariaDB 공식 웹사이트에서 MariaDB Connector/C를 다운로드합니다.
  2. 설치 후, libmariadb.dll을 프로젝트의 실행 파일과 같은 디렉토리에 복사합니다.

C++ 프로젝트에서 MariaDB 사용 설정

MariaDB에 연결하려면 mariadb/mysql.h 헤더를 포함하고, libmariadb 라이브러리를 링크해야 합니다.

CMake 예제 (Linux 환경)

cmake_minimum_required(VERSION 3.10)
project(GameServer)

find_package(PkgConfig REQUIRED)
pkg_check_modules(MARIADB REQUIRED mariadb)

add_executable(game_server main.cpp)
target_include_directories(game_server PRIVATE ${MARIADB_INCLUDE_DIRS})
target_link_libraries(game_server PRIVATE ${MARIADB_LIBRARIES})

MariaDB 연결 코드 (C++ 예제)

다음은 MariaDB와 C++을 연동하는 기본적인 코드입니다.

#include <iostream>
#include <mariadb/mysql.h>

int main() {
    MYSQL *conn = mysql_init(nullptr);

    if (conn == nullptr) {
        std::cerr << "MySQL 초기화 실패" << std::endl;
        return 1;
    }

    // 데이터베이스 연결 설정
    if (!mysql_real_connect(conn, "localhost", "game_user", "password", "game_ranking", 3306, nullptr, 0)) {
        std::cerr << "MySQL 연결 실패: " << mysql_error(conn) << std::endl;
        mysql_close(conn);
        return 1;
    }

    std::cout << "MariaDB 연결 성공!" << std::endl;

    mysql_close(conn);
    return 0;
}

컴파일 및 실행

Linux에서 g++을 사용하여 컴파일하는 방법:

g++ -o game_server main.cpp -lmariadb

Windows에서는 Visual Studio에서 libmariadb.lib를 프로젝트에 추가하여 빌드할 수 있습니다.

이제 C++ 프로그램에서 MariaDB에 연결할 수 있습니다. 다음 단계에서는 순위 테이블을 설계하는 방법을 다룹니다.

순위 테이블 설계

게임의 순위 시스템을 효과적으로 구축하려면, 데이터베이스 테이블을 효율적으로 설계하는 것이 중요합니다. 높은 성능을 유지하면서도 빠르게 순위를 조회할 수 있도록 테이블을 구성해야 합니다.

순위 테이블 기본 구조

게임의 순위를 저장하는 테이블에는 일반적으로 다음과 같은 필드가 포함됩니다.

  • id (INT, PRIMARY KEY, AUTO_INCREMENT): 각 플레이어의 고유한 식별자
  • player_name (VARCHAR): 플레이어 이름
  • score (INT): 플레이어의 점수
  • timestamp (DATETIME): 점수가 기록된 시간

다음은 MySQL/MariaDB에서 사용할 수 있는 기본적인 순위 테이블입니다.

CREATE TABLE ranking (
    id INT AUTO_INCREMENT PRIMARY KEY,
    player_name VARCHAR(50) NOT NULL,
    score INT NOT NULL,
    timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
);

인덱스 최적화

게임 서버에서는 점수를 기준으로 빠르게 순위를 조회해야 하므로, score 필드에 인덱스를 추가하여 검색 성능을 향상시킬 수 있습니다.

CREATE INDEX idx_score ON ranking (score DESC);

이렇게 하면 점수를 내림차순으로 정렬하는 쿼리의 성능이 향상됩니다.

데이터 입력 예제

게임 서버에서 새로운 점수를 기록할 때 다음과 같은 쿼리를 사용할 수 있습니다.

INSERT INTO ranking (player_name, score) VALUES ('Player1', 5000);
INSERT INTO ranking (player_name, score) VALUES ('Player2', 7200);
INSERT INTO ranking (player_name, score) VALUES ('Player3', 6200);

순위 조회 쿼리

최고 점수를 기준으로 상위 10명의 순위를 조회하는 SQL 문은 다음과 같습니다.

SELECT player_name, score FROM ranking ORDER BY score DESC LIMIT 10;

이제 C++ 게임 서버에서 이 테이블을 활용하여 순위를 업데이트하고 조회하는 방법을 다음 단계에서 다룹니다.

순위 업데이트 및 쿼리 처리

게임 서버에서 순위를 실시간으로 업데이트하고 특정 조건에 맞는 순위를 효율적으로 조회하는 방법은 성능과 안정성에 큰 영향을 미칩니다. 이 섹션에서는 순위 업데이트와 관련된 SQL 쿼리 및 C++에서 이를 처리하는 방법을 설명합니다.

순위 업데이트 쿼리

게임 중에 플레이어의 점수가 바뀔 때마다 순위를 업데이트해야 합니다. 이를 위해 UPDATE 쿼리나 INSERT 쿼리를 사용할 수 있습니다. 만약 특정 플레이어의 점수를 업데이트하려면 다음과 같은 쿼리를 사용할 수 있습니다.

UPDATE ranking SET score = 6000 WHERE player_name = 'Player1';

또는 새로운 점수를 추가하는 경우, 새로운 레코드를 INSERT 할 수 있습니다.

INSERT INTO ranking (player_name, score) VALUES ('Player4', 6800);

점수에 따른 순위 업데이트

게임 서버에서는 점수 변경이 발생할 때마다 순위가 실시간으로 반영되어야 합니다. 일반적으로 게임 서버에서는 트랜잭션을 사용하여 데이터의 일관성을 유지합니다. 트랜잭션을 사용하면 여러 명령이 동시에 실행될 때 데이터베이스의 무결성을 보장할 수 있습니다.

트랜잭션 예제 (C++에서 사용)

mysql_query(conn, "START TRANSACTION");

mysql_query(conn, "UPDATE ranking SET score = 6500 WHERE player_name = 'Player2'");
mysql_query(conn, "INSERT INTO ranking (player_name, score) VALUES ('Player5', 7100)");

mysql_query(conn, "COMMIT");

순위 조회 쿼리

실시간으로 순위를 조회하려면 점수를 내림차순으로 정렬하여 상위 N명의 플레이어를 선택할 수 있습니다. 예를 들어, 상위 10명의 순위를 조회하는 SQL 쿼리는 다음과 같습니다.

SELECT player_name, score FROM ranking ORDER BY score DESC LIMIT 10;

C++에서는 이 쿼리를 실행하고 결과를 처리하는 방식으로 순위를 출력할 수 있습니다.

C++ 코드 예제 – 순위 업데이트 및 조회

다음은 C++에서 MariaDB를 사용하여 순위를 업데이트하고 조회하는 예제입니다.

#include <iostream>
#include <mariadb/mysql.h>

void updateRanking(MYSQL* conn, const std::string& playerName, int score) {
    std::string query = "UPDATE ranking SET score = " + std::to_string(score) + " WHERE player_name = '" + playerName + "';";
    if (mysql_query(conn, query.c_str())) {
        std::cerr << "UPDATE 오류: " << mysql_error(conn) << std::endl;
    }
}

void insertNewPlayer(MYSQL* conn, const std::string& playerName, int score) {
    std::string query = "INSERT INTO ranking (player_name, score) VALUES ('" + playerName + "', " + std::to_string(score) + ");";
    if (mysql_query(conn, query.c_str())) {
        std::cerr << "INSERT 오류: " << mysql_error(conn) << std::endl;
    }
}

void getTopRankings(MYSQL* conn) {
    if (mysql_query(conn, "SELECT player_name, score FROM ranking ORDER BY score DESC LIMIT 10;")) {
        std::cerr << "SELECT 오류: " << mysql_error(conn) << std::endl;
        return;
    }

    MYSQL_RES* result = mysql_store_result(conn);
    if (result == nullptr) {
        std::cerr << "결과 저장 실패: " << mysql_error(conn) << std::endl;
        return;
    }

    MYSQL_ROW row;
    while ((row = mysql_fetch_row(result))) {
        std::cout << row[0] << " - " << row[1] << std::endl;
    }

    mysql_free_result(result);
}

int main() {
    MYSQL* conn = mysql_init(nullptr);
    if (!mysql_real_connect(conn, "localhost", "game_user", "password", "game_ranking", 3306, nullptr, 0)) {
        std::cerr << "MySQL 연결 실패: " << mysql_error(conn) << std::endl;
        return 1;
    }

    updateRanking(conn, "Player1", 8000);
    insertNewPlayer(conn, "Player6", 7000);
    getTopRankings(conn);

    mysql_close(conn);
    return 0;
}

성능 최적화 고려 사항

  • 배치 처리: 게임 서버에서는 순위 업데이트가 빈번하게 발생할 수 있습니다. 이럴 때는 한 번에 여러 업데이트를 처리하는 배치 작업을 고려하는 것이 좋습니다.
  • 인덱스: score 필드에 인덱스를 추가하여 순위를 조회할 때의 성능을 향상시킬 수 있습니다.
  • 쿼리 최적화: 데이터베이스에서 효율적인 순위 조회를 위해 LIMIT, OFFSET, JOIN 등을 잘 활용해야 합니다.

이제 실시간으로 순위를 업데이트하고 조회하는 방법에 대해 다뤘습니다. 다음 단계에서는 성능 최적화 방법을 다룰 것입니다.

성능 최적화 및 확장성 고려

게임 서버에서 순위 시스템을 구축할 때 성능과 확장성은 매우 중요한 요소입니다. 대규모 사용자 환경에서는 데이터베이스의 성능이 게임의 전반적인 반응 속도와 안정성에 영향을 미칠 수 있습니다. 본 섹션에서는 MariaDB를 사용하는 순위 시스템에서 성능을 최적화하고, 확장성을 고려한 방법을 소개합니다.

1. 인덱스 최적화

순위 테이블에서 scoretimestamp 필드는 자주 조회되기 때문에 인덱스를 추가하여 쿼리 성능을 최적화할 수 있습니다. 특히, score에 인덱스를 추가하면 내림차순으로 정렬된 순위를 빠르게 조회할 수 있습니다.

CREATE INDEX idx_score ON ranking (score DESC);
CREATE INDEX idx_timestamp ON ranking (timestamp);

점수와 시간을 기준으로 순위를 조회하는 쿼리가 자주 사용되므로, 이 두 필드에 인덱스를 추가하면 성능을 크게 향상시킬 수 있습니다.

2. 파티셔닝 (Partitioning)

게임의 순위 데이터는 시간이 지남에 따라 계속 증가합니다. 이러한 상황에서 MariaDB의 파티셔닝 기능을 활용하여 데이터를 여러 파티션으로 나누면 쿼리 성능을 개선할 수 있습니다. 예를 들어, 점수에 따라 데이터를 파티셔닝하거나, 날짜를 기준으로 분리하는 방법을 사용할 수 있습니다.

CREATE TABLE ranking (
    id INT AUTO_INCREMENT PRIMARY KEY,
    player_name VARCHAR(50) NOT NULL,
    score INT NOT NULL,
    timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
PARTITION BY RANGE (YEAR(timestamp)) (
    PARTITION p2023 VALUES LESS THAN (2024),
    PARTITION p2024 VALUES LESS THAN (2025)
);

이렇게 날짜별로 파티셔닝하면 오래된 데이터를 효율적으로 관리할 수 있고, 최신 데이터를 빠르게 조회할 수 있습니다.

3. 캐싱 (Caching)

순위 데이터를 자주 조회하는 경우, 캐싱을 사용하여 데이터베이스에 대한 부하를 줄일 수 있습니다. Redis와 같은 인메모리 데이터베이스를 사용하여 자주 조회되는 상위 순위 데이터를 캐시하고, 실제 데이터베이스에는 변동이 있을 때만 갱신하도록 할 수 있습니다.

// 예시: Redis 캐시에서 상위 순위 조회
redisContext *c = redisConnect("127.0.0.1", 6379);
redisReply *reply = redisCommand(c, "GET top_rankings");
if (reply->type == REDIS_REPLY_STRING) {
    // 캐시에서 순위 데이터가 있으면 사용
    std::cout << "캐시에서 순위 데이터: " << reply->str << std::endl;
} else {
    // 캐시가 없으면 데이터베이스에서 조회 후 캐시 갱신
}

캐싱은 데이터베이스의 부하를 줄이고, 더 빠른 응답을 제공할 수 있는 유효한 방법입니다.

4. 비동기 처리

게임 서버에서 순위 시스템은 실시간으로 점수를 업데이트하고 조회해야 합니다. 이를 위해 비동기 처리를 도입하여 데이터베이스와의 연결을 기다리는 시간을 최소화할 수 있습니다. C++에서는 std::async를 사용하여 비동기적으로 데이터베이스 쿼리를 실행할 수 있습니다.

#include <future>

// 비동기적으로 순위 업데이트
std::future<void> future = std::async(std::launch::async, [&]() {
    updateRanking(conn, "Player1", 9000);
});

비동기 처리로 여러 사용자 요청을 동시에 처리할 수 있으며, 서버의 응답 시간을 단축시킬 수 있습니다.

5. 읽기/쓰기 분리

게임 서버에서 순위 시스템의 경우, 읽기 작업(순위 조회)이 훨씬 더 빈번하게 발생합니다. 이를 위해 읽기 전용 복제본을 만들어 쓰기 작업은 주 데이터베이스에, 읽기 작업은 복제본에서 처리하도록 구성할 수 있습니다. 이를 통해 데이터베이스의 부하를 분산시킬 수 있습니다.

6. 클러스터링

게임 서버가 성장함에 따라 데이터베이스를 클러스터링하여 여러 서버에서 데이터를 분산 처리할 수 있습니다. MariaDB의 Galera Cluster를 사용하면 데이터베이스의 고가용성을 보장하고, 여러 서버에서 동시에 데이터를 처리할 수 있습니다. 클러스터링을 통해 확장성도 확보할 수 있습니다.

7. 쿼리 최적화

데이터베이스 쿼리는 성능에 큰 영향을 미칩니다. 순위 시스템에서 자주 사용되는 ORDER BY score DESC LIMIT N 쿼리 같은 경우, 쿼리 실행 계획을 최적화하고 인덱스를 적절히 사용하여 성능을 높일 수 있습니다. 또한, 복잡한 JOIN이나 서브쿼리 사용을 최소화하고, 쿼리의 실행 시간을 분석하여 불필요한 부분을 개선해야 합니다.

성능 모니터링 및 튜닝

MariaDB에서는 쿼리 실행 시간을 확인하고, 인덱스 사용 현황, 데이터베이스 상태 등을 모니터링할 수 있는 도구를 제공합니다. 이를 통해 병목 지점을 찾고, 성능을 개선할 수 있습니다. 예를 들어, EXPLAIN 명령어를 사용하여 쿼리 실행 계획을 분석할 수 있습니다.

EXPLAIN SELECT player_name, score FROM ranking ORDER BY score DESC LIMIT 10;

이 명령어를 통해 쿼리 실행 시 MariaDB가 어떻게 데이터를 처리하는지 확인하고, 최적화할 수 있습니다.

결론

게임 서버의 순위 시스템에서 성능 최적화는 매우 중요합니다. 적절한 인덱스 추가, 파티셔닝 기법, 캐싱, 비동기 처리, 읽기/쓰기 분리 등 다양한 최적화 방법을 사용하여 성능을 개선할 수 있습니다. 또한, 클러스터링과 쿼리 최적화를 통해 데이터베이스의 확장성도 확보할 수 있습니다. 이러한 최적화 기법들을 적절히 활용하면, 게임 서버의 안정성 및 반응 속도를 크게 향상시킬 수 있습니다.

트랜잭션을 활용한 데이터 무결성 보장

게임의 순위 시스템에서는 여러 플레이어가 동시에 점수를 기록하거나 갱신하는 경우가 많습니다. 이를 안전하게 처리하려면 트랜잭션(Transaction) 을 활용하여 데이터 무결성을 보장해야 합니다.

트랜잭션이 필요한 이유


트랜잭션을 사용하지 않으면 여러 개의 쿼리가 실행되는 도중 일부만 실행되고 실패할 가능성이 있습니다. 예를 들어, 플레이어의 점수를 기록하는 도중 네트워크 오류가 발생하면 일부 데이터만 업데이트될 수 있습니다. 트랜잭션을 사용하면 모든 작업이 원자적으로 처리되므로, 하나의 작업이 실패하면 전체 작업을 롤백(Rollback)하여 데이터 일관성을 유지할 수 있습니다.

MariaDB에서 트랜잭션 사용

MariaDB에서 트랜잭션을 활성화하려면 InnoDB 스토리지 엔진을 사용해야 합니다. 기본적으로 MyISAM 엔진은 트랜잭션을 지원하지 않으므로, 테이블을 생성할 때 다음과 같이 InnoDB를 명시해야 합니다.

CREATE TABLE ranking (
    id INT AUTO_INCREMENT PRIMARY KEY,
    player_name VARCHAR(50) NOT NULL,
    score INT NOT NULL,
    timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

트랜잭션 적용 예제

다음은 트랜잭션을 활용하여 점수를 업데이트하는 SQL 코드입니다.

START TRANSACTION;

UPDATE ranking SET score = 7500 WHERE player_name = 'Player1';

-- 오류 발생 시 롤백 가능
COMMIT;

만약 중간에 오류가 발생하면 ROLLBACK; 명령어를 실행하여 모든 변경 사항을 취소할 수 있습니다.

ROLLBACK;

C++ 코드에서 트랜잭션 적용

다음은 C++에서 MariaDB와 연동하여 트랜잭션을 수행하는 코드입니다.

#include <iostream>
#include <mariadb/mysql.h>

void updatePlayerScore(MYSQL* conn, const std::string& playerName, int newScore) {
    // 트랜잭션 시작
    mysql_query(conn, "START TRANSACTION");

    // 점수 업데이트 쿼리
    std::string query = "UPDATE ranking SET score = " + std::to_string(newScore) + " WHERE player_name = '" + playerName + "';";
    if (mysql_query(conn, query.c_str())) {
        std::cerr << "UPDATE 오류: " << mysql_error(conn) << std::endl;
        mysql_query(conn, "ROLLBACK");  // 실패 시 롤백
        return;
    }

    // 트랜잭션 커밋
    mysql_query(conn, "COMMIT");
    std::cout << playerName << "의 점수가 " << newScore << "로 업데이트되었습니다." << std::endl;
}

int main() {
    MYSQL* conn = mysql_init(nullptr);
    if (!mysql_real_connect(conn, "localhost", "game_user", "password", "game_ranking", 3306, nullptr, 0)) {
        std::cerr << "MySQL 연결 실패: " << mysql_error(conn) << std::endl;
        return 1;
    }

    updatePlayerScore(conn, "Player1", 9000);

    mysql_close(conn);
    return 0;
}

트랜잭션을 활용한 동시성 문제 해결

게임 순위 시스템에서는 여러 사용자가 동시에 같은 데이터를 수정하는 경우가 빈번합니다. 이를 해결하기 위해 락(Lock) 을 사용할 수 있습니다.

  • 낙관적 락(Optimistic Locking): 데이터를 수정하기 전에 기존 값을 확인하고 변경된 경우 업데이트를 거부하는 방식
  • 비관적 락(Pessimistic Locking): 데이터를 수정하는 동안 다른 사용자가 해당 데이터를 변경하지 못하도록 하는 방식

MariaDB에서는 SELECT ... FOR UPDATE를 사용하여 특정 행을 잠글 수 있습니다.

START TRANSACTION;

SELECT score FROM ranking WHERE player_name = 'Player1' FOR UPDATE;

UPDATE ranking SET score = 8000 WHERE player_name = 'Player1';

COMMIT;

이렇게 하면 Player1의 데이터를 수정하는 동안 다른 사용자가 해당 데이터를 변경할 수 없습니다.

트랜잭션을 활용한 데이터 무결성 유지 전략

  1. 순위 업데이트 시 트랜잭션 사용
  • 여러 개의 업데이트 쿼리를 실행할 때 부분적으로 실행되지 않도록 보호
  1. 동시 실행 방지
  • SELECT ... FOR UPDATE를 사용하여 여러 사용자가 동시에 같은 데이터를 변경하는 문제 방지
  1. 에러 발생 시 롤백
  • 네트워크 오류나 예상치 못한 문제 발생 시 ROLLBACK을 사용하여 데이터 무결성 유지

결론

트랜잭션을 사용하면 순위 시스템에서 데이터 정합성안정성을 확보할 수 있습니다. C++에서 MariaDB와의 연동 시 트랜잭션을 적절히 활용하여, 실시간으로 점수를 업데이트하고 데이터 무결성을 보장하는 것이 중요합니다.

보안 고려 사항: 데이터 보호와 권한 관리

게임 서버에서 순위 시스템을 구현할 때 보안은 중요한 부분입니다. 데이터 보호권한 관리는 시스템의 안전성을 유지하고, 사용자의 개인 정보를 보호하는 데 필수적인 요소입니다. 본 섹션에서는 MariaDB를 사용한 게임 서버 순위 시스템에서 보안을 강화하는 방법을 설명합니다.

1. 데이터 암호화

게임 서버에서 순위 데이터는 민감한 정보를 포함할 수 있습니다. 예를 들어, 사용자의 ID, 이메일 주소, 비밀번호 등이 노출되면 보안 사고가 발생할 수 있습니다. 이를 방지하기 위해 데이터 암호화를 적용할 수 있습니다. MariaDB에서는 TLS(Transport Layer Security)를 사용하여 서버와 클라이언트 간의 통신을 암호화할 수 있습니다.

-- MariaDB에서 암호화된 연결 설정

[mysqld]

require_secure_transport = ON

이를 통해 서버와 클라이언트 간의 모든 데이터 전송이 암호화되며, 중간자 공격(Man-in-the-middle attack)을 방지할 수 있습니다.

2. 비밀번호 암호화

사용자의 비밀번호는 반드시 암호화된 형태로 저장해야 합니다. 해싱(Hashing) 기법을 사용하여 비밀번호를 안전하게 저장할 수 있습니다. 예를 들어, bcryptSHA-256을 사용하여 비밀번호를 암호화하고, 데이터베이스에는 해시된 값을 저장합니다.

#include <iostream>
#include <bcrypt/BCrypt.hpp>

std::string hashPassword(const std::string& password) {
    return BCrypt::generateHash(password);
}

bool verifyPassword(const std::string& inputPassword, const std::string& storedHash) {
    return BCrypt::validatePassword(inputPassword, storedHash);
}

이렇게 하면 비밀번호를 평문으로 저장하지 않고, 사용자가 로그인할 때 암호화된 비밀번호를 비교하여 인증할 수 있습니다.

3. 권한 관리

게임 서버에서는 다양한 역할(Role)을 가진 사용자들이 있을 수 있습니다. 예를 들어, 일반 사용자, 관리자, 게임 운영자 등이 있으며, 각 사용자에게 필요한 권한만 부여해야 합니다. 이를 위해 최소 권한 원칙(Principle of Least Privilege)을 적용하여 사용자에게 필요한 권한만을 부여하는 것이 좋습니다.

MariaDB에서는 사용자 계정에 대해 세부적인 권한을 설정할 수 있습니다. 예를 들어, 게임 서버에서 순위 테이블에 대한 읽기/쓰기 권한을 설정할 수 있습니다.

-- 읽기 권한만 부여
GRANT SELECT ON ranking TO 'game_user'@'localhost';

-- 읽기 및 쓰기 권한 부여
GRANT SELECT, INSERT, UPDATE, DELETE ON ranking TO 'admin_user'@'localhost';

관리자일반 사용자의 권한을 분리하고, 게임 서버에서 데이터베이스 작업을 수행할 때 필요한 최소 권한만 부여함으로써 보안을 강화할 수 있습니다.

4. SQL 인젝션 방지

SQL 인젝션(SQL Injection)은 악의적인 사용자가 쿼리 문을 조작하여 데이터베이스에 대한 권한을 탈취하거나 데이터를 변경하는 공격입니다. 이를 방지하기 위해 준비된 문(Prepared Statements) 을 사용하는 것이 중요합니다. C++에서 MariaDB를 사용할 때는 mysql_real_prepare와 같은 함수로 쿼리를 준비하여 사용자 입력을 안전하게 처리할 수 있습니다.

#include <iostream>
#include <mysql/mysql.h>

void updatePlayerScore(MYSQL* conn, const std::string& playerName, int score) {
    const char* query = "UPDATE ranking SET score = ? WHERE player_name = ?";
    MYSQL_STMT* stmt = mysql_stmt_init(conn);
    if (mysql_stmt_prepare(stmt, query, strlen(query))) {
        std::cerr << "준비된 문장 준비 오류: " << mysql_error(conn) << std::endl;
        return;
    }

    MYSQL_BIND bind[2];
    memset(bind, 0, sizeof(bind));

    bind[0].buffer_type = MYSQL_TYPE_LONG;
    bind[0].buffer = &score;
    bind[1].buffer_type = MYSQL_TYPE_STRING;
    bind[1].buffer = const_cast<char*>(playerName.c_str());
    bind[1].buffer_length = playerName.length();

    if (mysql_stmt_bind_param(stmt, bind)) {
        std::cerr << "파라미터 바인딩 오류: " << mysql_error(conn) << std::endl;
        return;
    }

    if (mysql_stmt_execute(stmt)) {
        std::cerr << "쿼리 실행 오류: " << mysql_error(conn) << std::endl;
    }

    mysql_stmt_close(stmt);
}

준비된 문을 사용하면 SQL 인젝션 공격을 방지하고, 입력 값에 대한 안전한 처리가 가능해집니다.

5. 데이터베이스 접근 제어

게임 서버에서 데이터베이스에 대한 접근 제어는 매우 중요합니다. MariaDB에서 데이터베이스 접근을 제어하려면 방화벽(Firewall) 을 설정하고, IP 화이트리스트를 사용하여 허가된 IP에서만 데이터베이스에 접근할 수 있도록 제한할 수 있습니다. 예를 들어, MariaDB 설정에서 외부에서의 접근을 차단하고, 특정 IP에서만 연결을 허용하는 방식입니다.

-- 특정 IP에서만 접속 허용
GRANT ALL PRIVILEGES ON game_ranking.* TO 'game_user'@'192.168.1.100' IDENTIFIED BY 'password';

6. 보안 감사 및 로그 분석

게임 서버에서 보안을 강화하려면 보안 감사(Auditing)로그 분석을 통해 시스템의 이상 징후를 감지하고, 공격을 미리 예방할 수 있습니다. MariaDB에서는 general_logaudit_log를 사용하여 쿼리 실행 기록을 남길 수 있습니다.

-- 일반 로그 활성화
SET GLOBAL general_log = 'ON';

-- 감사 로그 활성화
SET GLOBAL audit_log_policy = 'ALL';

이렇게 로그를 분석하여 비정상적인 접근이나 이상 징후를 실시간으로 파악할 수 있습니다.

결론

게임 서버의 순위 시스템에서 보안을 강화하려면 데이터 암호화, 비밀번호 해싱, 권한 관리, SQL 인젝션 방지 등 다양한 보안 조치를 적용해야 합니다. 이를 통해 데이터 유출과 불법 접근을 방지하고, 사용자 데이터를 안전하게 보호할 수 있습니다.

요약

본 기사에서는 C++ 게임 서버에서 MariaDB를 연동하여 실시간 순위 시스템을 구현하는 방법을 설명했습니다.

  1. MariaDB 설치 및 설정 – Ubuntu 및 Windows 환경에서 MariaDB를 설치하고 게임 데이터베이스를 구성하는 방법을 다뤘습니다.
  2. C++에서 MariaDB 연결 – MariaDB Connector/C 라이브러리를 사용하여 데이터베이스와 C++ 서버를 연동하는 방법을 소개했습니다.
  3. 순위 테이블 설계 – 효율적인 순위 테이블 구조 및 인덱스 최적화 기법을 설명했습니다.
  4. 순위 업데이트 및 쿼리 처리 – 실시간으로 점수를 업데이트하고 순위를 조회하는 SQL 및 C++ 코드 예제를 제공했습니다.
  5. 트랜잭션 적용 – 동시성 문제를 해결하고 데이터 무결성을 보장하는 방법을 설명했습니다.
  6. 성능 최적화 – 인덱스, 캐싱, 비동기 처리, 읽기/쓰기 분리 등의 기법을 활용하여 게임 서버의 성능을 개선하는 방법을 다뤘습니다.
  7. 보안 고려사항 – SQL 인젝션 방지, 비밀번호 암호화, 접근 제어 및 보안 로그 분석을 통해 안전한 순위 시스템을 구축하는 방법을 소개했습니다.

이 기사를 통해 C++ 기반 게임 서버에서 안정적이고 확장 가능한 순위 시스템을 구축하는 데 필요한 핵심 기술을 습득할 수 있습니다.