Introduction & Requirements
아래 설계는 Spotify의 공개 문서와 제품 특성을 바탕으로 한 시스템 디자인 가정입니다. Spotify 내부 구현을 그대로 설명하는 글은 아닙니다.
이 글은 음악 추천 모델 전체나 오디오 스트리밍 플레이어 내부 구현을 설계하는 글이 아닙니다. 사용자의 청취 이벤트를 수집하고, 개인화 규칙을 적용하고, 연말에 빠르게 열리는 Wrapped 카드와 공유 이미지를 제공하는 시스템을 설계하는 글입니다.
“사용자가 들은 노래를 DB에 저장하고, 연말에 가장 많이 들은 곡과 아티스트를 집계해서 보여주면 되지 않을까요?”
작은 기능이라면 이 접근으로 화면 하나는 만들 수 있습니다. 사용자별로 track_id와 play_count를 저장하고, 연말에 count가 높은 순서대로 정렬하면 상위 곡 목록은 나옵니다. 하지만 Spotify Wrapped는 사용자가 단순히 “많이 들은 곡 목록”을 보려고 여는 기능이 아닙니다.
사용자는 자신의 1년을 음악 취향으로 다시 보고 싶어 합니다. 친구에게 공유하고 싶고, 특정 아티스트와 장르가 왜 나왔는지 납득하고 싶어 합니다. 그래서 Wrapped는 단순 청취 횟수 집계가 아니라, 1년 치 스트리밍 이벤트를 사용자별 스토리 카드로 바꾸는 대규모 개인화 시스템이 됩니다.
Spotify는 2025 Wrapped 설명에서 총 청취 시간이 실제 재생 시간 기준으로 계산된다고 설명합니다. 예를 들어 팟캐스트를 2배속으로 들으면 콘텐츠 길이가 아니라 실제로 들은 시간이 반영됩니다. 또한 Private Mode와 Exclude from Taste Profile로 제외한 콘텐츠는 총 청취 시간에는 포함될 수 있지만, 취향 기반 스토리에는 영향을 주지 않는다고 설명합니다.
이 설명만 봐도 Wrapped는 단순 카운터가 아닙니다. 같은 청취 이벤트라도 어떤 숫자에는 포함하고, 어떤 카드에는 제외하거나 낮은 가중치로 반영해야 합니다. 즉, 총 청취 시간 계산과 취향 기반 카드 계산은 서로 다른 규칙을 가져야 합니다.
Spotify는 사용자가 특정 트랙이나 플레이리스트를 Taste Profile에서 제외할 수 있는 기능도 제공합니다. Taste Profile은 사용자의 장기적인 음악 취향을 나타내기 위해 쓰이는 개인화 기준입니다. 사용자가 수면용 음악, 아이가 듣는 노래, 임시로 반복 재생한 트랙을 제외하고 싶을 수 있기 때문에 이런 기능이 필요합니다.
이 기능이 있으면 추천과 Wrapped가 같은 청취 이벤트를 보더라도 모든 이벤트를 같은 방식으로 반영하면 안 됩니다. 예를 들어 수면용 플레이리스트를 매일 밤 8시간씩 틀었다고 해서 그 음악이 사용자의 올해 대표 취향으로 나오는 것은 어색할 수 있습니다. 반면 총 청취 시간에서는 실제로 재생된 시간이므로 포함될 수 있습니다.
“우리가 설계하는 것은 단순 청취 횟수 집계인가요, 아니면 사용자별 제외 규칙과 공유 가능한 스토리 카드가 붙은 개인화 이벤트 시스템인가요?”
이 질문에 따라 저장 위치와 처리 흐름이 달라지게 됩니다. 단순 청취 횟수 집계라면 user_id, track_id, count만 있어도 됩니다. 하지만 Wrapped는 총 청취 시간, 상위 곡, 상위 아티스트, 장르, 팟캐스트, 오디오북, 취향 변화, 공유 카드, 글로벌 랭킹을 서로 다른 규칙으로 계산해야 합니다.
특히 Taste Profile 제외나 Private Mode처럼 어떤 화면에는 반영하고 어떤 카드에는 덜 반영해야 하는 조건이 생기면, 하나의 count 필드만으로는 설명하기 어렵습니다. 청취 이벤트 처리 단계에서부터 계산 목적에 맞는 policy_version, rule_version, content_type, session_mod_를 함께 기록해야 합니다.
여기서 또 하나의 질문이 나옵니다.
“사용자가 노래를 재생한 순간마다 바로 Wrapped 집계를 업데이트할 건가요, 아니면 이벤트를 모아 나중에 계산할 건가요?”
실시간 업데이트는 좋아 보일 수 있습니다. 오늘 들은 노래가 바로 올해 상위 곡 후보에 반영되면 화면이 최신처럼 보입니다. 하지만 수억 명의 사용자가 매일 음악을 듣는 환경에서 매 재생마다 모든 Wrapped 집계를 즉시 업데이트하면 이벤트 수집, 중간 집계, 카드 생성 경로가 동시에 느려질 수 있습니다.
Spotify는 2026년 데이터 트렌드 글에서 2025년 12월 24일이 음악 스트리밍 역사상 가장 큰 날이었고, 하루 110억 회 이상의 음악 스트림이 있었다고 공개했습니다. 이 정도 규모에서는 원본 청취 이벤트를 먼저 안전하게 수집하고, 사용자별 중간 집계를 시간 단위나 일 단위로 정리한 뒤, Wrapped 공개 전에 사용자별 결과를 미리 만들어 두는 방식이 더 안정적입니다.
Wrapped 카드가 사용자의 현재 취향을 보여줘야 하는지, 1년 전체를 보여줘야 하는지도 결정해야 합니다. 최근 한 달의 반복 청취가 전체 1년 취향을 지나치게 밀어내면 사용자는 “내 Wrapped가 이상하다”고 느낄 수 있습니다. 반대로 연초 청취가 너무 오래 유지되면 최근 취향 변화가 보이지 않습니다.
그래서 사용자 화면마다 freshness, 즉 최신 상태를 얼마나 강하게 요구할지 다르게 잡아야 합니다. 연말 Wrapped의 확정 카드는 일정 기간의 snapshot으로 만들고, 추천 홈 화면은 더 짧은 시간 창을 볼 수 있습니다. 여기서 snapshot은 특정 시점의 계산 결과를 고정해 둔 데이터입니다. 같은 청취 이벤트라도 Wrapped와 추천 홈은 읽는 기준이 달라질 수 있습니다.
“Wrapped를 모든 사용자에게 동시에 열어도 되나요?”
연말 공개 직후에는 많은 사용자가 동시에 앱을 열고, 스토리 카드를 넘기고, 공유 이미지를 생성합니다. 이때 모든 카드를 요청 시점에 계산하면 카드 생성 작업자와 이미지 렌더링 경로가 동시에 느려질 수 있습니다.
그래서 Wrapped는 사전에 사용자별 결과를 계산하고, 런칭 시점에는 빠른 조회용 데이터와 카드 파일을 내려주는 구조가 필요합니다. 실시간성 일부를 줄이더라도, 전 세계 사용자가 동시에 들어오는 시간에 첫 카드가 빠르게 열리는 쪽이 더 중요합니다.
Functional Requirements
사용자의 음악, 팟캐스트, 오디오북 청취 이벤트를 수집하고, 실제 재생 시간과 콘텐츠 종류를 구분할 수 있어야 합니다.
Taste Profile 제외, Private Mode, 지역, 계정 상태, 콘텐츠 타입별 규칙을 반영해 총 청취 시간 계산과 취향 기반 카드 계산을 분리할 수 있어야 합니다.
사용자별 상위 곡, 상위 아티스트, 장르, 청취 시간, 공유용 카드, 스토리 순서를 계산할 수 있어야 합니다.
Wrapped 공개 시점에 많은 사용자가 동시에 열어도 카드와 요약 결과를 낮은 지연 시간으로 제공할 수 있어야 합니다.
약한 네트워크에서도 사용자가 이미 받은 Wrapped 카드와 공유 이미지를 다시 볼 수 있도록 이벤트 동기화와 로컬 캐시를 제공할 수 있어야 합니다.
Non-Functional Requirements
Wrapped 홈 진입 후 첫 카드 표시까지 p95 500ms에서 1초 이내를 목표로 둡니다.
사전 계산 파이프라인이 느려져도 음악 재생 이벤트 수집과 일반 추천 경로가 같이 느려지지 않게 분리해야 합니다.
사용자의 제외 설정, 청취 기록 삭제, 계정 상태 변경이 뒤늦게 반영될 때 잘못된 알림이나 오래된 카드가 보이지 않도록 검증해야 합니다.
공개 첫날 카드 조회, 공유 이미지 조회, 이벤트 동기화가 동시에 증가해도 read path가 안정적으로 동작해야 합니다.
청취 이벤트 재처리가 필요할 때 원본 이벤트를 다시 읽을 수 있어야 하지만, 카드 조회 경로에서 원본 이벤트를 직접 스캔하면 안 됩니다.
Scope
이 글은 청취 이벤트 수집에서 시작합니다. 사용자가 음악, 팟캐스트, 오디오북을 재생하면 재생 서버는 content_id, content_type, played_ms, playback_speed, session_mode 같은 정보를 포함한 이벤트를 발행합니다. 여기서 played_ms는 실제로 들은 시간을 나타냅니다.
그리고 사용자별 중간 집계를 다룹니다. 연말에 원본 이벤트를 처음부터 다시 모두 읽으면 비용과 시간이 커집니다. 그래서 일별 또는 주별로 사용자별 재생 시간, 트랙 count, 아티스트 count, 제외 규칙 적용 결과를 미리 계산해야 합니다.
Taste Profile 제외 규칙도 범위에 들어갑니다. 사용자가 제외한 트랙이나 플레이리스트는 추천과 취향 기반 Wrapped 카드에 덜 반영되어야 합니다. 다만 총 청취 시간처럼 실제 재생 시간 자체를 보여주는 숫자에는 포함될 수 있습니다.
Wrapped 카드 생성도 다룹니다. 사용자가 보는 것은 원본 이벤트가 아니라 상위 곡, 상위 아티스트, 장르, 청취 시간, 공유 이미지, 카드 순서가 포함된 최종 카드 묶음입니다.
공유 이미지 생성도 포함합니다. 공유 이미지는 요청 시점에 매번 새로 만들면 공개일에 렌더링 경로가 느려질 수 있습니다. 그래서 사전 생성하거나, 템플릿과 사용자별 결과를 조합해 캐시 가능한 형태로 만들어야 합니다.
글로벌 런칭 캐시도 필요합니다. Wrapped 공개 직후에는 많은 사용자가 동시에 앱을 열게 됩니다. 이때 사용자별 story pack 상태, 첫 카드, 공유 이미지 pointer를 빠르게 읽을 수 있어야 합니다.
앱 이벤트 스트림, Event Sync, 로컬 카드 캐시, 약한 네트워크 복구도 다룹니다. 사용자가 지하철이나 약한 네트워크에서 Wrapped를 열어도 이미 받은 카드는 다시 볼 수 있어야 합니다. 다만 cached card가 언제 확인된 것인지 표시해야 합니다.


