본문 바로가기

CS 아티클 리뷰

[가상 면접 사례로 배우는 대규모 시스템 설계 기초] 채팅 시스템 설계 (2)


(6) 상세 설계

 - 개략적 설계안에 포함된 컴포넌트 가운데 몇 가지를 골라 좀 더 자세히 들여다 보게 되는 것은
   면접장에서 흔히 벌어지는 일이다.
   채팅 시스템의 경우에는 서비스 탐색(service discovery), 메시지 전달 흐름, 
   그리고 사용자 접속 상태를 표시하는 방법 정도가
   좀 더 자세히 살펴볼 만한 부분이다.

(6-1) 서비스 탐색
- 서비스 탐색 기능의 주된 역할은 클라이언트에게 가장 적합한 채팅 서버를 추천하는 것이다.
  이 때, 사용되는 기준으로는 클라이언트의 위치(geographical location), 서버의 용량(capacity) 등이 있다.
  서비스 탐색 기능을 구현하는 데 널리 쓰이는 오픈 소스 솔루션으로는 아파치 주키퍼(Apache Zookeeper) 같은 것이 있다.

- 사용 가능한 모든 채팅 서버를 여기 등록시켜 두고, 클라이언트가 접속을 시도하면 사전에 정한 기준에 따라
  최적의 채팅 서버를 골라 주면 된다.

- 그림 12-11은 주키퍼로 구현한 서비스 탐색 기능이 어떻게 동작하는지를 보여준다.

(1) 사용자 A가 시스템에 로그인을 시도한다.
(2) 로드밸런서가 로그인 요청을 API 서버들 가운데 하나로 보낸다.
(3) API 서버가 사용자 인증을 처리하고 나면 서비스 탐색 기능이 동작하여 해당 사용자를 서비스할 최적의 채팅 서버를 찾는다.
    이 예제의 경우에는, 채팅 서버 2가 선택되어 사용자 A에게 반환되었다고 하겠다.
(4) 사용자 A는 채팅 서버 2와 웹소켓 연결을 맺는다.


(6-2) 메시지 흐름
- 채팅 시스템에 있어서 종단 간 메시지 흐름을 이해하는 것은 흥미로운 주제다.
  이번 절에서는 1:1 채팅 메시지의 처리 흐름과 여러 단말 간 메시지 동기화 과정을 살펴본 다음,
  그룹 채팅 메시지의 처리 흐름도 살펴볼 것이다.


(1) 1:1 채팅 메시지 처리 흐름
- 그림 12-12는 1:1 채팅에서 사용자 A가 B에게 보낸 메시지가 어떤 경로로 처리되는지를 보여준다.
  1. 사용자 A가 채팅 서버 1로 메시지 전송
  2. 채팅 서버 1은 ID 생성기를 사용해 해당 메시지의 ID 결정
  3. 채팅 서버 1은 해당 메시지를 메시지 동기화 큐로 전송
  4. 메시지가 키-값 저장소에 보관됨
  5. (a) 사용자가 B가 접속 중인 경우 메시지는 사용자 B가 접속 중인 채팅 서버(본 예제의 경우에는 채팅 서버2)로 전송됨
     (b) 사용자 B가 접속 중이 아니라면 푸시 알림 메시지를 푸시 알림 서버로 보냄
  6. 채팅 서버 2는 메시지를 사용자 B에게 전송. 사용자 B와 채팅 서버 2 사이에는 웹소켓 연결이 있는 상태이므로
     그것을 이용


(2) 여러 단말 사이의 메시지 동기화
- 여러 개 단말을 사용하는 사람은 많다. 지금부터 여러 단말 사이에 메시지 동기화는 어떻게 하는지 설명할 것이다.
  그림 12-13은 그 한 사례다.

- 그림 12-13에서 사용자 A는 전화기와 랩톱의 두 대 단말을 이용하고 있다.
  사용자 A가 전화기에서 채팅 앱에 로그인한 결과로 채팅 서버 1과 해당 단말 사이에 웹 소켓 연결이 만들어져 있고,
  랩톱에서 로그인한 결과로 역시 별도 웹소켓이 채팅 서버1에 연결되어 있는 상황이다.

- 각 단말은 cur_max_message_id라는 변수를 유지하는데, 해당 단말에서 관측된 가장 최신 메시지의 ID를 추적하는 용도다.
  아래 두 조건을 만족하는 메시지는 새 메시지로 간주한다.

(1) 수신자 ID가 현재 로그인한 사용자 ID와 같다.
(2) 키-값 저장소에 보관된 메시지로서, 그 ID가 cur_max_message_id보다 크다.

- cur_max_message_id는 단말마다 별도로 유지 관리하면 되는 값이라 키-값 저장소에서 새 메시지를 가져오는 동기화 작업도 쉽게 구현할 수 있다.

 

 

(3) 소규모 그룹 채팅에서의 메시지 흐름

- 그림 12-14는 사용자 A가 그룹 채팅 방에서 메시지를 보냈을 때, 어떤 일이 벌어지는지 보여준다.

  해당 그룹에 3명의 사용자가 있다고 하자(사용자 A, B, C)

  우선 사용자 A가 보낸 메시지가 사용자 B와 C의 메시지 동기화 큐(message sync queue)에 복사된다.

  이 큐를 사용자 각각에 할당된 메시지 수신함 같은 것으로 생각해도 무방할 것이다.

  이 설계안은 소규모 그룹 채팅에 적합한데, 이유는 다음과 같다.

 

(3-1) 새로운 메시지가 왔는지 확인하려면 자기 큐만 보면 되니까 메시지 동기화 플로가 단순하다

(3-2) 그룹이 크지 않으면 메시지르 수신자별로 복사해서 큐에 넣는 작업의 비용이 문제가 되지 않는다

 

- 위챗(Wechat)이 이런 접근법을 쓰고 있으며, 그룹의 크기는 500명으로 제한하고 있다.

  하지만 많은 사용자를 지원해야 하는 경우라면 똑같은 메시지를 모든 사용자의 큐에 복사하는게 바람직하지 않을 것이다.

 

- 지금 설명한 메시지 흐름을 수신자 관점에서 살펴보면, 한 수신자는 여러 사용자로부터 오는 메시지를 수신할 수 있어야 한다. 따라서 각 사용자의 수신함, 즉 메시지 동기화 큐는 그림 12-15와 같이 여러 사용자로부터 오는 

  메시지를 받을 수 있어야 한다.

 

(4) 접속 상태 표시

- 사용자의 접속 상태를 표시하는 것은 상당수 채팅 애플리케이션의 핵심적 기능이다.

  채팅 애플리케이션을 사용하다 보면 사용자의 프로파일 이미지나 대화명 옆에 녹색 점이 붙어 있는 것을 

  보게 되는데, 이것이 바로 사용자가 접속중임을 나타낸다.

  이번 절에서는 그 녹색 점을 표현하기 위해 무엇이 필요한지 자세히 살펴보겠다. 

 

- 개략적 설계안에서는 접속상태 서버(presence server)를 통해 사용자의 상태를 관리한다고 했었다.

  접속 상태 서버는 클라이언트와웹소켓으로 통신하는 실시간 서비스의 일부라는 점에 유의하라.

  사용자의 상태가 바뀌는 시나리오는 몇 가지 있는데, 지금부터 하나씩 살펴보자.

 

(4-1) 사용자 로그인

- 사용자 로그인 절차에 대해서는 "서비스 탐색" 절에서 설명한 바 있다.

  클라이언트와 실시간 서비스(real-time service) 사이에 웹소켓 연결이 맺어지고 나면

  접속상태 서버는 A의 상태와 last_active_at 타임스탬프 값을 키-값 저장소에 보관한다.

  이 절차가 끝나고 나면 해당 사용자는 접속 중인 것으로 표시될 것이다. 

 

(4-2) 로그아웃

- 사용자 로그아웃은 그림 12-17과 같으 절차를 거친다.

   키-값 저장소에 보관된 사용자 상태가 online에서 offline으로 바뀌게 된다는 점에 유의하자.

   이 절차가 끝나고 나면 UI상에서 사용자의 ㅅ아태는 접속 중이 아닌 것으로 표시될 것이다.

 

(4-3) 접속 장애

- 인터넷을 통한 연결이 항상 안정적이라면 좋겠지만 그렇지 못하다는 것이 문제다.

  그러니 그런 상황에 대응할 수 있는 설계를 준비해야 한다.

  사용자의 인터넷 연결이 끊어지면 클라이언트와 서버 사이에 맺어진 웹소켓 같은 지속성 연결도 끊어진다.

  이런 장애에 대응하는 간단한 방법은 사용자를 오프라인 상태로 표시하고 연결이 복구되면 온라인 상태로

  변경하는 것이다.

 

- 하지만 이 방법에는 심각한 문제가 있다. 짧은 시간 동안 인터넷 연결이 끊어졌다 복구되는 일은 흔하다.

  사용자가 차를 타고 터널을 지나가는 상황을 생각해보자.

  이런 일이 벌어질 때마다 사용자의 접속 상태를 변경한다면 그것은 지나친 일일 것이고,

   사용자 경험 측면에서도 바람직하지 않을 것이다.

 

- 본 설계안에서는 박동(heartbeat) 검사를 통해 이 문제를 해결할 것이다.

  즉, 온라인 상태의 클라이언트로 하여금 주기적으로 박동 이벤트(heartbeat event)를 접속 상태 서버로

  보내도록 하고, 마지막 이벤트를 받은지 x초 이내에 또 다른 박동 이벤트 메시지를 받으면 

  해당 사용자의 접속 상태를 계속 온라인으로 유지하는 것이다.

  그렇지 않을 경우에만 오프라인으로 바꾸는 것이다.

 

- 그림 12-18의 예제에 등장하는 클라이언트는 박동 이벤트를 매 5초마다 서버로 보내고 있다.

  그런데 이벤트를 3번 보낸 후, x=30초 동안 아무런 메시지를 보내지 않아서 오프라인 상태로 변경되었다. 

 

(4-4) 상태 정보의 전송

- 그렇다면 사용자 A와 친구 관계에 있는 사용자들은 어떻게 해당 사용자의 상태 변화를 알게 될까?

  그림 12-19는 그 원리를 보여준다. 상태정보 서버는 발행-구독(publish-subscribe model)을 사용하는데,

  즉 각각의 친구관계마다 채널을 하나씩 두는 것이다.

 

- 가령 사용자 A의 접속 상태가 변경되었다고 하자. 그 사실을 세 개 채널, 즉 A-B, A-C, A-D에 쓰는 것이다.

   그리고 A-B는 사용자 B가 구독하고, A-C는 사용자 C가, 그리고 A-D는 사용자 D가 구독하도록 하는 것이다.

   이렇게 하면 친구 관계에 있는 사용자가 상태정보 변화를 쉽게 통지 받을 수 있게 된다.

   클라이언트와 서버 사이의 통신에는 실시간 웹소켓을 사용한다. 

 

- 이 방안은 그룹 크기가 작을 때는 효과적이다. 위챗은 그룹 크기 상한을 500으로 제한하고 있어서

   이와 유사한 접근법을 사용할 수 있었다.

   그룹 크기가 더 커지면 이런 식으로 접속 상태 변화를 알려서는 비용이나 시간이 많이 들게 되므로

   좋지 않다.

   가령 그룹 하나에 100,000 사용자가 있다고 하자. 그러면 상태변화 1건당 100,000개의 이벤트 메시지가 

   발생할 것이다. 이런 성능 문제를 해소하는 한 가지 방법은 사용자가 그룹 채팅에 입장하는 순간에만

   상태 정보를 읽어가게 하거나, 친구 리스트에 있는 사용자의 접속 상태를 갱신하고 싶으면

   수동으로(manual)하도록 유도하는 것이다. 

 

 

(5) 마무리

- 이번 장에서 우리는 1:1 채팅과 그룹 채팅을 전부 지원하는 채팅 시스템의 아키텍처를 살펴보았다.

  클라이언트와 서버 사이의 실시간 통신을 가능하도록 하기 위해 웹소켓을 사용하였으며,

  실시간 메시징을 지원하는 채팅 서버, 접속 상태 서버, 푸시 알림 서버, 채팅 이력을 보관할 키-값 저장소,

  그리고 이를 제외한 나머지 기능을 구현하는 데 쓰일 API 서버 등이 그 주요 컴포넌트였다.

 

- 면접 말미에 시간이 좀 남는다면 면접관과 다음과 같은 내용을 논의해도 좋을 것이다.

 

(5-1) 채팅 앱을 확장하여 사진이나 비디오 등의 미디어를 지원하도록 하는 방법

- 미디어 파일은 텍스트에 비해 크기가 크다. 그와 관련하여 압축 방식, 클라우드 저장소,

  섬네일(thumbnail) 생성 등을 논의해보면 재미있을 것이다.

  

(5-2) 종단간 암호화

- 왓츠앱은 메시지 전송에 있어 종단 간 암호화를 지원한다.

  메시지 발신인과 수신자 이외에는 아무도 메시지 내용을 볼 수 없다는 뜻이다.

  

(5-3) 캐시

- 클라이언트에 이미 읽은 메시지를 캐시해 두면 서버와 주고받는 데이터 양을 줄일 수 있다.

 

(5-4) 로딩 속도 개선

- 슬랙(Slack)은 사용자의 데이터, 채널 등을 지역적으로 분산하는 네트워크를 구축하여

  앱 로딩 속도를 개선하였다.

 

(5-5) 오류 처리

- 채팅 서버 오류

: 채팅 서버 하나에 수십만 사용자가 접속해 있는 상황을 생각해보자.

  그런 서버 하나가 죽으면 서비스 탐색 기능(주키퍼 같은)이 동작하여

  클라이언트에게 새로운 서버를 배정하고 다시 접속할 수 있도록 해야 한다.

- 메시지 재전송

: 재시도(retry)나 큐(queue)는 메시지의 안정적 전송을 보장하기 위해 흔히 사용되는 기법이다. 

 

 

 

 

 

 

 

 

    

 
 

```

'CS 아티클 리뷰' 카테고리의 다른 글

[Martin Fowler] Unit Test  (0) 2024.07.18
[채널톡 블로그] CRDT와 OT  (0) 2024.06.30
How discord stores billions of messages  (0) 2024.06.26