템플릿 둘러보기/트위터 / X (Twitter Clone)

트위터 / X (Twitter Clone)

사용자 계정, 트윗 콘텐츠, 미디어 첨부, 해시태그, 팔로우 관계, 좋아요, 리트윗, 알림, 다이렉트 메시지(DM) 및 실시간 트렌드를 포함하는 트위터/X 스타일의 마이크로블로깅 플랫폼용 전체 데이터베이스 스키마입니다.

PostgreSQL17개 테이블소셜
socialmicrobloggingreal-timefeednotifications
ERD Studio로 제작

이 스키마에 대하여

트윗을 가리킬 수 있는 것이 셋인데, 이 스키마는 그 셋을 갈라 둡니다. 답글은 트윗 자신의 reply_to_id, 인용은 트윗 자신의 quote_tweet_id, 그냥 리트윗은 retweets 의 한 행입니다. 리트윗은 새 글을 만들지 않으므로 본문이 null 인 트윗이 되어서는 안 됩니다.

이 구분이 설계의 전부입니다. 답글과 인용은 트윗입니다 — 본문이 있고, 답글이 달리고, 작성자 타임라인에 뜹니다. 리트윗은 지지 표시라 글보다 좋아요에 가깝습니다. retweets.comment 는 인용까지 이쪽에 담고 싶을 때를 위해 남겨 둔 자리입니다.

타임라인에서 읽히는 값은 전부 트윗에 비정규화되어 있습니다 — retweet_count, like_count, reply_count, view_count. 그 숫자를 만드는 테이블도 그대로 있으므로, 카운터는 다시 계산할 수 있는 진실의 사본이지 진실 자체가 아닙니다.

눈여겨볼 관계

tweets → tweets (두 번)
reply_to_id 와 quote_tweet_id 는 서로 다른 자기참조입니다. 둘 다 null 이면 원본 트윗이고, 둘이 동시에 채워지는 일은 없습니다.
tweets → retweets
리트윗은 트윗이 아니라 여기의 한 행입니다. "이 사용자의 트윗" 질의가 본문 없는 행을 걸러낼 필요가 없어지고, 리트윗 취소가 삭제 한 번이 됩니다.
hashtags → trending_topics
지역별로 started_at·ended_at 과 함께 남깁니다. 현재 상태 테이블이 아니라 기록이라, 어제 무엇이 트렌드였는지도 답할 수 있습니다.
notifications → users (두 번)
recipient_id 는 보는 사람, actor_id 는 일으킨 사람입니다. 둘 다 들고 있어야 누가 무엇을 했는지 다시 캐지 않고 알림을 그릴 수 있습니다.
blocks → users (두 번)
blocker_id 와 blocked_id 로 방향이 있습니다. 차단은 상호가 아니고, 방향이 누구의 타임라인에서 무엇을 숨길지 정합니다.

설계 판단

리트윗은 테이블, 답글과 인용은 컬럼

답글과 인용은 본문이 있어서 트윗이 가진 모든 것 — 미디어, 해시태그, 자기 답글 — 이 필요합니다. 포인터 컬럼 하나씩만 더하면 트윗으로 둘 수 있습니다. 반면 그냥 리트윗은 담을 것이 없습니다. 트윗으로 만들면 본문이 null 인 행이 생겨 모든 타임라인 질의가 예외 처리를 해야 하고, 리트윗 취소가 "글로 보이던 것"의 삭제가 됩니다. 따로 두면 좋아요처럼 삽입과 삭제로 끝납니다.

트윗은 소프트 삭제

지우지 않고 deleted_at 을 세웁니다. 트윗은 답글·인용·북마크·알림이 가리키고 있어서, 행을 지우면 답글까지 딸려 가거나 끊어진 포인터가 남습니다. 소프트 삭제면 스레드가 "삭제된 트윗" 자리를 남긴 채 유지되는데, 사람들이 기대하는 모습이 그것입니다.

트렌드는 기록을 남긴다

trending_topics 를 비우고 다시 쓰지 않고 started_at·ended_at 을 둡니다. 트렌드 목록은 지나고 나서가 오히려 쓸모 있습니다 — 그 시각에 무슨 일이 있었는지를 답하는 자료입니다. 매 주기 덮어쓰면 그것이 사라집니다. rank 를 저장하는 이유도 같습니다. 랭킹 계산의 결과지 나중에 다시 구할 수 있는 값이 아닙니다.

리스트는 누가 넣었는지 적는다

list_members.added_by 가 있는 이유는 리스트가 공동 작업일 수 있기 때문입니다. 없으면 참여자를 뺄 때 그 사람이 넣은 멤버가 누구였는지 알 길이 없고, 리스트에 들어가고 싶지 않은 멤버에게 설명할 근거도 없습니다.

이 템플릿의 테이블

users사용자
10 cols
tweets트윗
12 cols
tweet_media트윗 미디어
8 cols
hashtags해시태그
5 cols
tweet_hashtags트윗 해시태그 매핑
4 cols
likes좋아요
5 cols
follows팔로우
5 cols
retweets리트윗
5 cols
notifications알림
9 cols
direct_messages다이렉트 메시지 (DM)
8 cols
dm_messagesDM 메시지
8 cols
dm_participantsDM 참여자
5 cols
blocks차단
5 cols
bookmarks북마크
5 cols
lists리스트
9 cols
list_members리스트 멤버
5 cols
trending_topics실시간 트렌드
8 cols

자주 묻는 것

리트윗을 그냥 트윗으로 두면 안 되나요?
본문이 없기 때문입니다. 본문이 null 인 트윗은 모든 타임라인 질의가 "이걸 글로 그릴까 지지로 그릴까" 를 판단해야 하고, 리트윗 취소가 사용자에게는 글로 보이던 것의 삭제가 됩니다. 따로 두면 좋아요처럼 동작하는데, 실제로 그런 것입니다.
reply_to_id 와 quote_tweet_id 가 동시에 채워질 수 있나요?
그러면 안 됩니다 — 답글이거나 인용이지 둘 다일 수 없습니다. 스키마가 막고 있지는 않으니, 가정이 아니라 보장으로 두고 싶으면 체크 제약을 거세요.
likes 테이블이 있는데 like_count 를 왜 저장하나요?
타임라인 한 장이 트윗 수십 개를 그리고 각각에 숫자를 붙입니다. 트윗마다 likes 를 세는 질의는 인기 계정을 만나는 순간 버티지 못합니다. 테이블이 여전히 진실이라 카운터는 다시 만들 수 있습니다 — 대체가 아니라 맞춰 주는 작업이 딸린 캐시입니다.
차단은 어떻게 적용되나요?
스키마가 하지 않습니다. blocks 는 방향을 적을 뿐이고, 타임라인·알림·DM 을 거르는 것은 애플리케이션입니다. DB 에서 하려면 모든 읽기가 blocks 와 조인해야 해서, 보통은 사용자별로 캐시한 집합을 씁니다.
스레드는 어떻게 구성되나요?
reply_to_id 를 거슬러 올라가 null 인 트윗까지 가거나, 거기서 내려오는 사슬입니다. 긴 스레드를 재귀 CTE 로 읽어도 되지만, 대개는 트윗에 대화 루트를 같이 저장해서 스레드 전체를 인덱스 조회 한 번으로 가져옵니다.