상세 설계
- 서비스 탐색
- 클라이언트의 위치
- 서버의 용량
- 서비스 탐색 기능은 아파치 주키퍼 같은 오픈 소스 솔루션을 사용할 수 있다. 사용 가능한 채팅 서버를 여기에 등록해 두고, 클라이언트가 접속을 시도하면 사전에 정한 기준에 따라 최적의 채팅 서버를 골라준다.

- 사용자 A가 로그인을 한다.
- 로드 밸런서가 로그인 요청을 API 서버들 가운데
- API 서버가 사여용자 인증을 처리하고 나면, 서비스 탐색 기능(주키퍼)이 동작하여 해당 사용자를 서비스 할 최적의 채팅 서버를 찾는다.
- 위의 스크린샷에서는 채팅 서버2가 선택되서 사용자 A에게 반환을 한다.
- 사용자 A는 WebSocket 서버 2와 WebSocket 연결을 맺는다.
- 메시지 흐름
-
1:1 채팅 메시지 흐름

- 사용자 A가 채팅 서버1에게 메시지를 전송한다.
- 채팅 서버 1은 ID Generator를 통해 해당 메시지의 ID를 결정한다.
- 채팅 서버 1은 해당 메시지를 메시지 동기화 Queue로 전송한다.
- 메시지가 Key-Value Store에 보관된다.
- 사용자 B가 현재 접속중이면, 메시지는 사용자 B가 현재 접속중인 채팅 서버인 채팅 서버2로 전송된다.
- 사용자 B가 현재 접속중이 아니라면, 푸시 알림 메시지를 푸시 알림 서버로 보낸다.
- 채팅 서버 2는 메시지를 사용자 B에게 전송한다. 사용자 B와 채팅 서버 2 사이에는 WebSocket 연결이 되어있어서 Socket을 사용한다.
-
여러 단말 사이의 메시지 동기화

- 현재 사용자 A는 전화와 랩톱 2가지의 단말을 사용중이라고 가정한다.
- 사용자 A 전화 단말에서 채팅 앱에 로그인하면 채팅 서버 1과 전화 단말 사이에 WebSocket 연결이 된다.
- 랩톱에서도 마찬가지로 로그인하면 별도의 WebSocket 서버 1에 연결이 된다
- 각 단말은 cur_max_message_id 변수를 가지는데, 해당 단말에서 관측된 가장 최근 메시지의 ID를 추적하는 용도이다.
- 이 변수를 통해서, Key-Value Store에 새로운 메시지를 가져오는 동기화 작업을 쉽게 구현할 수 있다.
- 아래 2개의 조건을 만족한다면, 새로운 메시지로 여겨진다.
- 수신자 ID가 현재 로그인을 한 사용자 ID와 같다.
- Key-Value Store에 보관된 메시지로서, 그 ID가 cur_max_message_id보다 크다.
-
소규모 채팅

- 사용자 A가 그룹 채팅 방에서 메시지를 보냈을 때, 메시지가 사용자 B,C 앞에 존재하는 각각의 메시지 동기화 Queue에 들어간다. 이 Queue는 각 사용자를 위한 메시지 수신함과 비슷한 역할이다.
- 새로운 메시지가 왔는지 확인을 하려면, 본인의 Queue만 보면 된다. 그래서 메시지 동기화 flow가 간단하다.
- 그룹이 크지 않다면, 메시지 수신자별로 복사해서 Queue에 넣는 작업의 비용이 문제가 되지 않게 된다.
- 아래는 수신자 관점에서의 flow이다. 여러 사용자가 보내는 메시지를 받아야 한다. 그룹 채팅방을 생각해보면 된다.

접속 상태 표시
접속상태 서버는 WebSocket으로 통신하는 실시간 서비스의 일부이다.
우리가 인스타그램이나 페이스북 메시저를 사용하다면, 상대방이 현재 앱에 접속을 하였는지 확인할 수 있는 녹색점과 같은 표시가 존재한다.
이 책에서는 접속상태 서버를 통해 사용자의 상태를 관리한다고 한다. 사용자의 상태가 바뀌는 시나리오는 아래의 스크린샷과 같다.
- 사용자 로그인

- 클라이언트와 실시간 서비스 사이에서 WebSocket 연결이 맺어지면, 접속상태 서버는 사용자 A의 상태와 last_active_at timestamp의 값을 Key-Value Store에 보관한다.
- 위 절차가 끝나면, 사용자는 접속 중인 상태로 표시가 된다.
-
로그아웃

- 사용자가 로그아웃을 하면, Key-Value Store에서 저장되었던 사용자 상태가 online → offline으로 변경된다.
- 이 이후에는 사용자가 미접속 상태로 표시가 된다.
- 접속장애
- 네트워크는 환경에 따라 안정성이 다르기에 불안정 상황에 대응할 수 있는 설계를 통해 대비해야 한다. 네트워크가 불안정해서 인터넷 연결이 끊어지면, Socket 연결도 끊어지니깐 말이다.
- 위 상황을 극복하기 위해선, 단순하게 연결이 끊기면 오프라인 상태로 표시하고, 다시 연결이 복구가 되면 온라인 상태로 표시하면 된다.
- 하지만 위 해결방법은 짧은 시간동안 네트워크가 끊어졌다 복구되는 경우에는 바람직하지 않다.
- 이 책에서는 해당 문제를 heartbeat 검사를 통해 해결을 한다.
- heartbeat는 클라이언트가 주기적으로 heartbeat event를 접속상태 서버로 보내고, 마지막 event를 받은지 몇 초안에 또 다른 박동 event 메시지를 받으면, 해당 사용자의 접속상태를 online 상태로 유지를 하게 된다.

- 위 스크린샷은 클라이언트가 heartbeat event를 5초에 한번씩 서버로 보내는 중이다. Event를 3번 보낸 후, 클라이언트가 30초 동안 heartbeat event를 보내지 않았기 때문에, 서버는 사용자를 offline 상태로 인지를 하여 해당 상태로 변경한다.
- 상태 정보의 전송
- 상태정보 서버는 pub/sub model을 사용해서 접속 상태를 변경한다.
- 이러한 구조는 그룹 크기가 10만일 때 상태 변화 1건당 10만 건의 이벤트 메시지가 발행되기 때문에 소규모일 때만 사용할 수 있다.
- 대규모의 그룹을 가지는 경우엔 사용자의 접속상태를 manual로 관리한다. ex) 디스코드, slack

- 상태정보 서버는 pub/sub 구조를 사용한다고 하였고, 관계마다 1개의 채널을 둔다.
- 사용자 A의 접속상태가 변경 되었을때, A-B, A-C, A-D 3개의 채널을 사용한다.
- 채널 A-B : B가 구독
- 채널 A-C : C가 구독
- 채널 A-D : D가 구독
- 클라이언트와 서버 사이에는 실시간 WebSocket을 사용한다. 위에서 말했듯이, 그룹의 크기가 작을 때 효과적이다.
- 이를 해결하기 위해서는, 입장하는 순간에만 상태정보를 읽어가게 하거나, 친구 리스트에 있는 접속상태를 갱신하고 싶으면 수동으로 하게 할 수 있다.