이 글에서 얻는 것

  • 뉴스피드를 “테이블 하나”가 아니라, 쓰기(팬아웃)/읽기(머지)/캐시/카운터/정합성 문제로 분해할 수 있습니다.
  • Fan-out on write vs read의 트레이드오프를 이해하고, 팔로워 규모(celebrity)까지 고려한 하이브리드 설계를 만들 수 있습니다.
  • 운영에서 자주 터지는 것(핫 키, 백필, 카운터 정합성, 삭제/차단)을 포함해 현실적인 설계 기준을 정리할 수 있습니다.

0) 문제 정의: 어떤 피드인가

뉴스피드는 크게 두 타입으로 갈립니다.

  • 시간순(chronological): 최신 게시물이 위(설계가 비교적 단순)
  • 랭킹/추천(ranked): 점수 기반(후보 생성/랭킹/캐시 전략이 더 복잡)

이 글은 “팔로우 기반 + 최신순”을 기본으로 설명하고, 랭킹 피드로 확장될 때 어떤 점이 달라지는지까지 힌트를 남깁니다.

1) 핵심 요구사항(최소)

  • 사용자는 다른 사용자를 팔로우한다
  • 사용자는 게시물을 올린다
  • 사용자는 내 피드를 조회한다(최근 N개)
  • 피드에는 좋아요/댓글 수 같은 카운터가 보인다(약간의 지연 허용 가능)

핵심 트레이드오프:

  • 지연(latency) vs 비용(cost) vs 일관성(consistency)

2) 두 가지 큰 전략: Fan-out on write vs on read

2-1) Fan-out on write(쓰기 시 복사)

게시물이 생성될 때, 팔로워들의 타임라인에 “미리” 넣어 둡니다.

장점:

  • 읽기(피드 조회)가 빠릅니다(단순 조회/캐시 히트)

단점:

  • 쓰기 비용이 큽니다(팔로워 수만큼 기록/캐시)
  • 유명인(수백만 팔로워)이 글을 올리면 쓰기가 폭발합니다(핫 키/백로그)

2-2) Fan-out on read(읽기 시 머지)

피드를 읽을 때 팔로우한 사람들의 게시물을 모아 정렬/머지합니다.

장점:

  • 쓰기 비용이 작고, 실시간성이 좋습니다

단점:

  • 읽기 비용이 큽니다(조인/정렬/머지)
  • 캐시/프리컴퓨팅 없이는 고QPS에서 버티기 어렵습니다

3) 현실적 답: 하이브리드(팔로워 규모에 따라 분리)

대부분의 시스템은 “모두 write” 또는 “모두 read”로 끝나지 않습니다.

예:

  • 일반 유저(팔로워 적음): fan-out on write
  • 유명인(팔로워 많음): fan-out on read 또는 별도 경로(머지)

이렇게 하면:

  • 일반 유저는 읽기가 빠르고
  • 유명인은 쓰기 폭발을 피할 수 있습니다

4) 데이터 모델(개념)

4-1) 관계/게시물

  • follow(follower_id, followee_id)
  • post(post_id, author_id, created_at, ...)

4-2) 타임라인(팬아웃 결과)

팬아웃 on write를 쓰면 보통 “사용자별 타임라인”을 둡니다.

  • user_timeline(user_id, created_at, post_id, author_id, ...)

인덱스는 보통:

  • (user_id, created_at desc, post_id desc) 형태(키셋 페이징에 유리)

5) 쓰기 경로(게시물 생성)

기본 흐름:

  1. 게시물 저장
  2. 팬아웃 작업을 큐/스트림에 넣음(비동기)
  3. 워커가 팔로워 목록을 조회해 타임라인에 적재(배치/청크)

왜 비동기인가?

  • 팔로워 수가 많으면 요청이 타임아웃납니다
  • 백로그/재시도/모니터링이 가능한 구조가 필요합니다

유명인 처리:

  • 팔로워가 많은 author는 “팬아웃을 하지 않고” author의 게시물 목록만 저장
  • 읽기 시 “내 타임라인 + 유명인 게시물”을 머지

6) 읽기 경로(피드 조회): 캐시 + 백필(backfill)

가장 흔한 패턴:

  • Redis에 최근 N개를 캐시(정렬된 set/list)
  • 캐시에 부족하면 DB에서 백필(backfill)로 채움

페이징은 offset보다 “커서/키셋”이 안정적입니다.

  • (created_at, post_id)를 커서로 사용하면 중복/누락이 줄어듭니다.

7) 좋아요/댓글 카운터: 실시간보다 “확장”이 우선

카운터를 매번 DB join으로 가져오면 읽기 비용이 큽니다. 보통은:

  • 이벤트로 카운터를 집계(비동기)
  • Redis/캐시로 최신 카운터 제공
  • 최종값은 배치로 DB에 반영(또는 CQRS)

정합성:

  • 약간의 지연은 허용하는 대신, “음수/폭발” 같은 오류는 없게(멱등/중복 방지) 설계합니다.

8) 운영에서 꼭 나오는 이슈

  • 핫 키: 유명인/핫 유저의 타임라인 키가 폭주
  • 삭제/차단: 피드에서 특정 author를 숨겨야 함(차단/뮤트) → 읽기 머지 단계에서 필터링하거나, 팬아웃 데이터 정리 정책 필요
  • 백로그: 팬아웃 큐가 쌓이면 “피드가 늦게 갱신”됨 → 지연 지표/알람이 필요

9) 랭킹 피드로 확장되면 달라지는 것(힌트)

랭킹 피드는 보통:

  • 후보 생성(candidate generation)과
  • 점수 계산(ranking)을 분리하고,
  • 캐시/프리컴퓨팅으로 비용을 통제합니다.

즉, “단순 정렬” 문제에서 “추천 시스템/캐시 전략” 문제로 확장됩니다.

연습(추천)

  • 팔로워 100명/게시물 1일 1개/피드 조회 1일 10회를 가정하고, fan-out on write vs read의 비용을 대략 계산해보기
  • 유명인(팔로워 1,000,000명) 케이스에서 팬아웃을 하면 무엇이 병목이 되는지, 하이브리드로 어떻게 피할지 설계해보기
  • Redis 캐시 + DB 백필 구조에서 “중복/누락 없이” 페이징하는 커서 설계를 작성해보기