이전에 Core 라이브러리에 대해 설명했다.
여기선 해당 라이브러리를 활용해 작성된 Server에 대해 얘기해 보자.
1. 아키텍처 철학
Server는 "Massively Chat Server"를 지향하며, 다음 3가지 핵심 철학을 지향한다.
- Gateway-Centric Architecture:
- 클라이언트는 오직 Gateway(w/ L7 Load Balancer)하고만 연결한다.
- Gateway는 뒷단에 있는 N개의 Server Node 중 하나로 트래픽을 전달한다.
- Effect: 서버 노드 추가/삭제가 자유롭고(Elasticity), 클라이언트는 IP 변경 없이 그대로 사용 가능.
- Safety First (Concurrency):
- Global Mutex + JobQueue: 모든 패킷 처리는 I/O 스레드에서 분리되어
JobQueue를 통해 Worker 스레드에서 실행된다. - Strand per Room: 단일 방(Room)에 대한 처리는
boost::asio::strand를 통해 순차 실행을 보장하여, Lock 경합을 줄이면서도 동시성 문제(Race Condition)를 원천 차단한다.
- Global Mutex + JobQueue: 모든 패킷 처리는 I/O 스레드에서 분리되어
- Hybrid State Management:
- Memory: 빠른 응답을 위해 방 참여자 목록(
state_.rooms)은 메모리에 들고 있다. - Redis: 서버 간 동기화, 서비스 발견(Registry), 세션 스티키니스(Stickiness)는 Redis에 위임(Gateway에서 처리).
- DB (Postgres): 영구 보존이 필요한 데이터(메시지 로그, 유저 정보)는 DB에 저장하되, Write-Behind 패턴으로 지연 기록하여 성능 저하를 막는다.
- Memory: 빠른 응답을 위해 방 참여자 목록(
2. 디렉토리 및 모듈 구조
소스 코드는 ``server/src`` 하위에 기능별로 분리되어 있다.
server/src/
├── app/ # [Application Layer]
│ ├── bootstrap.cpp # 서버 구동의 시작점 (main 흐름)
│ ├── config.cpp # 환경변수 파싱 및 Config 객체 생성
│ ├── router.cpp # Dispatcher(Opcode -> Handler) 매핑
│ └── metrics_server.cpp# Prometheus 메트릭 수집 서버 (HTTP)
├── chat/ # [Business Logic Layer]
│ ├── chat_service_core.cpp # ChatService 클래스 공통 로직, 멤버 변수
│ ├── handlers_login.cpp # 로그인 처리
│ ├── handlers_join.cpp # 방 입장 처리
│ ├── handlers_leave.cpp # 방 퇴장 처리
│ ├── handlers_chat.cpp # 메시지 전송, 귓속말, 슬래시 커맨드
│ ├── handlers_ping.cpp # Ping/Pong (Liveness Check)
│ └── session_events.cpp # 세션 종료(Disconnect) 처리
├── state/ # [Distributed State Layer]
│ └── instance_registry.cpp # Redis 기반 서버 인스턴스 등록/조회
└── storage/ # [Infrastructure Layer]
├── postgres/ # PostgreSQL 연결 풀 및 Repository 구현
└── redis/ # Redis 클라이언트 Wrapper
3. 부트스트랩 시퀀스
서버가 시작될 때(``bootstrap.cpp``) 수행되는 **초기화 시퀀스**는 시스템의 핵심이다.
하나라도 실패하면 서버는 뜨지 못한다 (Fail-Fast).
| Step | Components | Description |
|---|---|---|
| 1 | io_context |
Boost.Asio의 핵심 객체. 모든 비동기 작업(Timer, Network I/O)이 여기에 등록된다. |
| 2 | JobQueue |
server::core::JobQueue를 생성한다. 네트워크 I/O 스레드가 로직 처리로 바빠지는 것을 막기 위해, 실제 로직은 이 큐를 통해 Worker 스레드로 넘긴다. |
| 3 | ServiceRegistry |
전역 의존성 주입(DI) 컨테이너(services::set())를 초기화한다. 싱글톤 패턴보다 테스트 용이성이 뛰어남. |
| 4 | DB Pool |
PostgreSQL Connection Pool을 생성하고 health_check()를 수행한다. DB가 죽어있으면 서버도 시작하지 않는다. |
| 5 | Redis Client |
Redis 연결을 맺고 PING을 날려 확인한다. Redis 없이는 서버 구실을 못하므로 필수 체크 항목이다. |
| 6 | InstanceRegistry |
내 서버의 ID와 IP/Port 정보를 담은 InstanceRecord를 생성하고, Redis에 등록할 준비를 한다. |
| 7 | Scheduler |
주기적인 작업(Heartbeat, Metric Tick)을 수행할 TaskScheduler를 시작한다. |
| 8 | ChatService |
로직의 본체인 ChatService를 생성한다. 이때 DB, Redis, JobQueue 객체를 주입받아 완성다. |
| 9 | Dispatcher |
Opcode와 ChatService의 메소드를 매핑한다. (예: MSG_CHAT_SEND -> chat.on_chat_send) |
| 10 | Metrics Server |
Prometheus가 긁어갈 수 있도록 별도의 포트에 HTTP 서버를 띄운다. 메인 로직과 스레드가 분리되어 있음. |
| 11 | Acceptor |
실제 클라이언트(또는 Gateway)가 접속할 TCP 포트를 연다. |
| 12 | Workers & Heartbeat |
CPU 코어 수만큼 Worker 스레드를 생성하여 JobQueue를 소비하기 시작하고, Redis에 첫 Heartbeat를 전송하여 서비스를 개시한다. |
4. 주요 컴포넌트
4.1. ChatService
// server/include/server/chat/chat_service.hpp
class ChatService {
struct State {
std::unordered_map<string, RoomSet> rooms; // Memory State
std::unordered_map<Session*, string> user;
} state_;
// Core Dependencies
server::core::JobQueue& job_queue_;
std::shared_ptr<IRedisClient> redis_;
std::shared_ptr<IConnectionPool> db_pool_;
};
- 역할: 서버의 CPU. 모든 로직(로그인, 채팅, 방 관리)을 총괄하고, DB와 Redis 사이를 중재(Mediator Pattern)한다.
- Design Patterns:
- Facade: 복잡한 서브시스템을 숨기고, 네트워크 계층에는
on_login,on_chat_send같은 단순한 인터페이스만 노출한다. Dispatcher는 내부 구현을 알 필요가 없다. - Mediator: 컴포넌트 간의 복잡한 통신(N:M 관계)을 한 곳으로 집중시킨다. "유저" 객체는 "Redis"를 모르며 당연히 알 필요가 없다. 오직
ChatService를 통해서만 상호작용한다. 이를 통해 결합도를 낮출 수 있다.
- Facade: 복잡한 서브시스템을 숨기고, 네트워크 계층에는
4.2. InstanceRegistry
// server/src/state/instance_registry.cpp
bool RedisInstanceStateBackend::write_record(const InstanceRecord& record) {
// 30초 TTL로 내 생존 신고 (Heartbeat)
return client_->setex("server:registry:" + record.instance_id, 30);
}
- 역할: "나 살아있다"고 Redis에 주기적으로 알린다. (Heartbeat)
- 이점: 중앙 관리 서버(Master) 없이도, 모든 서버가 서로의 존재를 알 수 있게 한다. (Peer-to-Peer Discovery) Gateway는 이 정보를 보고 각 서버로 트래픽을 분산시킨다.
4.3. Dispatcher (Packet Router)
// server/src/app/router.cpp
dispatcher.register_handler(MSG_CHAT_SEND,
[&chat](Session& s, std::span<const uint8_t> payload) {
chat.on_chat_send(s, payload);
});
- 역할:
Opcode(메시지 ID)와 처리 함수(Handler)를 1:1로 매핑한다. - 구현:
std::unordered_map이나 배열을 사용하여 O(1) 조회 속도를 보장. - 특징: 핸들러는
std::function으로 래핑되며,server::app::register_routes함수에서 일괄 등록된다. 이를 통해 네트워크 레이어(Core)와 애플리케이션 레이어(App)가 분리된다.
4.4. MetricsServer (Observability)
// server/src/app/metrics_server.cpp
void MetricsServer::do_accept() {
// 9090 포트로 들어오는 HTTP 요청을 수동으로 파싱
if (target == "/metrics") {
auto snap = runtime_metrics::snapshot();
// Prometheus 포맷 텍스트 생성
stream << "chat_job_queue_depth " << snap.job_queue_depth << "\n";
}
}
- 역할: Prometheus가 서버 상태를 수집(Scrape)할 수 있는 HTTP 엔드포인트(
/metrics)를 제공한다. - 이점: 별도의 무거운 웹 프레임워크 없이
boost::asio만으로 가볍게 구현되었다. Main Loop와는 별도의 스레드에서 돌아가므로, 메인 로직이 바빠도 모니터링은 죽지 않는다.
5. 요청 처리 흐름
패킷 하나가 들어왔을 때의 여정을 추적한다.
- Network I/O:
Session::read_loop가 TCP 버퍼에서 데이터를 읽는다. - Framing:
PacketHeader를 파싱하여 패킷을 확실히 식별한다. - Dispatch:
router.cpp에 등록된dispatcher가 Opcode를 확인한다. - Job Enqueue:
ChatService::on_chat_send가 호출되지만, 로직을 직접 수행하지 않고 람다(Lambda)로 감싸서JobQueue에 집어넣고 즉시 리턴한다. - Job Dequeue: Worker 스레드 중 하나가 람다를 꺼내 실행한다.
- Logic Execution: 권한 검사, DB 저장, Redis Pub/Sub, 브로드캐스트 패킷 생성이 진행된다.
- Send: 결과 패킷들을
Session::async_send대기열에 넣는다. (IO 스레드가 나중에 모아서 보냄 - Gather Write)
6. 핸들러
6.1. Login Handler (handlers_login.cpp)
가장 복잡한 핸들러 중 하나로, 세션의 시작을 알린다.
- Logic:
get_or_create_session_uuid: 현재 TCP 세션에 고유 UUID(v4)를 부여한다.ensure_unique_or_error: 닉네임 중복 체크. "guest"로 들어오면 자동으로 "guest-a1b2..." 같은 이름을 부여한다.state_.muLock: 전역 유저 맵(state_.user)에(Session Ptr -> Nickname)매핑을 등록한다.- DB Upsert:
users테이블에 접속 기록(IP, 시간)을 남기고, 필요하면 신규 유저 레코드를 생성한다. (Postgres UPSERT) - Audit Log: "누가 접속했다"는 시스템 로그를 로비 채팅창에 날린다.
- Redis Touch:
presence:user:{uid}키에 TTL을 설정하여 "나 온라인이야"라고 표시한다. - Write-Behind:
emit_write_behind_event("session_login", ...)를 통해 로그인 이벤트를 Redis Stream에 던진다.
6.2. Join Handler (handlers_join.cpp)
유저가 방을 옮길 때 호출된다.
- Logic:
- Redis Password Check: 입장하려는 방의 비밀번호를 Redis에서 먼저 조회한다. (Redis가 Master of Truth)
state_.muLock:- 현재 방에서
erase(퇴장 처리). - 비밀번호 입력값 검증 (
hash_room_password). -> 비밀번호가 없다면 그냥 입장할 수 있다. - 새 방에
insert(입장 처리). - 만약 이전 방이 비어버렸으면(
empty()), 방 객체를 메모리에서 제거함.
- 현재 방에서
- Broadcasting:
- 새 방 유저들에게: "XXX님이 입장했습니다." 등 전체 공지를 쏘는 식.
- Redis Pub/Sub을 통해 모든 서버에 브로드캐스팅된다.
- Sync Membership: DB
memberships테이블에(user_id, room_id, role)정보를 업데이트한다. - Snapshot: 클라이언트 UI 갱신을 위해 방 정보, 참여자 목록, 최근 메시지 20개를 한 번에 담은
MSG_STATE_SNAPSHOT을 전송한다.
6.3. Chat & Whisper Handler (handlers_chat.cpp)
- Regular Chat:
/로 시작하면 슬래시 커맨드 핸들러로 분기한다. (/refresh,/who등)- 권한 확인: 현재 사용자가 해당 방(Room)에 실제로 있는지
state_를 통해 검증한다. - Fan-out:
- Local: 내 서버 메모리에 있는 같은 방 유저들에게
async_send. - Global:
redis_->publish("fanout:room:{room}", msg)로 다른 서버에 있는 유저들에게도 메시지 전달.
- Local: 내 서버 메모리에 있는 같은 방 유저들에게
- Persistence: DB
messages테이블에 저장한다. 실패해도 채팅은 계속되어야 하므로try-catch로 감싸고 에러 로그만 남긴다.
- Whisper:
- TODO
- 클라이언트를 TUI에서 ImGui를 활용한 GUI로 변환하며 구현할 수도 있다.
6.4. Leave & Disconnect Handler (handlers_leave.cpp, session_events.cpp)
- Leave: 유저가 명시적으로 "나가기" 요청을 한 경우.
- 로직은
Join의 앞부분(퇴장 루틴)과 동일하다. - 항상 "Lobby"로 강제 이동시킨다. (고아 세션 방지)
- Lobby가 가장 기본인 방이므로 무조건 여기로 보내야 한다.
- 로직은
- Disconnect: 갑자기 랜선이 뽑히거나 강제종료된 경우.
session_events.cpp의on_session_close()가 호출된다.JobQueue에 정리 작업을 예약한다. (소멸자가 아님! ``shared_ptr`` 수명 관리 중요)- Redis Presence 키(
presence:user:{uid})를 즉시 삭제하지 않고 만료되게 두거나, 명시적으로 지울 수 있다.
7. 데이터 지속성 & Write-Behind
본 서버엔 데이터 쓰기 지연(Write-Behind) 패턴이 있다.
7.1. 흐름
- 핸들러가
ChatService::emit_write_behind_event()호출. - 이벤트 타입(
chat_msg,login,join)과 데이터를 JSON이 아닌 Key-Value 쌍으로 변환. REDIS XADD stream:events * type chat_msg user bvm ...명령 실행.- 끝. (DB 저장을 기다리지 않음 -> 매우 빠름)
7.2. 컨슈머 (Worker)
별도의 프로세스(또는 스레드)가 XREADGROUP으로 스트림을 읽어와서 Postgres에 INSERT 한다. DB가 일시적으로 느려지거나 죽어도, Redis Stream에 데이터가 쌓일 뿐 서비스는 멈추지 않는다.
현재 Redis는 단일 인스턴스이므로 SPOF이기 때문에 클러스터링이 필요하다.
8. 분산 상태 관리 (Redis)
Redis는 단순 캐시가 아니라 제2의 메모리이다.
server:registry:{id}: 서버 생존 신고 (Heartbeat).room:users:{room}: 해당 방에 누가 있는지 (Set).user_count조회용.room:password:{room}: 방 비밀번호 (Single Source of Truth).presence:user:{uid}: 유저가 온라인인지 여부.fanout:room:{room}: 채팅 메시지 Pub/Sub 채널.
9. Configuration & Metrics
9.1. Environment Variables
GATEWAY_ID: 나를 관리하는 Gateway 식별자.WRITE_BEHIND_ENABLED: 1이면 활성화.REDIS_URI:tcp://127.0.0.1:6379.DB_DSN: Postgres 접속 정보.
9.2. Prometheus Metrics (/metrics)
chat_session_active: 현재 접속자 수 (Gauge).chat_job_queue_depth: 처리 대기 중인 작업 수 (Gauge). 위험 신호 감지용.chat_dispatch_total: 처리한 패킷 총량 (Counter).chat_db_job_failed_total: DB 저장 실패 횟수 (Counter).
10. TODO
- SPOF 처리
- Redis
- Gateway
- PostgreSQL
- 위 셋 다 단일 인스턴스임!
- 인증 및 보안 관련 구현
- k8s를 통한 오케스트레이션
내가 구두로 지시해 AI가 작성한 코드긴 하지만, 그걸 내가 확실하게 짚고 갈 수 있느냐는 다른 문제다.
내가 생각하는 코드보다 더 깔끔한 코드를 대부분의 상황에서 작성해 주기 때문에 좋은 공부가 된다.
다음엔 클라이언트에 대해 얘기해 보자.
'Study > C++ & C#' 카테고리의 다른 글
| AI로 MSA 서버 만들어 보기 #4 : Write-Behind (0) | 2025.12.09 |
|---|---|
| AI로 MSA 서버 만들어 보기 #3 : Gateway (0) | 2025.12.09 |
| AI로 MSA 서버 만들어 보기 #1 : Core (0) | 2025.12.03 |
| [C++] 채팅서버에 DB 실장 (0) | 2024.05.14 |
| [C#] ###Clicker 동작 개선 (0) | 2024.05.05 |