AI로 MSA 서버 만들어 보기 #2 : Server

2025. 12. 7. 20:06·Study/C++ & C#

이전에 Core 라이브러리에 대해 설명했다.

여기선 해당 라이브러리를 활용해 작성된 Server에 대해 얘기해 보자.


1. 아키텍처 철학

Server는 "Massively Chat Server"를 지향하며, 다음 3가지 핵심 철학을 지향한다.

  1. Gateway-Centric Architecture:
    • 클라이언트는 오직 Gateway(w/ L7 Load Balancer)하고만 연결한다.
    • Gateway는 뒷단에 있는 N개의 Server Node 중 하나로 트래픽을 전달한다.
    • Effect: 서버 노드 추가/삭제가 자유롭고(Elasticity), 클라이언트는 IP 변경 없이 그대로 사용 가능.
  2. Safety First (Concurrency):
    • Global Mutex + JobQueue: 모든 패킷 처리는 I/O 스레드에서 분리되어 JobQueue를 통해 Worker 스레드에서 실행된다.
    • Strand per Room: 단일 방(Room)에 대한 처리는 boost::asio::strand를 통해 순차 실행을 보장하여, Lock 경합을 줄이면서도 동시성 문제(Race Condition)를 원천 차단한다.
  3. Hybrid State Management:
    • Memory: 빠른 응답을 위해 방 참여자 목록(state_.rooms)은 메모리에 들고 있다.
    • Redis: 서버 간 동기화, 서비스 발견(Registry), 세션 스티키니스(Stickiness)는 Redis에 위임(Gateway에서 처리).
    • DB (Postgres): 영구 보존이 필요한 데이터(메시지 로그, 유저 정보)는 DB에 저장하되, Write-Behind 패턴으로 지연 기록하여 성능 저하를 막는다.

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를 통해서만 상호작용한다. 이를 통해 결합도를 낮출 수 있다.

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. 요청 처리 흐름

패킷 하나가 들어왔을 때의 여정을 추적한다.

  1. Network I/O: Session::read_loop가 TCP 버퍼에서 데이터를 읽는다.
  2. Framing: PacketHeader를 파싱하여 패킷을 확실히 식별한다.
  3. Dispatch: router.cpp에 등록된 dispatcher가 Opcode를 확인한다.
  4. Job Enqueue: ChatService::on_chat_send가 호출되지만, 로직을 직접 수행하지 않고 람다(Lambda)로 감싸서 JobQueue에 집어넣고 즉시 리턴한다.
  5. Job Dequeue: Worker 스레드 중 하나가 람다를 꺼내 실행한다.
  6. Logic Execution: 권한 검사, DB 저장, Redis Pub/Sub, 브로드캐스트 패킷 생성이 진행된다.
  7. Send: 결과 패킷들을 Session::async_send 대기열에 넣는다. (IO 스레드가 나중에 모아서 보냄 - Gather Write)

6. 핸들러

6.1. Login Handler (handlers_login.cpp)

가장 복잡한 핸들러 중 하나로, 세션의 시작을 알린다.

  • Logic:
    1. get_or_create_session_uuid: 현재 TCP 세션에 고유 UUID(v4)를 부여한다.
    2. ensure_unique_or_error: 닉네임 중복 체크. "guest"로 들어오면 자동으로 "guest-a1b2..." 같은 이름을 부여한다.
    3. state_.mu Lock: 전역 유저 맵(state_.user)에 (Session Ptr -> Nickname) 매핑을 등록한다.
    4. DB Upsert: users 테이블에 접속 기록(IP, 시간)을 남기고, 필요하면 신규 유저 레코드를 생성한다. (Postgres UPSERT)
    5. Audit Log: "누가 접속했다"는 시스템 로그를 로비 채팅창에 날린다.
    6. Redis Touch: presence:user:{uid} 키에 TTL을 설정하여 "나 온라인이야"라고 표시한다.
    7. Write-Behind: emit_write_behind_event("session_login", ...)를 통해 로그인 이벤트를 Redis Stream에 던진다.

6.2. Join Handler (handlers_join.cpp)

유저가 방을 옮길 때 호출된다.

  • Logic:
    1. Redis Password Check: 입장하려는 방의 비밀번호를 Redis에서 먼저 조회한다. (Redis가 Master of Truth)
    2. state_.mu Lock:
      • 현재 방에서 erase (퇴장 처리).
      • 비밀번호 입력값 검증 (hash_room_password). -> 비밀번호가 없다면 그냥 입장할 수 있다.
      • 새 방에 insert (입장 처리).
      • 만약 이전 방이 비어버렸으면(empty()), 방 객체를 메모리에서 제거함.
    3. Broadcasting:
      • 새 방 유저들에게: "XXX님이 입장했습니다." 등 전체 공지를 쏘는 식.
      • Redis Pub/Sub을 통해 모든 서버에 브로드캐스팅된다.
    4. Sync Membership: DB memberships 테이블에 (user_id, room_id, role) 정보를 업데이트한다.
    5. 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)로 다른 서버에 있는 유저들에게도 메시지 전달.
    • 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. 흐름

  1. 핸들러가 ChatService::emit_write_behind_event() 호출.
  2. 이벤트 타입(chat_msg, login, join)과 데이터를 JSON이 아닌 Key-Value 쌍으로 변환.
  3. REDIS XADD stream:events * type chat_msg user bvm ... 명령 실행.
  4. 끝. (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

  1. SPOF 처리
    • Redis
    • Gateway
    • PostgreSQL
    • 위 셋 다 단일 인스턴스임!
  2. 인증 및 보안 관련 구현
  3. 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
'Study/C++ & C#' 카테고리의 다른 글
  • AI로 MSA 서버 만들어 보기 #4 : Write-Behind
  • AI로 MSA 서버 만들어 보기 #3 : Gateway
  • AI로 MSA 서버 만들어 보기 #1 : Core
  • [C++] 채팅서버에 DB 실장
BVM
BVM
  • BVM
    E:\
    BVM
  • 전체
    오늘
    어제
    • 분류 전체보기 (173)
      • Thoughts (14)
      • Study (75)
        • Japanese (3)
        • C++ & C# (50)
        • Javascript (3)
        • Python (14)
        • Others (5)
      • Play (1)
        • Battlefield (1)
      • Others (10)
      • Camp (73)
        • T.I.L. (57)
        • Temp (1)
        • Standard (10)
        • Challenge (3)
        • Project (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • 본 블로그 개설의 목적
  • 인기 글

  • 태그

    c#
    네트워크 프로그래밍
    암호화
    db
    Dalamud
    베데스다
    cloudtype
    포인터
    로깅
    스타필드
    Python
    discord py
    docker
    서버
    JS
    네트워크
    프로그래머스
    IOCP
    7계층
    OSI
    FF14
    Server
    Network
    bot
    Asio
    Selenium
    discord
    C++
    클라우드
    boost
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
BVM
AI로 MSA 서버 만들어 보기 #2 : Server
상단으로

티스토리툴바