Chapter 2

주변 친구

  • 2.1 문제 이해 및 설계 범위 확정
  • 2.2 개략적 설계안 제시
  • 2.3 상세 설계
  • 2.4 마무리

주변 친구(Nearby Friends) 기능은 카카오톡이나 페이스북 같은 앱에서 "친구 중에 지금 근처에 누가 있어?"를 보여주는 거야. 1장의 근접성 서비스와 비슷해 보이지만, 결정적인 차이가 있어 — 사업장은 안 움직이지만, 친구는 움직이거든. 실시간으로 변하는 위치를 다뤄야 하니 아키텍처가 완전히 달라지지.

핵심 기능은 이래.

  • 앱을 열면 주변에 있는 친구 목록이 표시돼 (반경 약 8km 이내)
  • 친구의 위치는 실시간으로 갱신돼 (30초마다 위치 업데이트)
  • 친구 목록에는 마지막 갱신 시각과 거리가 표시돼

비기능 요구사항은 낮은 지연시간이 핵심이야. 친구가 10분 전 위치에 찍혀 있으면 의미 없잖아. 또한 안정적인 실시간 통신이 필요하고, 일정 수준의 위치 오차는 허용 가능하지.

규모를 가늠해 보면 — 활성 사용자가 1억 명이고, 그 중 10%가 주변 친구 기능을 동시에 사용한다면 동시 접속자는 1,000만 명. 각 사용자가 30초마다 위치를 보내면 초당 약 33만 건의 위치 업데이트가 발생해.

1장에서는 사업장 위치가 고정이라 클라이언트가 HTTP로 요청 보내고 응답 받으면 끝이었어. 하지만 주변 친구는 서버가 클라이언트에게 먼저 데이터를 보내야 하는 상황이 생겨. 친구의 위치가 바뀌면 내 화면에도 반영돼야 하니까. HTTP의 요청-응답 모델로는 이걸 효율적으로 할 수 없지.

WebSocket은 클라이언트와 서버 간에 양방향 실시간 통신 채널을 열어둬. 한번 연결되면 서버가 원할 때 클라이언트에게 메시지를 보낼 수 있고, 클라이언트도 언제든 서버에게 보낼 수 있어.

주변 친구 시스템에서 WebSocket의 역할은 두 가지야.

  1. 클라이언트가 30초마다 자신의 위치를 서버로 전송
  2. 서버가 친구의 위치 변화를 클라이언트로 전송

WebSocket 서버 — 모든 클라이언트와의 실시간 연결을 관리해. 위치 업데이트를 받고, 친구에게 전달하는 허브 역할이지.

위치 캐시(Location Cache) — 각 사용자의 최신 위치를 Redis 같은 인메모리 저장소에 보관해. TTL을 설정해서 일정 시간 업데이트가 없으면 자동 만료되지.

위치 이력 DB — 위치 변화 이력을 저장해. 실시간 조회에는 쓰이지 않고, 분석이나 디버깅 용도야.

Redis Pub/Sub — 핵심 컴포넌트야. 사용자 간 위치 업데이트를 전달하는 메시지 채널 역할을 하지.

WebSocket 서버가 여러 대 있을 때 문제가 생겨. 내가 서버 A에 연결돼 있고, 친구가 서버 B에 연결돼 있다면? 친구가 서버 B에 위치를 업데이트해도 서버 A는 그걸 모르잖아.

이걸 해결하려면 서버 간에 메시지를 전달하는 메커니즘이 필요해. 여기서 Redis Pub/Sub이 등장하지.

각 사용자마다 Redis 채널을 하나 만들어. 예를 들어 사용자 A의 채널은 location:user_A. A의 모든 친구는 이 채널을 **구독(subscribe)**해.

A가 위치를 업데이트하면:

  1. WebSocket 서버가 A의 새 위치를 받는다
  2. A의 Redis 채널(location:user_A)에 **발행(publish)**한다
  3. A의 친구들이 연결된 WebSocket 서버들이 이 채널을 구독하고 있으므로 메시지를 받는다
  4. 각 서버가 자신에게 연결된 친구 클라이언트에게 WebSocket으로 전달한다

활성 사용자가 1,000만 명이면 채널도 1,000만 개야. Redis Pub/Sub은 채널이 가벼워 — 구독자가 없는 채널은 메모리를 거의 안 쓰거든. 메시지는 메모리에 보관하지 않고 즉시 전달(fire-and-forget)하기 때문에 메모리 부담이 작아.

다만 구독자 수에 비례해서 메시지 전달 부하가 커지니, 친구가 수천 명인 경우에는 팬아웃(fan-out) 비용을 주의해야 해.

메모리 관점에서 계산해 보면 — 각 채널의 구독 관계를 저장하는 데 약간의 메모리가 필요해. 1,000만 사용자 x 평균 친구 400명이면 40억 개의 구독 관계. 각 구독에 약 20바이트라 하면 약 100GB. Redis 서버 한 대가 감당 못 하니까 Redis 클러스터로 샤딩해야 하지.

위치 업데이트 흐름을 상세히 보면:

  1. 모바일 클라이언트가 WebSocket으로 자신의 위치(위도, 경도, 타임스탬프)를 전송
  2. WebSocket 서버가 위치 캐시(Redis)의 해당 사용자 키를 갱신
  3. WebSocket 서버가 위치 이력 DB에 비동기로 기록 (카프카 같은 메시지 큐를 거쳐서)
  4. WebSocket 서버가 해당 사용자의 Redis Pub/Sub 채널에 새 위치를 발행
  5. 이 채널을 구독 중인 다른 WebSocket 서버들이 메시지를 수신
  6. 수신한 서버는 해당 친구가 정말 "주변"인지 거리를 계산
  7. 주변이 맞으면 WebSocket으로 클라이언트에게 전달, 아니면 무시

6번이 중요해 — 모든 위치 업데이트를 클라이언트에 보내는 게 아니라, 서버에서 필터링하거든. 안 그러면 모바일 클라이언트의 데이터 사용량과 배터리 소모가 심해지지.

새 친구가 추가되면 해당 친구의 Redis Pub/Sub 채널을 구독해. 삭제되면 구독을 해제하고. WebSocket 연결이 끊기면(앱 종료 등) 모든 구독을 해제하고, 위치 캐시의 TTL이 만료되면 "주변 친구" 목록에서도 자연스럽게 사라지지.

WebSocket 서버는 상태를 가진(stateful) 서버야. 각 서버가 어떤 사용자와 연결돼 있는지 기억하고 있거든. 이 때문에 서버를 마음대로 내렸다 올리기가 어려운데, **우아한 종료(graceful shutdown)**가 중요해. 서버를 내리기 전에 연결된 클라이언트들에게 다른 서버로 재접속하라고 알려주는 절차가 필요하지.

주변 친구 시스템은 "실시간"이라는 한 가지 요구사항 때문에 아키텍처가 확 복잡해지는 좋은 예시야. HTTP 대신 WebSocket, 서버 간 메시지 전달을 위한 Pub/Sub, 상태를 가진 서버의 관리까지.


정리

2장 읽고 기억할 거 세 가지:

  1. 실시간 위치 공유에는 WebSocket이 필수야. 서버가 클라이언트에게 먼저 데이터를 보내야 하는 구조이므로 HTTP의 요청-응답 모델로는 부족하거든.
  2. Redis Pub/Sub은 사용자별 채널로 위치 업데이트를 팬아웃해. 사용자마다 채널을 만들고, 친구들이 구독하는 구조지. 채널이 가벼워서 수천만 개도 가능하고.
  3. WebSocket 서버는 상태를 가진다는 점이 운영의 핵심 고려사항이야. 스케일링, 배포, 장애 복구 모두 이 상태 관리를 중심으로 설계해야 하지.