12. 채팅 시스템

채팅

  • 다음과 같이 목적이 나뉜다.

    • 1:1 채팅에 집중하는 WhatsApp, WeChat, 카카오톡 같은 개인 앱

    • 그룹 채팅에 중점을 둔 Slack 같은 업무용 앱

    • 대규모 그룹의 소통과 응답 지연이 낮은 디스코드 같은 음성 채팅 앱

  • 1:1 채팅에 집중하는 경우 보통 다음과 같은 기능을 갖는다.

    • 응답 지연이 낮은 1:1 채팅이 가능해야 한다.

    • 수백명이 참여 가능한 그룹 채팅 기능도 제공해야 한다.

    • 사용자의 접속 상태를 표시하는 기능이 필요하다.

    • 다양한 단말에서 한 계정이 동시에 접근할 수 있어야 한다.

    • 푸시 알림을 지원해야 한다.

  • 채팅 서비스를 제공하여 클라이언트가 직접 통신하는 대신 채팅 서비스를 통해 통신하도록 해야 한다.

컴포넌트

무상태 서비스

  • 로그인, 회원가입, 사용자 프로필 표시 등을 처리한다.

  • Service Discovery 기능

    • 클라이언트가 접속할 채팅 서버의 DNS 호스트명을 클라이언트에 알려주는 기능을 제공해야 한다.

    • 이를 통해 특정 서버에 클라이언트가 몰리지 않도록 해야 한다.

    • ZooKeeper 등 분산 시스템을 이용해 사용 가능한 모든 채팅 서버에 클라이언트 연결을 분산시킬 수 있다.

상태 유지 서비스

  • 클라이언트가 채팅 서버와 독립적인 네트워크 연결을 유지해야 한다.

  • 다음 그림과 같은 형태로 메시지가 처리된다.

    • 사용자가 채팅 서버로 메시지를 보내면, 메시지 ID 생성기로 ID 생성 후 큐에 입력한다.

    • 큐에 있던 메시지는 저장소에 저장된다.

    • 사용자가 만약 채팅 서버에 접속중이라면 해당 서버로 메시지를 보내 실제 사용자 웹소켓으로 데이터가 보내지도록 한다. 접속 중이 아니라면 푸시 알림 메시지를 푸시 알림 서버에 보낸다.

연결

  • 단순 HTTP 통신서버

    • HTTP 프로토콜과 keep-alive 헤더를 사용해 클라이언트와 서버의 연결을 끊지 않고 계속 유지하도록 한다. 이를 통해 TCP handshake 횟수를 줄일 수 있다.

  • 폴링

    • 클라이언트가 주기적으로 서버에 요청을 보내 새 메시지가 있는지 확인하는 방식이다.

    • 폴링을 자주하면 비용이 많이 소요되고, 메시지가 한동안 없는 경우 비효율적으로 계속 요청을 보내게 된다.

  • 롱 폴링

    • 클라이언트는 새 메시지가 반환되거나 타임아웃 될 때 까지 연결을 유지한다.

    • 새 메시지를 받으면 기존 연결을 종료하고 새로운 연결을 맺는다.

    • 메시지를 보내는 클라이언트와 수신하는 클라이언트가 같은 채팅 서버에 접속하지 않을 수 있다. 동일한 채팅 서버에 접속하지 못하면, 새 메시지가 오더라도 해당 연결으로 데이터를 반환할 수 없다.

    • 폴링과 마찬가지로 비효율적인 면이 있다.

  • 웹소켓

    • 서버가 클라이언트에게 비동기 메시지를 보낼 때 가장 널리 사용하는 기술이다.

    • 처음에는 HTTP 연결이지만 특정 핸드쉐이크 절차를 거쳐 웹소켓 연결로 업그레이드된다.

    • 서버는 비동기적으로 클라이언트에게 메시지를 전송할 수 있다.

    • 80, 443 등 기본 포트를 사용하므로 기존 방화벽이 있더라도 문제 없다.

    • 연결을 계속 맺고 있어야 하므로 서버에서 연결 관리를 잘 해주어야 한다.

데이터 저장소

  • 채팅 시스템의 데이터인 채팅 이력을 어떻게 저장할 지 고민해야 한다.

  • 보통 최근에 주고받은 메시지를 조회하지만, 검색 기능이나 언급된 부분 조회, 특정 메시지로 점프하는 경우도 지원해야 한다.

  • key-value 저장소를 사용하면 데이터 접근 지연시간이 낮고, 데이터에 대한 무작위 접근을 처리하기에 RDBMS보다 낫다.

  • 1:1 채팅의 경우 메시지의 ID만 기본 키로 두면 되고, 그룹 채팅의 경우 메시지 ID와 그룹 ID를 함께 복합키로 설정해야 한다.

  • 메세지 ID는 고유해야 하고 시간순으로 정렬하는 기준이 되어야 한다. 이 때 모든 채팅방 사이에서 고유하지는 않아도 되고, 하나의 채팅방 내에서만 메시지 ID가 고유하면 된다.

여러 단말 동기화

  • cur_max_message_id 변수를 통해 해당 단말에서 관측된 가장 최신 메시지의 ID를 추적한다.

  • 아래 두 조건을 만족하는 메시지는 새로운 메시지로 간주한다. 새로운 메시지를 조회해 동기화해야 한다.

    • 메시지 수신자 ID가 현재 로그인한 사용자 ID와 동일하다.

    • 저장소의 메시지 ID가 cur_max_message_id보다 크다.

그룹 메시지

  • 사용자마다 메시지 동기화 큐를 부여해, 누군가 그룹 채팅방에 새 메시지를 남기면 속해있는 모든 사람들의 메시지 동기화 큐에 메시지를 입력해주어야 한다.

  • 새로운 메시지가 왔는지는 본인의 메시지 동기화 큐만 확인하면 된다.

  • 그룹에 속한 인원이 많으면 같은 메시지를 모든 사용자의 큐에 복사하는 것이 비효율적이게 된다.

접속 상태 표시

  • 클라이언트가 서버와 웹소켓 연결을 맺은 후에는 접속 상태를 활성화하고 last_active_at 타임스탬프 값을 바로 최신화한다.

  • 연결이 끊겼다면 연결이 일시적으로 끊겼다가 다시 회복되는 것이 반복될 때 잘 대처하기 위해 일정 시간동안 끊김이 계속 유지된 것을 확인 후 사용자의 접속 상태를 비활성화해야 한다.

  • 이를 확인하기 위해 클라이언트쪽에서 서버로 heartbeat 이벤트를 주기적으로 보내고, 서버는 이를 주기적으로 확인해 접속 상태를 확인하는 방식을 사용할 수 있다.

제3자 서비스 연동

  • 새 메시지가 왔을 때 푸시 알림을 주어야 한다.

Last updated