[2026] C++ 채팅 서버 완성하기 | 인증·방 관리·메시지 히스토리 구현 [#50-1]
이 글의 핵심
C++ 채팅 서버 완성하기: 인증·방 관리·메시지 히스토리 구현 [#50-1]. 실무에서 겪는 채팅 서버 이슈·전체 아키텍처.
들어가며: “기본 채팅 서버에 실전 기능을 추가하고 싶어요”
실전 채팅 서버의 요구사항
기본 브로드캐스트 채팅 서버를 만들었다면, 이제 사용자 인증, 여러 방 관리, 메시지 히스토리, 파일 전송, 재연결 처리 등 실무에서 필요한 기능을 추가해야 합니다. 목표:
- 사용자 인증 및 세션 관리
- 다중 방(채널) 생성 및 입장/퇴장
- 메시지 히스토리 저장 및 조회
- 파일 전송 프로토콜
- 재연결 시 상태 복구 요구 환경: C++17 이상, Boost.Asio, SQLite 또는 PostgreSQL 이 글을 읽으면:
- 실전 채팅 서버 아키텍처를 이해할 수 있습니다.
- 사용자 인증 및 세션 관리를 구현할 수 있습니다.
- 다중 방 관리 시스템을 만들 수 있습니다.
- 메시지 히스토리를 DB에 저장하고 조회할 수 있습니다.
실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
문제 시나리오: 실무에서 겪는 채팅 서버 이슈
시나리오 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. 전체 아키텍처
시스템 구성
다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
flowchart TB
Client["Client\n(WebSocket)"]
subgraph ChatServer[Chat Server]
CM["\"Connection Manager\n- 세션 관리\n- 인증 처리\""]
RM["\"Room Manager\n- 방 생성/삭제\n- 참가자 관리\""]
MR["\"Message Router\n- 메시지 라우팅\n- 브로드캐스트\""]
end
DB["\"Database\n- 사용자 정보\n- 메시지 히스토리\n- 방 정보\""]
Client --> ChatServer
CM --> RM
CM --> MR
ChatServer --> DB
아키텍처 다이어그램 (Mermaid)
다음은 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
핵심 클래스
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 기반 인증
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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;
}
}
};
세션 인증 처리
다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 구현
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 클래스 상세
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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. 메시지 히스토리
데이터베이스 스키마
아래 코드는 sql를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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);
히스토리 조회
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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;
}
};
페이지네이션
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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. 파일 전송
청크 기반 전송
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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. 재연결 처리
세션 복구
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 함수와 서버 기동
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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;
}
클라이언트-서버 메시지 프로토콜 예제
아래 코드는 json를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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 핸드셰이크 후 메시지 처리 루프
다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 디코딩 오류 (토큰에 공백/줄바꿈 포함) 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ✅ 토큰 검증 시 만료 시간 체크
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로 보관하면 순환 참조가 됩니다.
해결법:
아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 잘못된 예: 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() 없이 프로세스를 종료했거나, 네트워크 불안정입니다.
해결법:
다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ✅ 에러 시 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하면 락이 걸립니다.
해결법:
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ 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 미설정입니다.
해결법:
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ 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”
증상: 대용량 파일 업로드 중 디스크 풀 에러가 발생합니다. 원인: 업로드 디렉터리 용량 부족, 또는 업로드 전 크기 검증 없음. 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ✅ 업로드 전 크기 제한 (예: 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를 동시에 여러 번 호출하면 데이터가 섞입니다. 메시지 큐로 직렬화합니다.
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 쓰기를 수행합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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]를 하면 할당 오버헤드가 큽니다. 객체 풀 또는 고정 크기 버퍼를 재사용합니다.
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 고정 크기 버퍼 풀
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를 두면 병렬성이 올라갑니다. 다음은 간단한 cpp 코드 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.
class Room {
asio::strand<asio::io_context::executor_type> strand_;
// 각 Room이 자신만의 strand를 가짐
};
팁 5: 연결 수 제한
무제한 연결을 허용하면 메모리와 파일 디스크립터가 고갈됩니다. 최대 연결 수를 두고 초과 시 거부합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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)
아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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: 헬스 체크
다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 주기적으로 서버 상태 확인
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: 그레이스풀 셧다운
아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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: 로깅 및 메트릭
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 구조화된 로깅
spdlog::info("msg_sent room={} user={} size={}",
room_id, user_id, msg.size());
// Prometheus 메트릭 (예시)
metrics::counter messages_sent_total;
metrics::gauge active_connections;
패턴 5: 설정 외부화
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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. 메시지 큐 관리 (재확인)
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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. 부하 분산 (재확인)
아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.