채팅 시스템 구현하기 전에, 뭐가 필요한지 한번 알아보자(1부)

차_현·2025년 2월 6일
2
post-thumbnail

채팅 시스템은 이런 기능을 제공해야 한다.

  1. 클라이언트로부터 메시지를 수신
  2. 메시지 수신자를 결정하고 메시지 전달
  3. 수신자가 접속상태가 아닌(미접속) 경우에는 접속할 때까지 해당 메시지를 보관

메시지 송신 클라이언트 → 채팅 서비스(메시지 저장 및 전달) → 메시지 수신 클라이언트

채팅 서비스는 HTTP 프로토콜을 사용할 수도 있지만, 실시간으로 처리 되어야하기 때문에 HTTP 프로토콜을 사용하지는 않는다. 대신 WebSocket 을 사용하거나 혹은 Pub-Sub 구조를 사용을 한다.

Protocol

  • 송신 클라이언트가 채팅 서비스에 메시지를 송신할 때, HTTP 프로토콜을 사용한다.
  • 초기 페이스 북 메시지 앱의 송신 클라이언트는 서버에 HTTP 프로토콜을 이용해 수신 클라이언트에게 보낼 메시지를 정한다. 이때 keep-alive Header를 사용하면 TCP 핸드쉐이크 횟수를 줄일 수 있다.
  • HTTP는 클라이언트가 연결을 만드는 프로토콜이고, 서버에서 수신 클라이언트에 임의 시점에 메시지를 보낼 때는 사용하기 어렵다. 그래서 아래와 같은 기술이 사용된다.(서버가 연결을 만드는 것 처럼 동작할 수 있도록)

Polling

클라이언트가 주기적으로 서버와 통신하여 새 메시지가 있는지 확인하는 방법이다. Polling 주기가 짧으면 Polling 비용이 높아지고, 서버 자원이 불필요하게 낭비될 수 있다.

Long Polling

위의 Polling 기법을 개선한 방법으로, 클라이언트가 주기적으로 서버와 통신할 때 새 메시지가 반환되거나 타임아웃 될 때까지 연결을 유지하는 방법이다.

클라이언트는 새 메시지가 반환되거나 타임아웃 될 때까지 연결을 유지한다. 클라이언트가 새로운 메시지를 받으면, 연결을 종료하고 서버에 새로운 요청을 보내어 모든 절차를 다시 시작한다.

채팅 서버를 여러개로 두고, Load Balancer가 Round Robin 알고리즘을 사용할 때, 송신 클라이언트와 수신 클라이언트가 같은 채팅 서버에 접속하게 되지 않을 수도 있다. 메시지를 받은 서버는 해당 메시지를 받을 수신 클라이언트의 연결을 가지고 있을 수 있다는 뜻이고, Load Balancing에 sticky route 방식을 이용해야 한다.

서버 입장에서는 클라이언트가 연결을 해제했는지 알 수가 없으며, Polling 방식에 비해 불필요한 요청 횟수가 줄기는 하지만 여전히 메시지를 받지 않는 클라이언트도 타임아웃이 일어날 때마다, 주기적으로 서버에 다시 접속해야 한다.

WebSocket

WebSocket은 서버가 클라이언트에게 비동기 메시지를 보낼 때 가장 널리 사용한다.

WebSocket 연결은 클라이언트가 처음에 시작을 하게 된다. 첫 연결은 HTTP 핸드쉐이크(Handshake)를 사용하고, 그 이후에는 서버가 클라이언트가 요청하지 않아도, 클라이언트에게 비동기적으로 메시지를 전송할 수 있다. WebSocket은 방화벽이 있는 환경에서도 잘 동작하며, HTTP 프로토콜과 크게 다른 점은 양방향 통신이 가능하다는 점이다.

개략적 설계

채팅 시스템은 위의 스크린샷과 같이 1) HTTP 통신으로 전통적인 요청/응답 서비스를 제공하는 무상태 서비스, 2) 실시간 서비스를 제공하는 상태 유지 서비스, 3) 알림을 제공하는 서드 파티 서비스 세 부분으로 나눌 수 있다.

채팅 시스템은 대량의 트래픽을 처리해야 하는 경우에도 1대의 서버로 구현할 수 있긴 하지만, SPOF(Single-Point-Of-Failure)를 방지하기 위해 분산 서버를 사용하는 것이 좋다.

  1. 무상태 서비스
  • 로그인, 회원가입, 사용자 프로파일 표시 등을 처리하는 보편적인 기능
  • 서비스와 사용자 단말 간의 연결을 유지하는 것이 아닌, 그때 그때 요청을 처리
  • 로드밸런서 뒤에서 동작하기 때문에, 특정 서비스로 라우팅 될 수 있게 설계가 필요(서비스 디스커버리 서비스 등)
  1. 상태 유지 서비스
  • 채팅 서비스
  • 클라이언트와 WebSocket으로 채팅 서버 간 연결
  • 특정 서버로 부하가 가중되지 않게 적절히 분배
  1. 서드 파티 서비스(제 3자 연동 서비스)
  • 푸시 알람은 모바일 서비스 주체자와 연동이 필요
  1. 규모 확장성
  • 서버 1대로 모든 것을 처리하는 것은 아니기에 확장성이 필요
  • 서버 1대로 모든 연결을 처리하는 것이 아니라 서버 간의 처리량이 얼마나 되는지 계산이 필요

  • 채팅 서버는 클라이언트 사이에 메시지를 중계하는 역할
  • 접속 상태 서버는 사용자의 접속 여부를 관리
  • API 서버는 로그인, 회원가입, 프로필 변경 등 나머지 처리
  • 알림 서버는 푸시 알람을 보내는 서버
  • Key-Value Store에 채팅 이력을 보관

채팅은 I/O Latency 중요하기 때문에 I/O Latency가 낮은 Key-Value Store를 사용하는 것이 효율적이다.

저장소

데이터의 유형과 읽기/쓰기 연산의 패턴에 따라 SQL 혹은 NoSQL 중 하나를 선택해야 한다.

  • 채팅 시스템이 다루는 데이터는 2가지로 나눌 수 있다.
    • 사용자 프로필, 설정, 친구 목록과 같은 일반적인 데이터
      • 안정성을 보장하는 RDBMS에 보관을 한다.
      • 다중화(replication), 샤딩(sharding)은 데이터의 가용성과 규모 확장성을 보증하기 위해 보편적으로 사용되는 기술이다.
    • 채팅 시스템에 고유한 데이터
      • 채팅 이력 데이터
  • 읽기/쓰기 패턴
    • 페이스북 메신저는 매일 600억개의 메시지를 처리한다고 한다.
    • 그리고 사람들은 대부분 오래된 메시지를 잘 들여다 보지 않고, 최근 메시지를 빈번하게 본다.
    • 사용자는 대체로 최근 메시지를 보지만, 검색 기능, 특정 사용자가 언급된 메시지 이런 필터링으로 특정 메시지로 점프하여 무작위 데이터를 접근하는 경우도 있다.
    • 1:1 채팅 앱의 경우 읽기:쓰기 비율은 대략 1:1 정도이다.
    • 이런 이유로 NoSQL을 주로 선택한다.
  • 그 이유는 아래와 같다.
    • Key-Value Store는 수평적 규모 확장이 쉽다.
    • Key-Value Store는 데이터 접근 지연시간(latency)가 낮다.
    • RDBMS는 Long Tail에 해당하는 부분을 잘 처리하지 못하는 경향이 있다. Index가 커지면 데이터에 대한 무작위적 접근(random access)를 처리하는 비용이 늘어난다.
    • 페이스북 메신저는 HBase를 사용하고, 디스코드는 Cassandra NoSQL을 사용하고 있다.

데이터 모델

  • 1:1 채팅을 위한 메시지 테이블
    • 이 테이블의 기본 키는 message_id로, 메시지 순서를 쉽게 정할 수 있도록 하는 역할도 한다. 두 메시지가 동시에 만들어질 수 있기 때문에 created_at을 사용해서 메시지 순서를 정할 수는 없다.
  • 그룹 채팅을 위한 메시지 테이블
    • channel_id, message_id의 복합 키를 기본 키로 사용한다. channel_id는 파티션 키(partition key)로도 사용할 것인데, 그룹 채팅에 적용될 모든 질의는 특정 채널을 대상으로 할 것이기 때문이다.
  • 메시지 ID
    • 고유해야 하고, 정렬 가능해야하며, 시간 순서와 일치해야 한다. 새로운 ID는 이전 ID보다 큰 값이어야 한다.
    • NoSQL은 auto_increment를 제공하지 않기 때문에, Snowflake와 같은 전역적 64 bit 순서번호 생성기를 이용한다.

다음 포스팅에서 상세 설계 부분부터 다시 공부해보자!

0개의 댓글