본문으로 바로가기

채팅 서비스 시스템 디자인

category Coding/설계 | 경험 2023. 12. 13. 19:00
반응형

Step1. Understand the problem and establish design scope

첫 번째로 면접관이 채팅 시스템 설계를 요청할 때 정확히 어떠한 것을 염두에 두고 있는지 파악해야 한다. 최소한 1:1 채팅인지 그룹 채팅인지 정도는 알아야 한다는 말이다. 아래와 같은 질문을 던질 수 있겠다.

  • 나: 1:1 채팅 또는 그룹 채팅 중 어떠한 채팅 앱에 초점을 맞춰야하나요?
  • 면접관: 1:1 / 그룹 채팅 모두 지원해야 합니다.
  • 나: 모바일 앱, 웹 앱 중 어떤 것인가요? 둘 다 인가요?
  • 면접관: 둘 다 입니다.
  • 나: 스케일은 어느정도인가요? 스타트업 제품 수준인가요, 아니면 대규모 트래픽을 가졌나요?
  • 면접관: DAU 5천만을 감당할 수 있어야 합니다.
  • 나: 그룹 채팅의 경우 참여할 수 있는 참가자 수는 제한이 있나요?
  • 면접관: 최대 100명까지 참여할 수 있습니다.
  • 나: 가장 중요한 기능은 무엇인가요? 파일을 보낼 수 있나요?
  • 면접관: 1:1, 그룹 채팅 모두 텍스트 전송만 지원합니다.
  • 나: 메시지 크기 제한이 있나요?
  • 면접관: 텍스트는 10만 글자 이하여야 합니다.
  • 나: 종단간 암호화가 필요한가요?
  • 면접관: 지금은 필요하지 않지만 시간이 남으면 논의해볼 것입니다.
  • 나: 채팅 기록을 얼마나 오래 보관해야하나요?
  • 면접관: 영원히 보관해야합니다.

이번 챕터에서 우리는 아래의 항목에 초점을 맞춰 페이스북 메신저와 같은 채팅 서비스를 디자인해볼 것이다.

  • 낮은 레이턴시를 보장하는 1:1 채팅
  • 최대 100명까지 참여 가능한 그룹 채팅
  • 온라인 유/무 판단 가능
  • 다중 기기 지원. 하나의 계정으로 동시에 여러개의 기기에서 로그인할 수 있음.
  • 푸시 알림

시스템이 수용할 수 있는 트래픽은 굉장히 중요하므로 DAU 5천만을 지원하는 시스템을 디자인해본다.

Step2. Propose high-level design and get buy-in

높은 수준의 시스템을 디자인하기 위해서는 클라이언트와 서버간 통신에 대한 기본 지식이 필요하다. 채팅 시스템에서 클라이언트는 모바일 앱이나 웹 모두가 될 수 있다. 클라이언트는 서로 직접 통신하지 않는다. 대신 각 클라이언트는 채팅 서비스에 연결되고 해당 서비스는 위에서 언급한 모든 기능을 제공한다. 이제 핵심 기능에 대해 집중하자. 채팅 서비스는 반드시 아래의 기능을 지원해야한다.

  • 다른 클라이언트로부터 메시지 수신
  • 수신자를 찾고 해당 수신자에게 메시지를 전송
  • 수신자가 온라인 상태가 아니라면 수신자의 상태가 온라인이 될 때 까지 메시지를 서버에서 보관

WebSocket

웹 소켓은 서버에서 클라이언트로 메시지를 보내는 가장 일반적인 솔루션이다. Figure 5는 웹 소켓이 어떻게 동작하는지를 보여준다.

웹 소켓의 커넥션은 클라이언트로 인해 맺어진다. 그리고 이는 양방향이며 지속적이다. 지속적인 연결을 통해 서버는 클라이언트에 메시지를 보낼 수 있다. 웹 소켓 커넥션들은 일반적으로 방화벽이 설치된 경우에도 작동한다. 이는 일반적인 HTTP/HTTPS과 동일하게 80 또는 443포트를 사용하기 때문이다.

앞서 우리는 발신자 측에서 HTTP가 사용하기 좋은 프로토콜이라고 말했지만 웹 소켓은 양방향 통신을 지원하므로 굳이 웹 소켓을 사용하지 않을 이유를 찾을 수 없다. Figure 6은 발신자와 수신자 측면에서 웹 소켓이 어떻게 사용되는지를 나타낸다.

발신과 수신에서 웹 소켓을 사용하면 시스템 디자인이 좀 더 단순해지며 클라이언트와 서버의 구현 측면에서도 보다 간단한 방법을 제공한다.

High-level design

클라이언트와 서버의 양방향 통신을 위해 웹 소켓을 사용하지만, 모든 기능을 웹 소켓을 통해 구현하진 않는다. 실제로 채팅 서비스의 대다수의 기능(회원가입/로그인/유저 프로필 등)은 일반적인 HTTP통신을 사용할 수 있다. 이에 대해 조금만 더 이야기해보고 시스템의 상위 수준 구성요소를 살펴본다.

Figure 7에 나와있는 것 처럼 채팅 시스템은 Stateless, Stateful, Third-part 3개의 주요 카테고리로 나뉜다. 

Stateless Services

Stateless 서비스들은 요청 Path에 따라 올바른 서비스로 라우팅해주는 로드 밸런서 뒤에 위치한다. 이 서비스들은 마이크로 또는 모놀리틱 형태로 존재할 수 있다. 시장에 이미 손쉽게 통합할 수 있는 많은 서비스들이 존재하기에 우리가 모든 것을 구현할 필요는 없다. 여기에서 우리가 좀 더 자세하게 다룰 서비스는 Service discovery이다. 이는 클라이언트가 연결 가능한 채팅 서버의 DNS 호스트 이름 목록을 제공하는 주요 기능을 한다.

Stateful Services

Stateful 서비는 채팅 서비스 하나만 존재한다. 이는 각 클라이언트가 채팅 서비스와 지속적인 커넥션을 맺고 있어야 하기 때문이다. 이 서비스에서 클라이언트는 채팅 서비스가 가용 가능한 상태인 이상 다른 채팅 서버로 연결을 전환하지 않는다. Service discovery는 서버의 부하가 가중되는것을 막기 위해 채팅 서비스와 긴밀하게 협력하며 트래픽을 조정한다. 이는 아래에서 좀 더 자세히 다룬다.

Third-party integration

채팅 앱에서 푸시 알림은 가장 중요한 서드 파티 중 하나이다. 이는 앱이 실행 상태가 아니더라도 사용자에게 메시지가 왔음을 알려주는 방법이다. 그렇기에 적절한 푸시 알림과의 통합은 필수적이다. 

Scalability

작은 규모의 서비스에서는 위에서 설명한 모든 서비스가 하나의 서버에 존재할 수도 있다. 우리가 설계한 규모에서도 이론적으로는 하나의 클라우드 서버에 모든 사용자 연결을 수용하는것이 가능하다. 우리 시나리오에서는 동시 접속자가 100만명인 경우 각 연결에서 서버에 10K의 메모리가 필요하다고 가정하면 하나의 모든 연결을 유지하는데 약 10GB의 메모리가 필요하다.

모든 서비스를 하나의 서버로 구성하는 시스템 디자인을 한다면 면접관은 이를 좋지않게 볼 수 있다. 어떠한 기술자도 단일 서버에서 이러한 규모의 설계를 진행하진 않는다. 단일 서버 설계는 여러 요인으로 인해 시스템이 다운될 수 있고 단일 실패 지점이 생기는 가장 큰 문제점을 가지고 있다.

그러나 단일 서버로 디자인을 시작하는것은 괜찮다. 다만 면접관이 이것이 시작점이라는 것만 알고 있다면 말이다. Figure 8은 우리가 언급한 모든 내용을 종합하여 다시 설계된 디자인을 나타낸다.

Figure 8에서 클라이언트는 서버와 실시간 메시지 통신을 위해 웹 소켓을 사용한다.

  • 채팅 서버는 메시지 전송/수신을 용이하게 한다.
  • Presense 서버는 사용자의 온라인/오프라인 상태를 관리한다.
  • API 서버는 로그인/회원가입등의 모든 것을 다룬다.
  • Notification 서버는 푸시 알림을 보낸다.
  • 마지막으로 키-밸류 저장소는 채팅 내역을 저장한다. 오프라인 유저가 온라인으로 돌아오면 해당 유저의 이전 채팅 내역을 볼 수 있다.

Storage

여기까지 우리의 서버는 준비되었고 서드파티 통합까지 완료되었다. 기술 스택 아래에는 데이터 레이어가 존재한다. 데이터 레이어를 수정하려면 약간의 노력이 필요하다. 우리가 내려야 할 중요한 결정 사항은 관계형 데이터베이스 또는 NoSQL 데이터베이스 중 올바른 데이터베이스를 결정하는 것이다. 이를 위해서는 데이터의 타입과 읽기/쓰기 패턴에 대해 조사해봐야 한다.

일반적인 채팅 시스템에는 2가지 타입의 데이터가 존재한다. 첫 번째로 유저 프로필, 설정, 유저 친구 목록과 같은 일반적인 데이터이다. 이러한 데이터는 신뢰성이 강한 관계형 데이터베이스에 저장된다. 또한 Replication과 Sharding을 통해 가용성 및 확장성에 대한 요구사항을 충족시킬 수 있다.

두 번째로 채팅 시스템에 고유한 것으로써 채팅 기록 데이터가 있다. 읽기/쓰기 패턴을 이해하는것은 이 때문에 중요하다.

  • 채팅 시스템의 데이터 양은 굉장히 크다. 이전 조사에 따르면 페이스북 메신저와 왓츠앱은 하루에 600억개의 메시지를 처리한다고 한다.
  • 최근 대화 기록이 가장 자주 접근된다. 유저는 보통 이전 대화 기록에 자주 접근하지 않는다.
  • 대부분의 경우 최근 채팅 기록이 표시되지만 유저는 검색, 멘션 보기, 특정 메시지로 이동등과 같이 데이터에 대해 무작위 액세스가 필요한 기능을 사용할 수 있다. 이러한 경우는 데이터 액세스 레이어에서 지원된다.
  • 1:1 채팅 앱에서 읽기/쓰기 비율은 1:1이다.

우리의 요구사항에 맞는 저장소를 선택하는것은 굉장히 중요하다. 우리는 아래의 이유로 인해 키-밸류 저장소를 추천한다.

  • 키-밸류 저장소는 손쉽게 수평 확장할 수 있다.
  • 데이터에 접근할 때 굉장히 낮은 레이턴시를 보장한다.
  • 관계형 데이터베이스는 Long-tail 데이터를 잘 처리하지 못한다. 인덱스의 사이즈가 커갈수록 임의 데이터에 대한 접근 비용이 커진다.
  • 키-밸류 저장소는 신뢰할 수 있는 타 채팅 앱에서 이미 채택하여 사용중이다. 예를 들어 페이스북과 디스코드는 이미 키-밸류 저장소를 사용하고 있다. (페이스북 메신저는 HBase를, 디스코드는 Cassandra를 사용)

Data models

지금까지 키-밸류 저장소를 사용하는 방법에 대해 이야기했다. 우리 시스템에서 가장 중요한 데이터는 메시지 데이터인데 이제 이에 대해 자세히 다뤄본다.

Message table for 1 on 1 chat

Figure 9는 1:1 채팅을 위한 메시지 테이블을 나타낸다. message_id가 PK이며 메시지의 순서 결정을 도와준다. 두개의 메시지가 동시에 생성될 수 있으므로 메시지 순서를 위해 created_at은 사용할 수 없다.

Message table for group table 

Figure 10은 그룹 채팅에 대한 테이블을 나타낸다. channel_id와 message_id를 복합키로 설정했다. 채널과 그룹은 여기서 동일한 의미를 나타낸다. 그룹 채팅의 모든 쿼리는 채널에서 작동하므로 채널 ID는 파티션 키로 사용된다.

Message ID

message_id를 생성하는 방법은 살펴볼 가치가 있는 흥미로운 주제이다. message_id는 메시지 순서를 보장하는 역할을 한다. 메시지 순서를 확인하려면 message_id가 다음 두 가지 요구사항에 충족해야한다.

  • ID들은 유니크해야 한다.
  • ID들은 순서 기반으로 정렬할 수 있어야한다. 이는 새로운 로우가 기존 로우보다 높은 ID를 가지고 있음을 의미한다.

위 2가지 요구사항을 어떻게 충족시킬 수 있을까? 첫 번째로 MySQL의 auto_increment가 떠오른다. 그러나 NoSQL은 그러한 기능이 존재하지 않는다.

두 번째로 Snowflake와 같은 64비트 시퀀스 생성기를 사용하는 것이다. 이에 대해서는 Design a unique ID Generator in a distributed system 챕터에서 설명한다.

마지막으로 로컬 시퀀스 생성기를 사용하는 것이다. 로컬은 ID가 그룹 내에서만 고유함을 의미한다. 로컬 ID가 작동하는 이유는 일대일 채널이나 그룹 채널 내에서 메시지 순서를 유지하는 것으로 충분하기 때문이다. 이 방법은 글로벌 ID 생성기보다 손쉽게 구현할 수 있다.

Step 3. Design deep dive

시스템 디자인 인터뷰에서는 일반적으로 상위 수준 설계의 일부 구성 요소들에 대해 자세하게 질문할 것으로 예상된다. 채팅 시스템, Service discovery, 메시지 흐름, 온라인/오프라인 표시 등이 이에 해당한다.

Service discovery

Service discovery의 주요 역할은 위치, 서버 용량등과 같은 기준을 기반으로 클라이언트에게 가장 적합한 채팅 서버를 추천하는 것이다. 아파치 주키퍼는 굉장히 자주 쓰이는 Service discovery 오픈소스이다. 이는 사용 가능한 모든 채팅 서버를 등록하고 미리 정의된 기준에 따라 클라이언트에게 가장 적합한 채팅 서버를 전달한다. Figure 11은 Zookeeper의 동작에 대해 나타낸다.

  1. 유저 A가 앱을 통해 로그인을 시도한다.
  2. 로드 밸런서는 API서버에게 로그인 요청을 보낸다.
  3. 백엔드에서 유저의 인증을 마치면 Service discovery는 유저 A에게 가장 적합한 채팅 서버를 찾는다. 위 예제에서는 2번 채팅 서버가 선택되었고 해당 서버의 정보가 유저 A에게 반환된다.
  4. 유저 A는 웹 소켓을 통해 2번 채팅 서버에 접속한다.

Message flows

채팅 시스템에서 종단간의 흐름을 이해하는건 흥미롭다. 이번 섹션에서는 1:1 채팅 흐름, 여러 기기 간 메시지 동기화 및 그룹 채팅 흐름에 대해 알아본다.

1 on 1 chat flow

Figure 12는 유저 A가 B에게 메시지를 보냈을 때의 상황을 나타낸다.

 

  1. 유저 A가 1번 채팅 서버에 메시지를 전송한다.
  2. 1번 채팅 서버는 ID 생성기를 통해 ID를 획득한다.
  3. 1번 채팅 서버는 동기화 큐에 메시지를 전송한다.
  4. 메시지가 키-밸류 저장소에 저장된다.
  5. 유저 B가 
    1. 온라인 상태라면 메시지는 B 유저가 접속되어있는 2번 채팅 서버로 전달된다.
    2. 오프라인 상태라면 푸시 서버를 통해 알림이 전송된다.
  6. 2번 채팅 서버는 유저 B에게 메시지를 전달한다. 유저 B와 2번 채팅 서버는 웹 소켓을 통해 지속적으로 연결되어 있다.

Message synchronization across multiple devices

유저는 하나 이상의 기기를 가질 수 있다. 본 단락에서는 많은 기기들에 메시지를 동기화하는 방법을 설명한다. Figure 13은 메시지가 어떻게 동기화되는지를 나타낸다.

Figure 13에서 유저 A는 휴대폰과 랩탑 총 2개의 기기를 가지고 있다. 유저 A가 휴대폰을 통해 채팅 앱에 접속하면 1번 채팅 서버와 웹 소켓을 통해 연결을 맺는다. 동일하게 랩탑도 1번 채팅 서버와 커넥션을 맺는다.

각 기기는 cur_max_message_id라는 변수를 통해 가장 마지막 메시지 ID값을 추적한다. 다음 두 가지 조건을 만족하는 메시지는 새로운 메시지로 간주된다. 

  • 수신자의 ID와 현재 로그인된 유저의 ID가 동일한 경우
  • 키-밸류 저장소에 있는 메시지 ID가 cur_max_message_id보다 큰 경우

각 기기에 있는 고유한 cur_max_message_id를 통해 기기간 메시지 동기화는 손쉽게 해결할 수 있다. 키-밸류 저장소에서 새로운 메시지를 가져올 수 있기 때문이다.

Small group chat flow

1:1채팅과는 다르게 그룹 채팅의 로직은 좀 더 복잡하다. Figure 14, 15는 이를 나타낸다.

 

Figure 14는 유저 A가 그룹 채팅에 메시지를 전송했을때를 나타낸다. 예를 들어 그룹에 유저 A, B, C총 3명이 참여중이라고 가정해보자. 첫 번째로 A가 전송한 메시지는 각 그룹 멤버들의 메시지 동기화 큐로 복제되어 들어간다. (예제에서는 유저 B와 유저 C의 메시지 큐에 들어간다) 메시지 동기화 큐를 수신자의 받은 편지함이라고 생각해도 된다. 위 디자인은 소규모 그룹 채팅에 적합하다. 그 이유는,

  • 각 클라이언트는 새 메시지를 받기 위해 각자의 메시지 큐만 확인하면 되므로 메시지의 흐름이 단순화된다.
  • 그룹 규모가 작으면 메시지를 각 유저의 메시지 큐로 복제하는 비용이 적다.

위챗이 비슷한 방법을 사용한다. 그리고 하나의 채팅방에 최대 인원 수를 500명으로 제한하고 있다. 그러나 수많은 유저가 참여 가능한 채팅방의 경우 위처럼 메시지를 각 멤버들의 메시지 큐에 복제하는 방법을 사용하긴 힘들다.

수신자는 수많은 유저로부터 메시지를 수신받을 수 있다. 각 수신자는 메시지 큐가 존재하고 각기 다른 발신자가 전송한 메시지가 담겨있다. Figure 15는 이를 나타낸다.

Online presence

온라인 상태 유무 체크는 많은 채팅 어플리케이션에서 필수적인 기능이다. 보통 유저 프로필 옆 초록 점을 통해 이를 나타낸다. 이번 섹션에서는 이러한 기능이 어떻게 동작하는지에 대해 설명한다.

User login

유저 로그인의 흐름은 Service discovery 섹션에서 설명했다. 클라이언트와 서버가 웹 소켓으로 연결을 맺은 후 유저 A의 온라인 상태와 last_active_at이 키-밸류 저장소에 저장된다. 현재 상태 표시기는 사용자가 로그인한 후 온라인 상태임을 나타낸다.

User logout

유저가 로그아웃하면 Figure 17에 나온 흐름이 진행된다. 키-밸류에 저장된 온라인 상태는 오프라인 상태로 변경된다. 그리고 현재 상태 표시기는 유저를 오프라인으로 표시한다.

User disconnection

우리는 인터넷 연결이 지속적이고 안정적이길 바란다. 하지만 그렇지 않은 케이스도 존재하기에 해당 케이스를 시스템 디자인에서 다뤄야 한다. 유저가 인터넷에서 접속이 끊기면 클라이언트와 서버의 연결 또한 끊긴다. 연결 끊김에 대한 간단한 방법은 유저를 오프라인으로 표시하고 다시 연결될 때 상태를 온라인으로 변경하는 것이다. 하지만 이 방식에는 큰 결함이 존재한다. 유저에게 인터넷 접속/접속끊김 현상은 짧은 시간내에 꽤나 빈번하게 발생할 수 있다. 예를 들어 유저가 터널을 통과하는 동안 인터넷 연결이 잠시 끊길수도 있다. 매번 연결/연결끊김 시 온라인 상태를 수정하는 것은 상태를 너무 자주 바꾸기에 유저 경험에 좋지 않다.

이러한 문제를 해결하기 위해 하트비트 메커니즘을 사용할 수 있다. 클라이언트는 주기적으로 하트비트 이벤트를 현재 상태 표시기에 전송한다. 상태 표시 서버가 특정 시간 내에 하트 비트 이벤트를 받으면 사용자는 온라인으로 간주되며 그렇지 않으면 오프라인으로 간주된다. 

Figure 18에서 클라이언트는 5초마다 하트비트를 보낸다. 3번의 하트비트 이후 클라이언트는 연결이 끊겼고 30초 이내에 재연결 되지 않았다. 따라서 상태는 온라인에서 오프라인으로 변경된다.

Online status fanout

유저 A의 친구들은 어떻게 A의 상태가 변경됨을 알 수 있을까? FIgure 19는 이에 대한 동작을 나타낸다. 상태 표시 서버는 발행-구독 모델을 사용하여 각 친구들의 채널을 유지한다. 유저 A의 온라인 상태가 변경되면 A-B, A-C, A-D 총 3개의 채널에 이벤트를 발행한다. 이 3개의 채널은 B, C, D 유저가 각각 구독하고 있다. 따라서 친구들이 온라인 상태 업데이트를 쉽게 받을 수 있다. 클라이언트와 서버는 웹 소켓을 통해 실시간 통신한다.

위 디자인은 소규모 그룹 채팅에 적합하다. 예를 들어 위챗이 위와 비슷한 방법을 사용하고 있고, 그룹 채팅 정원을 500명으로 제한하고 있다. 큰 그룹인 경우 위처럼 온라인 상태를 모든 참여 유저에게 고지하는 것은 비용이 비싸고 오랜 시간이 걸린다. 예를 들어 10만 유저가 존재한다고 가정해보자. 하나의 상태 변경은 10만개의 이벤트를 생성할 것이다. 이러한 병목현상을 해결하기 위해 유저가 그룹에 입장했을때만 상태에 접근하거나 수동으로 친구 목록을 갱신하는 방법이 있다.

Step 4. Wrap up

이번 챕터에서는 1:1 채팅과 소규모 그룹 채팅을 모두 지원하는 채팅 시스템 아키텍처를 제시했다. 클라이언트와 서버는 웹 소켓을 통해 실시간 통신한다. 채팅 시스템은 다음과 같은 요소들을 포함한다

  • 실시간 메시지 통신을 위한 채팅 서버
  • 온라인 유무를 위한 상태 체크 서버
  • 푸시 알림을 위한 푸시 서버
  • 채팅 기록 저장을 위한 키-밸류 저장소
  • 그외 다른 기능들을 위한 API서버

만약 면접 중 추가 시간이 존재한다면 이야기해볼만한 포인트는 다음과 같다.

  • 사진이나 동영상을 제공하기 위한 채팅 앱의 확장. 미디어 파일은 텍스트에 비해 사이즈가 굉장히 크다. 압축, 클라우드 저장소, 썸네일 등은 흥미로운 주제이다.
  • 종단간 암호화. 왓츠앱은 메시지를 위한 종단간 암호화를 지원하며 오직 수신자 또는 발신자만 메시지를 읽을 수 있다.
  • 클라이언트에 메시지를 캐싱해두는 것은 클라이언트 - 서버 간 데이터 통신을 줄이는데에 효율적이다.
  • 로드 시간 개선. 슬랙은 빠른 로드 시간을 위해 지리적으로 분산된 네트워크를 구축하고 사용자의 데이터, 채널 정보등을 캐싱처리 해두었다.
  • 에러 핸들링
  • 채팅 서버 에러. 채팅 서버에는 수십만개 이상의 연결이 있을 수 있다. 만약 채팅 서버가 죽으면 Service discovery(Zookeeper)는 클라이언트가 새롭게 연결을 맺기 위해 다른 채팅 서버를 제공해줄 수 있다.
  • 메시지 재전송 메커니즘. 재시도 및 큐 저장은 메시지를 재전송하는 일반적인 기술이다.

Reference

https://bytebytego.com/courses/system-design-interview/design-a-chat-system

이미 알고있거나 불필요한 내용은 추가하지 않았으므로 원글을 확인하길 추천드립니다.

반응형