채팅 앱이라는 말은 사람마다 다르게 받아들인다. 그래서 무엇을 만들 것인지 먼저 좁히는 게 가장 중요하다.
실제로 시장의 앱들만 봐도 방향이 제각각이다.
| 앱 | 중점 |
|---|---|
| 메신저, 위챗, 왓츠앱 | 1:1 채팅 |
| 슬랙 | 업무용 그룹 채팅 |
| 디스코드 | 대규모 그룹 + 저지연 음성 |
면접관이 1:1 채팅을 생각하는데 그룹 채팅을 설계하면 곤란해진다. 최소한 1:1인지 그룹인지는 초반에 확정해야 한다.
메신저와 유사한 채팅 앱을 만든다.
클라이언트(모바일 앱 또는 웹 앱)는 서로 직접 통신하지 않는다. 모든 통신은 가운데 있는 채팅 서비스를 거친다.
채팅 서비스가 해야 할 일은 세 가지다.
일반적인 클라이언트/서버 구조에서 요청을 시작하는 건 클라이언트다. 채팅도 마찬가지로 송신자(sender)가 요청을 보낸다.
그래서 메시지를 보내는 쪽은 HTTP로 해결된다. 여기에 keep-alive 헤더를 쓰면 연결을 끊지 않고 재사용할 수 있어, TCP 핸드셰이크 횟수를 줄일 수 있다.
실제로 메신저를 포함한 여러 채팅 서비스가 초기에 HTTP를 사용했다.
HTTP는 클라이언트가 연결을 시작하는 프로토콜이다. 서버가 원하는 시점에 클라이언트로 메시지를 밀어넣는 데는 맞지 않는다.
그런데 채팅에서 수신은 정확히 그 반대 방향이다. 상대가 언제 메시지를 보낼지 모르는데, 클라이언트가 먼저 요청할 수는 없다.
이를 해결하기 위해 서버가 연결을 시작하는 것처럼 동작하게 만드는 기법들이 나왔다.
셋을 차례로 살펴본다.
클라이언트가 주기적으로 서버에 "새 메시지 있나요?"라고 물어보는 방식이다.

문제는 두 가지다.
폴링의 비효율을 줄이려고 나온 방식이다.
클라이언트가 요청을 보내면 서버는 새 메시지가 생기거나 타임아웃될 때까지 응답을 미룬다. 응답을 받은 클라이언트는 연결을 끊고 곧바로 새 요청을 보내 같은 과정을 반복한다.

그래도 세 가지 약점이 남는다.
① 송신자와 수신자가 다른 서버에 붙을 수 있다
HTTP 서버는 보통 무상태(stateless)다. 로드밸런서가 라운드 로빈으로 분배하면, 메시지를 받은 서버가 정작 그 메시지를 받을 클라이언트와 연결되어 있지 않을 수 있다.
② 연결 해제를 감지하기 어렵다
서버 입장에서 클라이언트가 연결을 끊었는지 알 마땅한 방법이 없다.
③ 여전히 비효율적이다
메시지를 거의 받지 않는 클라이언트도 타임아웃마다 계속 재접속한다.
서버가 클라이언트에게 비동기 메시지를 보낼 때 가장 널리 쓰이는 방식이다.

앞서 메시지 전송에는 HTTP도 괜찮다고 했다. 하지만 웹소켓은 양방향이므로 전송까지 함께 처리할 수 있다. 굳이 둘로 나눌 이유가 없다.
송수신에 같은 프로토콜을 쓰면 설계도 구현도 단순해진다.
다만 웹소켓 연결은 계속 유지되어야 하므로, 서버 측 연결 관리가 중요한 과제가 된다.
주 통신 프로토콜은 웹소켓으로 정했다. 하지만 모든 곳에 웹소켓을 쓸 필요는 없다. 회원가입, 로그인, 프로필 조회처럼 요청-응답으로 끝나는 기능은 일반 HTTP로 충분하다.
전체 시스템은 세 부분으로 나눌 수 있다.
로그인, 회원가입, 프로필 표시 등을 처리하는 전통적인 요청/응답 서비스다. 대부분의 웹사이트가 공통으로 갖는 기능이라 특별할 게 없다.

여기서 나중에 다시 볼 것이 서비스 탐색(service discovery) 이다. 클라이언트가 접속할 채팅 서버의 DNS 호스트명을 알려주는 역할을 한다.
상태를 유지해야 하는 건 채팅 서비스뿐이다.
클라이언트마다 채팅 서버와 독립적인 연결을 들고 있어야 하기 때문이다. 그리고 그 서버가 살아 있는 한 클라이언트는 보통 다른 서버로 옮기지 않는다.
그래서 특정 서버에 연결이 몰릴 수 있는데, 앞서 언급한 서비스 탐색이 채팅 서비스와 협력해 이 부하를 분산한다.
가장 중요한 건 푸시 알림이다. 앱이 실행 중이 아니어도 새 메시지를 알려줘야 하기 때문이다.
이론적으로는 서버 한 대로도 가능하다. 계산해보면 이렇다.
동시 접속 1M × 접속당 메모리 10KB ≈ 10GB
메모리 10GB면 100만 연결을 다 감당할 수 있다. (언어와 구현에 따라 달라지는 대략적인 수치다.)
하지만 서버 한 대짜리 설계안을 그대로 내밀면 안 된다. SPOF(단일 장애점)를 비롯해 실무에서 그렇게 하지 않는 이유가 여럿 있기 때문이다.
다만 한 대에서 출발해 점차 다듬어 나가는 접근 자체는 괜찮다. "이건 출발점일 뿐"이라는 걸 분명히 밝히면 된다.

클라이언트는 실시간 송수신을 위해 채팅 서버와 웹소켓 연결을 끊지 않고 유지한다.
| 컴포넌트 | 역할 |
|---|---|
| 채팅 서버 | 클라이언트 간 메시지 중계 |
| 접속상태 서버(presence server) | 사용자의 온라인 여부 관리 |
| API 서버 | 로그인, 회원가입, 프로필 변경 등 나머지 전부 |
| 알림 서버 | 푸시 알림 발송 |
| 키-값 저장소 | 채팅 이력 보관 |
데이터 계층에서 결정할 핵심 질문은 관계형이냐 NoSQL이냐다. 답을 내려면 데이터 유형과 읽기/쓰기 패턴을 봐야 한다.
채팅 시스템의 데이터는 두 종류다.
사용자 프로필, 설정, 친구 목록 등. 안정성이 중요하므로 관계형 DB에 보관하고, 다중화(replication)와 샤딩(sharding)으로 가용성과 확장성을 확보한다.
채팅 시스템에 고유한 데이터다. 읽기/쓰기 패턴이 독특하다.
키-값 저장소를 선택하는 이유
이어서 대윤아에게 맡기겠다
이 글을 읽고 폴링을 시작했습니다.
이웃님께 폴링 인 러브 ..!