채팅 서버는 많은 client connection을 오래 유지해야 하므로 WebSocket 같은 양방향 통신 방식을 사용할 수 있습니다.
서버는 connection 수, heartbeat, 인증, room 관리, message ordering, 재전송 정책을 관리해야 합니다.
1대 서버로 부족하면 여러 instance로 scale-out하고, Redis Pub/Sub, Kafka, NATS 같은 broker를 통해 room message를 fan-out할 수 있습니다. 느린 client 때문에 전체가 막히지 않도록 backpressure와 queue 제한도 필요합니다.
꼬리질문
조금 더 깊게 물어본다면
답변 뒤에 이어질 수 있는 질문들을 하나씩 열어볼 수 있어요.
WebSocket을 쓰는 이유는?
HTTP 요청/응답을 반복하는 것보다 하나의 연결에서 양방향 실시간 통신을 하기 쉽기 때문입니다.
느린 클라이언트는 어떻게 처리하나요?
client별 send queue를 제한하고, 일정 수준을 넘으면 오래된 메시지를 버리거나 연결을 끊는 backpressure 정책이 필요합니다.
10K 동시 연결을 유지하려면 서버를 어떻게 설계해야 하나요?
연결마다 thread를 하나씩 쓰는 blocking I/O 방식은 메모리와 컨텍스트 스위칭 비용이 크기 때문에, Node.js나 Go처럼 event-driven 혹은 goroutine 방식으로 적은 thread로 많은 연결을 처리하는 구조가 필요합니다. 운영체제 file descriptor 한도도 미리 늘려야 합니다.
채팅 서버에서 메시지 순서는 어떻게 보장하나요?
방 단위 sequence number, server timestamp, partition key를 사용해 순서를 관리할 수 있습니다. 분산 환경에서는 완전한 전역 순서보다 방 단위 순서를 보장하는 경우가 많습니다.
재접속한 사용자는 메시지를 어떻게 이어받나요?
마지막으로 받은 message id나 cursor를 client가 보내고, server가 그 이후 메시지를 storage에서 조회해 전달할 수 있습니다.
메시지 중복 전송을 어떻게 방지하나요?
client가 메시지마다 고유한 idempotency key를 생성해 서버에 함께 보내면, 서버가 같은 key의 메시지를 중복 처리하지 않도록 걸러낼 수 있습니다. 재전송 시에도 같은 key를 사용합니다.
서버가 여러 대로 늘어날 때 WebSocket은 어떻게 처리하나요?
WebSocket은 특정 서버와 연결이 유지되므로, 같은 방의 사용자가 서로 다른 서버에 연결돼 있으면 메시지를 직접 전달할 수 없습니다. 서버 간 메시지를 공유하기 위해 Redis Pub/Sub이나 메시지 브로커를 중간에 둡니다.
Redis pub/sub이 채팅 서버에 쓰이는 이유는?
채팅 서버가 여러 instance로 분산될 때, 특정 방의 메시지를 해당 방의 사용자가 연결된 모든 서버에 fan-out해야 합니다. Redis Pub/Sub은 channel 단위로 메시지를 발행하고 구독할 수 있어, 각 서버가 방 channel을 구독하면 인스턴스 간 메시지 전달을 처리할 수 있습니다.
부가 설명
저장 요구가 있으면 message DB, unread count, cursor, pagination도 설계해야 합니다.