12장 - 채팅 시스템 설계
목표
채팅 앱을 만들어보자!
1단계 문제 이해 및 설계 범위 확정
채팅 앱이라고 했을 때 사람들이 떠올리는 것은 제각각이다.
일대일 채팅, 그룹 채팅, 응답지연이 낮아야하는 음성 채팅등 여려 경우가 있다.
질문을 통해 원하는 앱이 무엇인지 정확히 알아내야한다.
지원자 질문
- 1:1 채팅 앱인지 그룹 채팅 앱인지
- 모바일 앱인지 웹 앱인지
- 처리해야하는 트래픽 규모는?
- 그룹 채팅의 경우에 인원 제한이 있는지?
- 중요기능으로 어떤 것이 있는지?
- 메시지 길이에 제한이 있는지?
- 종단간 암호화를 지원해야 하는지?
- 채팅 이력은 얼마나 오래 보관해야 하는지?
도출 된 정보
- 응답지연이 낮은 일대일 채팅 기능
- 최대 100명까지 참여할 수 있는 그룹 채팅 기능
- 사용자의 접속상태 표시 기능
- 다양한 단말 지원. 하나의 계정으로 여러 단말에 동시 접속 지원
- 푸시 알림
- 5천만 DAU를 처리할 수 있도록 할 것
2단계 개략적 설게안 제시 및 동의 구하기
클라이언트는 서로 직접 통신하지 않기 때문에 채팅 서비스와 통신을 해야한다.
채팅 서비스는 다음 기능을 제공해야 한다.
- 클라이언트들로부터 메시지 수신
- 메시지 수신자(recipeint) 결정 및 전달
- 수신자가 접속 상태가 아닌 경우 접속할 때까지 메시지 보관
채팅 서비스의 경우 어떤 통신 프로토콜을 사용할 것인가도 중요한 문제이다.
HTTP의 경우에는 메시지 전송 용도로 괜찮은 선택이다.
(keep-alive헤더를 통해 끊지않고 계속 유지할 수 도 있음)
하지만 메시지 수신 시나리오는 복잡하다.
HTTP는 클라이언트가 연결을 만드는 프로토콜이므로, 서버에서 클라이언트로 임의 시점에 메시지를 보내는 데는 쉽게 쓰일 수 없다.
서버가 연결을 만드는 것처럼 동작할 수 있도록 많은 기법들이 존재하는데 이에 대해 알아보자.
폴링
클라이언트가 주기적으로 서버에게 새 메시지가 있느냐고 물어보는 방법.
폴링을 자주하면 폴링비용이 올라감 + 메시지가 없는 경우 서버 자원 낭비ㄹ
롱 폴링
클라이언트는 새 메시지가 반환되거나 타임아웃 될 때까지 연결을 유지한다. 클라이언트는 새 메시지를 받으면 기존 연결을 종료하고 서버에 새로운 요청을 보내어 모든 절차를 다시 시작한다.

단점
- 메시지를 보내는 클라이언트와 수신하는 클라이언트가 같은 채팅 서버에 접속하게 되지 않을 수 있음
- 서버 입장에서는 클라이언트가 연결을 해제했는지 아닌지 알 좋은 방법이 없다
- 메시지를 많이 받지 않은 클라이언트도 주기적으로 서버에 다시 접속해야함
웹 소켓
서버가 클라이언트에게 비동기메시지를 보낼때 가장 널리 사용하는 기술
웹소켓 연결은 클라이언트가 시작하여, 한번 맺어진 연결은 항구적이며 양방향이다. 한번 만들어지면 클라이언트에게 비동기적으로 메시지를 전송할 수 있다.
웹소켓을 이용하면 메시지를 보낼 때나 받을 때 동일한 프로토콜을 사용할 수 있음 구현도 단순하고 직관적이다.
웹 소켓 연결은 항구적으로 유지되어야하기 때문에 연결관리를 효율적으로 해야한다.
개략적인 설계안
클라이언트와 주 서버 통신 프로토콜 : 웹 소켓 이외 대부분의 기능(회원가입, 로그인, 사용자 프로파일 등) : HTTP
무상태 서비스
전통적인 요청/응답 서비스로 로그인, 회원가입, 사용자 프로파일 표시 등

로드밸런서를 사용하며 모놀리틱, 마이크로서비스 등으로 구현한다.
상태 유지 서비스
각 클라이언트가 채팅 서버와 독립적인 네트워크 연결을 유지해야 한다.
클라이언트는 서버가 살아 있는 한 다른 서버로 연결을 변경하지 않는다.
서비스 탐색 서비스, 채팅 서비스와 협력하여 특정 서버에 부하가 몰리지 않도록 하여야 한다.
제3자 서비스 연동
제 3자 서비스 : 푸시 알림
새 메시지를 받았다면 설사 앱이 실행 중이지 않더라도 알림을 받아야 해서이다.
규모 확장성

이론적으로는 모든 사용자 연결을 최신 클라우드 서버 한대로 처리할 수 있기는 여러이유(SPOF등)으로 분할하나 한대를 갖는 설계안으로 출발하여 다듬어 나가는 것도 괜찮다.
키-값 저장소에는 채팅 이력을 보관한다.
저장소
어떤 데이터 베이스를 쓸 것 인가? 데이터의 유형과 읽기/쓰기 연산의 패턴을 중요하게 봐야한다.
채팅 시스템이 다루는 데이터
- 사용자 프로파일, 설정, 친구목록
안전성을 보장하는 관계형 데이터 베이스에 보관 - 채팅 이력 채팅 데이터는 패턴을 알아보자
채팅 데이터의 패턴
- 양이 엄청나다.
- 빈번하게 사용되는 것은 주로 최근에 주고받는 메시지다.
- 대체로 최근에 주고받은 메시지 데이터를 참고하나 특정 메시지로 점프하여 무작위적인 데이터 접근을 하게되는 일도 있다.
- 1:1 채팅 앱의 경우 읽기:쓰기 비율은 대략 1:1정도다.
다음과 같은 특징을 고려하였을때 키-값 저장소를 추천할 것이다.
- 수평적 규모확장이 쉽다.
- 데이터 접근 지연시간이 낮다.
- 관계형 데이터베이스는 롱테일이 해당하는 부분을 잘 처리하지 못하는 경향이 있다.(인덱스가 커지면 무작위 접근 비용이 증가)
- 이미 많은 안정적인 채팅 시스템이 키-값 저장소를 채택하고 있다.
데이터 모델
1:1 채팅을 위한 메시지 테이블
기본키를 message_id로, 메시지 순서를 쉽게 정 할 수 있도록 하는 역할도 담당하게 한다.
동시에 메시지가 생성될 수 있기 때문에 created_at으로 사용되지 않는다.
그룹 채팅을 위한 메시지 테이블
(channel_id, message_id)의 복합 키를 기본 키로 사용한다.
메시지 ID
- message_id의 값은 고유해야 한다.
- ID값는 정렬 가능해야하며 시간 순서와 일치해야한다.
이 두 조건을 만족하기 위해 auto_increment 또는 스토플레이크가 대안이 될 수 있다.
3단계 상세 설계
서비스 탐색
서비스 탐색 기능의 주 역할은 클라이언트에게 가장 적합한 채팅 서버를 추천하는 것이다.
클라이언트의 위치, 서버의 용량등을 기준으로 추천하며 아파치 주키퍼같은 오픈 소스 솔루션으로 이용한다.
다음은 주키퍼로 구현한 서비스 탐색 기능이 어떻게 동작하는지 보여준다.

- 로그인 시도
- API 서버가 서비스 탐색 기능이 동작하여 해당 사용자를 서비스할 최적의 채팅 서버를 찾는다.
- 사용자는 찾은 채팅서버와 웹 소켓 연결을 맺는다.
메시지 흐름
1:1 채팅 메시지 처리 흐름

- 사용자 A가 채팅 서버 1로 메시지 전송
- 채팅 서버 1은 ID생성기를 사용해 해당 메시지의 ID 결정
- 채팅 서버 1은 해당 메시지를 메시지 동기화 큐로 전송
- 메시지가 키-값 저장소에 보관됨
- 접속 미접속에 따라 경우 처리
- 사용자 B가 접속 중인 경우 : b가 접속 중인 채팅 서버로 전송
- 사용자 B가 접속 중이 아닌 경우 : 푸시 알림 메시지를 푸시 알림 서버로 전송
- 채팅서버는 메시지를 사용자 B에게 전송(웹 소켓)
여러 단말 사이의 메시지 동기화

한 사용자가 여러대의 단말을 이용하고 있는 경우 각 단말은 cur_max_message_id라는 변수를 유지하는데, 해당 단말에서 관측된 가장 최신 메시지의 ID를 추적하는 용도이다.
- 수신자 ID가 현재 로그인한 사용자 ID와 같다
- 키-값 저장소에 보관된 메시지로서, 그 ID가 cur_max_message_id 보다 크다.
위의 두가지 조건을 만족한 경우 새메시지로 간주하며 동기화 작업(키-값 저장소에서 새 메시지를 가져옴)을 구현한다.
소규모 그룹 채팅에서의 메시지 흐름

그룹 채팅의 경우 조금 더 복잡하다. 사용자 A가 메시지를 보낼 경우 사용자 B와 C의 메시지 동기화 큐에 복사된다.
- 새로운 메시지가 왔는지 확인하려면 자기 큐만 보면 되니까 메시지 동기화가 단순
- 그룹이 크지않으면 복사해서 큐에 넣는 작업의 비용이 크지않음
이러한 이유로 소규모 그룹 채팅에 적합하다.

수신자를 기준으로 그림을 보자면 다음과 같이 여러 사용자로 부터 오는 메시지를 받을 수 있어야한다.
접속상태 표시
접속상태 서버는 웹소켓으로 통신하는 실시간 서비스의 일부로 작동한다.
사용자 로그인
클라이언트와 실시간 서비스가 웹소켓으로 연결이 되면 접속상태 서버는 A의 상태와 last_active_at 타입스탬프 값을 키-값 저장소에 보관한다.
이 절차가 끝나고 나면 해당 사용자는 접속 중인 것으로 표시된다.
로그아웃
키-값 저장소에 보관된 상태를 offline으로 바꿈으로 로그아웃 상태가 된다.
접속장애
사용자의 인터넷 연결이 끊어지면 클라이언트와 서버 사이에 맺어진 웹소켓 같은 지속성 연결도 끊어진다.
매운 단순하게 사용자를 오프라인 상태로 표시하고 연결이 복구되면 온라인 상태로 변경하면되나 짧은 시간 동안 인터넷 연결이 끊어졌다
복구되는 일이 흔하기 때문에 사용자가 사용하는데 불편함을 겪게 될 것이다.

이를 박동(heartbeat) 검사를 통해 이 문제를 해결해보자. 클라이언트로 하여금 주기적으로 박동 이벤트를 서버로 보내도록 하고, 마지막 이벤트를 받은 지 x초 이내에 박동 이벤트 메시지를 받으면 해당 사용자의 접속상태를 온라인으로 유지하는 것이다.
상태 정보의 전송
사용자 A와 친구 관계에 있는 사용자들은 어떻게 해당 사용자의 상태 변화를 알게 될까?

상태정보 서버는 발행-구독 모델을 사용한다. 각각의 친구 관계마다 채널을 하나씩 두는 것이다. 이 방안은 그룹 크기가 작을 떄는 효과적이다. 값이 커질 경우 사용자가 그룹 채팅에 입장하는 순간에만 상태 정보를 읽어가게 하거나, 친구 리스트에 있는 사용자의 접속상태를 갱신하고 싶으면 수동으로 하도록 유도하는 것이다.
4단계 마무리
실시간 통신을 하도록 웹소켓을 사용하였으며, 채팅 서버, 접속 상태 서버,푸시 알림 서버, 채팅 이력을 보관한 키-값 저장소, API서버등 주요 컴포넌트에 대해 알아보았다.
시간이 남는다면 다음과 같은 내용을 논의해도 좋을 것이다.
- 사진이나 비디오 등의 미디어를 지원하도록 하는 방법 : 아북 방식, 클라우드 저장소, 섬내일 생성등을 이야기 해볼만 하다.
- 종단 간 암호화 : 메시지 전송에 있어 종단 간 암호화에 대해서
- 캐시 : 클라이언트에 이미 읽은 메시지를 캐시해 두면 서버와 주고받는 데이터 양을 줄일 수 있다.
- 로딩 속도 개선 : 슬랙은 데이터, 채널등을 지역적으로 분삲나는 네트워크를 구축하여 로딩 속도를 개선하였다.
- 오류 처리
- 채팅 서버 오류 : 수십만 사용자가 접속해 있는 서버 하나가 죽으면 주키퍼 같은 서비스 탐색 기능이 동작하여 새로운 서버를 배정하도록 해야한다..
- 메시지 재선송 : 재시도나 큐를 이용하여 안정적 전송을 보장하자.