이전에 작성했던 '가상 면접 사례로 배우는 대규모 시스템 설계 기초' 책에 대한 북스터디의 발표 자료입니다. 기존에 velog에 작성했던 글을 티스토리로 옮기다 보니 서식이 맞지 않는 문제가 있습니다. 조금 더 깔끔한 보기를 원하신다면 벨로그 혹은 깃허브를 통해 확인하실 수 있습니다. 원하신다면 벨로그나, 깃허브 글자를 클릭해주세요.
Intro
이 장은 대규모 채팅 시스템을 설계하는 방법에 대해서 다룹니다. 채팅 시스템은 카카오톡, 인스타DM, 라인 등 우리에게 굉장히 친숙한 기능이고 한번쯤은 개발해봤을 법 한 기능이니 자신이 만들어봤던 채팅 서비스에서 놓친 부분이 어떤 점이 있을지 다시 한 번 돌이켜보는 것도 좋아보입니다.
요구사항 분석
얻어야 하는 정보
채팅 시스템을 설계하기 위해서 면접관과 대화를 통해 얻어내야 하는 부분은 다음과 같습니다.
- 채팅의 형태는 어떠한가? (1대1 or 그룹)
- 클라이언트는 어떤 기기로 서비스를 이용하는가? (웹 or 앱)
- 처리해야 하는 트래픽은 어느정도인가?
- 채팅 외의 주요 기능은 무엇이 있는가?
- 메세지의 길이의 제한이 있는가?
- 종단 간 암호화를 해야 하는가?
- 채팅 이력을 얼마나 보관해야 하는가?
책의 요구사항
위의 질문을 통해서 책에서 얻어낸 요구사항은 다음과 같습니다.
- 응답지연이 낮은 1대1 채팅 기능
- 최대 100명까지 참가 가능한 그룹 채팅 기능
- 사용자의 접속 상태 표시 기능
- 다양한 단말 지원. 하나의 계정으로 여러 단말에 동시에 접속 가능
- 푸시 알림
- 일별 능동 사용자 수(DAU)는 5천만명
개략적 설계
앞선 요구사항을 기반으로 채팅 시스템을 설계하기 이전에, 사전 작업을 개략적 설계 작업을 진행해보자.
이 파트에서 진행할 작업은 다음과 같다.
- 실시간 채팅을 주고받을 방법 정하기
- 무상태 서비스와 상태 서비스 나누기
- 규모 확장성 고려하기
- 저장소 고르기
- 메세지 데이터 모델 정하기
하나씩 살펴보자.
실시간 채팅 주고 받을 방법 정하기
채팅을 주고 받는 방법으로는 3가지 방법이 있다. 폴링(Polling)과 롱 폴링, 그리고 웹 소켓이다.
이 세가지 방법에 대해서는 자주 접해봤고, 자세한 글들이 많으니 간단하게 그림으로 보고 넘어가자.
폴링
롱 폴링
웹 소켓
이 3가지 방법 중에서는 폴링과 롱 폴링은 매번 요청을 보내는 것에 대한 부하를 고려하여 웹 소켓으로 선택했다.
무상태 서비스와 상태 서비스 나누기
무상태 서비스와 상태 서비스를 나누는 기준은, 웹 소켓 영역의 실시간 서비스는 상태 서비스고 그 외의 채팅 서비스를 유지하기 위한 회원 로그인, 친구 만들기 등의 기능을 무상태 서비스로 구분한다.
규모 확장성 고려하기
서비스의 일간 사용자(DAU)가 5천 만명이나 되기 때문에, 규모 확장을 할 수 있는 아키텍쳐를 고려해야한다.
책에서는 아래와 같은 아키텍처를 작성했다.
무상태 서비스는 HTTP 통신으로 사용하는 API 서버를 구성했고, 이는 로드밸런싱 작업이 이루어진다. 그리고 실시간 서비스인 채팅 서비스와 접속 상태 서비스는 웹 소켓으로 통신을 한다. 서드 파티 서비스와 연결된 알림 서비스까지 모든 서비스들은 다중화가 되어 있으며, 이는 DB 역시 마찬가지다.
저장소 고르기
저장소의 경우 RDB와 NoSQL 중에서 선택을 해야한다. 이를 정하는 기준으로는 우리의 시스템이 어떤 작업을 위주로 하는가
를 생각해 봐야 하는데, 채팅 시스템의 특성은 아래와 같다고 설명한다.
- 채팅 이력에 저장되는 데이터의 양이 굉장히 많다.
- 이 중에서 자주 사용되는 데이터는 최근 데이터다.
- 검색이나 점프하는 기능이 있다.
- 쓰기와 읽기의 비율이 대체로 1대1이다.
이런 상황에서 책은 NoSQL 중에서 키-값 저장소를 추천한다고 한다.
그 이유는 키-값 저장소는 수평적 확장에 유리하고, 접근 지연 시간도 낮고, 많은 서비스들이 사용하고 있기 때문이라고 한다.
오픈소스 채팅 시스템인 Mattermost의 경우는 PostgreSQL을 사용한다고 한다.
데이터 모델 정하기
메세지 이력을 저장하기 때문에, 어떤 형태로 저장할지를 고려해야 한다.
앞선 요구사항에서 1대1 채팅과 그룹 채팅을 모두 만족시켜야 한다고 했는데, 이를 저장할 서로 다른 모델을 제작해야한다.
책에서는 아래와 같이 말한다. 이는 대체로 최소한의 것이기 때문에, 추가적인 내용을 담아도 좋아보인다.
1대1 메세지
Message
Name | Type |
---|---|
message_id | bigint |
message_from | bigint |
message_to | bigint |
content | text |
created_at | timestamp |
그룹 메세지 **
**group message
Name | Type |
---|---|
channel_id | bigint |
message_id | bigint |
message_to | bigint |
content | text |
created_at | timestamp |
상세 설계
상세 설계 단계에서는 3가지 작업에 대해서 고려해야한다.
서비스 탐색
서비스 탐색은 사용자가 상태 서비스(채팅, 접속 상태)를 사용할 때, 하나의 서버와 지속적인 관계를 가져야하는데, 여러 서버 중 하나를 선택해야한다.
이때 사용되는 것이 서비스 탐색이다. 주된 솔루션으로는 아파치 주키퍼가 있고, 스프링 환경이라면 유레카를 사용할 수 있을 것 같다.
서비스 탐색을 사용하는 이유로는 동적으로 서버를 등록, 해제할 수 있고, 서버를 선택하는 기준으로 클라이언트의 우이치, 서버의 용량 등을 고려할 수 있기 때문이다.
서비스 탐색을 이용하면 사용자가 로그인을 한 후, API 서버는 서비스 탐색을 통해 적합한 채팅 서버의 주소를 리턴해주고, 사용자는 해당 서버와 웹 소켓 연결을 하게된다.
메세지 호출
채팅 서버를 선택했다면, 어떻게 메세지를 전달할 것인지를 고려해야 한다. 이 상황에서 1대1 채팅방인지와 그룹 채팅방인지에 따라서 차이가 있다.
메세지를 전달할 때엔 메세지 브로커 서비스를 주로 이용한다.
1대1 채팅
1대1 채팅에서는 가장 아키텍처가 간단하다. 사용자 A가 보낸 메세지를 사용자 B에게 전달해주면 되기 때문이다.
이를 위한 아키텍처를 한번 봐보자.
사용자가 메세지를 전송하면, 채팅 서버는 이를 받고 ID 생성기에게 메세지의 ID를 생성한다. 그리고 메세지 동기화 큐로 전달하고, 해당 메세지를 키-값 저장소에 저장한다. 전달받을 사용자 B가 접속 상태라면 채팅을, 접속하지 않았다면 푸시 알림을 전송한다.
그림은 뭔가 메세지 동기화 큐가 4, 5 작업을 하는 것 처럼 그려려져있는데, 채팅 서버에서 이를 해주거나 또 다른 서비스가 이를 처리해줘야 할 듯 싶다.
여러 단말 메세지 동기화
요구 사항에서 복수의 단말에서 채팅이 가능해야 한다는 조건이 있었다. 이를 해결하는 방법은 크게 어렵지 않다. 해당 단말에서 받은 최대 메세지 id를 가지고 있다면, 그 이후의 메세지들을 전달받으면 되기 때문이다.
그룹 채팅
그룹 채팅의 경우 앞선 1대1 채팅보다는 조금 복잡하다. 하나의 메세지를 복수의 사용자들에게 전달해줘야 하기 때문이다.
이를 해결하는 방법으로는 사용자마다 자신의 메세지 큐를 두는 방법이다.
사용자는 각각의 큐가 있는 상황에서 사용자가 A,B,C가 있는 그룹에서 A가 메세지를 보내면 B의 큐, C의 큐에 메세지가 전달되고, 이를 받는다고 보면 된다.
이런 접근을 사용하면, 사용자들은 자신의 큐만 보면 되기 때문에 새로운 메세지를 관리하기 간단하다. 이런 장점으로 인해 그룹의 사람이 많지 않다면, 하나의 메세지를 N개로 복사하는 비용이 크지 않기 때문에 좋은 해결책이 된다.
접속상태 표시
채팅을 보낼 때, 이를 채팅방으로 알릴지, 푸시 알림을 보낼지를 알기 위해선 접속상태에 대한 정보를 가지고 있어야한다.
상태 정보를 획득하는 방법으로 이 책에서는 박동 서비스(heartbeat)를 선택한다.
heartbeat 방법의 장점을 소개하기 위해 그 이전에 선택할 수 있는 방법인 로그인-로그아웃 방법에 대해서 간단히 알아보자.
로그인을 할 때에, status를 online으로 만들고 웹 소켓에 연결을 한다. 연결이 되어있는 상태라면 접속한 것을 판단을 하고, 로그아웃을 하면 status를 offline으로 변경한 후, 소켓 연결을 종료한다.
하지만 이 상황에서 인터넷 연결의 접속 장애가 발생하면, 소켓의 연결이 지속되지 못하고 잠시 끊길 수 있는데, 그 때마다 접속 상태가 변경되는 것은 사용자 경험적인 측면에서 좋지 않기 때문에 박동 시스템을 사용한다.
박동 시스템
그럼 박동 시스템은 어떤 것인가?
박동 시스템은 일정 주기마다 자신의 구독자(친구들)에게 박동을 보내는 것으로 접속 상태를 알린다. 일정 주기가 지나도 박동이 오지 않는다면 오프라인 상태로 판단하게 된다.
책의 예시에서는 박동이 5초마다 전달이 되고, 30초간 박동이 전달되지 않는다면 오프라인 상태로 변경한다.
어떻게 전달할래?
그럼 박동을 어떤 형태로 보낼지 생각해야한다. 앞서서 이미 구독자들에게 보낸다고 언급했는데, 책에서는 모든 친구마다 채널을 하나씩 두고, 이에 대해서 박동을 보내라고 한다.
사용자가 A, B, C, D가 있다면 A-B, A-C, A-D 채널을 두고 3개 모두 박동을 보내라는 말이다.
그룹의 크기가 작을 때엔 효과적인데, 그룹이 커진다면 혹은 친구가 너무 많다면 성능 문제가 발생할 수 있기 때문에, 채팅 방에 들어가는 시점 혹은 수동으로 갱신하도록 한다고 한다.
개인적으로는 구독-발행 패턴을 사용하는데 왜 채널을 3개를 두는지를 잘 모르겠다. 그냥 A 접속 채널을 만들고, B,C,D가 이를 구독하고 있으면 되는 것이 아닐까?
마무리
지금까지 1대1, 그리고 그룹 채팅을 지원하는 시스템 설계 방법에 대해서 알아보았다. 개인적으로 몇 개는 내가 놓쳤구나 생각도 들면서, 특정 부분은 잘 이해가 안간다. 추후에 실습으로 개발을 해보고 싶다.
이 채팅을 더욱 확장한다면 이미지나 영상 업로드, 종단 간 암호화 캐싱, 로딩 속도 개선, 오류 처리, 메세지 재전송등을 고려할 수 있다고 한다.