데이터 모델이 병목일 때: Medium 피처 스토어에서 얻은 교훈

발행: (2026년 6월 9일 PM 11:40 GMT+9)
11 분 소요

출처: The New Stack

“독자를 계속 읽게 만들기”는 Medium 추천 시스템의 결코 간단하지 않은 목표입니다. 특정 독자에게 언제든 가장 매력적일 가능성이 높은 콘텐츠를 예측하기 위해 Medium은 사용자 활동 신호(읽은 스토리, 표시된 추천, 팔로우, 좋아요 등)를 지속적으로 처리합니다. 그런 다음 매달 수백만 건에 달하는 새로운 기사 스트림과 즉시 연관시킵니다.

똑똑한 모델과 좋은 추론 로직이 필요하지만 그것만으로는 충분하지 않습니다. 데이터는 사용자가 탐색하는 동안에도 관련성을 유지할 수 있을 만큼 빠르게 저장·조회되어야 합니다. 바로 그 역할을 하는 것이 Medium의 피처 스토어이며, 초당 100만 건의 작업 규모로 확장하면서 데이터 모델을 올바르게 설계하는 것이 크게 중요해졌습니다.

Medium 수석 소프트웨어 엔지니어인 Andréas Saudemont가 팀이 문제를 어떻게 식별했고 이를 해결하기 위해 무엇을 구축했는지 최근에 설명했습니다. 읽는 대신 영상을 보고 싶다면 두 가지 옵션이 있습니다: Monster Scale Summit에서 짧은 버전을 시청하거나, 확장된 후속 웨비나를 시청하세요.

피처 스토어와 Medium 추천 시스템에서의 역할

피처 스토어는 사용자 활동과 내부 이벤트를 모두 수집해 추천을 구동하는 머신러닝 모델에 전달함으로써 전체 파이프라인을 연결합니다. 로그인한 사용자에게 “For You” 피드를 제공하는 등 맞춤형 기능을 가능하게 합니다.

Medium의 “For you” 페이지 스크린샷.

각 피처는 일반적으로 사용자 혹은 스토리와 같은 엔터티의 속성입니다. 일부는 단순하고 정적이며, 예를 들어 사용자가 유료 멤버십을 보유하고 있는지 여부와 같습니다. 다른 피처는 상호작용 이력을 캡처합니다: 사용자가 읽은 스토리, 최근에 보여진 콘텐츠 등.

다음 다이어그램은 Medium 피처 스토어 아키텍처를 매우 단순화한 모습을 보여줍니다.

Medium 피처 스토어 아키텍처를 단순화해 보여주는 다이어그램.

관계형 피처 데이터 모델의 문제점

몇 년 전 피처 스토어를 구축할 때 Medium은 엔터티 간 관계를 표현하기 위해 관계형 피처를 사용했습니다. 일반 피처와 달리 관계형 피처는 하나의 엔터티 ID에 대해 여러 값을 가질 수 있습니다. 각 값은 관계 ID(연관된 엔터티의 ID)와 이벤트 발생 시점을 기록하는 타임스탬프로 정의됩니다.

예를 들어 “스토리를 읽은 사용자” 피처는 스토리 엔터티 타입에 연결됩니다. 이는 사용자 엔터티 타입과 연관되며, 값은 특정 사용자가 해당 스토리를 언제 읽었는지를 나타냅니다.

Andréas는 개념을 설명하기 위해 다음 스키마 다이어그램을 공유했습니다:

관계형 피처 데이터 모델을 설명하는 스키마 다이어그램.

피처는 중앙에 위치하고, 각각 엔터티 타입, 이름, 버전, 데이터 타입으로 정의됩니다. 비관계형 피처는 단순히 피처, 엔터티 ID, 값만 가집니다. 관계형 피처는 여기에 관계 ID(다른 엔터티 타입을 가리킴)와 값, 타임스탬프가 추가됩니다.

데이터 모델링 관점에서 이 접근 방식은 비효율적이었습니다. 관계형 피처는 두 엔터티 타입을 연결하기 때문에 데이터가 두 테이블에 나뉘어 저장됩니다: 하나는 엔터티 ID용, 다른 하나는 값용. 따라서 단일 쿼리로 두 정보를 모두 가져올 수 없습니다. 첫 번째 쿼리는 엔터티 ID만 반환하고(ALLOW FILTERING에 의존) 두 번째 쿼리를 각 ID마다 실행해 값을 조회해야 합니다. “값을 가져오고 싶은 엔터티 ID가 1000개라면, 1000번의 쿼리를 실행해야 합니다.”라고 Andréas는 말했습니다.

ALLOW FILTERING에 과도하게 의존하면서 상황은 더욱 악화되었습니다. “이건 안 좋은 설계입니다.”라며 Andréas는 모니터링 데이터를 인용했습니다. 해당 쿼리로 읽은 행의 90%가 단순히 버려졌던 것입니다. “우리는 필요 없는 데이터를 읽고 있었습니다. ALLOW_FILTERING은 탈출구일 뿐, 설계 패턴이 되어서는 안 됩니다.”

ALLOW_FILTERING은 탈출구일 뿐, 설계 패턴이 되어서는 안 됩니다.”

ALLOW FILTERING에 과도하게 의존해 쿼리로 읽은 행의 90.2%가 버려진 것을 보여주는 차트.

리스트 피처 모델

그래서 팀은 데이터 모델을 재구성하고 리스트 기반 피처 모델로 전환했습니다. 데이터를 두 테이블에 나누는 대신, 특정 엔터티에 대한 모든 정보를 한 곳에 저장하고 단일 쿼리로 조회합니다.

다른 피처와 마찬가지로 리스트 피처는 엔터티 타입, 이름, (선택적) 버전으로 정의됩니다. 차이점은 입니다. 비관계형 피처가 단일 값(true/false 등)을 갖는 반면, 리스트 피처의 값은 아이템 컬렉션이며 각 아이템은 값과 타임스탬프를 포함합니다. 아이템 값은 어떤 데이터 타입이든 가능하고, 피처 스토어는 리스트 내 일관성을 강제하지 않습니다.

리스트 피처 개념을 설명하는 다이어그램.

예를 들어 사용자의 읽기 이력을 생각해 보세요. 엔터티는 user, 피처 이름은 reading history, TTL은 6개월입니다. TTL이 지나면 데이터는 자동으로 삭제됩니다(오래된 이력은 추천에 쓸모가 없으므로). 특정 사용자의 리스트는 스토리 ID와 읽은 시점 타임스탬프의 컬렉션입니다. 같은 스토리가 여러 번 등장할 수 있고, 여러 아이템이 동일한 타임스탬프를 공유할 수도 있습니다.

사용자 읽기 이력 리스트 예시: 스토리 ID와 읽은 시점 타임스탬프 컬렉션.

지원해야 할 다양한 연산이 있습니다.

  • Create List / Delete List: 하루에 몇 번 정도 실행됩니다.
  • Remove List Items with Value: 사용자가 특정 스토리를 이력에서 삭제해 추천에 영향을 주지 않게 하는 연산으로 초당 1k~10k 회 수행됩니다.
  • Add List Items: 더 높은 빈도. 사용자가 읽은 모든 스토리와 보여진 모든 썸네일이 이벤트를 생성합니다.
  • Get List Items: 가장 빈번하게 호출되는 연산으로 초당 100k~1M 회 수행됩니다.

각 연산이 특정 시간대에 실행되는 횟수를 보여주는 표.

Add List Items와 특히 Get List Items 연산이 바로 효율적인 데이터 스토어가 필요한 가장 큰 이유입니다.”라고 Andréas는 강조했습니다.

여러 아이템, 하나의 타임스탬프

단순 효율성 외에도 새로운 데이터 모델은 동일 타임스탬프에 여러 아이템을 저장할 수 있어야 했습니다. Medium이 사용자에게 네 개의 스토리 썸네일을 동시에 보여줄 때, 네 개의 프레젠테이션 이벤트는 모두 같은 타임스탬프를 공유하지만 서로 다른 스토리 ID를 가집니다. 이를 제대로 처리하지 않으면 기본 키 충돌이 발생합니다.

팀은 list_items 테이블 하나에 모든 정보를 저장하는 방식을 선택했습니다.

![모든 데이터를 저장하는 list_items 테이블 코드 스크린샷.](https://cdn.thenewstack.io/media/2026/06/2e682d53

0 조회
Back to Blog

관련 글

더 보기 »