AI 시대에도 살아남는 개발자의 사고법, Meta Threads 시스템 디자인으로 배우는 병목과 Trade-off
한달 이내에도 스레드 Threads 서비스 개발이 안되는 설계를 배워봅니다.
Meta의 Threads 같은 대규모 소셜 피드 서비스를 설계하는 문제를 다룹니다.
Threads는 사용자가 짧은 글을 작성하고, 다른 사용자를 팔로우하며, 홈 피드에서 실시간에 가까운 게시물을 소비하는 서비스입니다. 겉으로 보면 “글 작성 → 팔로워에게 노출 → 좋아요/댓글/리포스트” 정도의 단순한 구조처럼 보이지만, 시스템 디자인 관점에서는 피드 생성, 팔로우 그래프, 실시간성, 랭킹, 알림, 검색, 스팸 방지, 대규모 트래픽 처리까지 함께 고려해야 하는 복합 시스템입니다.
사용자는 Threads에 짧은 텍스트 글을 작성합니다. 이후 그 글은 사용자의 팔로워 홈 피드에 노출되고, 다른 사용자들은 해당 글에 좋아요를 누르거나 댓글을 달거나 리포스트할 수 있습니다. 사용자는 관심 있는 계정을 팔로우하고, 시스템은 팔로우 관계와 사용자 행동 데이터를 바탕으로 각 사용자에게 적절한 피드를 제공합니다.
이때 단순히 “게시글을 어디에 저장할까?”가 아닙니다. “팔로워가 수천만 명인 사용자가 글을 올리면 피드를 어떻게 배포할까?”, “홈 피드는 미리 만들어둘까, 요청 시점에 조합할까?”, “좋아요와 댓글 수는 실시간으로 어떻게 집계할까?”, “팔로우 관계가 바뀌면 피드에는 어떻게 반영할까?”, “스팸, 봇, 악성 콘텐츠는 어떻게 줄일까?”, “사용자가 앱을 열었을 때 수백 밀리초 안에 개인화된 피드를 보여주려면 어떤 구조가 필요할까?”가 설계 관점 포인트들 입니다.
AI가 코드를 더 빠르게 작성하는 시대가 되면서, 개발자의 가치는 단순히 API 하나를 구현하는 능력보다 “왜 이 구조가 필요한지”, “어디서 병목이 생기는지”, “어떤 작업은 동기 처리하고 어떤 작업은 비동기로 분리해야 하는지”를 판단하는 능력으로 이동하고 있습니다.
Meta Threads 같은 소셜 피드 시스템은 이 역량을 훈련하기 좋은 문제입니다. 겉으로는 글 작성, 팔로우, 피드 조회처럼 단순해 보이지만 실제로는 read-heavy feed, write-heavy engagement, compute-heavy recommendation, bandwidth-heavy media, event-heavy notification이 동시에 얽혀 있습니다.
이 글을 끝까지 읽으면 다음 질문에 답할 수 있게 됩니다.
1. Meta Threads 같은 소셜 피드 시스템의 요구사항을 어떻게 정리할까?
2. Home Feed와 For You Feed는 왜 다르게 설계해야 할까?
3. fan-out on write와 fan-out on read는 언제 선택해야 할까?
4. 팔로워가 수천만 명인 사용자가 글을 쓰면 어디서 병목이 생길까?
5. 좋아요 수가 초당 수십만 번 증가하면 counter를 어떻게 설계해야 할까?
6. 이미지와 영상은 왜 API 서버가 아니라 Object Storage와 CDN으로 분리해야 할까?
7. 빅테크 인터뷰에서 면접관의 follow-up 질문에 어떻게 답해야 할까?
Functional Requirements - 기능적 요구사항 (스레드)
사용자는 계정을 생성하고 로그인할 수 있어야 합니다.
사용자는 짧은 텍스트 기반 게시글을 작성할 수 있어야 합니다.
사용자는 게시글에 이미지나 영상을 첨부할 수 있어야 합니다.
사용자는 다른 사용자를 팔로우하거나 언팔로우할 수 있어야 합니다.
사용자는 자신이 팔로우한 사람들의 게시글을 홈 피드에서 볼 수 있어야 합니다.
사용자는 추천 기반 게시글을 For You 피드에서 볼 수 있어야 합니다.
사용자는 게시글에 좋아요를 누르거나 취소할 수 있어야 합니다.
사용자는 게시글에 댓글을 작성할 수 있어야 합니다.
사용자는 게시글을 리포스트하거나 인용할 수 있어야 합니다.
사용자는 특정 게시글, 사용자, 키워드를 검색할 수 있어야 합니다.
사용자는 좋아요, 댓글, 팔로우, 멘션, 리포스트에 대한 알림을 받을 수 있어야 합니다.
사용자는 부적절한 게시글이나 사용자를 신고할 수 있어야 합니다.
사용자는 특정 사용자를 차단하거나 뮤트할 수 있어야 합니다.
사용자는 자신의 프로필, 프로필 사진, bio, 공개 범위 설정을 수정할 수 있어야 합니다.
관리자 또는 moderation 시스템은 스팸, 악성 콘텐츠, 정책 위반 게시글을 감지하고 제한할 수 있어야 합니다.
Non-Functional Requirements - 비기능적 요구사항
Threads 같은 소셜 피드 서비스는 단순히 게시글을 저장하고 보여주는 서비스가 아닙니다. 사용자가 앱을 열었을 때 즉시 피드가 보여야 하고, 유명인이 글을 올리면 수천만 명의 피드에 반영되어야 하며, 좋아요, 댓글, 리포스트 같은 상호작용은 초당 수십만 건 이상 발생할 수 있습니다.
따라서 이 스레드 시스템의 비기능 요구사항은 확장성, 낮은 지연 시간, 높은 가용성, 대량 쓰기 처리, 대량 읽기 처리, eventual consistency, idempotency, hot key 대응, 보안, moderation, 규제 대응입니다.
시스템은 수억 명 이상의 사용자를 지원할 수 있도록 확장 가능해야 합니다.
Availability
»ConsistencyAvailability over strict consistency
엄격한 최신성보다 가용성과 낮은 지연 시간을 우선한다
Threads의 홈 피드, 좋아요 수, 댓글 수, 추천 피드는 은행 계좌 잔액처럼 강한 정합성이 필요한 데이터가 아닙니다. 사용자가 어떤 게시글을 1~2초 늦게 보거나, 좋아요 수가 잠시 다르게 보이는 것은 허용 가능합니다. 반면 앱을 열었는데 피드가 로딩되지 않는 것은 사용자 경험에 훨씬 치명적입니다.
따라서 이 시스템에서는 모든 데이터를 강한 정합성으로 맞추기보다, 사용자 경험상 즉시 필요한 데이터와 비동기 반영이 가능한 데이터를 분리합니다.
Threads 같은 서비스는 초기에는 단순한 게시글 서비스처럼 보이지만, 규모가 커지면 모든 컴포넌트가 병목이 될 수 있습니다. 게시글 저장소, 팔로우 그래프, 홈 피드, 알림, 검색, 추천, 미디어 저장소가 모두 독립적으로 확장 가능해야 합니다. 가장 큰 문제는 모든 기능의 트래픽 패턴이 다르다는 점입니다.
게시글 작성은 write-heavy에 가깝고, 홈 피드 조회는 read-heavy입니다. 팔로우 그래프는 관계 조회가 중요하고, 검색은 inverted index가 필요하며, 알림은 event-driven pipeline이 필요합니다. 이 모든 것을 하나의 데이터베이스에 넣으면 특정 기능의 트래픽 증가가 전체 시스템 장애로 이어질 수 있습니다.
그래서 각 도메인을 분리해서 설계해야 합니다.
예를 들어, 게시글은 Post Service가 관리하고, 팔로우 관계는 Follow Graph Service가 관리하며, 피드는 Feed/Timeline Service가 담당합니다. 미디어 파일은 Object Storage와 CDN에 저장하고, 검색은 Search Index로 분리합니다. 좋아요, 댓글, 리포스트 카운터는 Counter Service나 별도 집계 파이프라인으로 처리할 수 있습니다.
Threads의 홈 피드는 은행 계좌 잔액처럼 강한 정합성이 반드시 필요한 데이터가 아닙니다. 사용자가 어떤 게시글을 1~2초 늦게 보거나, 좋아요 수가 잠시 다르게 보이는 것은 큰 문제가 아닙니다. 하지만 앱을 열었는데 피드가 아예 로딩되지 않거나, 게시글을 볼 수 없는 것은 큰 문제입니다.
그래서 Threads 같은 소셜 서비스에서는 보통 Consistency보다 Availability를 우선합니다.
예를 들어, 어떤 사용자가 게시글을 작성했을 때 모든 팔로워의 피드에 즉시 100% 반영되지 않아도 됩니다. 일부 사용자는 몇 초 뒤에 볼 수 있어도 괜찮습니다. 좋아요 수나 댓글 수 역시 정확한 최신 숫자가 아니라 약간 지연된 숫자를 보여줘도 사용자 경험에는 큰 문제가 없습니다.
이를 위해 시스템은 eventual consistency를 받아들입니다. 게시글 작성 이벤트가 발생하면 Kafka 같은 메시지 큐에 이벤트를 발행하고, Feed Fanout Worker, Notification Worker, Search Indexer, Counter Aggregator 등이 비동기로 처리합니다.
이렇게 하면 하나의 기능이 느려지거나 실패해도 전체 게시글 작성 플로우가 막히지 않습니다.
홈 피드는 사용자가 앱을 열었을 때 낮은 지연 시간으로 로드되어야 합니다.
Low-latency
사용자가 앱을 열었을 때 홈 피드가 2~3초씩 늦게 뜨면 사용자는 바로 이탈합니다. 소셜 피드 서비스에서 가장 중요한 UX는 “앱을 열자마자 볼 것이 있어야 한다”는 점입니다.
문제는 홈 피드를 요청 시점에 매번 실시간으로 계산하면 너무 느리다는 것입니다. 사용자가 팔로우한 수백~수천 명의 최근 게시글을 가져오고, 차단/뮤트 여부를 확인하고, 랭킹 점수를 계산하고, 광고나 추천 게시글까지 섞는다면 요청 시간이 길어질 수밖에 없습니다.
이를 해결하기 위해 홈 피드는 보통 미리 계산하거나 캐싱합니다.
대표적인 방식은 두 가지입니다.
fan-out on write입니다. 사용자가 글을 작성하는 순간, 그 글을 팔로워들의 timeline cache에 미리 넣어둡니다. 그러면 사용자가 앱을 열 때는 이미 준비된 피드를 빠르게 읽기만 하면 됩니다.
fan-out on read입니다. 사용자가 피드를 요청하는 순간, 팔로우한 사람들의 게시글을 모아서 피드를 생성합니다. 이 방식은 쓰기 비용은 낮지만 읽기 지연 시간이 길어질 수 있습니다.
실무에서는 보통 하이브리드 방식을 사용합니다. 일반 사용자의 게시글은 fan-out on write로 빠르게 배포하고, 팔로워가 수천만 명인 유명인의 게시글은 fan-out on read 또는 별도 celebrity feed로 처리합니다.
즉, 낮은 지연 시간을 위해서는 피드를 요청 시점에 모두 계산하지 않고, 캐시와 사전 계산(precompute)을 적극적으로 활용해야 합니다.
게시글 작성 후 팔로워 피드나 작성자 프로필에 빠르게 반영되어야 합니다.
좋아요, 댓글 수, 리포스트 수와 같은 카운터는 높은 쓰기 트래픽을 처리할 수 있어야 합니다.
Write-Heavy Scaling
Threads에서는 게시글 작성보다 좋아요, 댓글, 리포스트, 조회수 같은 상호작용 이벤트가 훨씬 더 많이 발생합니다. 특히 인기 게시글 하나가 바이럴되면 짧은 시간 안에 좋아요가 수십만 개씩 증가할 수 있습니다.
이때 모든 좋아요 이벤트마다 Post 테이블의
like_count를 직접 업데이트하면 문제가 생깁니다.같은 게시글 row에 쓰기가 집중되면서 lock contention이 발생하고, 데이터베이스가 병목이 됩니다. 이를 해결하려면 쓰기 경로를 분산해야 합니다.
예를 들어 좋아요 이벤트는 먼저 Like Event Store나 Kafka에 기록하고, Counter Aggregator가 비동기로 집계합니다.
카운터는 Redis 같은 in-memory store에 shard 단위로 나눠 저장할 수 있습니다. 예를 들어, 하나의 게시글에 대한 좋아요 카운터를 하나의 key로만 관리하지 않고, 다음처럼 여러 shard로 나눌 수 있습니다.
post:123:like_count:shard:0 post:123:like_count:shard:1 post:123:like_count:shard:2 ... post:123:like_count:shard:N좋아요가 들어올 때 랜덤 shard를 증가시키고, 조회할 때 여러 shard 값을 합산합니다. 이렇게 하면 하나의 hot key에 모든 쓰기가 몰리는 문제를 줄일 수 있습니다.
좋아요, 댓글, 공유하기 등의 상호작용 이벤트를 즉시 강하게 반영하려 하지 말고, 이벤트 로그와 비동기 집계를 통해 write pressure를 분산하는 것입니다.
피드 데이터는 eventually consistent해도 되지만, 사용자가 직접 작성한 게시글은 즉시 확인 가능해야 합니다.
Eventual Consistency
Threads에서는 모든 데이터가 항상 완벽하게 최신일 필요는 없습니다.
예를 들어 좋아요 수가 105개였다가 1초 뒤 108개로 바뀌는 것은 괜찮습니다. 어떤 사용자의 피드에 새 글이 몇 초 늦게 나타나는 것도 허용 가능합니다. 하지만, 사용자가 직접 작성한 글이 자기 프로필에 바로 보이지 않으면 문제가 됩니다. 사용자는 “내 글이 사라졌나?, 잘 올라간건가?”라고 느낄 수 있습니다.
따라서 consistency 요구사항을 데이터 종류별로 나눠야 합니다.
사용자 본인의 write path는 read-your-write consistency를 제공해야 합니다. 즉, 사용자가 글을 작성하면 본인 프로필과 작성 완료 화면에서는 즉시 확인 가능해야 합니다.
반면 팔로워들의 홈 피드 반영, 검색 인덱스 반영, 추천 시스템 반영, 알림 발송은 eventual consistency로 처리할 수 있습니다.
예를 들어, 게시글 작성 시스템 흐름은 이렇게 구성할 수 있습니다.
1. 사용자가 게시글 작성 요청 2. Post Service가 Post DB에 저장 3. 사용자에게 작성 성공 응답 4. PostCreated 이벤트 발행 5. Feed Fanout Worker가 팔로워 timeline에 반영 6. Search Indexer가 검색 인덱스에 반영 7. Notification Worker가 멘션/팔로워 알림 처리이 구조에서는 게시글 저장만 성공하면 사용자에게 빠르게 응답할 수 있고, 나머지 작업은 비동기로 처리할 수 있습니다.
팔로우, 언팔로우, 게시글 작성, 좋아요 같은 액션은 중복 요청에 안전해야 합니다.
Idempotency
모바일 앱에서는 네트워크가 불안정할 수 있습니다.
사용자가 게시글 작성 버튼을 눌렀는데 응답이 늦어서 다시 누를 수도 있고, 클라이언트가 같은 요청을 자동 재시도할 수도 있습니다.
이때 서버가 같은 요청을 두 번 처리하면 문제가 생깁니다. 게시글이 두 번 생성되거나, 좋아요가 두 번 반영되거나, 팔로우 관계가 중복 저장될 수 있습니다.
그래서 해당 문제를 해결하기 위해서는 시스템 액션에는 idempotency가 필요합니다. 게시글 작성 요청에는 클라이언트가 생성한
request_id또는idempotency_key를 포함할 수 있습니다.서버는 이 키를 저장하고, 같은 키로 다시 요청이 오면 새로운 게시글을 만들지 않고 기존 결과를 반환합니다.
좋아요는 더 단순하게 설계할 수 있습니다. Like 테이블에
(user_id, post_id)를 unique key로 두면 같은 사용자가 같은 게시글에 좋아요를 여러 번 눌러도 한 번만 저장됩니다.팔로우 관계도
(follower_id, followee_id)를 unique key로 관리할 수 있습니다.Like Table - user_id - post_id - created_at Unique Key: (user_id, post_id)이렇게 하면 중복 요청, 재시도, 네트워크 장애 상황에서도 데이터가 깨지지 않습니다.
시스템은 특정 유명인이나 인플루언서의 게시글처럼 트래픽이 몰리는 hot key 문제를 견딜 수 있어야 합니다.
Hot key problem
Threads에서 가장 어려운 문제 중 하나는 hot key입니다. 이 문제는 소셜 서비스에서는 무조건적으로 필요한 부분임으로 기억해두시는 것을 권장드립니다. 일반 사용자가 글을 쓰면 수백 명에게만 영향을 주지만, 팔로워가 수천만 명인 유명인이 글을 쓰면 엄청난 트래픽이 발생합니다.
문제는 두 가지입니다.
유명인의 게시글을 모든 팔로워 timeline에 fan-out on write로 넣으려 하면 너무 많은 쓰기가 발생합니다.
바이럴 게시글의 좋아요 수, 댓글 수, 조회수가 특정 key에 집중되면서 캐시나 데이터베이스가 병목이 됩니다.
해결책은 유명인 계정을 일반 사용자와 다르게 처리하는 것입니다.
일반 사용자의 게시글은 fan-out on write로 팔로워 timeline에 미리 넣습니다. 하지만 유명인 계정은 fan-out on read로 처리합니다. 즉, 팔로워들의 timeline에 미리 다 넣지 않고, 사용자가 피드를 요청할 때 유명인 게시글을 별도로 가져와서 섞습니다.
카운터는 shard counter로 분산하고, 인기 게시글은 hot content cache에 올려서 반복 조회를 줄입니다. 댓글 목록도 첫 페이지는 캐싱하고, 이후 페이지는 pagination으로 처리합니다.
즉, hot key 문제는 모든 사용자를 동일하게 처리하지 않고, 트래픽 특성에 따라 다른 전략을 적용하는 것입니다.
시스템은 일부 서비스 장애가 발생하더라도 게시글 조회, 피드 조회, 프로필 조회 같은 기능을 가능한 한 유지해야 합니다.
Fault Tolerance
Threads는 여러 서비스로 구성됩니다.
Post Service, Feed Service, Follow Service, Media Service, Notification Service, Search Service, Recommendation Service, Moderation Service 등이 있을 것으로 예상합니다.
이 중 하나가 장애가 난다고 전체 앱이 멈춰서는 안 됩니다.
예를 들어, 추천 시스템이 느려지면 홈 피드 전체가 멈추는 대신, 팔로우 기반 피드만 보여줄 수 있어야 합니다. 알림 시스템이 장애가 나도 게시글 작성은 가능해야 합니다. 검색 인덱싱이 지연되어도 사용자는 글을 작성하고 피드를 볼 수 있어야 합니다.
Recommendation Service 장애 → 추천 게시글 제외 → 팔로우 기반 피드만 제공 Notification Service 장애 → 알림 발송 지연 → 게시글 작성은 정상 처리 Search Indexer 장애 → 검색 반영 지연 → 기존 검색 결과는 계속 제공 Media Processing 장애 → 원본 업로드는 저장 → 썸네일/트랜스코딩은 나중에 재시도이런 구조에서는 일부 기능이 느려지거나 실패해도 유지됩니다.
이미지와 영상은 빠르게 로딩되어야 하며, 글로벌 사용자에게 CDN을 통해 제공되어야 합니다.
Read Heavy Scaling / Media / Uploader & Viewer
Threads는 텍스트 중심 서비스처럼 보이지만, 실제로는 이미지와 영상이 많이 사용됩니다. 미디어는 크기가 크기 때문에 API 서버나 데이터베이스에서 직접 제공하면 안 됩니다.
사용자가 이미지를 업로드하면 Media Service는 파일을 Object Storage에 저장합니다. 이후 썸네일 생성, 압축, 포맷 변환, 영상 트랜스코딩 같은 작업은 비동기 worker가 처리합니다.
사용자가 이미지를 볼 때는 API 서버를 거치지 않고 CDN URL을 통해 직접 다운로드합니다.
Client → Upload URL 요청 → Object Storage에 직접 업로드 → MediaUploaded 이벤트 발행 → Thumbnail/Transcoding Worker 처리 → CDN을 통해 미디어 제공이렇게 하면 API 서버의 부하를 줄이고, 글로벌 사용자에게 빠르게 미디어를 제공할 수 있습니다.
이미지와 영상은 API 서버가 직접 처리하면 안 됩니다. 이유는 API 서버의 역할이 compute와 routing인데, 대용량 파일 업로드는 bandwidth와 storage workload이기 때문입니다.
API 서버로 파일을 업로드하면 다음 문제가 생깁니다.
1. 서버 bandwidth 병목
2. 대용량 request timeout
3. 모바일 네트워크 실패 시 재시도 비용 증가
4. API 서버 scale-out 비용 증가
5. DB나 application layer가 file bytes와 metadata를 함께 다루게 됨
따라서 API 서버는 signed URL만 발급하고, 클라이언트는 Object Storage로 직접 multipart upload를 수행합니다. API 서버는 metadata, 권한, 상태만 관리합니다.
검색과 추천 피드는 최신성과 개인화를 적절히 균형 있게 제공해야 합니다.
Threads의 검색과 추천 피드는 서로 다른 요구사항을 가집니다.
검색은 사용자가 입력한 키워드에 대해 관련성 높은 결과를 빠르게 보여줘야 합니다. 이를 위해 게시글이 작성되면 Search Indexer가 Elasticsearch, OpenSearch 같은 검색 엔진에 비동기로 색인합니다.
추천 피드는 더 복잡합니다. 사용자의 팔로우 관계, 좋아요 기록, 클릭, 체류 시간, 리포스트, 관심 주제, 차단/뮤트 관계 등을 기반으로 게시글을 랭킹해야 합니다. 하지만 모든 추천 점수를 요청 시점에 실시간 계산하면 너무 느립니다. 그래서 후보군 생성과 랭킹을 분리합니다.
Candidate Generation → 사용자가 관심 가질 만한 게시글 후보 수천 개 생성 Ranking → 후보 게시글에 점수 부여 Filtering → 차단, 뮤트, 신고, 정책 위반 콘텐츠 제거 Feed Assembly → 최종 피드 구성최신성도 중요하기 때문에, 너무 오래된 게시글만 추천하면 피드가 죽어 보이고, 너무 최신 글만 보여주면 개인화 품질이 떨어질 수 있습니다.
그래서 추천 피드는 보통 relevance score와 freshness score를 함께 사용하게 할 수 있습니다.
알림 시스템은 대량 이벤트를 처리하되, 중복 알림이나 과도한 알림을 줄여야 합니다.
소셜 서비스에서 알림은 engagement를 높이는 기능입니다.
하지만 너무 많은 알림은 사용자를 피곤하게 만들고, 중복 알림은 서비스 품질을 낮춥니다. 예를 들어, 인기 사용자가 글을 올릴 때 모든 팔로워에게 즉시 push notification을 보내면 notification storm이 발생할 수 있습니다.
또 한 게시글에 좋아요가 100개 달릴 때마다 알림을 보내면 사용자는 피로감을 느낍니다. 그래서 알림 시스템은 이벤트 기반으로 설계하되, rate limiting과 batching이 필요합니다.
LikeCreated 이벤트 CommentCreated 이벤트 FollowCreated 이벤트 MentionCreated 이벤트 RepostCreated 이벤트 ↓ Notification Service ↓ Deduplication ↓ Batching ↓ User Preference Check ↓ Push / In-app Notification예를 들어 “A님이 좋아요를 눌렀습니다”를 계속 보내는 대신, “A님 외 10명이 회원님의 게시글을 좋아합니다”처럼 묶어서 보낼 수 있습니다.
또한 사용자의 알림 설정을 확인해야 합니다. 사용자가 좋아요 알림을 꺼두었다면 push notification을 보내면 안 됩니다. 또한, 시스템에서는 상대방의 핸드폰이 꺼져있다면 이 알림을 어떻게 처리할 것인지도 언급을 해주면 좋습니다.
Push notification은 APNS/FCM 같은 push provider에 전달하고, provider의 TTL/priority 정책에 따라 재전송되거나 만료될 수 있습니다. 중요한 알림은 Notification DB에 in-app notification으로 저장해두고, 사용자가 앱을 다시 열었을 때 조회할 수 있게 합니다.
Scope 정리
In Scope
- 사용자 계정과 프로필
- 게시글 작성/삭제/조회
- 이미지/영상 첨부
- 팔로우/언팔로우
- Home Feed
- For You Feed
- 좋아요/댓글/리포스트
- 알림
- 검색
- 차단/뮤트/신고
- 기본 moderation pipeline
Partially In Scope
- 스팸/봇 탐지
- 개인정보 보호
- 지역별 규제 대응
- 콘텐츠 삭제 요청 처리
이번 글에서는 위 항목들을 시스템 설계 관점에서 어디에 배치해야 하는지만 다루고, ML 모델 학습 방식, 법무/정책 워크플로우, 관리자 dashboard 상세 구현은 제외합니다.
Out of Scope
- DM
- 광고 시스템
- 결제/수익화
- A/B testing platform 상세 구현
- ML 모델 학습 파이프라인 상세 구현
- 관리자용 moderation dashboard UI
지금까지의 내용을 정리해보면, 아래와 같습니다.
1. 수억 명 사용자를 위해 서비스와 저장소를 도메인별로 분리한다.
2. 홈 피드는 low latency가 중요하므로 precompute와 cache를 활용한다.
3. 피드와 카운터는 eventual consistency를 허용한다.
4. 사용자가 직접 작성한 글은 read-your-write consistency를 보장한다.
5. 좋아요, 댓글, 리포스트는 event log와 비동기 집계로 처리한다.
6. 유명인 계정과 바이럴 게시글은 hot key 전략으로 따로 처리한다.
7. 일부 서비스 장애 시에도 피드 조회와 게시글 조회는 유지한다.
8. 미디어는 object storage와 CDN으로 분리한다.
9. 검색, 추천, 알림은 비동기 파이프라인으로 확장한다.
10. 보안, privacy, moderation, 삭제 요청은 모든 조회 경로에 적용한다.
가정하는 규모
- DAU: 1억 명
- Peak QPS:
- Home Feed read: 수십만 QPS
- Post write: 수만 QPS
- Like/View events: 수십만~수백만 events/sec
- Feed first page latency:
- p95 < 500ms
- p99 < 1s
- Post creation latency:
- p95 < 300ms for original post write
- Feed freshness:
- 일반 사용자 게시글은 수 초 내 팔로워 피드에 반영
- 유명인 게시글은 fan-out on read로 피드 조회 시 merge
- Counter freshness:
- 좋아요/댓글/리포스트 수는 1~5초 지연 허용
- Media:
- 원본 업로드는 Object Storage로 직접 처리
- 썸네일/트랜스코딩은 비동기 처리
Core Entities
Threads 스레드 소셜 피드 시스템을 설계할 때는 하나의 거대한 테이블에 모든 데이터를 넣는 방식이 아니라, 사용자, 게시글, 팔로우 그래프, 피드, 상호작용, 미디어, 알림, 검색, moderation처럼 접근 패턴이 다른 도메인별로 데이터를 분리한 관점에서 바라보아야 합니다.
앞서 정리한 내용처럼 게시글 작성, 피드 조회, 좋아요/댓글/리포스트, 검색, 알림은 각각 트래픽 특성과 consistency 에 대한 요구사항이 다르기 때문입니다.
왜 RDB vs NoSQL/Cache/Search Index로 나뉘는지?
1.posts
- 원본 게시글 데이터
- consistency와 durability가 중요
- RDB 또는 distributed SQL / wide-column store 가능
- 단, like_count 같은 high-write counter는 posts에 직접 쓰지 않음
2.follows
- follower/followee 관계
- graph lookup과 fanout에 사용
- write는 비교적 단순하지만 read pattern이 중요
- RDB로 시작할 수 있지만 규모가 커지면 sharded graph store 또는 key-value 기반 adjacency list 필요
3.timelines / feed_items
- 사용자별 피드 후보
- read-heavy, low-latency
- Redis, Cassandra, DynamoDB, ScyllaDB 같은 high-throughput store 적합
4.post_counters / counter_shards
- write-heavy counter
- Redis / distributed counter store 적합
- hot key 방지를 위해 shard counter 사용
5.search_index_records
- 검색 색인 상태 추적
- 실제 검색은 OpenSearch/Elasticsearch
- DB에는 색인 상태와 재시도 metadata 저장
6.post_media
- 파일 자체가 아니라 metadata만 저장
- 실제 file bytes는 Object Storage + CDN
1. users
사용자 계정 정보를 저장하는 테이블입니다.
Threads의 모든 기능은 사용자 중심으로 동작하기 때문에 가장 기본이 되는 엔티티입니다. 게시글 작성자, 팔로워, 좋아요를 누른 사람, 댓글 작성자, 알림 수신자 모두 user_id를 기준으로 연결됩니다.
2. user_profiles
사용자의 공개 프로필 정보를 저장하는 테이블입니다.
계정 인증 정보와 프로필 정보를 분리하면, 로그인/보안 관련 데이터와 프로필 화면에서 자주 읽히는 데이터를 서로 다른 방식으로 최적화할 수 있습니다. 예를 들어 bio, 프로필 이미지, 공개 범위, 표시 이름 같은 정보가 여기에 들어갑니다.
3. posts
사용자가 작성한 원본 게시글을 저장하는 테이블입니다.
Threads의 중심 엔티티입니다. 텍스트 본문, 작성자, 생성 시간, 공개 범위, 삭제 여부 같은 정보를 관리합니다. 홈 피드, 프로필 피드, 검색, 추천, 알림, 댓글, 좋아요는 모두 결국 특정 post_id를 중심으로 연결됩니다.
4. post_media
게시글에 첨부된 이미지나 영상 정보를 저장하는 테이블입니다.
미디어 파일 자체는 데이터베이스에 저장하지 않고 Object Storage와 CDN을 통해 제공하는 것이 일반적입니다. 이 테이블은 게시글과 미디어 URL, 썸네일, 타입, 처리 상태 같은 메타데이터를 연결하는 역할을 합니다.
5. follows
사용자 간 팔로우 관계를 저장하는 테이블입니다.
홈 피드를 만들 때 가장 중요한 데이터입니다. 사용자가 누구를 팔로우하는지 알아야 팔로우 기반 피드를 생성할 수 있습니다. 또한 fan-out on write 방식에서는 게시글 작성 시 작성자의 팔로워 목록을 조회하는 데 사용됩니다.
6. blocks
차단 관계를 저장하는 테이블입니다.
차단은 단순한 UI 기능이 아니라 피드, 검색, 프로필 조회, 알림 발송 전반에 적용되어야 하는 privacy 제약입니다. A가 B를 차단했다면 B는 A의 게시글을 추천 피드나 검색 결과에서도 볼 수 없어야 합니다.
7. mutes
뮤트 관계를 저장하는 테이블입니다.
차단보다 약한 형태의 필터링입니다. 사용자가 특정 계정의 게시글을 보고 싶지 않지만 관계를 완전히 끊고 싶지 않을 때 사용됩니다. 홈 피드와 추천 피드 생성 시 이 테이블을 참고해 특정 사용자의 콘텐츠를 제외합니다.
8. likes
게시글 좋아요 정보를 저장하는 테이블입니다.
사용자가 어떤 게시글에 좋아요를 눌렀는지 기록합니다. (user_id, post_id) 조합을 unique하게 관리하면 중복 좋아요 요청을 막을 수 있고, 모바일 네트워크 재시도 상황에서도 idempotency를 보장할 수 있습니다.
9. comments
게시글 댓글 정보를 저장하는 테이블입니다.
댓글은 게시글과 별도로 쓰기 트래픽이 많고 pagination이 필요한 데이터입니다. 인기 게시글에서는 댓글 조회와 작성이 동시에 많이 발생하므로, 게시글 원본과 분리해서 확장하는 것이 좋습니다.
10. reposts
리포스트 또는 quote post 관계를 저장하는 테이블입니다.
Threads 같은 서비스에서는 사용자가 다른 사람의 게시글을 자신의 네트워크에 다시 노출할 수 있어야 합니다. 단순 리포스트와 인용 리포스트를 구분하면, 원본 게시글 확산 경로와 사용자 생성 콘텐츠를 함께 추적할 수 있습니다.
11. timelines
사용자별 홈 피드 후보를 저장하는 테이블 또는 캐시성 저장소입니다.
fan-out on write를 사용할 경우, 사용자가 앱을 열기 전에 팔로워들의 게시글을 미리 timeline에 넣어둘 수 있습니다. 이렇게 하면 홈 피드를 요청할 때 매번 팔로우 그래프를 계산하지 않아도 되므로 낮은 지연 시간을 달성할 수 있습니다.
12. feed_items
피드에 실제로 노출될 단위 데이터를 저장하는 테이블입니다.
홈 피드는 단순 게시글 목록이 아니라 팔로우 게시글, 추천 게시글, 리포스트, 광고, 랭킹 결과 등이 섞일 수 있습니다. feed_items는 특정 사용자에게 어떤 콘텐츠를 어떤 순서로 보여줄지 관리하는 추상화 계층입니다.
13. post_counters
게시글별 좋아요 수, 댓글 수, 리포스트 수, 조회수 같은 집계 값을 저장하는 테이블입니다.
이 값을 posts 테이블에 직접 계속 업데이트하면 인기 게시글에서 lock contention과 hot row 문제가 생길 수 있습니다. 따라서 카운터는 별도 저장소나 Counter Service로 분리해 비동기 집계하는 것이 좋습니다.
14. counter_shards
인기 게시글의 카운터 쓰기 부하를 분산하기 위한 shard counter 테이블입니다.
좋아요나 조회수가 특정 게시글 하나에 몰리면 하나의 key 또는 row가 병목이 됩니다. 이를 여러 shard로 나눠 증가시키고, 읽을 때 합산하면 hot key 문제를 완화할 수 있습니다.
15. notifications
사용자에게 보여줄 인앱 알림을 저장하는 테이블입니다.
좋아요, 댓글, 팔로우, 멘션, 리포스트 같은 이벤트를 사용자에게 전달하기 위해 필요합니다. 알림은 중복 제거, 묶음 처리, 사용자 설정 확인이 필요하므로 게시글/댓글 데이터와 분리해서 관리하는 것이 좋습니다.
16. notification_preferences
사용자별 알림 설정을 저장하는 테이블입니다.
모든 이벤트가 push notification으로 전달되면 사용자는 피로감을 느낄 수 있습니다. 사용자가 좋아요 알림, 댓글 알림, 팔로우 알림 등을 켜고 끌 수 있도록 별도 설정 테이블이 필요합니다.
17. idempotency_keys
중복 요청을 방지하기 위한 요청 키를 저장하는 테이블입니다.
게시글 작성, 댓글 작성, 리포스트, 팔로우 같은 요청은 모바일 네트워크 재시도 때문에 중복 처리될 수 있습니다. 클라이언트가 보낸 idempotency_key를 저장해두면 같은 요청이 다시 들어왔을 때 기존 결과를 반환할 수 있습니다.
18. search_index_records
검색 색인 상태를 추적하기 위한 테이블입니다.
실제 검색은 Elasticsearch나 OpenSearch 같은 별도 검색 엔진에서 처리하더라도, 어떤 게시글이 색인되었는지, 색인 실패가 있었는지, 재색인이 필요한지 추적하는 메타 테이블이 필요합니다.
19. reports
사용자 신고 데이터를 저장하는 테이블입니다.
부적절한 게시글, 스팸 계정, 악성 콘텐츠를 신고할 수 있어야 합니다. 신고 데이터는 moderation pipeline의 입력으로 사용되며, 반복 신고나 위험 계정 탐지에도 활용됩니다.
20. moderation_actions
관리자 또는 자동화 시스템이 수행한 제재 기록을 저장하는 테이블입니다.
게시글 숨김, 계정 제한, 검색 노출 제한, 삭제 처리 같은 moderation 결과를 기록합니다. 이 데이터는 감사 로그와 이의제기 처리에도 필요합니다.
21. user_sessions
로그인 세션과 디바이스 정보를 관리하는 테이블입니다.
사용자가 여러 기기에서 로그인할 수 있으므로 세션, refresh token, device 정보 등을 관리해야 합니다. 보안 이벤트나 비정상 로그인 탐지에도 활용될 수 있습니다.
22. devices
사용자의 모바일 디바이스와 push token을 저장하는 테이블입니다.
Push notification을 보내려면 사용자의 디바이스별 토큰이 필요합니다. 한 사용자가 여러 기기를 사용할 수 있으므로 사용자와 디바이스를 분리해서 관리하는 것이 좋습니다.
Core Entities
=============================
users
user_profiles
posts
post_media
follows
blocks
mutes
likes
comments
reposts
timelines
feed_items
post_counters
counter_shards
notifications
notification_preferences
idempotency_keys
search_index_records
reports
moderation_actions
user_sessions
devicesAPI Signatures
Threads 소셜 피드 시스템의 API 설계는 아래와 같습니다.
1. User / Profile API
2. Post API
3. Media API
4. Follow Graph API
5. Feed / Timeline API
6. Engagement API
7. Comment API
8. Notification API
9. Search API
10. Safety / Privacy API
11. Internal Async APIs / Events모든 API를 강한 정합성으로 처리하지 않는 것이며, 사용자가 직접 작성한 게시글, 좋아요, 팔로우 같은 요청은 빠르게 성공 응답을 주고, 피드 반영, 카운터 집계, 검색 색인, 알림 발송은 비동기 이벤트로 처리하는 구조가 적합합니다.
위에서 정리한 것처럼 Threads 시스템은 low latency, availability, eventual consistency, idempotency, hot key 대응이 중요하며, 기능적 요구사항을 충족시키는 설계여야 합니다.
1. User / Profile APIs
Create User
POST /v1/usersRequest Body
{
"email": "user@example.com",
"username": "user",
"display_name": "user title",
"password": "plain_password_over_tls"
}Response
{
"user_id": "user_123",
"username": "user name",
"created_at": "2026-05-01T10:00:00Z"
}사용자 계정을 생성하는 API입니다. 실제 서비스에서는 OAuth, Apple Login, Google Login 같은 인증 제공자와 연결될 수 있습니다.
실제 저장 시에는 서버에서 bcrypt/argon2 같은 password hashing을 적용합니다. 또는 OAuth/Apple/Google Login을 사용할 수 있습니다.
각 요청마다 사용자 정보는 헤더(세션 토큰 또는 JWT) 로 전달됩니다. 이는 API에서 일반적인 패턴이며, 보안을 유지하면서 사용자가 해당 작업을 수행할 인증, 인가를 받았는지 보장하는 좋은 방법입니다. 사용자 정보를 요청 본문에 넣는 것은 지양해야 합니다. 클라이언트가 이를 쉽게 조작할 수 있기 때문입니다.
Get User Profile
GET /v1/users/profile
Header JWT token - 보안을 위해 JWT 토큰을 사용하도록 합니다. JWT 토큰을 통해서 사용자 데이터를 가져올 수 있도록 합니다. 따라서 유저 정보 데이터는 요청 데이터에 포함할 필요가 없습니다. Response
{
"user_id": "user_123",
"username": "user name",
"display_name": "display name",
"bio": "Software Engineer",
"profile_image_url": "https://cdn.example.com/profile/user_123.jpg",
"follower_count": 1200,
"following_count": 300,
"is_private": false,
"viewer_relationship": {
"is_following": true,
"is_blocked": false,
"is_muted": false
}
}
프로필 화면을 보여주기 위한 API입니다. 단순히 사용자 정보만 반환하는 것이 아니라, 현재 viewer 기준의 팔로우/차단/뮤트 관계도 함께 내려주는 것을 권장 드립니다.
Update User Profile
PATCH /v1/users/profile
Header JWT token Request Body
{
"display_name": "display name",
"bio": "Building AI tools",
"profile_image_media_id": "media_123",
"is_private": false
}Response
{
"user_id": "user_123",
"updated_at": "2026-05-01T10:05:00Z"
}사용자의 프로필 정보를 수정하는 API입니다. 프로필 이미지는 직접 파일을 API 서버에 보내기보다, 별도 Media API를 통해 업로드한 뒤 media_id만 연결하는 방식을 사용합니다.
2. Post APIs
Create Post
POST /v1/posts
Authorization: Bearer JWT_TOKEN
Idempotency-Key: 7f1f9c2a-8c2b-4d4c-9f5a-123456789abcRequest Body
{
"text": "Threads system design is harder than it looks.",
"media_ids": ["media_123", "media_456"],
"visibility": "public",
"reply_to_post_id": null,
"quote_post_id": null
}Response
{
"post_id": "post_123",
"author_id": "user_123",
"text": "Threads system design is harder than it looks.",
"created_at": "2026-05-01T10:10:00Z",
"status": "created"
}게시글 작성 API입니다. 이 API는 반드시 idempotency를 지원해야 합니다. 모바일 네트워크 문제로 같은 요청이 여러 번 들어와도 게시글이 중복 생성되면 안되기 때문에 Idempotency 를 사용하도록 했습니다.
author_id는 request body에서 받지 않습니다. Gateway/Auth Middleware가 JWT를 검증한 뒤 actor_id를 내부 request context에 주입합니다.
게시글이 DB에 저장되면 서버는 사용자에게 빠르게 성공 응답을 반환하도록 하며, 그 이후 PostCreated 이벤트를 발행해서 feed fanout, search indexing, notification, moderation 작업을 비동기로 처리합니다.
Get Post
GET /v1/posts/{post_id}Response
{
"post_id": "post_123",
"author": {
"user_id": "user_123",
"username": "user name",
"display_name": "display name"
},
"text": "Threads system design is harder than it looks.",
"media": [
{
"media_id": "media_123",
"type": "image",
"url": "https://cdn.example.com/media/image.jpg"
}
],
"counters": {
"like_count": 120,
"comment_count": 18,
"repost_count": 9,
"view_count": 1000
},
"viewer_state": {
"liked": true,
"reposted": false,
"muted_author": false
},
"created_at": "2026-05-01T10:10:00Z"
}특정 게시글 상세를 조회하는 API입니다. 이때 좋아요 수, 댓글 수, 리포스트 수는 약간 지연된 값이어도 괜찮습니다. 대신 게시글 본문과 작성자는 정확히 조회되어야 합니다.
Delete Post
DELETE /v1/posts/{post_id}
Header JWT Token - 사용자의 게시물을 삭제할 수 있어야 하기 때문에 JWT 토큰을 통해 사용자 게시물인지 구분이 가능합니다. Response
{
"post_id": "post_123",
"status": "deleted",
"deleted_at": "2026-05-01T10:20:00Z"
}게시글 삭제 API입니다. 실제로는 hard delete보다 soft delete 또는 tombstone 방식이 안전합니다. 삭제 이후 PostDeleted 이벤트를 발행해서 timeline cache, search index, recommendation candidate, CDN cache에서 제거되도록 해야 합니다.
Get User Posts
GET /v1/users/posts?cursor={cursor}&limit=20
Header JWT Token Response
{
"items": [
{
"post_id": "post_123",
"text": "Threads system design is harder than it looks.",
"created_at": "2026-05-01T10:10:00Z"
}
],
"next_cursor": "cursor_abc"
}특정 사용자의 프로필 피드를 조회하는 API입니다. 사용자가 방금 작성한 글은 자기 프로필에서 즉시 보여야 하므로 read-your-write consistency를 고려해야 합니다.
3. Media APIs
Create Media Upload URL
POST /v1/media/upload-url
Header JWT TokenRequest Body
{
"file_name": "photo.jpg",
"content_type": "image/jpeg",
"file_size_bytes": 1024000
}Response
{
"media_id": "media_123",
"upload_url": "https://storage.example.com/signed-upload-url",
"expires_at": "2026-05-01T10:30:00Z"
}
이미지나 영상을 업로드하기 위한 signed URL을 발급하는 API입니다. 대용량 미디어를 API 서버로 직접 받지 않고 Object Storage에 직접 업로드하게 만드는 것이 좋습니다.
이 그림을 통해 업로드 Upload 프로세스를 알 수 있습니다. 사용자가 upload API 를 통해서 파일을 업로드 할 수 있지만 그렇게 되면, 그림의 File Service 를 통해 DB 에 바이너리 데이터로 저장을 하게 됩니다. 하지만 그렇게 되면 파일이 10mb 가 넘어갈 경우 부하가 발생하게 됩니다. 그래서 Compute 영역과 Blob Storage 로 분리해서 DB 의 성능을 유지하도록 합니다.
따라서, 100MB 이상의 파일들을 업로드 해야하고, 서버를 통해 blob storage 에 업로드 한다면 병목이 일어날 수 있기 때문에, 이 문제를 해결하기 위해서 pre-signed url 을 통해 blob storage 에 직접 업로드 할 수 있도록 합니다. Presigned URL은 특정 object key, HTTP method, 만료 시간, 일부 header 조건을 포함해 서명할 수 있습니다. Content-Type이나 Content-Length 조건을 제한할 수 있지만, 파일 내용 자체의 무결성을 검증하려면 checksum header나 별도 metadata 검증을 추가해야 합니다. 그리고, url 을 가진 누구나 업로드 할 수 있기 때문에 length 에 대한 제한과 유효시간을 두어서 만료시간 내에 업로드할 수 있도록 지정합니다. 만약 업로드하는 파일에 규제/또 다른 워크로드가 필요하다면 프록시를 통해 구현할 수 있습니다.
Multipart upload의 UploadId와 part 상태는 Object Storage가 관리하지만, 애플리케이션 레벨에서 업로드 세션, 사용자, media_id, 완료 상태를 추적하기 위해 별도 DB에 upload session metadata를 저장할 수 있습니다.
예를 들면, multipart-upload 를 사용해서 presigned url 로 직접 blob storage에 업로드 하도록 합니다. 그러면 파일의 분할된 chunk 를 독립적으로 업로드할 수 있어서 실패한다면 실패한 지점부터 다시 업로드할 수 있도록 지원할 수 있습니다. dynamodb 에서 chunk 업로드 상태에 대한 데이터를 저장해서 사용할 수 있습니다. 이러한 시스템을 통해 sessionid(uploadid, upload url) 포맷으로 chunk 에 대한 데이터를 추적할 수 있습니다.
이 부분에 대한 개념과 자세한 사항은 아래 게시물을 읽어주세요!
Complete Media Upload
POST /v1/media/{media_id}/complete
Authorization: Bearer JWT_TOKENResponse:
{
"media_id": "media_123",
"status": "processing"
}미디어 업로드가 완료되었음을 서버에 알리는 API입니다. 이후 썸네일 생성, 압축, 트랜스코딩은 비동기 worker가 처리합니다.
uploader_id는 request body에서 받지 않습니다. Media Service는 JWT에서 추출한 actor_id가 해당 media_id의 owner인지 확인합니다. 이후 Object Storage에 실제 파일이 존재하는지 확인하고, media 상태를 processing으로 변경한 뒤 MediaUploaded 이벤트를 발행합니다.
이 API는 사용자가 이미지나 영상을 Object Storage에 직접 업로드한 뒤, 서버에게 “업로드가 끝났다”고 알려주는 역할을 합니다.
처음부터 API 서버가 이미지나 영상을 직접 받지 않는 이유는 미디어 파일이 크기 때문입니다. 모든 이미지와 영상을 API 서버를 거쳐 업로드하면 서버 네트워크 bandwidth, memory, timeout 문제가 쉽게 발생합니다. 그래서 일반적으로 서버는 먼저 signed upload URL을 발급하고, 클라이언트는 그 URL을 사용해 Object Storage에 직접 파일을 업로드합니다.
그 다음 클라이언트가 POST /v1/media/{media_id}/complete를 호출하면, 서버는 해당 미디어가 실제로 storage에 존재하는지 확인하고, 미디어 상태를 uploaded 또는 processing으로 바꾼 뒤, 비동기 처리를 위한 이벤트를 발행합니다.
1. uploader_id와 media_id 권한 확인
2. Object Storage에 실제 파일이 존재하는지 확인
3. media 상태를 processing으로 변경하고 MediaUploaded 이벤트 발행Get Media
GET /v1/media/{media_id}Response
{
"media_id": "media_123",
"type": "image",
"status": "ready",
"cdn_url": "https://cdn.example.com/media/media_123.jpg",
"thumbnail_url": "https://cdn.example.com/media/media_123_thumb.jpg"
}Get Media API는 이미지나 영상 파일 자체를 내려주는 API가 아닙니다. 이 API의 역할은 미디어를 화면에 렌더링하기 위해 필요한 메타데이터와 접근 가능한 CDN URL을 반환하는 것입니다.
즉, 실제 이미지나 영상 파일은 API 서버가 직접 전달하지 않습니다. API 서버는 미디어 상태, 타입, 썸네일 URL, CDN URL, 재생 URL, 크기 정보 같은 메타데이터만 반환하고, 클라이언트는 이후 CDN에서 파일을 직접 다운로드합니다.
이렇게 설계하는 이유는 Threads 같은 서비스에서는 미디어 조회 트래픽이 매우 크기 때문입니다. 사용자가 홈 피드를 스크롤할 때마다 수많은 이미지와 영상이 로딩됩니다. 이 파일들을 API 서버가 직접 제공하면 서버 bandwidth와 latency가 크게 증가합니다. 따라서 미디어 파일은 Object Storage와 CDN을 통해 제공하고, API 서버는 lightweight metadata lookup만 담당하는 구조가 좋습니다.
4. Follow Graph APIs
Threads 같은 소셜 피드 서비스에서 Follow / Unfollow API는 단순히 관계 row 하나를 추가하거나 삭제하는 기능처럼 보일 수 있습니다.
하지만 실제로는 홈 피드, 추천 피드, 알림, 프로필 카운터, privacy, ranking feature까지 영향을 주는 API입니다. 특히 팔로우 관계는 홈 피드 생성의 기반이 되기 때문에, 중복 요청 방지, 낮은 지연 시간, 비동기 fanout, eventual consistency를 함께 고려해야 합니다.
Follow User
POST /v1/users/{target_user_id}/follow
Idempotency-Key: follow-user_123-user_456
JWT TokenResponse
{
"follower_id": "user_123",
"followee_id": "user_456",
"status": "following",
"created_at": "2026-05-01T10:30:00Z"
}팔로우 API입니다. (follower_id, followee_id)는 unique하게 관리해야 합니다. 같은 요청이 여러 번 들어와도 팔로우 관계가 중복 생성되면 안 됩니다.
Follow User API는 사용자가 다른 사용자를 팔로우할 때 호출됩니다.
여기서 follower_id는 팔로우를 요청한 사용자이고, target_user_id는 팔로우 대상 사용자입니다.
user_123 → follows → user_456이 API 는 팔로우 관계를 빠르게 저장하고, 이후 필요한 부가 작업은 이벤트 기반으로 분리하는 것입니다.
Unfollow User
DELETE /v1/users/{target_user_id}/follow
Authorization: Bearer JWT_TOKENResponse
{
"follower_id": "user_123",
"followee_id": "user_456",
"status": "unfollowed"
}Unfollow User API는 사용자가 기존 팔로우 관계를 끊을 때 호출됩니다.
언팔로우 이후 기존 timeline에 남아 있는 게시글을 즉시 모두 제거할 수도 있고, 이후 피드 조회 단계에서 필터링할 수도 있습니다. 대규모 시스템에서는 보통 즉시 전체 삭제보다 조회 시 필터링 또는 비동기 cleanup을 사용합니다.
follower_id는 JWT에서 추출합니다. 클라이언트가 body로 보낸 follower_id는 신뢰하지 않습니다.
Before:
user_123 → follows → user_456
After:
user_123 no longer follows user_456언팔로우는 팔로우보다 더 조심해야 합니다. 왜냐하면 기존 timeline에 이미 user_456의 게시글이 들어가 있을 수 있기 때문입니다.
Get Followers
GET /v1/users/followers?cursor={cursor}&limit=50
Header JWT Token Response
{
"items": [
{
"user_id": "user_789",
"username": "alice",
"display_name": "Alice"
}
],
"next_cursor": "cursor_123"
}특정 사용자의 팔로워 목록을 조회하는 API입니다.
Get Following
GET /v1/users/{user_id}/following?cursor={cursor}&limit=50Response:
{
"items": [
{
"user_id": "user_456",
"username": "bob",
"display_name": "Bob"
}
],
"next_cursor": "cursor_456"
}
특정 사용자가 팔로우하는 계정 목록을 조회하는 API입니다.
5. Feed / Timeline APIs
Get Home Feed
GET /v1/feed/home?cursor={cursor}&limit=20
Header JWT TokenResponse
{
"items": [
{
"feed_item_id": "feed_item_123",
"type": "post",
"post_id": "post_123",
"source": "following",
"ranking_score": 0.91,
"created_at": "2026-05-01T10:10:00Z"
},
{
"feed_item_id": "feed_item_124",
"type": "repost",
"post_id": "post_456",
"source": "repost",
"ranking_score": 0.88,
"created_at": "2026-05-01T10:11:00Z"
}
],
"next_cursor": "cursor_next"
}홈 피드를 조회하는 API입니다. low-latency가 가장 중요한 API 중 하나입니다.
이 API는 매번 팔로우 그래프를 실시간 계산하면 느려지기 때문에, timelines 또는 feed_items에 미리 계산된 후보를 읽고, 차단/뮤트/삭제 상태를 필터링한 뒤 반환하는 방식이 좋습니다.
Get Home Feed API는 사용자가 앱을 열거나 홈 탭을 새로고침할 때 호출되는 API입니다. Threads 같은 소셜 서비스에서 가장 중요한 API 중 하나입니다. 사용자가 앱을 열었을 때 피드가 늦게 뜨면 바로 이탈할 수 있기 때문입니다.
따라서 이 API의 목표는 단순히 “정확한 최신 게시글을 모두 가져오는 것”이 아니라, 충분히 최신이고 개인화된 피드를 매우 낮은 지연 시간으로 반환하는 것입니다.
설계 방향은 다음과 같습니다.
Do
- 미리 계산된 timeline/feed_items를 먼저 읽는다.
- cursor 기반 pagination을 사용한다.
- 차단/뮤트/삭제/moderation 상태를 필터링한다.
- 필요한 게시글/작성자/미디어/counter 정보를 batch로 가져온다.
- 일부 추천 서비스가 실패해도 기본 팔로우 피드는 반환한다.
Do not
- 요청 시점마다 전체 팔로우 그래프를 실시간 계산하지 않는다.
- 각 feed item마다 DB를 하나씩 호출하지 않는다.
- 모든 ranking을 요청 시점에 처음부터 계산하지 않는다.
- 카운터 값의 완벽한 최신성을 강제하지 않는다.앞서 정리한 것처럼 홈 피드는 low latency가 중요하기 때문에, 요청 시점에 모든 것을 계산하는 대신 precomputed timeline과 cache를 적극적으로 활용해야 합니다.
Get For You Feed
GET /v1/feed/for-you?&cursor={cursor}&limit=20
Header JWT Token Response:
{
"items": [
{
"feed_item_id": "feed_item_999",
"type": "post",
"post_id": "post_999",
"source": "recommended",
"ranking_score": 0.97,
"reason": "popular_in_your_network"
}
],
"next_cursor": "cursor_next"
}
Get For You Feed API는 사용자가 직접 팔로우하지 않은 계정의 게시글까지 포함해서, 사용자가 관심 가질 만한 콘텐츠를 추천하는 API입니다.
Home Feed가 주로 “내가 팔로우한 사람들의 게시글”을 보여주는 피드라면, For You Feed는 “내가 좋아할 가능성이 높은 게시글”을 찾아서 보여주는 피드입니다.
이 API는 단순히 인기 게시글을 보여주는 것이 아닙니다. 사용자의 팔로우 관계, 좋아요 기록, 댓글, 리포스트, 체류 시간, 관심 주제, 지역, 언어, 차단/뮤트 관계, 신고 이력 등을 반영해서 개인화된 피드를 구성해야 합니다.
또한 추천 서비스는 계산량이 크기 때문에, API 요청 시점에 모든 것을 처음부터 계산하면 안 됩니다.
후보군 생성, 랭킹, 필터링 일부는 사전에 계산하고, 요청 시점에는 빠르게 조립하는 구조가 필요합니다. 앞서 정리한 내용처럼 Threads 시스템은 low latency, read-heavy scaling, eventual consistency, graceful degradation이 중요하기 때문에 이렇게 설계를 했습니다.
Refresh Timeline
POST /v1/feed/home/refresh
Header JWT TokenRequest Body
{
"last_seen_at": "2026-05-01T10:00:00Z"
}
Response
{
"new_item_count": 12,
"refresh_cursor": "cursor_refresh_123"
}
사용자가 pull-to-refresh를 했을 때 새 게시글 개수를 확인하거나 최신 피드 cursor를 갱신하기 위한 API입니다.
6. Engagement APIs
Like Post
POST /v1/posts/{post_id}/like
Idempotency-Key: like-user_123-post_456
header: JWT Token Response
{
"post_id": "post_456",
"user_id": "user_123",
"liked": true
}좋아요 API입니다. likes 테이블에서는 (user_id, post_id)를 unique key로 관리합니다. 좋아요 수는 즉시 posts.like_count를 업데이트하기보다 LikeCreated 이벤트를 발행하고 Counter Service가 비동기로 집계하는 방식이 안전합니다.
Unlike Post
DELETE /v1/posts/{post_id}/like
header: JWT Token Response
{
"post_id": "post_456",
"user_id": "user_123",
"liked": false
}좋아요 취소 API입니다. 이 API 역시 중복 요청에 안전해야 합니다. 이미 좋아요가 취소된 상태에서 다시 요청이 와도 성공처럼 처리할 수 있습니다.
Repost Post
POST /v1/posts/{post_id}/repost
Idempotency-Key: repost-user_123-post_456
header: JWT Token Request Body
{
"quote_text": null
}
Response
{
"repost_id": "repost_123",
"post_id": "post_456",
"user_id": "user_123",
"type": "repost",
"created_at": "2026-05-01T10:40:00Z"
}리포스트 API입니다. 단순 리포스트와 quote post를 모두 지원할 수 있습니다. quote_text가 있으면 quote post로 처리하고, 없으면 일반 repost로 처리합니다.
Delete Repost
DELETE /v1/posts/{post_id}/repost
header: JWT Token Response
{
"post_id": "post_456",
"user_id": "user_123",
"reposted": false
}리포스트 취소 API입니다.
Record Post View
POST /v1/posts/{post_id}/views
header: JWT Token Request Body
{
"session_id": "session_123",
"client_event_id": "event_123",
"viewed_at": "2026-05-01T10:41:00Z"
}
Response
{
"accepted": true
}조회수 이벤트를 기록하는 API입니다. 조회수는 write-heavy 성격이 강하므로 동기적으로 DB row를 업데이트하면 안 됩니다. 이벤트 로그에 적재한 뒤 비동기 집계하는 것이 좋습니다.
7. Comment APIs
Create Comment
POST /v1/posts/{post_id}/comments
Idempotency-Key: comment-user_123-post_456-abc
header: JWT Token Request Body:
{
"text": "This is a great breakdown.",
"parent_comment_id": null
}
Response:
{
"comment_id": "comment_123",
"post_id": "post_456",
"user_id": "user_123",
"text": "This is a great breakdown.",
"created_at": "2026-05-01T10:45:00Z"
}
댓글 작성 API입니다. 댓글 작성 후 CommentCreated 이벤트를 발행해서 댓글 수 집계, 알림 발송, moderation을 비동기로 처리합니다.
Get Comments
GET /v1/posts/{post_id}/comments?cursor={cursor}&limit=50&sort=topResponse:
{
"items": [
{
"comment_id": "comment_123",
"post_id": "post_456",
"author": {
"user_id": "user_123",
"username": "user name "
},
"text": "This is a great breakdown.",
"like_count": 12,
"created_at": "2026-05-01T10:45:00Z"
}
],
"next_cursor": "cursor_next"
}
댓글 목록 조회 API입니다. 인기 게시글의 댓글 첫 페이지는 캐싱하는 것이 좋습니다. 이후 페이지는 cursor 기반 pagination으로 처리합니다.
Delete Comment
DELETE /v1/comments/{comment_id}
header: JWT Token Response:
{
"comment_id": "comment_123",
"status": "deleted"
}
댓글 삭제 API입니다. 댓글 역시 hard delete보다 soft delete가 안전합니다.
8. Notification APIs
Get Notifications
GET /v1/notifications?cursor={cursor}&limit=30
header: JWT Token Response:
{
"items": [
{
"notification_id": "notif_123",
"type": "like",
"actor_user_ids": ["user_456", "user_789"],
"target_type": "post",
"target_id": "post_123",
"message": "Bob 외 1명이 회원님의 게시글을 좋아합니다.",
"read": false,
"created_at": "2026-05-01T10:50:00Z"
}
],
"next_cursor": "cursor_next"
}인앱 알림 목록을 조회하는 API입니다. 좋아요, 댓글, 팔로우, 멘션, 리포스트 이벤트를 기반으로 생성됩니다.
Mark Notification as Read
PATCH /v1/notifications/{notification_id}
header: JWT Token Request Body:
{
"read": true
}
Response:
{
"notification_id": "notif_123",
"read": true
}
특정 알림을 읽음 처리하는 API입니다.
Update Notification Preferences
PATCH /v1/users/notification-preferences
header: JWT Token Request Body:
{
"likes": true,
"comments": true,
"follows": true,
"mentions": true,
"reposts": false,
"push_enabled": true
}
Response:
{
"user_id": "user_123",
"updated_at": "2026-05-01T10:55:00Z"
}
사용자별 알림 설정을 수정하는 API입니다. Notification Service는 알림을 보내기 전에 반드시 이 설정을 확인해야 합니다.
Register Device
POST /v1/devices
header: JWT Token Request Body:
{
"device_id": "device_abc",
"platform": "ios",
"push_token": "apns_token_123"
}
Response:
{
"device_id": "device_abc",
"status": "registered"
}
Push notification 발송을 위해 사용자 디바이스와 push token을 등록하는 API입니다.
9. Search APIs
Search
GET /v1/search?q={query}&type=all&cursor={cursor}&limit=20
Authorization: Bearer JWT_TOKEN optionalResponse:
{
"query": "system design",
"results": [
{
"type": "post",
"post_id": "post_123",
"text_snippet": "Threads system design is harder than it looks.",
"author": {
"user_id": "user_123",
"username": "user name"
}
},
{
"type": "user",
"user_id": "user_456",
"username": "systemdesign"
}
],
"next_cursor": "cursor_next"
}
게시글, 사용자, 키워드를 검색하는 API입니다. 실제 검색은 OpenSearch, Elasticsearch 같은 검색 인덱스에서 처리합니다.
검색 결과를 반환하기 전 차단, 뮤트, 비공개 계정, 삭제된 게시글, moderation 상태를 필터링해야 합니다.
비로그인 public search를 허용할 수도 있지만, 로그인 사용자에게는 JWT에서 viewer_id를 추출해 block/mute/private/moderation filtering을 적용합니다.
Search Users
GET /v1/search/users?q={query}&cursor={cursor}&limit=20Response:
{
"items": [
{
"user_id": "user_456",
"username": "systemdesign",
"display_name": "System Design"
}
],
"next_cursor": "cursor_next"
}
사용자 검색 API입니다.
Search Posts
GET /v1/search/posts?q={query}&cursor={cursor}&limit=20Response:
{
"items": [
{
"post_id": "post_123",
"text_snippet": "Threads system design is harder than it looks.",
"created_at": "2026-05-01T10:10:00Z"
}
],
"next_cursor": "cursor_next"
}게시글 검색 API입니다.
10. Safety / Privacy APIs
Block User
POST /v1/users/{target_user_id}/block
Idempotency-Key: block-user_123-user_456
header: JWT Token Response:
{
"blocker_id": "user_123",
"blocked_id": "user_456",
"status": "blocked"
}
차단 API입니다. 차단 관계는 피드, 검색, 프로필, 알림에서 모두 적용되어야 합니다.
Unblock User
DELETE /v1/users/{target_user_id}/block
header: JWT Token Response:
{
"blocker_id": "user_123",
"blocked_id": "user_456",
"status": "unblocked"
}
차단 해제 API입니다.
Mute User
POST /v1/users/{target_user_id}/mute
Idempotency-Key: mute-user_123-user_456
header: JWT Token Response:
{
"muter_id": "user_123",
"muted_id": "user_456",
"status": "muted"
}뮤트 API입니다. 차단보다 약한 필터링으로, 홈 피드와 추천 피드에서 특정 사용자의 콘텐츠를 숨기는 데 사용됩니다.
Unmute User
DELETE /v1/users/{target_user_id}/mute
header: JWT TokenResponse:
{
"muted_id": "user_456",
"status": "unmuted"
}
뮤트 해제 API입니다.
Report Content
POST /v1/reports
Idempotency-Key: report-user_123-post_456
header: JWT Token Request Body:
{
"target_type": "post",
"target_id": "post_456",
"reason": "spam",
"description": "This post contains suspicious links."
}
Response:
{
"report_id": "report_123",
"status": "submitted",
"created_at": "2026-05-01T11:00:00Z"
}
게시글, 댓글, 사용자 신고 API입니다. 신고 데이터는 moderation pipeline의 입력으로 사용됩니다.
11. Internal Async APIs / Events
대규모 Threads 시스템에서는 모든 작업을 synchronous API 안에서 처리하면 안 됩니다. 사용자 요청은 빠르게 완료하고, 나머지 작업은 이벤트 기반으로 분리해야 합니다.
아래 이벤트들은 외부 사용자가 직접 호출하는 API라기보다, Kafka/PubSub 같은 메시지 큐를 통해 내부 서비스들이 소비하는 event signature에 가깝습니다.
PostCreated Event
{
"event_type": "PostCreated",
"event_id": "event_123",
"post_id": "post_123",
"author_id": "user_123",
"visibility": "public",
"created_at": "2026-05-01T10:10:00Z"
}
소비하는 서비스
Feed Fanout Worker
Search Indexer
Notification Worker
Moderation Worker
Recommendation Candidate Generator
게시글 작성 후 피드 반영, 검색 색인, 알림, moderation, 추천 후보 생성을 비동기로 처리하기 위해 필요합니다.
PostDeleted Event
{
"event_type": "PostDeleted",
"event_id": "event_124",
"post_id": "post_123",
"author_id": "user_123",
"deleted_at": "2026-05-01T10:20:00Z"
}
소비하는 서비스
Timeline Cleanup Worker
Search Indexer
Recommendation Cleanup Worker
Notification Cleanup Worker
CDN Cache Invalidation Worker
삭제된 게시글이 피드, 검색, 추천, CDN 캐시에 계속 남지 않도록 downstream 시스템에 전달합니다.
LikeCreated Event
{
"event_type": "LikeCreated",
"event_id": "event_200",
"post_id": "post_456",
"actor_id": "user_123",
"post_author_id": "user_999",
"created_at": "2026-05-01T10:40:00Z"
}
소비하는 서비스
Counter Aggregator
Notification Worker
Recommendation Feature Worker
Abuse Detection Worker
좋아요 수 집계, 알림 생성, 추천 모델 feature 업데이트, 비정상 행동 탐지에 사용됩니다.
LikeDeleted Event
{
"event_type": "LikeDeleted",
"event_id": "event_201",
"post_id": "post_456",
"actor_id": "user_123",
"deleted_at": "2026-05-01T10:41:00Z"
}
좋아요 취소 후 카운터 감소와 추천 feature 업데이트를 위해 사용됩니다.
CommentCreated Event
{
"event_type": "CommentCreated",
"event_id": "event_300",
"comment_id": "comment_123",
"post_id": "post_456",
"actor_id": "user_123",
"post_author_id": "user_999",
"created_at": "2026-05-01T10:45:00Z"
}
댓글 수 집계, 알림 생성, moderation에 사용됩니다.
FollowCreated Event
{
"event_type": "FollowCreated",
"event_id": "event_400",
"follower_id": "user_123",
"followee_id": "user_456",
"created_at": "2026-05-01T10:30:00Z"
}
팔로우 알림, 추천 그래프 업데이트, timeline warmup에 사용됩니다.
RepostCreated Event
{
"event_type": "RepostCreated",
"event_id": "event_500",
"repost_id": "repost_123",
"post_id": "post_456",
"actor_id": "user_123",
"post_author_id": "user_999",
"created_at": "2026-05-01T10:40:00Z"
}
리포스트 카운터 집계, 알림, 피드 재배포에 사용됩니다.
MediaUploaded Event
{
"event_type": "MediaUploaded",
"event_id": "event_600",
"media_id": "media_123",
"uploader_id": "user_123",
"media_type": "image",
"storage_url": "s3://bucket/media_123",
"created_at": "2026-05-01T10:25:00Z"
}
썸네일 생성, 영상 트랜스코딩, CDN 준비 작업에 사용됩니다.
API Design Summary
High-level 시스템 디자인 인터뷰나 글에서는 아래 API들만 먼저 잡아도 충분합니다.
내 리소스:
GET /v1/me/profile
PATCH /v1/me/profile
GET /v1/me/posts
GET /v1/me/followers
GET /v1/me/following
PATCH /v1/me/notification-preferences
다른 사용자 리소스:
GET /v1/users/{user_id}/profile
GET /v1/users/{user_id}/posts
GET /v1/users/{user_id}/followers
GET /v1/users/{user_id}/following
Post
POST /v1/posts
GET /v1/posts/{post_id}
DELETE /v1/posts/{post_id}
Media
POST /v1/media/upload-url
POST /v1/media/{media_id}/complete
GET /v1/media/{media_id}
Follow
POST /v1/users/{target_user_id}/follow
DELETE /v1/users/{target_user_id}/follow
Feed
GET /v1/feed/home
GET /v1/feed/for-you
POST /v1/feed/home/refresh
Engagement
POST /v1/posts/{post_id}/like
DELETE /v1/posts/{post_id}/like
POST /v1/posts/{post_id}/repost
DELETE /v1/posts/{post_id}/repost
POST /v1/posts/{post_id}/views
Comments
POST /v1/posts/{post_id}/comments
GET /v1/posts/{post_id}/comments
DELETE /v1/comments/{comment_id}
Notifications
GET /v1/notifications
PATCH /v1/notifications/{notification_id}
POST /v1/devices
Search
GET /v1/search
GET /v1/search/users
GET /v1/search/posts
Safety / Privacy
POST /v1/users/{target_user_id}/block
DELETE /v1/users/{target_user_id}/block
POST /v1/users/{target_user_id}/mute
DELETE /v1/users/{target_user_id}/mute
POST /v1/reports
이 API 설계에서 가장 중요한 포인트는 이것입니다.
Write API는 빠르게 성공 응답을 반환한다.
피드 반영, 알림, 검색 색인, 카운터 집계는 이벤트 기반으로 비동기 처리한다.
좋아요, 팔로우, 게시글 작성은 idempotency를 반드시 지원한다.
피드 조회 API는 low-latency를 위해 precomputed timeline/cache를 읽는다.
검색/추천/알림/카운터는 각각 독립적으로 확장 가능한 내부 서비스로 분리한다.즉, Threads API 설계는 사용자 경험상 즉시 필요한 작업과 비동기로 처리해도 되는 작업을 분리하는 것입니다.




