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

12장 채팅 시스템 설계
김주혁's avatar
Apr 13, 2024
가상면접 사례로 배우는 대규모 시스템 설계 기초(12) - 채팅 시스템 설계
💡
목차

 
채팅 시스템 설계는 개발자라면, 누구나 한 번쯤은 생각해본 주제다.
 
그러나, 채팅 앱을 설계할 때 1:1 채팅만을 고민한다면 면접관은 곤란할 것이다.
 

1단계 문제 이해 및 설계 범위 확장

디스코드 처럼 대규모 그룹 간의 소통이나 페이스북 메신저같은 1:1 앱도 존재한다.
 
정확한 요구사항을 파악해야 한다.
 
  1. Cross Platform을 지원해야 한다.
  1. 1:1 및 그룹 채팅을 모두 지원해야 한다.
  1. 그룹 채팅의 경우 최대 100명까지 지원한다.
  1. 채팅 접속 시, 사용자의 접속상태를 표시할 수 있어야 한다. 텍스트 메세지만 주고받을 수 있다.
  1. 메세지 길이는 100,000자 이하다.
  1. 종단 간 암호화는 지원하지 않는다.
  1. 채팅 이력은 영원히 보관한다.
 
나온 Application의 스펙은 다음과 같이 정의된다.
  • 응답지연이 낮은 일대일 채팅 가능
  • 최대 100명까지 참여할 수 있는 그룹 채팅 기능
  • 사용자 접속상태 표시 기능
  • 다양한 단말 지원
  • Push 알림
  • 일 5천만 DAU 처리 가능
 

2단계 개략적 설계안 제시 및 동의 구하기

기본적으로 채팅 서비스는 다음과 같은 기능을 지원해야 한다.
 
  • 클라이언트로부터 메세지 수신
  • 메세지 수신자 결정 및 전달
  • 수신자가 접속 상태가 아닌 경우 접속할 때 까지 메세지 보관
 
둘 사이의 관계를 요약하면 이렇다.
notion image
 
채팅 서비스의 경우 어떤 프로토콜을 사용할지도 매우 중요한 요소다. 대부분 Client/Server Application에서 요청을 보내는 것은 클라이언트다. 보통은 수신자에게 요청을 보낼 때 가장 오래됐으며 대중적인 HTTP 프로토콜을 사용한다.
 
메세지 송신의 경우 채팅 서비스와의 접속에는 keep-alive 헤더를 사용하면 효율적이다. 클라이언트와 서버 간 연결을 끊지 않고 계속 유지할 수 있기 때문이다. TCP 헤더 간의 핸드셰이크 횟수를 줄일 수 있음도 물론이다.
 
메세지 수신의 경우 이것보다 더 복잡하다. HTTP는 클라이언트가 연결을 만드는 프로토콜이며,서버에서 클라이언트로 임의 시점에 보내는 데는 쉽게 쓰일 수 없다. 서버가 계속해서 연결을 만들어 메세지를 수신하고 다시 수신한 메세지를 보여주게 동작하는 방안으로 많은 기법이 제안됐는데, 폴링(polling), 롱 폴링(long polling), 웹 소켓(Web Socket)이 등이 대표적이다.
 

폴링(Polling)

폴링은 클라이언트가 주기적으로 서버에게 새 메세지가 있는지 요청하는 방법이다. 메세지가 없는 경우 자원을 낭비하는 단점이 있지만, 안정적이다.
notion image

롱 폴링(Long Polling)

notion image
폴링이 여러 가지로 비효율적이라 나온 것이 롱 폴링이다. 롱 폴링의 경우 클라이언트는 새 메세지가 반환되거나, 타임아웃 될 때까지 연결을 유지한다. 클라이언트는 새 메세지를 받으면 기존 연결을 종료하고 서버에 새로운 요청을 보내 모든 절차를 다시 시작한다.
 
  • 메세지를 보내는 클라이언트와 수신하는 클라이언트가 같은 채팅 서버에 접속하지 않을 수도 있다. 로드밸런싱을 위해 라운드 로빈을 사용하는 경우, 메세지를 받은 서버는 메세지를 수신할 클라이언트와 같은 롱 폴링 연결을 가지고 있지 않은 서버일 수 있다.
  • 서버 입장에선 클라이언트가 연결을 해제했는지 아닌지 알 좋은 방법이 없다.
  • 여전히 비효율 적이다. 메세지를 많이 받지 않은 클라이언트도 타임아웃이 일어날 때마다 주기적으로 서버에 다시 접속할 것이다.
 

웹 소켓(Web Socket)

웹 소켓은 서버가 클라이언트에게 비동기 메세지를 보낼 때 널리 사용하는 기술이다.
notion image
 
소켓 연결은 클라이언트가 시작하며, 맺어진 연결은 항구적이며 양방향이다. 연결은 초기에는 HTTP지만 핸드셰이크 절차를 거지고 나서 웹 소켓 연결로 업그레이드된다.
 
일단 연결이 만들어지고 나면 서버는 클라이언트에 비동기적으로 메세지를 전송할 수 있다. HTTP와 HTTPS의 80 또는 443 포트를 그대로 사용하기 때문에 방화벽 환경에서도 동작한다.
 
웹 소켓은 양방향 메세지 전송을 가능하게 만들어주기 때문에 안정적으로 소켓 기반 서비스를 구축할 수 있다면 굳이 폴링 방식을 사용할 필요는 없다.
 

개략적 설계안

설계에서 웹 소켓으로 연결하기로 결정됐다. 무엇을 고민하고 결정해야 할까?
 
  • 무상태 서비스(Stateless Service) / 상태 서비스(Stateful Service)
    •  
      notion image
      설계안은 무상태를 기준으로 설계 됐다. 무상태 서비스는 보통 로드밸런서 뒤쪽에 위치하는데, 채팅 서비스는 상태를 유지해야 한다.
       
      그렇다면, 채팅 서비스의 연결을 유지한 채로 로드밸런서로 부하를 분산시킬 수 있어야 한다.
 
  1. 제3자 서비스 연동
    1.  
      Push 알림과 통합될 수 있어야 한다.
 
  1. 규모 확장성
    1.  
      대용량 트래픽이 몰리는 경우, 확장성에 대한 고민 없이는 몰려드는 트래픽에 대한 처리가 불가능 해 채팅 서버를 운영하기 어렵다.
       
      이 때 따져봐야 하는 것은 서버 한 대로 얼마나 많은 양을 처리할 수 있는지다.
       
      여기서는 동시 접속자가 1M이고, 접속당 10kb의 서버 메모리가 필요하다면 10GB의 메모리 정도면 모든 연결을 다 처리할 수 있다.
      메모리 사용량 = 동시 접속자 수 × 접속자당 메모리 사용량 = 100만 × 10KB 이를 계산하면, 메모리 사용량 = 1000000 × 10KB = 10,000,000KB = 10,000MB = 10GB
       
      하지만 모든 트래픽을 서버 한대로 처리하는 것은 정답이 아닐 것이다. SPOF 문제도 있고 갑자기 예상보다 트래픽이 더 몰릴 수도 있기 때문에 확장성에 대한 고민을 해야한다.
       
       
      계속해서 유의할 점은, 실시간으로 메세지를 주고 받기 위해 웹 소켓으로 연결된 클라이언트와 서버는 연결을 끊지 않고 유지해야 한다는 점이다.
       
      • 채팅 서버는 클라이언트 사이의 메세지를 중계한다.
      • 접속상태 서버는 사용자의 접속 여부를 관리한다.(투 머치 하다고 생각)
      • API 서버는 로그인, 회원가입, 프로파일 변경 등 그 외 나머지 전부를 처리한다.
      • 알림 서버는 푸시 알람을 보낸다.
      • 키-값 저장소에는 채팅 이력을 보관한다. 시스템에 접속한 사용자는 이전 채팅 이력을 볼 수 있다.
 
  1. 저장소
    1.  
      어떤 데이터베이스를 사용하는 가는 중요한 문제다. 채팅 서비스에 있어 중요한 것은 I/O 연산의 패턴이다.
       
      채팅 시스템은 일반적으로 2가지 데이터를 다룬다.
       
    2. 사용자 Profile 같은 일반적인 데이터
    3. 채팅 시스템의 고유 데이터, 채팅 이력
    4.  
      채팅 이력은 엄청난 데이터를 다루며, 최근 메세지들은 빈번하게 사용된다. 검색 기능이나 특정 사용자가 말한 내용만을 탐색하나거나 무작위적으로 채팅 이력에 접근할 수 있다.
       
      본 설계안에선 키-값 저장소를 추천한다. 이유는 다음과 같다.
       
      • 키-값 저장소는 수평적 규모확장이 쉽다.
      • 키-값 저장소는 데이터 접근 Latency가 낮다.
      • 관계형 데이터베이스는 롱 테일에 해당하는 부분을 잘 처리하지 못하는 경향이 있다. 인덱스가 커지면 무작위적인 접근 처리 비용이 늘어난다.
      • 페이스북이나 디스코드 같은 서비스에서 키-값 저장소를 채택하고 있다.
 
  1. 데이터 모델
    1.  
      저장소를 정했으니, 메세지 데이터를 어떻게 관리할지 정해야 한다.
       
      1:1 채팅을 위한 메세지 테이블은 오른 쪽과 같이 정한다.
       
      notion image
그룹 채팅을 위한 메세지 테이블은 다음과 같이 지정한다.
 
notion image
 
 
한 가지 주의깊게 살펴볼만한 것은, 메세지 id를 만드는 방법이다.
 
메세지 id는 고유해야 하며 정렬 가능해야 하고 시계열로도 정렬될 수 있어야 한다. 이전 장에서 살펴본 스노 플레이크도 있겠지만, 지역적 순서 번호 생성기라는 것도 있다. ID의 유일성을 같은 그룹 안에서만 보장하는 것이다. 혹은 1:1 채팅 세션 안에서만 유지되면 된다는 것이다.
 

3단계 상세 설계

채팅 시스템에선 서비스 탐색, 메세지 전달 흐름, 사용자 접속 상태 표시정도를 살펴볼 수 있다.
 
  • 서비스 탐색
    •  
      책에서는 아파치 주키퍼 같은 걸 사용했지만, 아파치 카프카나 Java나 Node.js를 통해 구현할 수도 있다.
      notion image
       
      1. 사용자가 A가 시스템에 로그인을 시도한다.
      1. 로드 밸런서가 로그인 요청을 API 서버들 중 하나로 보낸다.
      1. API 서버가 인증을 처리하고 나면, 서비스 탐색 기능이 작동하여 해당 사용자를 서비스할 최적 채팅 서버로 보낸다.
      1. 사용자 A는 채팅 서버 2와 웹 소켓 연결을 맺는다.
 
  • 메세지 흐름
    •  
      각 종단 간 메세지 흐름을 이해하는 것은 중요한 일이다.
       
    • 1:1 메세지 처리 흐름
      • notion image
         
         
        1. 사용자 A가 채팅 서버 1로 메세지 전송
        1. 채팅 서버 1은 ID 생성기로 해당 메세지 ID 결정
        1. 채팅 서버 1은 해당 메세지를 메세지 동기화 큐로 전송
        1. 메세지가 키-값 저장소에 보관됨
        1. 사용자 B가 접속 중인 경우, B가 접속 중인 채팅 서버로 전송된다. 접속 중이 아니라면 Push 알람을 보낸다.
        1. 채팅 서버 2는 사용자 B에게 메세지를 전송, 사용자 B와 채팅 서버 2사이에는 웹 소켓으로 연결되있기 때문에 소켓 서버를 이용 한다.
         
    • 여러 단말 사이의 메세지 동기화
      •  
        notion image
        두 사용자 간 단말이 다를 때는 두 단말이 웹 소켓으로 연결되있다는 가정으로,
         
        각 단말은 cur_max_message_id라는 변수를 유지하는데, 해당 단말에서 관측된 가장 최신 메세지 ID를 추적하는 용도다. 아래 두 조건을 만족한다면 메세지는 새 메세지로 간주한다.
         
      • 수신자 ID가 현재 로그인한 사용자 ID랑 같다.
      • 키-값 저장소에 보관된 메세지로서, 그 ID가 cur_max_message_id보다 크다.
      •  
        이런식으로 두 단말간 메세지를 동기화하여 관리한다.
       
    • 소규모 그룹 채팅에서의 메세지 흐름
      • notion image
        위 그림은 사용자의 한 채팅이 어떤 영향을 미치는지 보여준다.
         
        사용자가 3명이라고 하면, A가 보낸 메세지가 사용자 B와 C의 메세지 동기화 큐에 복사된다. 이 큐는 사용자 각각마다 할당된 메세지 수신함이라고 생각하면 좋다.
         
        왜 소규모 일까?
         
      • 새로운 메세지가 왔는지 확인하려면 본인 큐만 보면 되니까 메세지 동기화가 단순해진다.
      • 그룹이 크지 않다면 메세지를 수신자별로 복사하여 큐에 넣는 작업 비용이 문제가 되지 않는다. 위챗이 이런 접근법을 사용하고 있다. 그래서 그룹 크기를 500명으로 제한하고 있다.
      •  
        여러 사용자를 지원해야 한다면, 이런 접근법은 바람직하지 않을 것이다. 여태껏 설명한 관점에서 살펴보면 수신자는 사용자들로부터 오는 메세지를 수신할 수 있어야 한다. 메세지 동기화는 여러 사용자로부터 오는 메세지를 받을 수 있어야 한다.
        notion image
       
 
  • 접속상태 표시
    • 아마도 디스코드의 이런 표시를 보고 쉽게 이해할 수 있을 것이다. 사용자의 접속 상태 표시 또한 실시간이라는 점을 유의해야 한다.
       
      notion image
       
    • 사용자 로그인
      •  
        notion image
        실시간 서비스 사이에 웹 소켓이 연결되고 나면, 접속상태 서버는 A의 상태와 list_active_at 타임스탬프 값을 키-값 저장소에 보관한다.이 절차가 끝나면 사용자는 접속 중인 것으로 표시될 것이다.
       
    • 로그아웃 키-값 저장소에 보관된 사용자 상태가 online에서 offline으로 바뀌게 된다는 점에 유의하자.
      • notion image
         
        이 절차가 끝나면 UI 상으로 접속 중이 아닌것으로 표시될 것이다.
       
    • 접속 장애
      •  
        notion image
        인터넷이 끊어지면 접속 연결도 끊어질 것이다. 이런 장애 대응의 간단한 방법은 사용자를 오프라인 상태로 만들고 다시 연결이되면 온라인으로 만드는 것이다.
         
        본 설계안에선 박동 검사를 통해 이 문제를 해결한다. 온라인 상태의 클라이언트로 하여금 주기적으로 박동 이벤트를 접속상태 서버로 보내도록 하고, 마지막 이벤트를 받은 지 n초가 지나면 오프라인으로 바꾸는 것이다.
       
    • 상태 정보의 전송
      •  
        상태정보 서버는 발행-구독 모델을 하고 있는데, 각각 친구마다 채널을 하나씩 두고
         
        사용자 A가 접속했다고 치면, 그 사실을 세 개의 채널 A-B, A-C, A-D에 쓰는 것이다. A-B는 사용자 B가 구독하고, A-C는 사용자 C가 A-D는 사용자 D가 구독하도록 하는 것이다.
         
        친구 관계의 접속 상태를 쉽고 빠르게 알아보도록 하는 것이다.
         
        한편, 위 방식은 그룹 크기가 작다면 효과적이지만 크기가 더 커지면 이런 변화를 유지하는 것이 리소스 낭비로 이어질 수 있다.
         
        따라서 직접 수동으로 갱신하거나, 주기가 길게 폴링하거나 하는 식으로 어느 정도 유연함을 보여줄 필요가 있다.
       

4단계 마무리

추가적으로 고민해 본다면,
 
  • 사진이나 비디오 등 미디어 파일을 지원하는 방법
  • 종단 간 암호화 : 메세지 전송은 당사자들 외에 아무도 볼 수 없는 것이 좋다.
  • 캐시: 이미 읽은 메세지를 캐시해두면 데이터 양을 줄일 수 있다.
  • 로딩 속도 개선 : 슬랙은 사용자의 데이터, 채널 등을 지역적으로 분산 네트워크를 구축하여 속도를 개선했다.
  • 오류 처리 :
    • 하나의 채팅 서버에 수십만이 접속했다고 가정한다면.. 재해복구가 필요하다.
    • 메세지 재전송 : 재시도나 큐 기법을 통해 재시도 정책을 구비해두는 것이 좋다.
 
 

 
대부분의 사진 출처
 
 
Share article
RSSPowered by inblog