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는 한 우체국에서 여러 배달부(스레드·핸들러)가 일을 나누는 구조로 보시면 됩니다.
목차
- 문제 시나리오 상세
- Acceptor-Worker 아키텍처
- 방 관리 시스템
- 메시지 라우팅
- 커넥션 풀
- 완전한 채팅 서버 아키텍처 예제
- 자주 발생하는 에러와 해결법
- 모범 사례
- 프로덕션 패턴
- 정리와 다음 단계
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_acquire가 Ok, 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 |