GraphQL 비디오 검색 API 구축하기
비디오 데이터베이스가 수십만 개의 레코드로 늘어나면서, API가 병목 현상의 원인이 되었습니다.
화면 하나를 구성하는 데 인기 비디오, 채널 상세 정보, 태그, 조회수, 추천 영상을 보여주기 위해 다섯 번의 서로 다른 REST 호출이 필요했습니다. 연결 속도가 느린 모바일 사용자는 페이지 하나를 로드하는 데만 30개의 요청을 보내야 했습니다.
GraphQL은 이 문제를 해결합니다. 클라이언트는 단 한 번의 요청으로 필요한 데이터만 정확히 요청할 수 있습니다.
저는 Strawberry와 FastAPI를 사용하여 프로덕션 환경에서 사용할 비디오 검색 API를 구축했습니다. 그 과정을 소개합니다.
왜 Strawberry인가?
Graphene과 Ariadne를 테스트해 보았지만, 다음과 같은 이유로 Strawberry를 선택했습니다:
• 타입 힌트가 곧 스키마입니다. Python 코드를 작성하면 Strawberry가 GraphQL 타입을 생성하므로 별도의 수동 동기화가 필요 없습니다. • async-native 방식입니다. 덕분에 API가 이벤트 루프를 차단하지 않고도 많은 요청을 처리할 수 있습니다. • FastAPI와의 통합이 매끄럽습니다. GraphQL 엔드포인트가 기존 REST 라우트 바로 옆에 위치할 수 있습니다. • 내장된 DataLoader. 이 도구는 N+1 문제를 즉시 해결해 줍니다.
데이터 스택
데이터는 PHP 사이트에서 관리하는 SQLite 데이터베이스에 저장됩니다. 빠른 전체 텍스트 검색을 위해 SQLite FTS5를 사용합니다.
Python 서비스는 이 데이터베이스를 읽기 전용 레이어로 읽어옵니다. 이를 통해 단일 진실 공급원(single source of truth)을 보장합니다.
검색 경험을 개선하기 위해 FTS5 관련성 점수와 인기 신호를 결합했습니다. 이를 통해 특정 바이럴 영상 하나가 다른 모든 검색 결과를 압도하는 것을 방지합니다.
N+1 문제 해결하기
GraphQL에서 흔히 발생하는 함정은 N+1 문제입니다. 만약 20개의 비디오를 가져온 뒤 각 비디오의 채널 정보를 가져온다면, 단순한 API는 21번의 데이터베이스 쿼리를 실행하게 됩니다.
DataLoader는 배칭(batching)을 통해 이를 해결합니다. 한 번의 틱(tick) 동안 모든 채널 ID를 수집한 다음, 단 한 번의 쿼리로 이를 모두 가져옵니다. 이를 통해 쿼리 시간을 40ms에서 5ms 미만으로 단축했습니다.
주요 설계 선택 사항
• 불투명한 ID (Opaque IDs): ID로 문자열을 사용합니다. 이를 통해 클라이언트 앱을 깨뜨리지 않고도 향후 변경이 가능합니다. • 의도적인 Null 허용 (Intentional Nullability): 어떤 필드가 비어 있을 수 있는지 정의합니다. 이는 클라이언트가 데이터 누락 시 크래시 없이 처리할 수 있도록 돕습니다. • 읽기 전용 스키마: GraphQL API는 쓰기 작업을 처리하지 않습니다. 이를 통해 많은 보안 문제를 사전에 방지할 수 있습니다. • 엣지 캐싱 (Edge Caching): GraphQL은 기본적으로 POST를 사용하므로 캐싱이 어렵습니다. 저는 Cloudflare를 통한 캐싱을 허용하기 위해 Automatic Persisted Queries (APQ)를 사용합니다.
너무 많은 커스텀 엔드포인트나 복잡한 쿼리 파라미터로 어려움을 겪고 있다면, 이 스택이 강력한 대안이 될 수 있습니다.
