C++ 채팅 서버 아키텍처 완벽 가이드 | Acceptor-Worker·방 관리·메시지 라우팅·커넥션 풀

C++ 채팅 서버 아키텍처 완벽 가이드 | Acceptor-Worker·방 관리·메시지 라우팅·커넥션 풀

이 글의 핵심

C++ 채팅 서버 아키텍처 완벽 가이드에 대한 실전 가이드입니다. Acceptor-Worker·방 관리·메시지 라우팅·커넥션 풀 등을 예제와 함께 상세히 설명합니다.

들어가며: “채팅 서버 아키텍처를 어떻게 설계해야 할까요?”

문제 시나리오

채팅 서버를 만들다 보면 이런 고민이 생깁니다:

  • 동시 접속 1만 명을 처리하려면 단일 io_context로 충분할까요? 아니면 Acceptor-Worker 구조가 필요한가요?
  • 채널이 100개일 때 방별 참가자 목록을 어떻게 관리하고, 메시지를 어떤 경로로 전달할까요?
  • 느린 클라이언트 1명이 전체 채팅을 지연시키지 않으려면 어떻게 해야 할까요?
  • 연결 폭주 시 서버가 다운되지 않으려면 커넥션 풀이나 Rate Limiter가 필요한가요?

채팅 서버 아키텍처의 핵심Acceptor-Worker 패턴(연결 수락과 처리 분리), 방 관리(채널별 참가자·히스토리), 메시지 라우팅(발신자 제외 브로드캐스트), 커넥션 풀(연결 수 제한·재사용)을 올바르게 조합하는 것입니다.

목표:

  • Acceptor-Worker 아키텍처로 연결 수락과 비즈니스 로직 분리
  • 방 관리 시스템 (채널 생성/삭제, 참가자 입퇴장)
  • 메시지 라우팅 (방별 브로드캐스트, DM 지원)
  • 커넥션 풀 (연결 수 제한, 세션 재사용)
  • 일반적인 에러와 해결법
  • 프로덕션 패턴 (모니터링, 스케일 아웃)

요구 환경: C++17 이상, Boost.Asio 1.70+

개념을 잡는 비유

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


목차

  1. 문제 시나리오 상세
  2. Acceptor-Worker 아키텍처
  3. 방 관리 시스템
  4. 메시지 라우팅
  5. 커넥션 풀
  6. 완전한 채팅 서버 아키텍처 예제
  7. 자주 발생하는 에러와 해결법
  8. 모범 사례
  9. 프로덕션 패턴
  10. 정리와 다음 단계

1. 문제 시나리오 상세

시나리오 1: 동시 접속 폭주로 서버 다운

증상: 새 사용자가 대량으로 접속할 때 서버가 응답하지 않거나 크래시합니다.

원인: 단일 스레드에서 async_accept와 모든 I/O를 처리하면, 연결 수락 자체가 병목이 됩니다. 또한 io_context::run()이 블로킹되면 새 연결 수락이 지연됩니다.

해결: Acceptor를 전용 스레드에서 돌리고, Worker 스레드 풀에서 세션 I/O를 처리합니다.

시나리오 2: 100명이 동시에 메시지 전송 시 데이터 레이스

증상: 서버 크래시, 메시지 누락, iterator 무효화 에러.

원인: participants_를 순회하는 동안 다른 스레드가 leave()를 호출해 컨테이너가 수정됩니다.

해결: strand로 join, leave, deliver를 직렬화하거나, shared_ptr 기반 세션 수명 관리.

시나리오 3: 느린 클라이언트 1명이 전체 채팅 지연

증상: 한 사용자의 네트워크가 느리면 다른 사용자들도 메시지 수신이 늦어집니다.

원인: write_queue_가 무한 증가하고, 브로드캐스트 시 모든 세션에 순차적으로 async_write를 걸어 한 세션의 지연이 전체에 영향을 줍니다.

해결: write_queue_ 크기 제한, 느린 클라이언트 강제 퇴장, 비블로킹 전송.

시나리오 4: 채널별 메시지가 섞임

증상: #general과 #random 메시지가 한곳에 섞여 전달됩니다.

원인: 단일 Room 구조로 모든 참가자를 한 목록에 관리합니다.

해결: 채널 ID별로 Room을 분리하고, 세션이 현재 속한 방만 대상으로 메시지 라우팅.

시나리오 5: 연결 수 제한 없이 메모리 폭발

증상: DDoS나 스파이크 트래픽 시 연결이 무한 증가해 OOM이 발생합니다.

원인: async_accept가 제한 없이 새 연결을 수락합니다.

해결: 커넥션 풀로 동시 연결 수를 제한하고, 초과 시 대기 또는 거부합니다.

시나리오 요약표

시나리오증상원인해결
동시 접속 폭주서버 다운단일 스레드 병목Acceptor-Worker
메시지 전송 시 크래시데이터 레이스participants_ 동시 수정strand 직렬화
느린 클라이언트전체 지연write_queue 무한 증가큐 크기 제한, 강제 퇴장
채널 메시지 혼합잘못된 방에 전달단일 Room채널별 Room 분리
연결 폭주OOM무제한 accept커넥션 풀, Rate Limiter

2. Acceptor-Worker 아키텍처

아키텍처 다이어그램

flowchart TB
    subgraph Clients["클라이언트"]
        C1[클라이언트 1]
        C2[클라이언트 2]
        C3[클라이언트 N]
    end

    subgraph Acceptor["Acceptor 스레드"]
        A[async_accept]
        A -->|새 소켓| Q[소켓 큐]
    end

    subgraph Workers["Worker 스레드 풀"]
        W1[Worker 1]
        W2[Worker 2]
        W3[Worker 3]
        W4[Worker 4]
    end

    subgraph Sessions["세션들"]
        S1[Session A]
        S2[Session B]
        S3[Session C]
    end

    C1 --> A
    C2 --> A
    C3 --> A

    Q --> W1
    Q --> W2
    Q --> W3
    Q --> W4

    W1 --> S1
    W2 --> S2
    W3 --> S3

Acceptor-Worker 패턴 개요

Acceptor: 새 TCP 연결만 수락하고, 소켓을 Worker 풀에 전달합니다. 연결 수락 자체는 가벼운 작업이므로 단일 스레드로 충분합니다.

Worker: 전달받은 소켓으로 Session을 생성하고, async_read/async_write를 처리합니다. CPU 코어 수만큼 Worker 스레드를 두어 병렬 처리를 극대화합니다.

Acceptor 구현

#include <boost/asio.hpp>
#include <queue>
#include <mutex>
#include <condition_variable>

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

class Acceptor {
public:
    Acceptor(asio::io_context& io, const tcp::endpoint& endpoint)
        : acceptor_(io, endpoint)
        , io_(io) {}

    void start() {
        do_accept();
    }

    // Worker가 호출: 새 소켓 대기 (블로킹)
    tcp::socket wait_for_socket() {
        std::unique_lock lock(mutex_);
        cv_.wait(lock, [this] { return !socket_queue_.empty(); });
        tcp::socket sock = std::move(socket_queue_.front());
        socket_queue_.pop();
        return sock;
    }

private:
    void do_accept() {
        acceptor_.async_accept(
            [this](boost::system::error_code ec, tcp::socket socket) {
                if (!ec) {
                    {
                        std::lock_guard lock(mutex_);
                        socket_queue_.push(std::move(socket));
                    }
                    cv_.notify_one();
                }
                do_accept();
            });
    }

    tcp::acceptor acceptor_;
    asio::io_context& io_;
    std::queue<tcp::socket> socket_queue_;
    std::mutex mutex_;
    std::condition_variable cv_;
};

Worker 풀 구현

class WorkerPool {
public:
    WorkerPool(Acceptor& acceptor, size_t num_workers)
        : acceptor_(acceptor)
        , num_workers_(num_workers) {}

    void start() {
        for (size_t i = 0; i < num_workers_; ++i) {
            workers_.emplace_back([this] { worker_loop(); });
        }
    }

    void join() {
        for (auto& w : workers_) {
            if (w.joinable()) w.join();
        }
    }

private:
    void worker_loop() {
        while (true) {
            tcp::socket socket = acceptor_.wait_for_socket();
            // 각 Worker는 자신만의 io_context를 가짐
            asio::io_context io;
            auto session = std::make_shared<Session>(std::move(socket), io);
            session->start();
            io.run();  // 이 연결의 I/O가 끝날 때까지 실행
        }
    }

    Acceptor& acceptor_;
    size_t num_workers_;
    std::vector<std::thread> workers_;
};

io_context 풀 방식 (대안)

Acceptor와 Worker가 같은 io_context 풀을 공유하는 방식입니다. 연결 수락 후 소켓을 풀의 아무 io_context에 포스트합니다.

class AcceptorWithIOPool {
public:
    AcceptorWithIOPool(const tcp::endpoint& endpoint, size_t pool_size)
        : acceptor_(*io_contexts_[0], endpoint)
        , pool_size_(pool_size)
        , next_io_(0) {
        for (size_t i = 0; i < pool_size; ++i) {
            io_contexts_.push_back(std::make_shared<asio::io_context>());
        }
    }

    void run() {
        std::vector<std::thread> threads;
        for (auto& io : io_contexts_) {
            threads.emplace_back([&io] { io->run(); });
        }

        do_accept();

        for (auto& t : threads) {
            t.join();
        }
    }

private:
    void do_accept() {
        acceptor_.async_accept(
            [this](boost::system::error_code ec, tcp::socket socket) {
                if (!ec) {
                    auto& io = *io_contexts_[next_io_ % pool_size_];
                    next_io_++;

                    asio::post(io, [socket = std::move(socket), &io, this]() mutable {
                        auto session = std::make_shared<Session>(std::move(socket), room_);
                        session->start();
                    });
                }
                do_accept();
            });
    }

    tcp::acceptor acceptor_;
    std::vector<std::shared_ptr<asio::io_context>> io_contexts_;
    size_t pool_size_;
    size_t next_io_;
    ChatRoom& room_;
};

3. 방 관리 시스템

방 관리 아키텍처

flowchart TB
    subgraph RoomManager["RoomManager"]
        RM[rooms_ map]
    end

    subgraph Rooms["채널별 Room"]
        R1[Room #general]
        R2[Room #random]
        R3[Room #dev]
    end

    subgraph Participants["참가자"]
        P1[Session A]
        P2[Session B]
        P3[Session C]
    end

    RM --> R1
    RM --> R2
    RM --> R3

    R1 --> P1
    R1 --> P2
    R2 --> P3

RoomManager 구현

#include <unordered_map>
#include <shared_mutex>

class RoomManager {
public:
    RoomManager(asio::io_context& io)
        : io_(io) {}

    std::shared_ptr<ChatRoom> get_or_create(const std::string& channel_id) {
        std::unique_lock lock(mutex_);
        auto it = rooms_.find(channel_id);
        if (it == rooms_.end()) {
            it = rooms_.emplace(
                channel_id,
                std::make_shared<ChatRoom>(io_, channel_id)
            ).first;
        }
        return it->second;
    }

    void remove_if_empty(const std::string& channel_id) {
        std::unique_lock lock(mutex_);
        auto it = rooms_.find(channel_id);
        if (it != rooms_.end() && it->second->participant_count() == 0) {
            rooms_.erase(it);
        }
    }

    std::vector<std::string> list_channels() const {
        std::shared_lock lock(mutex_);
        std::vector<std::string> result;
        for (const auto& [id, room] : rooms_) {
            if (room->participant_count() > 0) {
                result.push_back(id);
            }
        }
        return result;
    }

private:
    asio::io_context& io_;
    std::unordered_map<std::string, std::shared_ptr<ChatRoom>> rooms_;
    mutable std::shared_mutex mutex_;
};

ChatRoom 확장 (채널 ID 지원)

class ChatRoom {
public:
    ChatRoom(asio::io_context& io, const std::string& channel_id)
        : strand_(asio::make_strand(io))
        , channel_id_(channel_id)
        , max_history_(100) {}

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

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

    size_t participant_count() const {
        return participants_.size();
    }

    // deliver, broadcast_join, broadcast_leave, send_history 등
    // (기존 ChatRoom과 동일)

private:
    asio::strand<asio::io_context::executor_type> strand_;
    std::string channel_id_;
    std::set<std::shared_ptr<Session>> participants_;
    std::deque<std::string> history_;
    size_t max_history_;
};

세션의 다중 방 지원

한 세션이 여러 방에 동시에 참가할 수 있도록 합니다.

class Session : public std::enable_shared_from_this<Session> {
public:
    void join_room(const std::string& channel_id) {
        auto room = room_manager_.get_or_create(channel_id);
        room->join(shared_from_this());
        current_rooms_.insert(channel_id);
    }

    void leave_room(const std::string& channel_id) {
        auto room = room_manager_.get_or_create(channel_id);
        room->leave(shared_from_this());
        current_rooms_.erase(channel_id);
        room_manager_.remove_if_empty(channel_id);
    }

    void leave_all_rooms() {
        for (const auto& ch : current_rooms_) {
            auto room = room_manager_.get_or_create(ch);
            room->leave(shared_from_this());
            room_manager_.remove_if_empty(ch);
        }
        current_rooms_.clear();
    }

private:
    RoomManager& room_manager_;
    std::set<std::string> current_rooms_;
};

4. 메시지 라우팅

메시지 라우팅 흐름

sequenceDiagram
    participant S1 as Session A
    participant Router as MessageRouter
    participant RM as RoomManager
    participant R1 as Room #general
    participant S2 as Session B
    participant S3 as Session C

    S1->>Router: route("general", "안녕", sender=A)
    Router->>RM: get_or_create("general")
    RM->>R1: Room 반환
    Router->>R1: deliver("안녕", sender=A)
    R1->>S2: deliver("안녕")
    R1->>S3: deliver("안녕")

MessageRouter 구현

class MessageRouter {
public:
    MessageRouter(RoomManager& room_mgr)
        : room_mgr_(room_mgr) {}

    // 방에 메시지 브로드캐스트 (발신자 제외)
    void broadcast_to_room(
        const std::string& channel_id,
        const std::string& message,
        std::shared_ptr<Session> sender
    ) {
        auto room = room_mgr_.get_or_create(channel_id);
        room->deliver(message, sender);
    }

    // DM (Direct Message): 특정 사용자에게만 전송
    void send_dm(
        const std::string& target_user_id,
        const std::string& message,
        std::shared_ptr<Session> sender
    ) {
        auto session = session_registry_.get(target_user_id);
        if (session) {
            session->deliver(message);
        }
    }

    // 시스템 메시지: 특정 방의 모든 참가자에게
    void broadcast_system(
        const std::string& channel_id,
        const std::string& system_message
    ) {
        auto room = room_mgr_.get_or_create(channel_id);
        room->broadcast_system(system_message);
    }

private:
    RoomManager& room_mgr_;
    SessionRegistry& session_registry_;
};

프로토콜 기반 라우팅

클라이언트 메시지 형식에 따라 라우팅 대상을 결정합니다.

// 프로토콜: JOIN <channel>, LEAVE <channel>, MSG <channel> <content>, DM <user_id> <content>
void Session::handle_message(const std::string& raw) {
    std::istringstream iss(raw);
    std::string cmd;
    iss >> cmd;

    if (cmd == "JOIN") {
        std::string channel;
        iss >> channel;
        join_room(channel);
    } else if (cmd == "LEAVE") {
        std::string channel;
        iss >> channel;
        leave_room(channel);
    } else if (cmd == "MSG") {
        std::string channel, content;
        iss >> channel;
        std::getline(iss, content);
        if (!content.empty()) content = content.substr(1);  // 앞 공백 제거
        router_.broadcast_to_room(channel, nickname_ + ": " + content + "\n", shared_from_this());
    } else if (cmd == "DM") {
        std::string target_id, content;
        iss >> target_id;
        std::getline(iss, content);
        if (!content.empty()) content = content.substr(1);
        router_.send_dm(target_id, "[DM] " + nickname_ + ": " + content + "\n", shared_from_this());
    }
}

5. 커넥션 풀

커넥션 풀의 목적

  • 동시 연결 수 제한: 서버 리소스 보호
  • 대기 큐: 제한 초과 시 연결을 거부하거나 대기시킴
  • 모니터링: 현재 연결 수, 대기 수 추적

ConnectionLimiter 구현

class ConnectionLimiter {
public:
    explicit ConnectionLimiter(size_t max_connections)
        : max_connections_(max_connections)
        , current_connections_(0) {}

    bool try_acquire() {
        std::lock_guard lock(mutex_);
        if (current_connections_ >= max_connections_) {
            return false;
        }
        ++current_connections_;
        return true;
    }

    void release() {
        std::lock_guard lock(mutex_);
        if (current_connections_ > 0) {
            --current_connections_;
        }
    }

    size_t current_count() const {
        std::lock_guard lock(mutex_);
        return current_connections_;
    }

private:
    size_t max_connections_;
    size_t current_connections_;
    mutable std::mutex mutex_;
};

Acceptor에 연결 제한 적용

void do_accept() {
    acceptor_.async_accept(
        [this](boost::system::error_code ec, tcp::socket socket) {
            if (!ec) {
                if (!limiter_.try_acquire()) {
                    // 연결 거부: "서버 만료" 응답 후 소켓 닫기
                    std::string msg = "ERROR: Server at capacity\n";
                    asio::write(socket, asio::buffer(msg));
                    socket.close();
                } else {
                    auto session = std::make_shared<Session>(
                        std::move(socket), room_,
                        [this]() { limiter_.release(); }  // 퇴장 시 release
                    );
                    session->start();
                }
            }
            do_accept();
        });
}

Session에 release 콜백 추가

class Session : public std::enable_shared_from_this<Session> {
public:
    using ReleaseCallback = std::function<void()>;

    Session(tcp::socket socket, ChatRoom& room, ReleaseCallback on_release)
        : socket_(std::move(socket))
        , room_(room)
        , on_release_(std::move(on_release)) {}

    void handle_error(boost::system::error_code ec) {
        if (ec == asio::error::eof || ec == asio::error::connection_reset) {
            room_.leave(shared_from_this());
            room_.broadcast_leave(nickname_);
            if (on_release_) {
                on_release_();
            }
        }
    }

private:
    ReleaseCallback on_release_;
};

대기 큐 방식 (선택)

연결 수 초과 시 즉시 거부 대신 대기 큐에 넣어 짧은 시간 후 재시도할 수 있게 합니다. try_acquireOk, Rejected, Queued 중 하나를 반환하고, Queued일 때는 별도 스레드가 연결 해제 시 큐에서 꺼내 처리합니다.


6. 완전한 채팅 서버 아키텍처 예제

전체 구조 다이어그램

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

    subgraph Server["채팅 서버"]
        subgraph Acceptor["Acceptor"]
            A[async_accept]
            LIM[ConnectionLimiter]
        end

        subgraph IOPool["io_context 풀"]
            IO1[io_context 1]
            IO2[io_context 2]
            IO3[io_context 3]
            IO4[io_context 4]
        end

        subgraph Core["핵심 컴포넌트"]
            RM[RoomManager]
            MR[MessageRouter]
        end

        subgraph Rooms["Room 인스턴스"]
            R1[#general]
            R2[#random]
        end
    end

    C1 --> A
    C2 --> A
    C3 --> A

    A --> LIM
    LIM --> IOPool

    IOPool --> Core
    RM --> R1
    RM --> R2
    MR --> R1
    MR --> R2

통합 서버 클래스

class ChatServerArchitecture {
public:
    ChatServerArchitecture(
        const tcp::endpoint& endpoint,
        size_t io_pool_size = 4,
        size_t max_connections = 10000
    )
        : limiter_(max_connections)
        , room_mgr_(main_io_)
        , router_(room_mgr_)
        , pool_size_(io_pool_size)
        , next_io_(0) {
        for (size_t i = 0; i < io_pool_size; ++i) {
            io_pools_.push_back(std::make_shared<asio::io_context>());
        }
        acceptor_ = std::make_unique<tcp::acceptor>(*io_pools_[0], endpoint);
    }

    void run() {
        std::vector<std::thread> threads;
        for (auto& io : io_pools_) {
            threads.emplace_back([&io] { io->run(); });
        }

        do_accept();

        for (auto& t : threads) {
            t.join();
        }
    }

private:
    void do_accept() {
        acceptor_->async_accept(
            [this](boost::system::error_code ec, tcp::socket socket) {
                if (!ec) {
                    if (!limiter_.try_acquire()) {
                        socket.close();
                    } else {
                        auto& io = *io_pools_[next_io_ % pool_size_];
                        next_io_++;

                        auto room = room_mgr_.get_or_create("general");
                        auto session = std::make_shared<Session>(
                            std::move(socket), *room, room_mgr_, router_,
                            [this]() { limiter_.release(); }
                        );

                        asio::post(io, [session]() mutable {
                            session->start();
                        });
                    }
                }
                do_accept();
            });
    }

    ConnectionLimiter limiter_;
    asio::io_context main_io_;
    std::unique_ptr<tcp::acceptor> acceptor_;
    std::vector<std::shared_ptr<asio::io_context>> io_pools_;
    size_t pool_size_;
    size_t next_io_;
    RoomManager room_mgr_;
    MessageRouter router_;
};

빌드 및 실행

# 빌드 (Boost.Asio 필요)
g++ -std=c++17 -O2 -pthread -o chat_server_arch \
    chat_server_architecture.cpp \
    -lboost_system

# 실행
./chat_server_arch

# 테스트 (다른 터미널에서)
nc localhost 9000
NICK alice
JOIN general
MSG general 안녕하세요

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

에러 1: “iterator invalidation” / “vector subscript out of range”

원인: participants_를 순회하는 동안 leave()가 호출되어 컨테이너가 수정됩니다.

해결:

// ❌ 잘못된 코드
for (auto& p : participants_) {
    p->deliver(msg);  // deliver 내부에서 leave 호출 가능 → iterator 무효화
}

// ✅ 올바른 코드: 복사본 순회
auto copy = participants_;
for (auto& p : copy) {
    if (participants_.count(p)) {  // 아직 남아있는지 확인
        p->deliver(msg);
    }
}

또는 strand로 모든 수정을 직렬화합니다.

에러 2: “Bad file descriptor” / “Connection reset by peer”

원인: 세션이 소멸된 후에도 async_write 완료 핸들러가 실행되거나, 이미 닫힌 소켓에 쓰기를 시도합니다.

해결:

// ✅ shared_from_this()로 수명 유지
void do_write() {
    asio::async_write(socket_, asio::buffer(msg),
        [self = shared_from_this()](error_code ec, size_t) {
            if (ec) {
                self->handle_error(ec);
                return;
            }
            // self가 살아있는 동안만 접근
        });
}

에러 3: “Address already in use”

원인: 이전 서버 프로세스가 종료되지 않았거나, TIME_WAIT 상태의 소켓이 포트를 점유합니다.

해결:

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

에러 4: “메시지 순서 뒤바뀜”

원인: 여러 스레드에서 같은 세션의 async_write를 동시에 걸어 순서가 보장되지 않습니다.

해결: write_queue를 두고, 한 번에 하나의 async_write만 실행합니다.

void deliver(const std::string& msg) {
    bool in_progress = !write_queue_.empty();
    write_queue_.push_back(msg);
    if (!in_progress) {
        do_write();
    }
}

void do_write() {
    if (write_queue_.empty()) return;
    const auto& msg = write_queue_.front();
    asio::async_write(socket_, asio::buffer(msg),
        [self = shared_from_this()](error_code ec, size_t) {
            self->write_queue_.pop_front();
            if (!self->write_queue_.empty()) {
                self->do_write();
            }
        });
}

에러 5: “메모리 누수 - Room이 삭제되지 않음”

원인: 참가자가 0명이 되어도 rooms_에서 제거하지 않습니다.

해결: leave()remove_if_empty() 호출.

void leave(std::shared_ptr<Session> session) {
    asio::post(strand_, [this, session]() {
        participants_.erase(session);
        if (participants_.empty()) {
            room_manager_.remove_if_empty(channel_id_);
        }
    });
}

에러 6: “async_read_until으로 DoS 공격”

원인: async_read_until(socket, buf, '\n')\n이 올 때까지 무제한 버퍼링합니다. 악의적 클라이언트가 \n 없이 대량 데이터를 보내면 메모리 폭발합니다.

해결: 최대 메시지 크기 제한.

asio::async_read_until(socket_, read_buf_, '\n',
    asio::transfer_at_least(1),
    [self = shared_from_this()](error_code ec, size_t n) {
        if (self->read_buf_.size() > 64 * 1024) {  // 64KB 제한
            self->socket_.close();
            return;
        }
        // ...
    });

8. 모범 사례

1. strand로 공유 상태 보호

participants_, history_ 등 공유 컨테이너는 반드시 같은 strand에서만 접근합니다.

void deliver(const std::string& msg, std::shared_ptr<Session> sender) {
    asio::post(strand_, [this, msg, sender]() {
        history_.push_back(msg);
        for (auto& p : participants_) {
            if (p != sender) p->deliver(msg);
        }
    });
}

2. shared_ptr로 세션 수명 관리

비동기 연산 완료 시점에 세션이 살아있어야 하므로 shared_from_this()를 캡처합니다.

asio::async_read_until(socket_, read_buf_, '\n',
    asio::bind_executor(strand_, [self = shared_from_this()](error_code ec, size_t) {
        if (ec) { self->handle_error(ec); return; }
        self->process_message();
        self->do_read();
    }));

3. write_queue 크기 제한

느린 클라이언트가 전체를 지연시키지 않도록 큐 크기를 제한합니다.

void deliver(const std::string& msg) {
    if (write_queue_.size() >= 500) {
        socket_.close();  // 강제 퇴장
        return;
    }
    // ...
}

4. 메시지 크기 제한

단일 메시지가 너무 크면 DoS에 취약합니다. 4KB~64KB 정도로 제한합니다.

5. 로깅 및 메트릭

연결 수, 방 수, 메시지 처리량을 주기적으로 로깅합니다.

void log_metrics() {
    spdlog::info("Connections: {}, Rooms: {}, Messages/sec: {}",
        limiter_.current_count(),
        room_mgr_.room_count(),
        messages_per_sec_);
}

9. 프로덕션 패턴

패턴 1: 헬스 체크 엔드포인트

로드 밸런서나 오케스트레이터가 서버 상태를 확인할 수 있도록 합니다.

// 별도 포트(예: 9001)에서 HTTP 헬스 체크 수신
void run_health_check_server(uint16_t port) {
    asio::io_context io;
    tcp::acceptor acc(io, tcp::endpoint(tcp::v4(), port));
    tcp::socket sock(io);
    acc.async_accept(sock, [&](error_code ec) {
        if (!ec) {
            std::string response = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK";
            asio::write(sock, asio::buffer(response));
        }
    });
    io.run();
}

패턴 2: Graceful Shutdown

새 연결 수락을 중단하고, 기존 연결이 정상 종료될 때까지 대기합니다.

void shutdown() {
    acceptor_.close();
    for (auto& session : all_sessions_) {
        session->close();
    }
    io_.stop();
}

패턴 3: 수평 확장 (멀티 서버)

여러 채팅 서버 인스턴스 앞에 메시지 브로커(Redis Pub/Sub, RabbitMQ)를 두어, 서버 간 메시지를 동기화합니다.

[Client A] ---> [Server 1] ---> [Redis Pub/Sub] ---> [Server 2] ---> [Client B]

패턴 4: 백프레셔 (Backpressure)

클라이언트가 메시지를 너무 빠르게 보낼 때, 서버가 “천천히 보내라”는 신호를 보냅니다.

if (messages_received_per_second_ > 100) {
    send_to_client("RATE_LIMIT: slow down");
    return;
}

패턴 5: 모니터링 대시보드

Prometheus + Grafana로 연결 수, 방 수, 지연 시간, 에러율을 시각화합니다.

// Prometheus 메트릭 노출
// chat_connections_total, chat_rooms_total, chat_messages_total

10. 정리와 다음 단계

핵심 요약

컴포넌트역할
Acceptor-Worker연결 수락과 I/O 처리 분리, 스레드 풀로 병렬화
RoomManager채널별 Room 생성/삭제, 참가자 관리
MessageRouter방별 브로드캐스트, DM 라우팅
ConnectionLimiter동시 연결 수 제한, 리소스 보호
strand공유 상태 접근 직렬화, 데이터 레이스 방지

구현 체크리스트

  • Acceptor-Worker 또는 io_context 풀 적용
  • RoomManager로 채널별 Room 분리
  • MessageRouter로 메시지 라우팅 (방/DM)
  • ConnectionLimiter로 연결 수 제한
  • strand로 participants_ 접근 직렬화
  • write_queue 크기 제한 (느린 클라이언트 대응)
  • 메시지 크기 제한 (DoS 방지)
  • Graceful Shutdown 구현
  • 헬스 체크 엔드포인트
  • 로깅 및 메트릭

다음 글

  • C++ 채팅 서버 만들기 (#31-1): 기본 브로드캐스트 구현
  • C++ REST API 서버 (#50-2): HTTP API 연동
  • C++ 멀티스레드 서버 (#29-3): strand, io_context 풀 상세

참고 자료:


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ 채팅 서버 만들기 | 다중 클라이언트와 메시지 브로드캐스트 완벽 가이드 [#31-1]
  • C++ 멀티스레드 네트워크 서버 완벽 가이드 | io_context 풀·strand·data race 방지
  • C++ REST API 서버 만들기 | 라우팅·미들웨어·인증·Swagger 문서화 [#50-2]
  • C++ 백엔드·게임 서버 시스템 디자인 | 대규모 동시 접속과 메모리 풀 [#46-1]
  • C++ 디버깅 완벽 가이드 | GDB·Sanitizer·메모리 누수·멀티스레드 디버깅 실전
  • C++ 프로토콜 설계와 직렬화 | TCP 메시지 경계·길이 프리픽스·바이너리 포맷 완벽 가이드 [#30-3]

이 글에서 다루는 키워드 (관련 검색어)

C++, 채팅서버, 아키텍처, Asio, Acceptor-Worker, 방관리, 메시지라우팅, 커넥션풀 등으로 검색하시면 이 글이 도움이 됩니다.

실전 체크리스트

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

코드 작성 전

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

코드 작성 중

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

코드 리뷰 시

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

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


자주 묻는 질문 (FAQ)

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

A. 채팅 서버 아키텍처 설계: Acceptor-Worker 패턴, 방 관리, 메시지 라우팅, 커넥션 풀, 일반적인 에러와 프로덕션 패턴까지 실전 구현. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

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

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

Q. 더 깊이 공부하려면?

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


관련 글

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