1. 컨셉
단순한 서버는 "DB 트랜잭션이 끝나야만 유저에게 OK를 보낸다"는 Write-Through 방식을 많이 사용한다.
하지만, 이는 DB가 병목이 될 경우 전체 서버의 반응성을 크게 저하할 수 있다.
Write-Behind (Write-Back) 패턴은 이를 해결할 수 있다.
- Memory First: 메모리(또는 빠른 캐시인 Redis)에 먼저 쓰고, 유저에게 즉시 OK를 준다.
- Disk Later: 실제 견고한 저장소(RDBMS)에는 별도의 워커가 천천히, 모아서 기록한다.
본 서버는 이 패턴을 Redis Streams를 통해 구현했다.
2. 아키텍처

- Decoupling: 채팅 로직(Fast)과 저장 로직(Slow)이 완벽히 분리된다.
- Peak Load Handling: 트래픽이 폭주하면 Redis Stream에 데이터가 쌓일 뿐, 서버의 응답 속도는 느려지지 않는다.
3. 생산자 구현 (emit_write_behind_event)
서버 코드(server/src/chat/chat_service_core.cpp)에서 이벤트를 발행하는 부분이다.
void ChatService::emit_write_behind_event(const std::string& type,
const std::string& session_id,
...) {
// 1. 데이터 준비 (Key-Value Vector)
std::vector<std::pair<std::string, std::string>> fields;
fields.emplace_back("type", type);
fields.emplace_back("ts_ms", std::to_string(now_ms));
fields.emplace_back("session_id", session_id);
// ... (user_id, room_id 등 추가 필드)
// 2. Redis Stream XADD (Non-blocking)
// "stream:events" 라는 키에 데이터를 append 한다.
// maxlen을 설정하여 스트림이 무한히 커지는 것을 방지한다. (Ring Buffer 효과)
redis_->xadd("stream:events", fields, nullptr, 100000 /*maxlen*/, true /*approx*/);
}
- 특징:
- JSON Free: 문자열 파싱/생성 비용을 줄이기 위해 Redis의 Native Map 구조를 사용한다.
- Fire and Forget:
xadd의 결과를 기다리지 않거나, 실패해도 로그만 남기고 넘어간다 (서버 동작이 우선).
4. Redis Stream
서버가 사용하는 주요 Redis Stream 커맨드이다.
| Command | Usage | Role |
|---|---|---|
XADD key * field value ... |
Producer | 스트림 끝에 새 항목 추가. ID는 자동 생성(timestamp-sequence). |
XREADGROUP GROUP g1 CONSUMER c1 BLOCK 2000 STREAMS key > |
Consumer | 컨슈머 그룹 g1의 멤버 c1이 되어, 아직 아무도 안 읽은(>) 메시지를 읽어옴. 없으면 2초 대기. |
XACK key g1 id ... |
Consumer | "처리 완료" 서명. 이 처리가 되어야 PEL(Pending Entries List)에서 제거됨. |
5. 소비자 구현 (The Worker)
현재 구현체는 독립 프로그램 (wb_worker)으로 제공된다.
scripts/smoke_wb.ps1을 통해 빌드 및 간단한 스모크 테스트를 수행할 수 있다.
흐름 (wb_worker)
- Fetch:
XREADGROUP으로 배치 단위(예: 100개)로 이벤트를 가져온다. - Transform: 이벤트 타입(
type=chat_msg)에 따라 적절한 SQL(INSERT INTO messages ...)을 생성한다. - Batch Insert: DB 부하를 줄이기 위해
INSERT를 한 번의 트랜잭션으로 묶어서 실행한다 (Bulk Insert). - Acknowledge: DB 커밋이 성공하면
XACK를 날려 Redis에서 해당 항목을 처리 완료로 마킹한다.
6. 문제 대응
"메모리에만 썼는데 서버가 죽으면 어떡하지?"
- Redis Persistence: Redis 자체의 AOF (Append Only File) 기능을 켜두면, Redis가 죽었다 살아나도 스트림 데이터는 보존된다.
- Consumer Crash:
- 워커가 데이터를 읽어갔는데(
XREADGROUP) DB에 넣기 전에 죽었다면? XACK를 못 보냈으므로 해당 메시지는 PEL (Pending Entries List)에 남는다.- 워커가 재가동되면
XREAD ... 0(또는XAUTOCLAIM)을 통해 처리되지 않은 메시지를 다시 가져와서 재시도한다. -> At-least-once Delivery 보장.
- 워커가 데이터를 읽어갔는데(
Trade-offs
- Consistency Gap: 아주 짧은 순간(수 ms ~ 수 초) DB와 캐시(메모리) 간의 데이터 불일치가 존재할 수 있다.
- Complexity: 시스템 구성 요소가 늘어난다. (Redis 관리, 워커 모니터링 필요)
- 하지만 큰 트래픽을 받는 서버에서 "압도적인 쓰기 성능"을 얻기 위해 이 정도의 복잡도는 충분히 감수할 가치가 있다.
이러면 대충 백엔드 쪽은 간단하게나마 정리됐다.
다른 기능이나 클러스터링을 구현하면 그때 내용을 추가로 보충할 수 있을 것이다.
클라이언트는 ImGui로 다시 작성하고 나면 그 때 글로 정리해 볼 수 있을 것 같다.
'Study > C++ & C#' 카테고리의 다른 글
| AI로 MSA 서버 만들어 보기 #3 : Gateway (0) | 2025.12.09 |
|---|---|
| AI로 MSA 서버 만들어 보기 #2 : Server (0) | 2025.12.07 |
| AI로 MSA 서버 만들어 보기 #1 : Core (0) | 2025.12.03 |
| [C++] 채팅서버에 DB 실장 (0) | 2024.05.14 |
| [C#] ###Clicker 동작 개선 (0) | 2024.05.05 |