Design Substack, 하나의 게시물을 웹, 이메일, 앱, 오디오, 영상, 유료 구독으로 배포하는 서비스
창작자가 게시 버튼을 누르면 콘텐츠 저장, 접근 권한 확인, 이메일 발송, 앱 피드 반영, 미디어 처리, 구독 상태 동기화가 서로 다른 처리 경로에서 진행됩니다.
Introduction & Requirements
아래 설계는 Substack의 공개 문서와 제품 기능을 바탕으로 한 시스템 디자인 가정입니다. Substack 내부 구현을 그대로 설명하는 글은 아닙니다.
Substack에서는 창작자가 텍스트, 오디오, 영상 게시물을 발행하고 구독자를 관리할 수 있습니다. 독자는 무료 또는 유료로 publication을 구독하고 웹, 앱, 이메일, 팟캐스트 앱에서 콘텐츠를 볼 수 있습니다. Notes를 통해 짧은 글, 링크, 이미지, 인용문을 공유할 수도 있으며, 라이브 영상은 전체 사용자, 전체 구독자, 유료 구독자처럼 접근 범위를 다르게 설정할 수 있습니다.
처음 이 문제를 보면 이메일 뉴스레터 서비스로 설계하기 쉽습니다.
“창작자가 글을 작성하면 DB에 저장하고, 구독자 이메일 목록을 읽어 메일을 보내면 되지 않을까요?”
구독자 수가 수백 명이고 텍스트 게시물만 지원한다면 이 방식으로 시작할 수 있습니다. 게시물 row를 저장하고 구독자 목록을 조회한 뒤 이메일 발송 API를 반복 호출하면 됩니다.
하지만 실제 창작자 플랫폼에서는 하나의 게시물이 여러 위치에 나타나게 됩니다.
Publication Website
Substack App
Email Inbox
Home Feed
Reader Library
Podcast RSS
Push Notification
Creator Analytics
Search Index게시물이 텍스트인지, 오디오인지, 영상인지에 따라 처리 과정도 달라집니다. 유료 구독자에게만 공개할 수도 있고, 무료 미리보기만 제공할 수도 있으며, 이메일에는 전체 글을 넣고 웹에서는 paywall을 적용하는 정책도 가능합니다.
여기서 paywall은 사용자가 콘텐츠를 열 때 현재 구독 상태를 확인하고, 접근할 수 없는 콘텐츠에는 구독 화면을 표시하는 기능입니다.
사용자가 유료 콘텐츠를 열 수 있는 상태를 이 글에서는 접근 권한(entitlement)이라고 부르겠습니다.
접근 권한에는 다음 정보가 들어갈 수 있습니다.
어떤 publication에 대한 권한인가?
무료 콘텐츠만 볼 수 있는가?
유료 콘텐츠도 볼 수 있는가?
권한이 언제까지 유지되는가?
무료 체험이나 선물 구독으로 받은 권한인가?
환불이나 결제 실패로 제한된 상태인가?결제 성공과 접근 권한 갱신도 항상 같은 순간에 끝나지 않을 수 있습니다. 결제 제공자가 성공 응답을 보냈지만 webhook이 늦게 도착할 수 있고, 사용자가 구독을 취소해도 이미 결제한 기간이 끝날 때까지 유료 콘텐츠를 계속 볼 수 있습니다.
그래서 결제 상태와 콘텐츠 접근 상태를 하나의 boolean 값으로 처리하면 운영 중에 문제가 생길 수 있습니다.
예를 들어 다음과 같은 상태가 필요합니다.
subscription_state = cancelled
access_until = 2026-08-31T23:59:59Z사용자는 갱신을 취소했지만 이미 결제한 기간까지는 유료 콘텐츠를 볼 수 있습니다.
면접관은 이렇게 물을 수 있습니다.
“우리가 설계하는 것은 이메일 발송 서비스인가요, 아니면 콘텐츠 발행, 구독 결제, 접근 권한, 다중 채널 배포가 결합된 publishing platform인가요?”
이 질문에 따라 저장소와 처리 경로가 달라지게 됩니다.
이메일 발송만 필요하다면 수신자 목록과 이메일 template만 있어도 됩니다. 하지만 Substack과 같은 플랫폼에서는 게시물 원본, 수정 이력, 웹 페이지, 이메일 버전, 오디오 파일, 영상 파일, 구독 plan, 접근 권한, 댓글, Notes, 피드, 발송 결과를 함께 관리해야 합니다.
게시 버튼을 누른 뒤 모든 작업을 같은 HTTP 요청 안에서 끝내는 방식도 적합하지 않습니다.
게시물을 저장한 뒤 다음 작업이 이어질 수 있습니다.
웹 페이지 생성
CDN cache 갱신
구독자 대상 계산
이메일 발송 작업 생성
앱 피드 반영
push notification 생성
팟캐스트 RSS 갱신
미디어 변환
검색 인덱스 반영
분석 이벤트 생성구독자가 수백만 명인 publication에서는 게시물 하나가 수백만 개의 이메일 발송 작업으로 확장될 수 있습니다.
하나의 이벤트가 내부의 여러 작업으로 확장되는 현상을 fan-out이라고 부르겠습니다.
1 PostPublished event
-> 1 web page update
-> 1 feed projection
-> 1 podcast RSS update
-> 5,000,000 email delivery tasks
-> 2,000,000 push candidates
-> analytics events게시물 저장과 수백만 건의 발송을 같은 요청에 넣으면 게시 API 연결이 오랫동안 유지되고, 일부 이메일 제공자가 느려질 때 게시 자체가 실패할 수 있습니다.
그래서 게시물 저장과 채널별 배포를 분리해야 합니다.
게시물은 먼저 영속 저장소에 확정하고, 이후 웹, 이메일, 앱, push, 팟캐스트 배포를 이벤트 기반으로 진행하게 됩니다.
여기서 이벤트 기반 처리는 게시 API가 각 후속 작업을 직접 호출하지 않고, 게시 완료 이벤트를 발행한 뒤 각 작업자가 필요한 처리를 독립적으로 수행하는 구조입니다.
또 다른 문제가 있습니다.
“게시물이 발행된 뒤 창작자가 오타를 수정하면 이미 전송된 이메일도 바뀌어야 하나요?”


