스터디 노트

Chapter 12

채팅 시스템 설계

웹소켓 기반 실시간 양방향 통신이 핵심. 폴링·롱폴링·웹소켓 비교, 채팅 이력용 키-값 저장소, 메시지 흐름·동기화, 접속상태 표시.

이 챕터의 요지

채팅 시스템의 핵심은 실시간 양방향 통신과 메시지 보관이다

채팅 시스템은 클라이언트끼리 직접 통신하지 않고 채팅 서비스를 거친다. 서버가 임의 시점에 클라이언트로 메시지를 밀어 넣어야 하므로 클라이언트와 서버 사이의 주 통신 프로토콜로 웹소켓을 쓴다. 채팅 이력처럼 양이 많고 접근이 잦은 데이터는 키-값 저장소에 보관하고, 메시지는 메시지 동기화 큐와 접속상태에 따라 수신자에게 전달하거나 푸시 알림으로 처리한다.

12장 스터디 구성

12장은 페이스북 메신저와 유사한 채팅 앱을 시스템 설계 면접의 4단계 접근법에 따라 설계한다.

1

1단계: 문제 이해 및 설계 범위 확정

  1. 어떤 채팅 앱인가 (1대1 / 그룹)
  2. 지원 기능과 그룹 인원 제한
  3. 트래픽 규모 (DAU)
  4. 메시지 타입·암호화·이력 보관
2

2단계: 개략적 설계안 제시 및 동의 구하기

  1. 통신 프로토콜: 폴링 / 롱 폴링 / 웹소켓
  2. 무상태·상태 유지·제3자 서비스
  3. 규모 확장성과 주요 컴포넌트
  4. 저장소 선택
  5. 데이터 모델과 메시지 ID
3

3단계: 상세 설계

  1. 서비스 탐색
  2. 1대1 메시지 흐름
  3. 여러 단말 사이의 메시지 동기화
  4. 소규모 그룹 채팅 메시지 흐름
  5. 접속상태 표시
4

4단계: 마무리

  1. 미디어 지원
  2. 종단 간 암호화
  3. 캐시와 로딩 속도
  4. 오류 처리

1단계: 문제 이해 및 설계 범위 확정

어떤 채팅 앱을 설계하려는지 확실히 해 두는 것이 가장 중요하다. 페이스북 메신저·위챗·왓츠앱처럼 1대1에 집중하는 앱, 슬랙 같은 그룹 채팅 업무용 앱, 디스코드처럼 대규모 그룹·저지연 음성에 집중하는 앱이 모두 다르다. 적어도 1대1 채팅 앱인지 그룹 채팅 앱인지는 확정해야 한다.

면접에서 확정할 질문

  • 1대1 전용인가, 그룹 채팅도 지원하는가?
  • 모바일 앱인가, 웹 앱인가, 둘 다인가?
  • 처리해야 하는 트래픽 규모는? (DAU 기준)
  • 그룹 채팅의 최대 인원 수 제한은?
  • 지원 메시지 타입은? 텍스트만? 사진·동영상도?
  • 메시지 크기 제한, 종단 간 암호화 필요 여부, 이력 보관 기간은?

설계 대상의 기능

이 장에서는 페이스북 메신저와 유사한 채팅 앱을 설계하며, 다음 기능을 가진다.

응답 지연이 낮은 1대1 채팅
최대 100명까지 참여하는 그룹 채팅
사용자 접속상태 표시
여러 단말 지원 (한 계정 다중 단말 동시 접속)
푸시 알림
5천만 DAU 처리

2단계: 개략적 설계안 제시 및 동의 구하기

클라이언트는 서로 직접 통신하지 않고, 모든 기능을 지원하는 채팅 서비스와 통신한다. 채팅 서비스는 메시지 수신, 수신자 결정·전달, 미접속 수신자를 위한 메시지 보관을 담당한다.

메시지를 보내는 쪽은 오랫동안 검증된 HTTP로 충분하다. 문제는 받는 쪽이다. HTTP는 클라이언트가 연결을 만드는 프로토콜이라, 서버가 임의 시점에 클라이언트로 메시지를 밀어 넣기 어렵다. 이를 풀기 위한 세 가지 기법을 비교한다.

통신 프로토콜 — 폴링 vs 롱 폴링 vs 웹소켓

기법방식특징
폴링 (Polling)클라이언트가 주기적으로 '새 메시지 있나요?'를 물음구현 단순. 주기 짧으면 비용↑, 길면 지연↑. 답할 메시지 없으면 자원 낭비.
롱 폴링 (Long Polling)새 메시지가 오거나 타임아웃까지 연결을 유지송·수신자가 다른 서버에 붙을 수 있음. 연결 해제 감지 어려움. 여전히 비효율.
웹소켓 (WebSocket)HTTP에서 업그레이드된 양방향 영구 연결서버→클라이언트 푸시 가능. 송·수신 경로를 한 프로토콜로 통일. 80/443 포트.

서버가 클라이언트로 비동기 메시지를 보내야 하므로, 주 통신 프로토콜로 웹소켓을 채택한다. 단, 로그인·회원가입· 프로파일 변경 같은 기능은 굳이 웹소켓이 필요 없고 일반 HTTP로 구현해도 된다.

세 부류의 서비스

무상태 서비스

로그인·회원가입·프로파일 표시 같은 요청/응답 서비스. 로드 밸런서 뒤에 둔다. 그중 서비스 탐색이 클라이언트에게 접속할 채팅 서버를 알려 준다.

상태 유지 서비스

상태를 유지하는 것은 채팅 서비스뿐이다. 각 클라이언트가 채팅 서버와 웹소켓 연결을 지속 유지하므로, 연결이 살아 있는 한 다른 서버로 옮기지 않는다.

제3자 연동

가장 중요한 연동은 푸시 알림(10장)이다. 앱이 실행 중이지 않아도 새 메시지 도착을 알릴 수 있어야 한다.

규모 확장성과 주요 컴포넌트

접속당 약 10KB 메모리로 추정하면 5천만 동시 연결도 이론상 한 대로 감당 가능하지만, SPOF 등의 이유로 서버 한 대 설계는 출발점일 뿐이다. 다듬은 설계의 주요 컴포넌트는 다음과 같으며, 클라이언트는 실시간 송수신을 위해 채팅 서버와 웹소켓 연결을 유지한다.

채팅 서버

클라이언트 사이에 메시지를 중계한다. 웹소켓 연결을 유지하는 상태 유지 서비스다.

접속상태 서버

사용자의 접속 여부(online/offline)를 관리한다.

API 서버

로그인, 회원가입, 프로파일 변경 등 나머지 전부를 처리한다.

알림 서버

푸시 알림을 보낸다 (10장과 연동).

키-값 저장소

채팅 이력을 보관한다. 접속한 사용자는 이전 이력을 모두 볼 수 있다.

저장소 — 채팅 이력엔 키-값 저장소

데이터는 크게 두 종류다.

  • 일반 데이터(프로파일·설정·친구 목록) — 안정성이 보장되는 관계형 DB에 두고 다중화·샤딩한다.
  • 채팅 이력 데이터 — 양이 막대하다(메신저·왓츠앱은 하루 600억 건). 주로 최근 메시지만 읽지만 검색·점프 같은 무작위 접근도 필요하고, 1대1 채팅의 읽기:쓰기 비율은 약 1:1이다.

채팅 이력에 키-값 저장소를 쓰는 이유:

  • 데이터 접근 지연이 낮다.
  • 관계형 DB는 데이터의 롱테일을 잘 처리하지 못한다. 인덱스가 커지면 무작위 접근 비용이 급증한다.
  • 검증된 채팅 시스템이 이미 채택했다 — 페이스북 메신저는 HBase, 디스코드는 Cassandra.
  • 수평적 규모 확장이 쉽다.
용어: 롱테일(long tail)

데이터 접근 빈도를 그래프로 그리면, 소수의 인기 데이터에 접근이 몰리고(머리, head) 나머지 방대한 양의 데이터는 가끔씩만 접근되는(꼬리, tail) 모양이 된다. 이 길게 늘어진 꼬리 부분이 롱테일이다. 채팅에서는 최근 메시지(머리)는 자주 읽히지만, 오래된 메시지(꼬리)는 가끔 검색·점프로만 접근된다. 관계형 DB는 이 꼬리 데이터까지 인덱스가 비대해지면 무작위 접근 비용이 급격히 늘어, 막대한 채팅 이력을 다루기에 불리하다.

데이터 모델

1대1 채팅 메시지 테이블
message_id유일 식별자(PK), 순서 결정
message_from송신자 ID
message_to수신자 ID
content메시지 본문
created_at생성 시각
그룹 채팅 메시지 테이블
channel_id파티션 키 (질의는 채널 단위)
message_id유일 식별자
user_id보낸 사용자 ID
content메시지 본문
created_at생성 시각

그룹 채팅에서는 channel_id를 파티션 키로 써 (channel_id, message_id)를 기본 키로 삼는다. 그룹의 모든 질의가 채널 단위로 일어나기 때문이다.

메시지 ID

message_id유일성 시간 순 정렬 가능성(나중 메시지가 더 큰 ID)을 만족해야 한다. 만드는 방법:

  • RDBMS의 auto_increment — 쉽지만 NoSQL KV 저장소는 보통 미제공.
  • 전역 64비트 ID 생성기 — 트위터 Snowflake(7장).
  • 지역적 순서 번호 생성기 — 유일성을 같은 채널 안에서만 보장하면 충분하다는 점에 착안. 구현이 더 쉽다.
복습: 스노우플레이크 (7장)

트위터 스노우플레이크(Snowflake)는 분산 환경에서 64비트 정수 ID를 만드는 방식이다. 64비트를 타임스탬프 + 데이터센터 ID + 머신 ID + 일련번호로 나눠 채워, 여러 서버가 중앙 조율 없이도 ID를 발급할 수 있다. 맨 앞에 타임스탬프가 오므로 나중에 만든 ID가 항상 더 크다 — 즉 시간 순 정렬이 가능해, 메시지 ID가 요구하는 ‘유일성 + 정렬 가능성’을 동시에 만족한다.

3단계: 상세 설계

더 깊이 볼 컴포넌트는 서비스 탐색, 메시지 흐름, 접속상태 관리다.

서비스 탐색

클라이언트에게 가장 적합한 채팅 서버를 추천하는 역할. 기준은 클라이언트의 지리적 위치, 서버 용량 등이다. 널리 쓰이는 솔루션은 아파치 주키퍼(Zookeeper)로, 가용 채팅 서버를 모두 등록해 두고 최적 서버를 골라 준다.

① 사용자 A 로그인 시도
② 로드 밸런서 → API 서버
③ 인증 후 서비스 탐색이 최적 채팅 서버 선택 (예: 채팅 서버 2)
④ 사용자 A가 웹소켓으로 채팅 서버 2에 접속

1대1 메시지 흐름

내가 메시지를 보내면 어떤 컴포넌트를 거쳐 상대에게 도착하는지 아래에서 직접 단계별로 따라가 볼 수 있다. 수신자 접속 여부에 따라 흐름이 갈린다.

💬 메시지 보내기 — 1대1 흐름 따라가기

“메시지 보내기”를 누르면 내가 보낸 메시지가 어떤 컴포넌트를 거쳐 상대에게 도착하는지 단계별로 보여 준다.

사용자 A
송신
채팅 서버 1
ID 생성기
message_id
메시지 동기화 큐
키-값 저장소
채팅 이력
채팅 서버 2
사용자 B
수신
아직 보내기 전이다. 아래 버튼을 눌러 시작하라.

여러 단말 사이의 메시지 동기화

한 사용자가 휴대폰·노트북 등 여러 단말을 동시에 쓰면 모든 단말이 같은 메시지를 봐야 한다. 각 단말은 cur_max_message_id(그 단말이 가진 가장 최신 메시지 ID)를 유지한다. 새 메시지로 간주되는 조건:

  • 수신자 ID가 현재 로그인한 사용자 ID와 같고,
  • KV 저장소의 message_id가 그 단말의 cur_max_message_id보다 큰 메시지.

각 단말은 자신의 cur_max_message_id보다 새로운 메시지를 KV 저장소에서 가져와 동기화한다. 덕분에 단말마다 독립적으로 빠짐없이 최신 메시지를 받는다.

🔄 여러 단말 동기화 — 따라가기

단말마다 마지막으로 받은 메시지가 다를 때, 어떻게 모두 최신 상태로 맞춰지는지 단계별로 보여 준다.

키-값 저장소 (채팅 이력)
#101점심 뭐 먹지?
#102회의 3시로 옮겼어
#103자료 공유함
#104확인했어 👍
📱휴대폰
cur_max_message_id = 103
💻노트북
cur_max_message_id = 101
시작 — 단말마다 cur_max_message_id가 다름
휴대폰은 103까지, 노트북은 101까지 받은 상태다. KV 저장소에는 104까지 있다.

소규모 그룹 채팅에서의 메시지 흐름

소규모 그룹에서는 사용자 A의 메시지를 그룹 내 각 사용자의 메시지 동기화 큐(받은 편지함, inbox)에 복사한다.

  • 수신 로직이 단순 — 각 클라이언트는 자기 받은 편지함만 확인하면 된다.
  • 소규모에선 부담 작음 — 위챗이 이 방식을 쓰며 그룹 크기를 500명으로 제한한다.

👥 소규모 그룹 채팅 — 받은 편지함 복사 따라가기

A가 그룹에 보낸 메시지가 어떻게 각 멤버의 받은 편지함으로 복사되는지 단계별로 보여 준다.

✍️
사용자 A
송신
🙂사용자 B받은 편지함
비어 있음
😎사용자 C받은 편지함
비어 있음
🧑사용자 D받은 편지함
비어 있음
시작 — 사용자 A가 그룹에 메시지 작성
“다들 회의 3시 괜찮아?” A가 그룹 채팅에 메시지를 보낸다.

그룹이 아주 크면 모든 멤버 큐에 사본을 저장하는 비용이 커진다. 그때는 수신자가 직접 가져가는(fetch) 방식이 더 낫다.

그룹이 아주 크면 모든 사용자 큐에 사본을 저장하는 비용이 커지므로, 그땐 수신자가 가져가는(fetch) 방식이 더 낫다. 한 사용자는 여러 그룹에 속하므로, 받은 편지함은 여러 사람이 보낸 메시지를 받을 수 있어야 한다.

접속상태 표시

접속상태(presence)는 접속상태 서버가 클라이언트와 웹소켓으로 통신하며 관리한다.

사용자 로그인

웹소켓 연결이 맺어지면 상태와 last_active_at를 KV에 저장하고 online으로 표시한다.

로그아웃

로그아웃 API 호출 시 상태가 online → offline으로 바뀌고 KV에 반영된다.

접속 장애 (heartbeat)

연결은 끊겼다 살아나길 반복한다. 클라이언트가 주기적으로 박동 이벤트를 보내고, 정해진 시간 안에 받으면 online 유지, 아니면 offline. 깜박임을 막는다.

상태 정보의 전송

발행-구독(pub-sub) 모델로 친구 관계마다 채널을 두고, 상태 변화를 구독자에게 전송한다. 큰 그룹은 입장 시점·수동 새로고침 때 가져오는 방식으로 비용을 줄인다.

4단계: 마무리

실시간 통신을 위해 웹소켓을 쓰고, 채팅 서버·접속상태 서버·푸시 알림 서버·채팅 이력용 키-값 저장소·API 서버로 시스템을 구성했다. 시간이 남으면 다음 주제를 더 논의할 수 있다.

  • 미디어 지원 — 사진·동영상은 용량이 크다. 압축, 클라우드 저장소, 썸네일 생성을 함께 고려한다.
  • 종단 간 암호화 — 왓츠앱처럼 송신자와 수신자만 메시지를 읽을 수 있게 한다.
  • 캐시 — 클라이언트에 이미 읽은 메시지를 캐시해 서버와 주고받는 데이터를 줄인다.
  • 로딩 속도 개선 — 사용자·채널 데이터를 지역적으로 분산한다(슬랙이 그런 네트워크를 구축).
  • 오류 처리 — 채팅 서버가 죽으면 서비스 탐색(주키퍼)이 새 서버를 배정한다. 메시지는 재시도·큐로 유실을 막는다.
채팅 시스템 설계의 핵심은 서버가 클라이언트로 메시지를 밀어 넣을 수 있는 웹소켓 기반의 실시간 통신과, 막대한 채팅 이력을 낮은 지연으로 보관·전달하는 키-값 저장소 + 메시지 동기화 큐 구조에 있다.