C++ 채팅 서버 완성하기 | 인증·방 관리·메시지 히스토리 구현 [#50-1]

C++ 채팅 서버 완성하기 | 인증·방 관리·메시지 히스토리 구현 [#50-1]

이 글의 핵심

C++ 채팅 서버 완성하기에 대한 실전 가이드입니다. 인증·방 관리·메시지 히스토리 구현 [#50-1] 등을 예제와 함께 상세히 설명합니다.

들어가며: “기본 채팅 서버에 실전 기능을 추가하고 싶어요”

실전 채팅 서버의 요구사항

기본 브로드캐스트 채팅 서버를 만들었다면, 이제 사용자 인증, 여러 방 관리, 메시지 히스토리, 파일 전송, 재연결 처리 등 실무에서 필요한 기능을 추가해야 합니다.

목표:

  • 사용자 인증 및 세션 관리
  • 다중 방(채널) 생성 및 입장/퇴장
  • 메시지 히스토리 저장 및 조회
  • 파일 전송 프로토콜
  • 재연결 시 상태 복구

요구 환경: C++17 이상, Boost.Asio, SQLite 또는 PostgreSQL

이 글을 읽으면:

  • 실전 채팅 서버 아키텍처를 이해할 수 있습니다.
  • 사용자 인증 및 세션 관리를 구현할 수 있습니다.
  • 다중 방 관리 시스템을 만들 수 있습니다.
  • 메시지 히스토리를 DB에 저장하고 조회할 수 있습니다.

문제 시나리오: 실무에서 겪는 채팅 서버 이슈

시나리오 1: “누가 보낸 메시지인지 알 수 없어요”

기본 채팅 서버는 연결만 받고 사용자 식별이 없습니다. “안녕하세요” 메시지가 왔을 때 누가 보냈는지 알 수 없어, 답장이나 멘션(@username) 기능을 구현할 수 없습니다. 해결: JWT 기반 인증으로 연결 시점에 사용자 ID를 확정하고, 모든 메시지에 user_id를 포함합니다.

시나리오 2: “방이 하나뿐이라 대화가 뒤섞여요”

단일 방만 있으면 팀 A와 팀 B 대화가 한곳에 섞입니다. 프로젝트별, 채널별로 다중 방이 필요합니다. 해결: RoomManager로 방 생성/삭제/입장/퇴장을 관리하고, 각 방마다 독립적인 참가자 목록과 히스토리를 유지합니다.

시나리오 3: “나중에 들어온 사람이 이전 대화를 못 봐요”

새로 입장한 사용자에게 최근 N개 메시지를 보내주지 않으면 대화 맥락을 놓칩니다. 해결: 메시지를 DB에 저장하고, 입장 시 get_messages(room_id, 100)로 최근 100개를 조회해 전송합니다.

시나리오 4: “네트워크 끊김 후 재접속하면 방 목록이 사라져요”

모바일에서는 네트워크가 자주 끊깁니다. 재연결 시 이전에 있던 방 목록놓친 메시지를 복구해야 합니다. 해결: 세션 상태(현재 방 목록, 마지막 확인 시각)를 서버에 저장하고, 재연결 시 복구합니다.

시나리오 5: “대용량 파일 전송 시 메모리 폭발”

10MB 파일을 한 번에 메모리에 올리면 여러 클라이언트가 동시에 업로드할 때 OOM이 발생합니다. 해결: 청크 단위(예: 64KB)로 나눠 전송하고, Base64 인코딩으로 JSON에 실어 보냅니다.

시나리오 6: “동시 접속 1만 명에서 브로드캐스트가 느려요”

한 방에 1000명이 있을 때 for (auto& p : participants_) p->send(msg)를 순차 실행하면 지연이 누적됩니다. 해결: strand로 직렬화하되, async_write를 병렬로 걸고, 필요 시 여러 서버로 부하 분산합니다.

개념을 잡는 비유

소켓과 비동기 I/O는 우편함 주소와 배달 경로로 이해하면 편합니다. 주소(IP·포트)만 맞으면 데이터가 들어오고, Asio는 한 우체국에서 여러 배달부(스레드·핸들러)가 일을 나누는 구조로 보시면 됩니다.


목차

  1. 전체 아키텍처
  2. 사용자 인증 시스템
  3. 다중 방 관리
  4. 메시지 히스토리
  5. 파일 전송
  6. 재연결 처리
  7. 완전한 채팅 서버 예제
  8. 자주 발생하는 에러와 해결법
  9. 성능 최적화 팁
  10. 프로덕션 패턴

1. 전체 아키텍처

시스템 구성

flowchart TB
    Client["Client<br/>(WebSocket)"]
    
    subgraph ChatServer["Chat Server"]
        CM["Connection Manager<br/>- 세션 관리<br/>- 인증 처리"]
        RM["Room Manager<br/>- 방 생성/삭제<br/>- 참가자 관리"]
        MR["Message Router<br/>- 메시지 라우팅<br/>- 브로드캐스트"]
    end
    
    DB["Database<br/>- 사용자 정보<br/>- 메시지 히스토리<br/>- 방 정보"]
    
    Client --> ChatServer
    CM --> RM
    CM --> MR
    ChatServer --> DB

아키텍처 다이어그램 (Mermaid)

flowchart TB
    subgraph Clients["클라이언트"]
        C1[WebSocket A]
        C2[WebSocket B]
        C3[WebSocket C]
    end

    subgraph Server["채팅 서버"]
        CM[Connection Manager]
        RM[Room Manager]
        MR[Message Router]
    end

    subgraph DB["데이터베이스"]
        Users["(users)"]
        Rooms["(rooms)"]
        Messages["(messages)"]
    end

    C1 --> CM
    C2 --> CM
    C3 --> CM

    CM --> RM
    CM --> MR
    RM --> Rooms
    MR --> Messages
    CM --> Users

핵심 클래스

class ChatServer {
    asio::io_context& io_context_;
    tcp::acceptor acceptor_;
    ConnectionManager connection_mgr_;
    RoomManager room_mgr_;
    MessageRouter router_;
    Database db_;

public:
    void start();
    void stop();
};

class Session : public std::enable_shared_from_this<Session> {
    tcp::socket socket_;
    std::string user_id_;
    std::string current_room_;
    bool authenticated_ = false;

public:
    void start();
    void authenticate(const std::string& token);
    void join_room(const std::string& room_id);
    void send_message(const std::string& content);
};

class Room {
    std::string id_;
    std::string name_;
    std::set<std::shared_ptr<Session>> participants_;
    std::deque<Message> history_;

public:
    void join(std::shared_ptr<Session> session);
    void leave(std::shared_ptr<Session> session);
    void broadcast(const Message& msg);
    std::vector<Message> get_history(size_t count);
};

2. 사용자 인증 시스템

JWT 기반 인증

class AuthManager {
    std::string secret_key_;

public:
    std::string generate_token(const std::string& user_id) {
        // JWT 토큰 생성
        json payload = {
            {"user_id", user_id},
            {"exp", std::time(nullptr) + 3600}  // 1시간
        };
        return jwt::create()
            .set_payload_claim("data", jwt::claim(payload.dump()))
            .sign(jwt::algorithm::hs256{secret_key_});
    }

    std::optional<std::string> verify_token(const std::string& token) {
        try {
            auto decoded = jwt::decode(token);
            auto verifier = jwt::verify()
                .allow_algorithm(jwt::algorithm::hs256{secret_key_});
            verifier.verify(decoded);

            auto payload = json::parse(
                decoded.get_payload_claim("data").as_string()
            );
            return payload["user_id"];
        } catch (const std::exception&) {
            return std::nullopt;
        }
    }
};

세션 인증 처리

void Session::handle_auth_message(const json& msg) {
    std::string token = msg["token"];

    auto user_id = auth_mgr_.verify_token(token);
    if (!user_id) {
        send_error("Invalid token");
        socket_.close();
        return;
    }

    user_id_ = *user_id;
    authenticated_ = true;

    // 사용자 정보 로드
    auto user_info = db_.get_user(user_id_);

    send_response({
        {"type", "auth_success"},
        {"user", user_info}
    });

    // 이전 방 목록 로드
    auto rooms = db_.get_user_rooms(user_id_);
    send_response({
        {"type", "room_list"},
        {"rooms", rooms}
    });
}

3. 다중 방 관리

RoomManager 구현

class RoomManager {
    std::unordered_map<std::string, std::shared_ptr<Room>> rooms_;
    std::mutex mutex_;

public:
    std::shared_ptr<Room> create_room(
        const std::string& name,
        const std::string& creator_id
    ) {
        std::lock_guard lock(mutex_);

        std::string room_id = generate_uuid();
        auto room = std::make_shared<Room>(room_id, name, creator_id);
        rooms_[room_id] = room;

        // DB에 저장
        db_.insert_room(room_id, name, creator_id);

        return room;
    }

    std::shared_ptr<Room> get_room(const std::string& room_id) {
        std::lock_guard lock(mutex_);
        auto it = rooms_.find(room_id);
        return it != rooms_.end() ? it->second : nullptr;
    }

    void delete_room(const std::string& room_id) {
        std::lock_guard lock(mutex_);
        rooms_.erase(room_id);
        db_.delete_room(room_id);
    }

    std::vector<RoomInfo> list_rooms() {
        std::lock_guard lock(mutex_);
        std::vector<RoomInfo> result;
        for (const auto& [id, room] : rooms_) {
            result.push_back(room->get_info());
        }
        return result;
    }
};

Room 클래스 상세

class Room {
    std::string id_;
    std::string name_;
    std::string creator_id_;
    std::set<std::shared_ptr<Session>> participants_;
    std::deque<Message> recent_messages_;  // 최근 100개
    asio::strand<asio::io_context::executor_type> strand_;

public:
    void join(std::shared_ptr<Session> session) {
        asio::post(strand_, [this, session]() {
            participants_.insert(session);

            // 입장 알림
            broadcast({
                {"type", "user_joined"},
                {"user_id", session->user_id()},
                {"room_id", id_}
            });

            // 최근 메시지 전송
            session->send_history(recent_messages_);
        });
    }

    void leave(std::shared_ptr<Session> session) {
        asio::post(strand_, [this, session]() {
            participants_.erase(session);

            // 퇴장 알림
            broadcast({
                {"type", "user_left"},
                {"user_id", session->user_id()},
                {"room_id", id_}
            });
        });
    }

    void broadcast(const json& msg) {
        asio::post(strand_, [this, msg]() {
            std::string data = msg.dump();
            for (auto& participant : participants_) {
                participant->send(data);
            }

            // 메시지 히스토리 저장
            if (msg["type"] == "message") {
                Message m{
                    msg["user_id"],
                    msg["content"],
                    std::time(nullptr)
                };
                recent_messages_.push_back(m);
                if (recent_messages_.size() > 100) {
                    recent_messages_.pop_front();
                }

                // DB에 저장
                db_.insert_message(id_, m);
            }
        });
    }
};

4. 메시지 히스토리

데이터베이스 스키마

CREATE TABLE messages (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    room_id TEXT NOT NULL,
    user_id TEXT NOT NULL,
    content TEXT NOT NULL,
    timestamp INTEGER NOT NULL,
    FOREIGN KEY (room_id) REFERENCES rooms(id),
    FOREIGN KEY (user_id) REFERENCES users(id)
);

CREATE INDEX idx_messages_room_time
ON messages(room_id, timestamp DESC);

히스토리 조회

class Database {
    sqlite3* db_;

public:
    std::vector<Message> get_messages(
        const std::string& room_id,
        size_t count,
        int64_t before_timestamp = 0
    ) {
        std::string sql = R"(
            SELECT user_id, content, timestamp
            FROM messages
            WHERE room_id = ?
        )";

        if (before_timestamp > 0) {
            sql += " AND timestamp < ?";
        }

        sql += " ORDER BY timestamp DESC LIMIT ?";

        sqlite3_stmt* stmt;
        sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr);

        int idx = 1;
        sqlite3_bind_text(stmt, idx++, room_id.c_str(), -1, SQLITE_TRANSIENT);
        if (before_timestamp > 0) {
            sqlite3_bind_int64(stmt, idx++, before_timestamp);
        }
        sqlite3_bind_int(stmt, idx++, static_cast<int>(count));

        std::vector<Message> messages;
        while (sqlite3_step(stmt) == SQLITE_ROW) {
            messages.push_back({
                reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0)),
                reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1)),
                sqlite3_column_int64(stmt, 2)
            });
        }

        sqlite3_finalize(stmt);
        std::reverse(messages.begin(), messages.end());
        return messages;
    }
};

페이지네이션

void Session::handle_history_request(const json& msg) {
    std::string room_id = msg["room_id"];
    size_t count = msg.value("count", 50);
    int64_t before = msg.value("before", 0);

    auto messages = db_.get_messages(room_id, count, before);

    send_response({
        {"type", "history"},
        {"room_id", room_id},
        {"messages", messages},
        {"has_more", messages.size() == count}
    });
}

5. 파일 전송

청크 기반 전송

struct FileTransfer {
    std::string file_id;
    std::string filename;
    size_t total_size;
    size_t received_size = 0;
    std::ofstream file;
};

void Session::handle_file_upload(const json& msg) {
    if (msg["type"] == "file_start") {
        std::string file_id = generate_uuid();
        std::string filename = msg["filename"];
        size_t size = msg["size"];

        FileTransfer transfer{
            file_id,
            filename,
            size,
            0,
            std::ofstream("uploads/" + file_id, std::ios::binary)
        };

        file_transfers_[file_id] = std::move(transfer);

        send_response({
            {"type", "file_ready"},
            {"file_id", file_id}
        });
    }
    else if (msg["type"] == "file_chunk") {
        std::string file_id = msg["file_id"];
        std::string data = msg["data"];  // Base64 encoded

        auto& transfer = file_transfers_[file_id];
        std::vector<uint8_t> chunk = base64_decode(data);

        transfer.file.write(
            reinterpret_cast<const char*>(chunk.data()),
            chunk.size()
        );
        transfer.received_size += chunk.size();

        if (transfer.received_size >= transfer.total_size) {
            transfer.file.close();

            // 방에 파일 메시지 브로드캐스트
            room_->broadcast({
                {"type", "file_message"},
                {"user_id", user_id_},
                {"file_id", file_id},
                {"filename", transfer.filename},
                {"size", transfer.total_size}
            });

            file_transfers_.erase(file_id);
        }
    }
}

6. 재연결 처리

세션 복구

class SessionManager {
    std::unordered_map<std::string, SessionState> saved_states_;
    std::mutex mutex_;

public:
    void save_state(const std::string& user_id, const SessionState& state) {
        std::lock_guard lock(mutex_);
        saved_states_[user_id] = state;
    }

    std::optional<SessionState> restore_state(const std::string& user_id) {
        std::lock_guard lock(mutex_);
        auto it = saved_states_.find(user_id);
        if (it != saved_states_.end()) {
            auto state = it->second;
            saved_states_.erase(it);
            return state;
        }
        return std::nullopt;
    }
};

void Session::handle_reconnect() {
    auto state = session_mgr_.restore_state(user_id_);
    if (!state) {
        send_error("No saved state");
        return;
    }

    // 이전 방 재입장
    for (const auto& room_id : state->room_ids) {
        auto room = room_mgr_.get_room(room_id);
        if (room) {
            room->join(shared_from_this());
        }
    }

    // 놓친 메시지 전송
    for (const auto& room_id : state->room_ids) {
        auto messages = db_.get_messages(
            room_id,
            100,
            state->last_seen_timestamp
        );
        send_response({
            {"type", "missed_messages"},
            {"room_id", room_id},
            {"messages", messages}
        });
    }
}

7. 완전한 채팅 서버 예제

최소 동작 예제: main 함수와 서버 기동

// chat_server_main.cpp
#include <boost/asio.hpp>
#include <iostream>
#include <memory>

namespace asio = boost::asio;
using tcp = asio::ip::tcp;

int main(int argc, char* argv[]) {
    try {
        asio::io_context io_context;
        tcp::acceptor acceptor(io_context, tcp::endpoint(tcp::v4(), 9000));

        auto do_accept = [&]() {
            acceptor.async_accept(
                [&](boost::system::error_code ec, tcp::socket socket) {
                    if (!ec) {
                        // 새 세션 생성 및 시작
                        auto session = std::make_shared<Session>(
                            std::move(socket), room_mgr_, db_
                        );
                        session->start();
                    }
                    do_accept();  // 다음 연결 대기
                });
        };
        do_accept();

        std::cout << "Chat server listening on port 9000\n";
        io_context.run();
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << "\n";
        return 1;
    }
    return 0;
}

클라이언트-서버 메시지 프로토콜 예제

// 1. 인증 요청 (클라이언트 → 서버)
{"type": "auth", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}

// 2. 인증 성공 (서버 → 클라이언트)
{"type": "auth_success", "user": {"id": "user1", "name": "홍길동"}}

// 3. 방 입장 요청
{"type": "join_room", "room_id": "room-abc-123"}

// 4. 메시지 전송
{"type": "message", "room_id": "room-abc-123", "content": "안녕하세요!"}

// 5. 히스토리 요청 (페이지네이션)
{"type": "history", "room_id": "room-abc-123", "count": 50, "before": 1709876543}

WebSocket 핸드셰이크 후 메시지 처리 루프

void Session::do_read() {
    auto self(shared_from_this());
    socket_.async_read_some(
        asio::buffer(buffer_),
        [this, self](boost::system::error_code ec, std::size_t length) {
            if (!ec) {
                std::string data(buffer_.data(), length);
                auto msg = json::parse(data);

                std::string type = msg["type"];
                if (type == "auth") {
                    handle_auth_message(msg);
                } else if (!authenticated_) {
                    send_error("Not authenticated");
                    return;
                } else if (type == "join_room") {
                    handle_join_room(msg);
                } else if (type == "message") {
                    handle_message(msg);
                } else if (type == "history") {
                    handle_history_request(msg);
                } else if (type == "file_start" || type == "file_chunk") {
                    handle_file_upload(msg);
                } else if (type == "reconnect") {
                    handle_reconnect();
                }

                do_read();  // 다음 메시지 대기
            } else {
                connection_mgr_.stop(shared_from_this());
            }
        });
}

8. 자주 발생하는 에러와 해결법

에러 1: “Invalid token” / JWT 검증 실패

증상: 클라이언트가 토큰을 보냈는데 서버가 “Invalid token”을 반환합니다.

원인:

  • 토큰 만료 (exp 초과)
  • 시크릿 키 불일치 (서버 재시작 시 환경 변수 누락)
  • Base64 디코딩 오류 (토큰에 공백/줄바꿈 포함)

해결법:

// ✅ 토큰 검증 시 만료 시간 체크
std::optional<std::string> verify_token(const std::string& token) {
    try {
        auto decoded = jwt::decode(token);
        auto exp = decoded.get_expires_at();
        if (exp && std::chrono::system_clock::now() > *exp) {
            return std::nullopt;  // 만료됨
        }
        // ... 나머지 검증
    } catch (const std::exception& e) {
        spdlog::warn("JWT verify failed: {}", e.what());
        return std::nullopt;
    }
}

에러 2: “double free” / shared_ptr 순환 참조

증상: 프로그램이 크래시하거나 메모리 누수가 발생합니다.

원인: RoomSessionshared_ptr로 보관하고, SessionRoomshared_ptr로 보관하면 순환 참조가 됩니다.

해결법:

// ❌ 잘못된 예: Room이 Session을 shared_ptr로, Session이 Room을 shared_ptr로
class Room {
    std::set<std::shared_ptr<Session>> participants_;  // Session 소유
};
class Session {
    std::shared_ptr<Room> room_;  // Room 소유 → 순환!
};

// ✅ 올바른 예: Session은 Room을 weak_ptr로 참조
class Session {
    std::weak_ptr<Room> room_;  // 소유하지 않고 참조만
};

에러 3: “Connection reset by peer” / 비정상 종료

증상: 클라이언트가 갑자기 끊기고 서버 로그에 “Connection reset”이 찍힙니다.

원인: 클라이언트가 close() 없이 프로세스를 종료했거나, 네트워크 불안정입니다.

해결법:

// ✅ 에러 시 graceful shutdown
void Session::do_read() {
    socket_.async_read_some(asio::buffer(buffer_),
        [this, self = shared_from_this()](error_code ec, size_t length) {
            if (ec) {
                if (ec != asio::error::operation_aborted) {
                    spdlog::info("Client disconnected: {}", ec.message());
                }
                connection_mgr_.stop(self);
                return;
            }
            // ... 처리
            do_read();
        });
}

에러 4: “SQLITE_BUSY” / DB 락 충돌

증상: 메시지 저장 시 SQLITE_BUSY 에러가 발생합니다.

원인: SQLite는 기본적으로 한 번에 하나의 쓰기만 허용합니다. 여러 스레드가 동시에 INSERT하면 락이 걸립니다.

해결법:

// ✅ WAL 모드 + busy_timeout 설정
sqlite3_exec(db_, "PRAGMA journal_mode=WAL;", nullptr, nullptr, nullptr);
sqlite3_busy_timeout(db_, 5000);  // 5초 대기

// 또는 쓰기 전용 connection pool 사용

에러 5: “Address already in use” / 포트 충돌

증상: 서버 기동 시 bind: Address already in use 에러가 납니다.

원인: 이전 프로세스가 아직 종료되지 않았거나, SO_REUSEADDR 미설정입니다.

해결법:

// ✅ SO_REUSEADDR 설정
acceptor_.open(endpoint.protocol());
acceptor_.set_option(asio::socket_base::reuse_address(true));
acceptor_.bind(endpoint);
acceptor_.listen();

에러 6: 파일 업로드 시 “No space left on device”

증상: 대용량 파일 업로드 중 디스크 풀 에러가 발생합니다.

원인: 업로드 디렉터리 용량 부족, 또는 업로드 전 크기 검증 없음.

해결법:

// ✅ 업로드 전 크기 제한 (예: 50MB)
constexpr size_t MAX_FILE_SIZE = 50 * 1024 * 1024;
if (msg["size"].get<size_t>() > MAX_FILE_SIZE) {
    send_error("File too large");
    return;
}

// 디스크 여유 공간 확인
namespace fs = std::filesystem;
auto space = fs::space("uploads/");
if (space.available < msg["size"].get<size_t>()) {
    send_error("Insufficient storage");
    return;
}

9. 성능 최적화 팁

팁 1: 메시지 큐로 쓰기 직렬화

한 세션에서 async_write를 동시에 여러 번 호출하면 데이터가 섞입니다. 메시지 큐로 직렬화합니다.

class MessageQueue {
    std::deque<std::string> queue_;
    bool writing_ = false;

public:
    void push(const std::string& msg) {
        queue_.push_back(msg);
        if (!writing_) {
            do_write();
        }
    }

    void do_write() {
        if (queue_.empty()) {
            writing_ = false;
            return;
        }

        writing_ = true;
        auto& msg = queue_.front();

        asio::async_write(socket_, asio::buffer(msg),
            [this](error_code ec, size_t) {
                queue_.pop_front();
                if (!ec) {
                    do_write();
                } else {
                    writing_ = false;
                }
            });
    }
};

팁 2: DB 쓰기 비동기화

메시지 저장을 동기로 하면 브로드캐스트가 블로킹됩니다. 별도 스레드 풀에서 DB 쓰기를 수행합니다.

// DB 쓰기를 thread pool로 오프로드
void Room::broadcast(const json& msg) {
    // 1. 먼저 브로드캐스트 (빠른 경로)
    std::string data = msg.dump();
    for (auto& p : participants_) {
        p->send(data);
    }

    // 2. DB 저장은 나중에 (백그라운드)
    if (msg["type"] == "message") {
        db_executor_.post([this, msg]() {
            db_.insert_message(id_, Message{...});
        });
    }
}

팁 3: 메모리 풀 for 메시지 버퍼

매 메시지마다 new char[size]를 하면 할당 오버헤드가 큽니다. 객체 풀 또는 고정 크기 버퍼를 재사용합니다.

// 고정 크기 버퍼 풀
class BufferPool {
    std::vector<std::array<char, 4096>> pool_;
    std::mutex mutex_;

public:
    std::span<char> acquire() {
        std::lock_guard lock(mutex_);
        if (pool_.empty()) {
            pool_.emplace_back();
        }
        auto& buf = pool_.back();
        pool_.pop_back();
        return std::span(buf);
    }
    void release(std::span<char> s) {
        std::lock_guard lock(mutex_);
        // 버퍼를 풀에 반환
    }
};

팁 4: 방 단위 strand 분리

모든 방이 하나의 strand를 쓰면 한 방의 브로드캐스트가 다른 방을 블로킹합니다. 방마다 별도 strand를 두면 병렬성이 올라갑니다.

class Room {
    asio::strand<asio::io_context::executor_type> strand_;
    // 각 Room이 자신만의 strand를 가짐
};

팁 5: 연결 수 제한

무제한 연결을 허용하면 메모리와 파일 디스크립터가 고갈됩니다. 최대 연결 수를 두고 초과 시 거부합니다.

class ConnectionManager {
    std::set<std::shared_ptr<Session>> sessions_;
    static constexpr size_t MAX_CONNECTIONS = 10000;

public:
    bool try_add(std::shared_ptr<Session> session) {
        if (sessions_.size() >= MAX_CONNECTIONS) {
            return false;
        }
        sessions_.insert(session);
        return true;
    }
};

10. 프로덕션 패턴

패턴 1: 부하 분산 (Round-Robin)

class LoadBalancer {
    std::vector<std::shared_ptr<ChatServer>> servers_;
    std::atomic<size_t> next_server_{0};

public:
    std::shared_ptr<ChatServer> get_server() {
        size_t idx = next_server_.fetch_add(1) % servers_.size();
        return servers_[idx];
    }
};

패턴 2: 헬스 체크

// 주기적으로 서버 상태 확인
void ChatServer::start_health_check() {
    asio::steady_timer timer(io_context_);
    std::function<void()> check;
    check = [&]() {
        timer.expires_after(std::chrono::seconds(30));
        timer.async_wait([&](error_code ec) {
            if (!ec) {
                spdlog::info("Connections: {}, Rooms: {}",
                    connection_mgr_.size(), room_mgr_.size());
                check();
            }
        });
    };
    check();
}

패턴 3: 그레이스풀 셧다운

void ChatServer::stop() {
    acceptor_.close();
    connection_mgr_.stop_all();
    io_context_.stop();
    // 모든 세션이 정리될 때까지 대기
    while (connection_mgr_.size() > 0) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

패턴 4: 로깅 및 메트릭

// 구조화된 로깅
spdlog::info("msg_sent room={} user={} size={}",
    room_id, user_id, msg.size());

// Prometheus 메트릭 (예시)
metrics::counter messages_sent_total;
metrics::gauge active_connections;

패턴 5: 설정 외부화

// config.json 또는 환경 변수
struct ServerConfig {
    int port = 9000;
    size_t max_connections = 10000;
    size_t max_file_size = 50 * 1024 * 1024;
    std::string db_path = "chat.db";
    std::string jwt_secret;
};

ServerConfig load_config() {
    ServerConfig cfg;
    if (const char* p = std::getenv("CHAT_PORT")) {
        cfg.port = std::stoi(p);
    }
    if (const char* s = std::getenv("JWT_SECRET")) {
        cfg.jwt_secret = s;
    }
    return cfg;
}

프로덕션 체크리스트

항목확인
JWT 시크릿 환경 변수로 관리
DB 백업 스케줄 설정
로그 로테이션 (logrotate)
최대 연결 수 제한
파일 업로드 크기 제한
SSL/TLS 적용 (wss://)
헬스 체크 엔드포인트
그레이스풀 셧다운

핵심 구현 재확인

아래는 앞선 장에서 다룬 패턴을 한 번에 다시 보는 용도입니다. 새로운 개념이라기보다, 배포 전에 구조를 점검할 때 참고하시면 됩니다.

1. 메시지 큐 관리 (재확인)

class MessageQueue {
    std::deque<std::string> queue_;
    bool writing_ = false;

public:
    void push(const std::string& msg) {
        queue_.push_back(msg);
        if (!writing_) {
            do_write();
        }
    }

    void do_write() {
        if (queue_.empty()) {
            writing_ = false;
            return;
        }

        writing_ = true;
        auto& msg = queue_.front();

        asio::async_write(socket_, asio::buffer(msg),
            [this](error_code ec, size_t) {
                queue_.pop_front();
                if (!ec) {
                    do_write();
                } else {
                    writing_ = false;
                }
            });
    }
};

2. 부하 분산 (재확인)

class LoadBalancer {
    std::vector<std::shared_ptr<ChatServer>> servers_;
    std::atomic<size_t> next_server_{0};

public:
    std::shared_ptr<ChatServer> get_server() {
        size_t idx = next_server_.fetch_add(1) % servers_.size();
        return servers_[idx];
    }
};

정리

기능구현 방법
인증JWT 토큰
방 관리RoomManager + strand
히스토리SQLite + 페이지네이션
파일 전송청크 기반 + Base64
재연결세션 상태 저장/복구

핵심 원칙:

  1. 모든 상태 변경은 strand로 직렬화
  2. 메시지는 DB에 저장하여 영속성 보장
  3. 파일은 청크 단위로 전송
  4. 재연결 시 놓친 메시지 전송
  5. 부하 분산으로 확장성 확보

실전 체크리스트

실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.

코드 작성 전

  • 이 기법이 현재 문제를 해결하는 최선의 방법인가?
  • 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
  • 성능 요구사항을 만족하는가?

코드 작성 중

  • 컴파일러 경고를 모두 해결했는가?
  • 엣지 케이스를 고려했는가?
  • 에러 처리가 적절한가?

코드 리뷰 시

  • 코드의 의도가 명확한가?
  • 테스트 케이스가 충분한가?
  • 문서화가 되어 있는가?

이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. 실시간 채팅 서비스, 게임 로비 시스템, 협업 도구, IoT 디바이스 통신 등 다중 클라이언트 실시간 메시징이 필요한 모든 서비스에 활용합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.

한 줄 요약: 인증, 방 관리, 히스토리, 파일 전송, 재연결 처리로 실전 채팅 서버를 완성할 수 있습니다.

다음 글: [C++ 실전 가이드 #50-2] REST API 서버 만들기: 라우팅·미들웨어·인증

이전 글: [C++ 실전 가이드 #49-3] Asio 데드락 디버깅


관련 글

  • C++ 시리즈 전체 보기
  • C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
  • C++ ADL |
  • C++ Aggregate Initialization |