DailyWatch의 "관련 동영상" 패널은 처음에는 소박한 SQLite 쿼리로 시작되었습니다. 세 개의 테이블을 조인하고, 중복되는 태그를 카운트하여 순위가 매겨진 목록을 반환하는 방식이었습니다. 카탈로그 규모가 작을 때는 그것으로 충분했습니다. "italian"과 "pasta" 태그가 달린 요리 영상은 동일한 태그를 가진 다른 영상들을 노출했고, 사용자들은 이를 클릭했습니다. 메타데이터가 깔끔하고 라이브러리 규모가 작았기 때문에 결과물은 관련성 있어 보였습니다.
하지만 카탈로그가 성장하면서 사용자의 기대치도 변했습니다. 사람들은 단순히 동일한 태그를 가진 영상을 더 많이 보기를 원하지 않았습니다. 그들은 현재 시청 중인 영상 바로 다음에 시청자의 40%가 선택한 클립을 원했습니다. 설명에는 시작 영상에 대한 언급이 전혀 없고 태그도 부실하지만, 심야 시청 세션에서 계속해서 나타나는 니치(niche) 채널을 원했습니다. 이것은 메타데이터의 일치가 아니라 행동 패턴의 문제입니다. SQLite는 셀프 조인(self-join)과 재귀적 공통 테이블 식(recursive CTE)이 얽힌 유지보수 불가능한 구조로 전락하지 않고서는 이러한 패턴을 표현할 수 없었습니다. 공동 시청(co-viewership)이나 세션 인접성(session adjacency) 등 우리가 모델링하려는 새로운 신호가 추가될 때마다 지연 시간과 정신적 비용이 늘어났습니다. 우리는 관계형 데이터베이스라는 틀 안에 억지로 끼워 맞춘 그래프 문제를 안고 있었습니다.
저는 추천 레이어를 Apache AGE로 옮겼습니다.
Apache AGE는 이미 사용 중인 데이터베이스에 openCypher 그래프 쿼리를 추가해 주는 PostgreSQL 확장 기능입니다. 별도의 서버가 아닙니다. 사이드카(sidecar)도 아닙니다. Postgres 내부에서 실행되므로, 전용 Neo4j 클러스터를 구축하거나 완전히 새로운 운영 플레이북을 익힐 필요 없이 공동 시청 네트워크를 구축할 수 있음을 의미했습니다. 데이터베이스 신뢰성 엔지니어(DBRE)가 없는 소규모 팀에게 이 차이는 엄청나게 중요했습니다.
왜 확장이 새로운 데이터베이스보다 나은가
스택에 그래프 데이터베이스를 추가하는 것은 화이트보드 위에서는 쉬워 보이지만, 실제 운영 환경에서는 비용이 많이 듭니다. 새로운 모니터링 대시보드, 새로운 백업 절차, 새로운 커넥션 풀, 그리고 새로운 장애 조치(failover) 로직이 필요하기 때문입니다. AGE는 기존 Postgres 인스턴스 내부에서 작동하므로 이 모든 문제를 우회합니다.
AGE가 우리에게 효과적이었던 네 가지 실질적인 이유가 있습니다.
- 새로운 인프라가 필요 없음. AGE는 확장 기능이므로 현재의
pg_dump일정, 기존 복제본(replicas), 표준 Postgres 상태 점검(health checks)이 모두 그대로 작동합니다. 운영 팀에 또 다른 데이터 저장소를 관리해 달라고 설득할 필요가 없습니다. - 한 번의 쿼리로 처리하는 혼합 워크로드. AGE를 사용하면 SQL 내부에서 Cypher를 작성할 수 있습니다. 그래프 탐색을 통해 후보 영상을 찾은 다음, 그 결과 세트를 관계형
users테이블과 조인하여 지역별 콘텐츠 제한을 적용하거나, 관계형sponsorships테이블과 조인하여 특정 채널의 우선순위를 낮출 수 있습니다. 단 한 번의 라운드 트립으로 두 가지 쿼리 언어가 협력합니다. - 이식성. Cypher는 개방적이고 문서화가 잘 된 그래프 쿼리 언어입니다. 나중에 DailyWatch가 AGE의 규모를 넘어서서 Neo4j나 Memgraph로 마이그레이션해야 하더라도, 쿼리 로직을 최소한의 재작성만으로 옮길 수 있습니다. 특정 업체에 종속된 독점적 문법에 갇히지 않습니다.
- 호환성. 데이터가 궁극적으로 PostgreSQL에 저장되므로, 기존의 PHP 또는 Python 툴링을 변경할 필요가 없습니다. 동일한 드라이버로 연결하고, 동일한 연결 문자열을 처리하며, 동일한 방식으로 행(row)을 가져옵니다. 그래프 로직은 애플리케이션 레이어가 아닌 쿼리 레이어에 위치합니다.
공동 시청 모델링
구현은 간단합니다. 우리가 관심을 두는 엔티티인 Video와 Channel에 대한 노드를 정의했습니다. 그런 다음 그들 사이의 관계를 나타내는 엣지를 정의했습니다. PUBLISHED 엣지는 Channel을 Video에 연결합니다. CO_VIEWED 엣지는 한 Video를 다른 Video에 연결하며, 두 영상이 동일한 시청 세션에 얼마나 자주 나타났는지를 나타내는 weight 속성을 가집니다.
이 모델은 태그 기반의 SQL이 포착할 수 없는 것, 즉 암시적 구조를 포착합니다.
