12장 - 채팅 시스템 설계
- 태그
- Interview
- 생성일
- 2024년 7월 20일 PM 3:22
1. 문제 이해 및 설계 범위 확정#
- 앱은 그룹, 1:1 채팅을 지원해야 한다.
- 모바일과 웹을 지원해야 한다.
- DAU는 5천만 명을 동시에 처리할 수 있을 수준이여야 한다.
- 1:1 채팅, 그룹채팅, 사용자 접속상태를 표시할 수 있어야하고, 텍스트만 지원한다.
- 메세지는 100,000자 이하로 지원한다.
- 채팅 이력은 영원히 보관해야 한다.
2. 개략적 설계안 제시 및 동의 구하기#
- 채팅 서비스가 제공해야 하는 기능
- 클라이언트로부터 메시지 수신
- 메시지 수신자 결정 및 전달
- 수신자가 접속 상태가 아닌 경우 접속할 때까지 해당 메시지 보관
- 어떤 통신 프로토콜을 사용할 것인지에 대해서도 고민해야 한다.
- HTTP를 사용할 경우 Keep-Alive 옵션을 사용하여 핸드셰이크 횟수를 줄일 수 있다.
- HTTP는 클라이언트가 연결을 만드는 프로토콜이기에 임의 시점에 메세지를 보내는 데는 쉽게 쓰이기 어렵다.
- 때문에 폴링, 롱 폴링, 웹소켓 등의 기술들이 나왔다.
2.1. 폴링#
- 클라이언트가 서버에 지속적으로 새로운 메세지가 있는지 확인하는 방법이다.
- 폴링을 자주할 수록 코스트가 올라가고, 서버 자원이 불필요하게 낭비된다.
2.2 롱 폴링#
- 폴링과 비슷한데 서버측에서 새로운 메세지가 올때까지 커넥션을 유지하다가 타임아웃이 발생하면 다시 요청하는 방식이다.
- 서버 입장에서는 연결 해제를 알 수 있는 좋은 방법이 없다.
- 커넥션을 계속 유지해야 해서 비용이 증가하고 비효율적이다.
2.3. 웹소켓#
- 웹소켓은 서버가 클라에게 비동기 메세지를 보낼 때 가장 널리 사용되는 기술이다.
- 웹소켓을 잘 이용하면 메시지를 보낼 때나 받을 때 동일한 프로토콜을 사용할 수 있으므로 설계뿐 아니라 구현도 단순하고 직관적이다.
- 단, 서버 측에서 연결 관리를 효율적으로 해야한다.
2.4. 개략적 설계안#
- 주 통신 프로토콜로 웹소켓을 사용하겠다.

2.4.1. 무상태 서비스#
- 무상태 서비스는 로드밸러서 뒤에 위치한다.
- 서비스탐색 (Service Discovery) = 클라이언트가 접속할 채팅 서버의 DNS 호스트명을 클라이언트이게 알려주는 역할을 한다.
2.4.2. 상태 유지 서비스#
- 해당 설계에서 유일하게 상태 유지가 필요한건 채팅 서비스다.
- 각 클라이언트가 채팅 서버와 독립적인 네트워크 연결을 유지해야 하기 때문
- 클라는 보통 서버가 살아 있는 한 다른 서버로 연결을 변경하지 않는다.
2.4.3. 제3자 서비스 연동#
- 채팅 앱에서 가장 중요한 서드파티는 푸시 알림이다.
- 새 메시지를 받았다면 설사 앱이 실행 중이지 않더라도 알림을 받아야 한다.
2.4.4. 규모 확장성#
- 대량의 트래픽을 처리해야 하는 경우 따져봐야 하는건 서버 한 대로 얼마나 많은 접속을 동시처리 가능하냐다.
- 접속당 10K의 메모리가 필요하면, 동시 접속자 1M는 10GB의 메모리만 있으면 모든 연결을 다 처리할 수 있다.
- 하지만 한대의 서버만 있을 경우 SPOF문제를 가지게 된다.
- 서버 한대에서 점차 개선시켜 나가는것도 괜찮은데 면접관에게는 시작이라고 정확하게 전달하도록 하자.

- 채팅 서버는 클라이언트 사이에 메시지를 중계한다.
- 접속 상태 서버는 사용자의 접속 여부를 관리한다.
- API 서버는 로그인, 회원가입, 프로파일 변경 등 그 외 나머지를 전부 처리한다.
- 알림 서버는 푸시 알림을 보낸다.
- 키-값 저장소에는 채팅 이력을 보관한다.
2.4.5. 저장소#
- 데이터의 유형과 읽기/쓰기 연산의 패턴에 따라 저장소 종류는 선택한다.
- 사용자 프로파일, 설정, 친구 목록과 같은 일반적 데이터는 관계형 데이터베이스에 저장한다.
- 채팅 이력은 주로 최근에 주고받은 메시지 데이터만 보게되며, 1:1 채팅은 읽기와 쓰기 비율도 1:1 정도다.
- 본 설계안에서는 키-값 저장소를 추천한다.
- 키-값 저장소는 수평적 규모확장이 쉽다.
- 키-값 저장소는 데이터 접근 Latency가 낮다.
- 관계형 데이터베이스는 롱 테일에 해당하는 부분을 잘 처리하지 못하는 경향이 있다.
- 인덱스가 커지면 무작위적인 접근 처리 비용이 늘어난다.
- 페이스북이나 디스코드 같은 서비스에서 키-값 저장소를 채택하고 있다.
2.5. 데이터 모델#
- 메세지 ID
- message_id를 만드는 기법은 자세히 논의할 만한 가치가 있는 흥미로운 주제다.
- 메세지 id는 고유해야 하며 시간 순서로 정렬 가능해야 한다.
3. 상세 설계#
3.1. 서비스 탐색#
- 서비스 탐색은 클라이언트에게 가장 적합한 채팅 서버를 추천한다.
- 기준으로는 클라이언트의 위치와 서버의 용량 등이 있다.
- 가장 널리 쓰이는 오픈소스 솔루션으로는 아피치 주키퍼 같은 것이 있다.
- 서비스 탐색 기능의 동작 순서
- 사용자가 A가 시스템에 로그인을 시도한다.
- 로드 밸런서가 로그인 요청을 API 서버들 중 하나로 보낸다.
- API 서버가 인증을 처리하고 나면, 서비스 탐색 기능이 작동하여 해당 사용자를 서비스할 최적 채팅 서버로 보낸다.
- 사용자 A는 반환된 채팅 서버와 웹 소켓 연결을 맺는다.
3.2. 메시지 흐름#
3.2.1. 1:1 채팅 메시지 처리 흐름#

- 사용자 A가 채팅 서버 1로 메세지 전송
- 채팅 서버 1은 ID 생성기로 해당 메세지 ID 결정
- 채팅 서버 1은 해당 메세지를 메세지 동기화 큐로 전송
- 메세지가 키-값 저장소에 보관됨
- 사용자 B가 접속 중인 경우 B가 접속 중인 채팅 서버로 전송된다.
- 접속 중이 아니라면 푸시 알람을 보낸다.
- 채팅 서버 2는 사용자 B에게 메세지를 전송
- 사용자 B와 채팅 서버 2사이에는 웹 소켓으로 연결되있기 때문에 이를 이용
3.2.2. 여러 단말 사이의 메시지 동기화#

- 사용자 A는 전화기와 랩톱의 두 대 단말을 이용하고 있다.
- 각 단말은 cur_max_message_id라는 변수를 유지하는데, 해당 단말에서 관측된 가장 최신 메세지 ID를 추적하는 용도다.
- 두 조건을 만족한다면 메세지는 새 메세지로 간주한다.
- 수신자 ID가 현재 로그인한 사용자 ID랑 같다.
- 키-값 저장소에 보관된 메세지로서, 그 ID가 cur_max_message_id보다 크다.
3.2.3. 소규모 그룹 채팅에서의 메시지 흐름#
- 사용자가 3명인 소규모 그룹이 있다고 상정해보자
- A가 메시지를 보내면 B와 C의 메시지 동기화 큐에 복제된다.
- 각 큐는 메시지 수신함 같은 것으로 생각해도 무관하다.
- 이 설계안은 소규모 그룹 채팅에 적합하다.
- 새로운 메세지가 왔는지 확인하려면 본인 큐만 보면 되니까 메세지 동기화가 단순해진다.
- 그룹이 크지 않다면 메세지를 수신자별로 복사하여 큐에 넣는 작업 비용이 문제가 되지 않는다.
- 하지만 많은 사용자를 지원해야 하는 경우라면 모든 사용자의 큐에 복사하는게 바람직하지 않다.
- 메세지 동기화는 여러 사용자로부터 오는 메세지를 받을 수 있어야 한다.
3.3. 접속상태 표시#
3.3.1. 사용자 로그인#
- 클라이언트와 실시간 서비스 사이에 웹소켓 연결이 맺어지고 나면 접속상태 서버는 사용자 상태와 last_active_at 타임스탬프 값을 키-값 저장소에 보관하여 이를 토대로 접속 중으로 표시한다.
3.3.2. 로그아웃#

- 키-값 저장소에 보관된 사용자 상태가 online에서 offline으로 바뀌게 된다는 점에 유의하자.
3.3.3. 접속 장애#
- 본 설계안에선 박동 검사를 통해 인터넷 연결 문제를 해결한다.
- 온라인 상태의 클라이언트로 하여금 주기적으로 박동 이벤트를 접속상태 서버로 보내도록 하고
- 마지막 이벤트를 받은 지 x초가 지나면 오프라인으로 바꾸는 것이다.
3.3.4. 상태 정보의 전송#

- 상태정보 서버는 발행-구독 모델을 사용해 친구 사용자의 상태가 변경될 경우 이 사실을 연관 채널에 알린다.
- 이 방은 그룹 크기가 작을 때 효과적이다.
- 그룹이 커지면 비용과 시간이 많이 들어 좋지않다.
- 너무 많은 메시지가 발생한다면 채팅 방에 입장하는 순간에만 읽거나, 수동으로 갱신하도록 한다.