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. 전체 아키텍처
시스템 구성
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 순환 참조
증상: 프로그램이 크래시하거나 메모리 누수가 발생합니다.
원인: Room이 Session을 shared_ptr로 보관하고, Session이 Room을 shared_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 |
| 재연결 | 세션 상태 저장/복구 |
핵심 원칙:
- 모든 상태 변경은 strand로 직렬화
- 메시지는 DB에 저장하여 영속성 보장
- 파일은 청크 단위로 전송
- 재연결 시 놓친 메시지 전송
- 부하 분산으로 확장성 확보
실전 체크리스트
실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.
코드 작성 전
- 이 기법이 현재 문제를 해결하는 최선의 방법인가?
- 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
- 성능 요구사항을 만족하는가?
코드 작성 중
- 컴파일러 경고를 모두 해결했는가?
- 엣지 케이스를 고려했는가?
- 에러 처리가 적절한가?
코드 리뷰 시
- 코드의 의도가 명확한가?
- 테스트 케이스가 충분한가?
- 문서화가 되어 있는가?
이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.
자주 묻는 질문 (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 |