가상 면접 사례로 배우는 대규모 시스템 설계 기초 11장: 뉴스 피드 시스템 설계

송현진·6일 전
0

System Design

목록 보기
8/11

문제 정의

사용자가 팔로우하는 사람들의 게시물, 행동(좋아요, 리쉐어 등)을 최신성 + 개인화 기준으로 정렬해 무한 스크롤 피드로 제공한다. 대용량 트래픽에서도 낮은 지연과 높은 가용성을 유지해야 한다.

요구사항 정리

기능

  • 게시물 작성/삭제(텍스트·이미지·동영상)
  • 팔로우/언팔로우
  • 내 뉴스피드 조회(무한 스크롤, 커서 기반 페이지네이션)
  • 좋아요/댓글 수 등 메타데이터 반영
  • 순서: 최신성 + 랭킹(친밀도/참여도 등)

비기능

  • 낮은 P95 지연(피드 첫 화면 < 200~300ms)
  • 수평 확장성(수천만 DAU 가정)
  • 내구성(게시물 영속), 캐시 일관성(최소화된 지연 허용)
  • 멱등성, 장애 격리, 백프레셔

핵심 설계 결정

A. Fan-out 전략

  • Fan-out on write(푸시): 글 작성 시 팔로워들의 타임라인에 미리 복제
    • 장점: 읽기 빠름(핫패스)
    • 단점: 유명인(팔로워 수 ↑) 글 1건이 거대한 쓰기 폭발
  • Fan-out on read(풀): 조회 시 팔로우 목록을 모아 최신 글을 집계
    • 장점: 쓰기 비용 낮음, 유명인 이슈 완화
    • 단점: 읽기 비용 ↑, 조인/머지 비용 큼
  • 하이브리드(권장): 평범한 사용자는 푸시, 팔로워가 임계치(X만) 이상이면 풀로 전환(“셀럽 예외”)

B. 순서/ID

  • 단조증가 ID(Snowflake)로 전역 정렬 + 샤딩 친화
  • 정렬 키: (score, createdAt, id) 형태의 스코어 + 시간 기반 커서

C. 저장소 선택

  • Write Path: 게시물 메타는 RDB/NoSQL, 미디어는 객체스토리지(S3) + CDN
  • Timeline: 읽기 최적화 NoSQL(Cassandra 등) 또는 Redis List/Sorted Set(핫 구간 캐시)

API 설계

POST /api/v1/posts
GET  /api/v1/feed?limit=30&cursor=<opaque>
POST /api/v1/follows   { followerId, followeeId }
DELETE /api/v1/follows/{id}
POST /api/v1/posts/{id}/like

커서 기반 페이지네이션: cursor(lastScore,lastId)를 인코딩한 opaque 토큰

데이터 모델

-- 게시물
Post(id PK, author_id, text, media_urls[], created_at, visibility, like_count, comment_count, ...)

-- 팔로우 그래프
Follow(follower_id, followee_id, created_at, PRIMARY KEY(follower_id, followee_id))

-- 타임라인(푸시형 복제 저장)
Timeline(user_id, post_id, score, created_at,
         PRIMARY KEY(user_id, score DESC, post_id DESC))
-- score = f(recency, affinity, engagement)

하이브리드 시 셀럽 글은 Timeline에 복제하지 않고 Read 시점 합산한다.

아키텍처 개요

[Client]
   |
[API Gateway/Auth]
   |
+--+-------------------------------+
|          Backend Services        |
|  PostSvc  FollowSvc  FeedSvc     |
|        |            |            |
|        v            |            |
|     [Kafka/PostTopic]            |
|        |            |            |
|   FanoutWorkers  (rank/affinity)|
|        |                         |
|   [Timeline Store]  <---- [Social Graph DB]
|        |                         |
|    [Redis Cache]  <---- [Feature Store / CTR]
+--------|-------------------------+
         v
       [CDN/S3 for media]
  • Write Path: PostSvc → Kafka(PostTopic) → FanoutWorkers
    • 일반 사용자: follower_id 파티셔닝으로 타임라인에 미리 적재(푸시)
    • 셀럽: 복제 생략, 메타만 저장
  • Read Path: FeedSvc
    • Redis 캐시 히트 시 즉시 반환
    • 미스 시 TimelineStore + (필요하면) 셀럽 Pull 병합 → 랭킹 → 캐시 적재

랭킹(간단 신호 & 온라인/오프라인 분리)

  • 신호: 최신성(가중치 높은 감쇠), 친밀도(팔로우 상호작용), 참여도(좋아요/댓글/체류), 콘텐츠 품질(블락/리포트 반영)
  • 온라인 재랭킹(FeedSvc): 빠른 점수 계산(선형 결합 + 감쇠), 피처는 Feature Store/캐시에서 서빙
  • 오프라인 모델: CTR/Engagement 예측(배치·스트리밍 학습), AB 테스트로 적용 폭 조절

캐시 전략

  • Per-user feed cache: feed:{userId} → 상위 N개(100~300) ID 리스트
  • TTL + soft refresh: 배경 재빌드(Write/Follow 변경 시 invalidation 이벤트)
  • 항목 캐시: post:{id}(메타), user:{id}(프로필/친밀도 피처)
  • 일관성: 최종적 일관성 허용, 중요 신호(삭제/블락)는 즉시 무효화

큐·비동기·멱등성

  • 토픽: PostTopic(새 글), FanoutTopic(복제 작업), SignalTopic(좋아요/댓글)
  • 파티셔닝: (followerId % P)로 타임라인 순서 유지
  • 멱등 키: timeline:{userId}:{postId} 중복 삽입 방지
  • DLQ & 재시도: 지수 백오프, 포이즌 메시지 격리

페이지네이션(커서)

  • 커서 = base64(score:lastId)
  • 조회 쿼리: “WHERE (score,id) < (cursorScore,cursorId) ORDER BY score DESC, id DESC LIMIT K”
  • 역방향 스크롤·신규 도착: 상단에 신규가 생기면 새로고침 배지로 합리화

셀럽 이슈(Write 폭발 완화)

  • followers_of(author) > CELEB_THRESHOLD이면 풀 전략:
    • 조회 시 recent posts by celeb을 한 번만 읽어 모든 팔로워 피드에 병합
  • 추가 완화: 셀럽 전용 파티션/캐시, 상위 M개만 병합

장애·성능·운영

  • 백프레셔: Fanout 지연 시 우선순위 큐, 셀럽 글 지연 허용(읽기 병합으로 보완)
  • Circuit Breaker: 캐시/NoSQL 장애 시 디그레이드(Top-k 최신만, 랭킹 생략)
  • 관측: p50/p95 지연, 캐시 히트율, fanout 지연, drop률, 피드 품질 KPI(세션 길이/CTR)
  • 보안/프라이버시: 공개 범위, 차단/리포트 전파, 삭제/비공개 즉시 반영

📝 배운점

뉴스피드 시스템 설계는 단순히 “게시물을 모아 보여주는 것”이 아니라 읽기와 쓰기의 균형을 어떻게 잡을지라는 근본적인 문제를 다루는 작업이었다. 처음에는 단순히 최신 글을 모아서 보여주면 될 것 같았지만 실제로는 fan-out on writefan-out on read 각각의 방식이 가진 장단점을 고려해야 했고 특히 팔로워 수가 많은 셀럽 계정을 처리할 때 쓰기 부하가 폭발적으로 늘어난다는 점에서 현실적인 해법은 결국 하이브리드 전략임을 깨달을 수 있었다.

또한 단순한 시간순 정렬만으로는 좋은 사용자 경험을 줄 수 없다는 것도 중요한 배움이었다. 최신성과 개인화 요인을 동시에 고려해 (score, createdAt, id)로 정렬하고 온라인에서는 빠른 점수 계산으로 재랭킹을 적용하며 오프라인에서는 CTR이나 참여도를 학습해 품질을 높이는 이원화된 구조는 실시간성과 추천 품질을 동시에 만족시키는 설계 패턴으로 이해할 수 있었다.

운영적인 측면에서도 많은 고민거리가 있었다. 모든 캐시에 강한 일관성을 보장하려 하기보다는 삭제나 차단 같은 민감한 이벤트만 즉시 반영하고 나머지는 최종적 일관성을 허용하는 것이 성능과 사용자 경험을 균형 있게 맞추는 방법이었다. 여기에 큐 기반 아키텍처에서 멱등 키를 사용하고 DLQ·재시도로 장애를 흡수하는 방식은 대규모 분산 시스템에서 안정성을 확보하는 기본기라는 점도 새삼 느꼈다.

결국 뉴스피드 시스템은 데이터 모델링, 랭킹 알고리즘, 캐시 전략, 장애 대응까지 아우르는 종합 설계 과제였고 모든 의사결정의 기준은 “대규모 트래픽 환경에서 비용을 어떻게 통제할 것인가”였다. 동시에 단순히 기술적 효율만이 아니라 사용자가 체감하는 품질이 최종 지표가 되어야 한다는 점을 다시금 확인할 수 있었다.

profile
개발자가 되고 싶은 취준생

0개의 댓글