[대규모 시스템 설계 스터디] 12장 정리

김연준·2026년 8월 17일
post-thumbnail

채팅 시스템 설계


1. 요구사항 확인

설계에 앞서 다음 사항을 먼저 확인해야 함.

  • 어떤 앱을 설계해야 하는가

    • 1:1 채팅 앱
    • 그룹 채팅 앱
  • 처리해야 하는 트래픽 규모는 어느 정도인가(DAU)

  • 그룹 채팅의 참여 인원 제한은 몇 명인가

  • 어떤 기능이 필요한가

    • 접속 상태 표시
    • 첨부파일 지원 여부
    • 채팅방 유형
  • 메시지 길이 제한은 어떻게 되는가

  • 종단 간 암호화를 지원하는가

  • 채팅 이력을 얼마나 보관하는가

  • 어떤 통신 프로토콜을 사용할 것인가


2. 채팅 서비스의 역할

클라이언트끼리 직접 통신하는 것이 아니라, 위의 기능을 모두 지원하는 채팅 서비스를 거쳐 통신함.

채팅 서비스는 다음 기능을 제공해야 함.

  • 클라이언트로부터 메시지 수신
  • 메시지 수신자 결정 및 전달
  • 수신자가 접속 상태가 아닌 경우 접속할 때까지 메시지 보관

메시지 송신

송신 클라이언트에서 채팅 서비스로 메시지를 보낼 때는 HTTP 프로토콜을 사용함.

이때 keep-alive 헤더를 사용하는 것이 효율적임.

  • 클라이언트와 서버 사이의 연결을 끊지 않고 유지할 수 있음
  • TCP 접속 과정에서 발생하는 핸드셰이크 횟수를 줄일 수 있음

메시지 수신

채팅 서비스가 수신 클라이언트에게 메시지를 보낼 때는 일반 HTTP를 사용할 수 없음.

HTTP는 클라이언트가 서버에 요청하는 방식이지, 서버가 클라이언트에게 먼저 보내는 방식이 아니기 때문임.

이를 해결하기 위한 기법으로 폴링, 롱 폴링, 웹소켓이 있음.

폴링, 롱 폴링
→ HTTP 프로토콜 위에서 동작

웹소켓
→ HTTP로 연결을 시작한 뒤 다른 프로토콜로 전환

3. 폴링 · 롱 폴링 · 웹소켓

폴링

클라이언트가 주기적으로 서버에 새 메시지가 있는지 물어보는 방법임.

  • 폴링을 자주 할수록 비용이 올라감
  • 메시지가 없는 경우 서버 자원이 불필요하게 낭비됨

롱 폴링

폴링을 변형한 방법으로, 새 메시지가 반환되거나 타임아웃될 때까지 연결을 유지함.

클라이언트는 새 메시지를 받으면 기존 연결을 종료하고 새 요청을 보내어 모든 절차를 다시 시작함.

단점:

  • HTTP 서버는 보통 무상태이므로, 라운드로빈 방식으로 로드밸런싱하면 송신 클라이언트와 수신 클라이언트가 서로 다른 채팅 서버에 접속할 수 있음

    • 이 경우 송신 클라이언트가 메시지를 보내도 롱 폴링이 끝나지 않음
  • 서버 입장에서 클라이언트가 연결을 해제했는지 알아낼 좋은 방법이 없음

  • 메시지를 많이 받지 않는 클라이언트도 타임아웃마다 주기적으로 접속하므로 여전히 비효율적임

웹소켓

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

  • 연결이 한 번 맺어지면 항구적으로 유지됨

  • 80번(HTTP), 443번(HTTPS) 포트를 그대로 사용하므로 방화벽이 있는 환경에서도 잘 동작함

    • 스킴은 ws://, wss://를 사용함

세 방식 비교

폴링
→ 요청 → 없음 → 대기 → 요청 → 없음 ...
→ 빈 응답이 대부분, 자원 낭비

롱 폴링
→ 요청 → (메시지 올 때까지 대기) → 응답 → 재요청
→ 빈 응답은 줄지만 재연결 반복

웹소켓
→ 연결 1회 → 양방향 송수신 지속
→ 서버가 먼저 보낼 수 있음

4. 웹소켓의 한계

웹소켓이 모든 상황에서 우월한 것은 아니며, 다음과 같은 비용이 따름.

  • 서버가 상태를 갖게 됨

    • 연결이 특정 서버에 묶이므로 무상태 서버처럼 자유롭게 분산할 수 없음
    • 세션 고정이나 서비스 탐색 같은 별도 장치가 필요함
  • 유휴 연결도 자원을 소모함

    • 연결 하나마다 메모리와 소켓이 점유됨
    • 메시지를 전혀 주고받지 않는 사용자도 비용을 발생시킴
  • 재배포와 장애에 취약함

    • 서버를 재시작하면 그 서버에 붙어 있던 모든 연결이 끊김
    • 끊긴 클라이언트가 동시에 재연결을 시도하면 부하가 몰림
  • HTTP의 이점을 잃음

    • 캐싱, 상태 코드, 표준 미들웨어 같은 기존 인프라를 그대로 쓸 수 없음
    • 일부 프록시나 방화벽은 프로토콜 전환을 차단하므로 롱 폴링 폴백이 필요함
  • 신뢰성을 직접 구현해야 함

    • 연결이 끊긴 사이 유실된 메시지의 재전송, 순서 보장, 수신 확인을 애플리케이션이 처리해야 함
HTTP
→ 무상태 / 확장 쉬움 / 서버가 먼저 못 보냄

웹소켓
→ 상태 유지 / 확장 복잡 / 실시간 양방향

즉 실시간 양방향 통신이 필요한 구간에만 웹소켓을 쓰고, 나머지는 일반 HTTP로 처리하는 것이 적절함.


5. 개략적인 설계안

채팅 시스템은 크게 세 부분으로 나눌 수 있음.

무상태 서비스

로그인, 회원가입, 사용자 프로파일 표시 등을 처리하는 전통적인 요청/응답 서비스임.

많은 웹사이트와 앱이 보편적으로 제공하는 기능에 해당함.

상태 유지 서비스

각 클라이언트가 채팅 서버와 독립적인 네트워크 연결을 유지해야 하므로 필요함.

서버가 살아 있는 한 다른 서버로 연결을 변경하지 않음.

제3자 서비스 연동

앱이 실행 중이지 않더라도 알림을 받아야 하므로 푸시 알림 서비스와의 통합이 중요함.

컴포넌트별 역할

  • 채팅 서버

    • 클라이언트 간 메시지 중계
    • 실시간 송수신을 위해 웹소켓 연결을 유지
  • 접속상태 서버

    • 사용자의 접속 여부 관리
  • API 서버

    • 로그인, 회원가입, 프로파일 변경 등 나머지 전부 처리
  • 알림 서버

    • 푸시 알림 전송
  • 키-값 저장소

    • 채팅 이력 보관

6. 저장소 선택

RDB와 NoSQL 중 무엇을 쓸지 결정하려면 데이터의 유형과 읽기/쓰기 연산 패턴을 함께 고려해야 함.

채팅 시스템이 다루는 데이터

일반적인 데이터

사용자 프로파일, 설정, 친구 목록 등이 해당함.

  • 안정성 보장을 위해 RDB에 보관
  • 가용성과 규모 확장성을 위해 다중화와 샤딩을 사용

채팅 이력

  • 빈번하게 사용되는 것은 최근에 주고받은 메시지
  • 검색, 특정 사용자가 언급된 메시지 조회, 특정 메시지로 점프 등 무작위 접근을 지원하는 기능도 필요
  • 1:1 채팅의 경우 읽기와 쓰기 비율은 대략 1:1 정도라고 함

키-값 저장소를 선택하는 이유

  • 수평적 규모 확장이 쉬움
  • 데이터 접근 지연 시간이 낮음
  • RDB는 롱테일에 해당하는 부분을 잘 처리하지 못하는 경향이 있음
  • 실제로 페이스북 메신저와 디스코드가 키-값 저장소를 채택하고 있음

롱테일

원래는 통계 그래프의 모양을 가리키는 용어임.

  • 머리(head)

    • 소수의 항목이 압도적으로 많은 양을 차지
  • 꼬리(tail)

    • 개별 항목의 양은 적지만 항목 수 자체가 많아 합치면 무시할 수 없는 비중을 차지

채팅 시스템에서는 채팅방 하나에 쌓이는 메시지 개수의 분포를 가리킴.

대부분의 1:1 대화, 소규모 채팅방
→ 메시지 수십~수백 개

극소수의 대규모 그룹, 활성 커뮤니티
→ 메시지 수백만 개

RDB가 롱테일을 잘 다루지 못하는 이유

  • 인덱스가 커질수록 무작위 접근 비용이 증가함

    • 메시지 전체가 하나의 테이블에 쌓이면서 인덱스 규모가 계속 커짐
    • 최근 메시지가 아닌 오래된 메시지를 조회할수록 캐시에 없는 페이지를 읽어야 하므로 디스크 랜덤 I/O가 늘어남
    • 꼬리 쪽 데이터가 전체의 대부분을 차지하므로 이 비용이 무시할 수 없는 수준이 됨
  • 핫스팟 문제

    • 메시지가 몰린 소수의 채팅방은 읽기와 쓰기 요청도 함께 몰림
    • chat_id 같은 키로 샤딩하면 이 소수의 채팅방 때문에 특정 샤드에만 부하가 집중됨
    • 나머지 샤드는 한가한 상태로 남아 불균형이 발생함

7. 데이터 모델

1:1 채팅을 위한 메시지 테이블

메시지 ID를 기본 키로 사용하면서 메시지 순서를 정하는 역할도 맡김.

작성 일시는 서로 다른 메시지가 동시에 생성될 수 있어 순서를 정할 수 없는 경우가 있기 때문임.

그룹 채팅을 위한 메시지 테이블

채널 ID메시지 ID를 기본 키로 사용함.

채널은 채팅 그룹과 같은 뜻이며, 메시지 ID는 파티션 키로도 사용함.

메시지 ID

메시지 ID가 만족해야 할 조건은 다음과 같음.

  • 고유한 값이어야 함
  • 정렬 가능해야 함
  • 시간 순서와 일치해야 함

auto_increment를 쓰면 간단히 해결될 것 같지만, NoSQL에서는 보통 해당 기능을 제공하지 않음.

따라서 다음 방법을 사용함.

  • 스노플레이크 같은 전역적 순서 번호 생성기

  • 지역적 순서 번호 생성기

    • ID의 유일성을 같은 그룹 안에서만 보증하면 충분하다는 뜻

8. 서비스 탐색

클라이언트에게 가장 적합한 채팅 서버를 추천하는 역할임.

기준으로는 클라이언트의 위치, 서버의 용량 등을 사용함.

오픈소스 솔루션으로는 아파치 주키퍼가 널리 쓰임.


9. 메시지 전달 흐름

1:1 채팅 메시지 처리 흐름

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

한 사용자가 여러 단말을 사용하는 경우, 각 단말은 저마다 독립적인 웹소켓 연결을 유지함.

따라서 어떤 단말에서 접속하더라도 동일한 메시지를 빠짐없이 받아야 함.

이를 위해 각 단말은 자신이 마지막으로 확인한 메시지의 ID를 보관함.

단말이 보관하는 값
→ cur_max_message_id

새 메시지 판별 조건
→ 수신자가 나이고
→ message_id > cur_max_message_id

message_id가 시간 순서와 일치하는 값이므로, 이 조건만으로 각 단말이 놓친 메시지를 판별할 수 있음.

단말 A: cur_max_message_id = 1005
단말 B: cur_max_message_id = 1002

→ 단말 B는 1003~1005를 추가로 내려받아 동기화

즉 서버가 단말별 상태를 일일이 관리하지 않아도, 단말이 보관한 값 하나로 동기화가 성립함.

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

그룹 채팅에서는 메시지를 그룹 구성원 각각의 메시지 동기화 큐에 사본으로 넣음.

사용자 A가 메시지 전송
→ A를 제외한 구성원 각각의 큐에 사본 저장
→ 각 사용자는 자기 큐만 확인

이 방식의 장점은 다음과 같음.

  • 수신자는 자기 큐 하나만 보면 되므로 메시지 동기화 흐름이 1:1 채팅과 동일해짐
  • 여러 사용자로부터 온 메시지를 한 곳에서 순서대로 확인할 수 있음
  • 그룹 규모가 작으면 사본을 여러 벌 저장하는 비용이 크지 않음
수신자 관점
→ 큐 1개
→ 발신자가 누구든 동일하게 처리

다만 사본의 개수가 그룹 크기에 비례하므로, 인원이 매우 많은 그룹에는 적합하지 않음.

이 경우에는 사본을 만들지 않고 채널 단위로 메시지를 보관한 뒤 읽는 시점에 조회하는 방식을 사용함.


10. 접속 상태 표시

접속

사용자가 로그인하여 웹소켓 연결이 맺어지면, 접속상태 서버는 해당 사용자의 상태와 last_active_at 타임스탬프를 키-값 저장소에 보관함.

이 절차가 끝나면 접속 중으로 표시됨.

접속 장애

클라이언트가 보내는 Heartbeat Event를 사용하여 접속 상태를 판단함.

매우 짧은 시간 동안 발생한 접속 장애를 모두 반영해 상태를 변경하면 지나칠 뿐 아니라 UX에도 좋지 않기 때문임.

상태 정보의 전송

발행-구독 모델을 사용하여 접속 상태 변경을 알림.

다만 이 방법은 소규모 그룹에서만 효과적이고, 대규모 그룹에서는 비용과 시간이 많이 듦.

따라서 대규모 그룹에서는 다음과 같은 방법을 사용함.

  • 사용자가 채팅에 입장하는 순간에만 상태 정보를 읽어가게 함
  • 접속 상태를 수동으로 갱신하도록 유도함

11. 최종 정리

채팅 시스템의 전체 구조는 다음과 같음.

로그인, 회원가입, 프로파일
→ API 서버(무상태)

메시지 송수신
→ 채팅 서버(웹소켓, 상태 유지)
→ 메시지 동기화 큐
→ 키-값 저장소

접속 상태
→ 접속상태 서버 + 발행-구독

앱 미실행 시
→ 알림 서버 → 푸시 알림

핵심은 다음과 같음.

  • 서버가 먼저 보내야 하는 구간에만 웹소켓을 쓰고 나머지는 HTTP로 처리할 것
  • 웹소켓은 상태를 갖는 대신 확장이 어려워지므로 서비스 탐색 같은 장치가 필요함
  • 채팅 이력은 롱테일 분포를 가지므로 키-값 저장소가 유리함
  • message_id는 고유하면서 시간 순서와 일치해야 하며, 이 값 하나로 다중 단말 동기화가 가능함
  • 그룹 채팅은 규모에 따라 사본 저장과 채널 단위 보관 중에서 선택해야 함
profile
Live a life you will remember

0개의 댓글