카카오엔터테인먼트가 버추얼 걸그룹 서바이벌 소녀 리버스의 영상 서비스를 9개월간 만들며 영상 CMS 인 VODKA 를 구축한 기록이다. 프론트 Admin 프레임워크를 네 후보에서 Refine 으로 좁힌 과정, Apigee 로 인증을 한곳에 모은 구조, Tus 프로토콜 기반 대용량 업로드 서버를 만들며 겪은 성능 문제를 순서대로 다룬다.
핵심 포인트- 설계가 문서화되지 않은 시기라 DDD 의 이벤트 스토밍 워크숍을 이틀간 진행해 서브도메인과 제약사항, 소프트웨어 모델을 뽑아냈다.
- Admin 프레임워크 후보는 React Admin, Refine, Retool, AdminJS 넷이었고 Refine 을 골랐다. React Admin 은 사용법이 엄격하고 opinionated 한 반면 Refine 은 커스텀 UI 자유도가 높아서다.
- Retool 은 화면을 드래그로 그리는 속도가 특출났지만 직접 개발이 제한되고, 당시 셀프 호스팅이 엔터프라이즈 전용이라 검토 시간과 가격 문제로 포기했다.
- 인증과 권한 처리를 한곳에 모으려고 GCP 의 Apigee 를 API Gateway 로 두고 Shared Flows 로 OKTA 기반 인증을 붙였다.
- 대용량 업로드는 Tus 프로토콜로 구현했다. HTTP 기반이라 브라우저에서 바로 쓸 수 있고 청크 전송과 이어올리기를 지원한다.
상세 정리- 프레임워크 선정 기준은 React 기반일 것, 운영에 필요한 UI 컴포넌트 내장, 데이터 연동 자동화, 사용자 수, 문서와 예제, 커스텀 UI 자유도, 유료 지원, GraphQL 지원이었다. GraphQL 은 결국 쓰지 않았다.
- React 를 조건에 넣은 이유는 사내 프론트 개발자 다수가 React 를 써서 막혔을 때 조언을 구할 수 있기 때문이다.
- Refine 은 App 최상단을 Refine 컴포넌트로 감싸는 것으로 준비가 끝난다. 연동 로직은 Provider 형태로 제공돼 기본 Provider 의 함수를 오버라이드해 커스텀한다.
- Resource 는 메뉴가 담당하는 개념 단위로 DB 한 테이블의 모델과 비슷하다. Resource 를 지정하면 사이드바 메뉴와 라우팅, 화면별 API 연결이 자동으로 만들어지고, CRUD 가 아닌 화면은 routes 를 직접 추가한다.
- 화면은 List, Create, Edit, Show 기본 컴포넌트와 refine-antd 의 Ant Design 컴포넌트로 구성했고 그 하위는 평범한 리액트 컴포넌트로 개발했다. 파일 업로드 컴포넌트는 기존 것으로 어려워 직접 만들며 자유도의 이점을 체감했다.
- Refine 의 아쉬운 점 둘을 적는다. CRUD 가 아닌 API 호출에는 Hook 이 제한적이라 개발 기간이 넉넉하면 연동 함수를 직접 만드는 편이 나을 수 있고, 버전업이 잦은데 문서에 버저닝이 없어 GitHub Release 탭에서 변경사항을 하나씩 확인해야 했다.
- Tus 를 고른 이유는 스토리지 벤더가 주는 클라이언트가 플랫폼 종속적이고 설치가 필요한 경우가 많아서다. GCS 든 NFS 든 단일 진입점으로 쓸 수 있다.
- 초기에는 서비스 백엔드가 업로드를 받아 Tus 로 넘겼는데 그만큼 시간이 더 걸리고 요청 버퍼 때문에 메모리 문제도 생겼다. 전처리 후 Signed URL 을 발급해 프론트엔드와 Tus 가 직접 통신하도록 바꿨고, 이미 인증된 URL 이라 TTL 을 필수로 걸었다.
- 디스크에 쓴 뒤 GCS 로 옮기면 저장소가 추가로 필요하고 업로드 시간이 두 배가 돼, 오브젝트 스토리지에 바로 저장하도록 개선했다.
- 동시 업로드 충돌을 막는 lock 이 필수인데 파일시스템 기반 lock 은 서버가 여러 대인 분산 환경에서 쓸 수 없다. Redis 의 SETNX 로 분산 lock 을 구현하고 잠금 해제 실패나 교착 상태 대비로 TTL 을 걸었다. 클러스터로 가면 redlock 으로 개선할 계획이다.
- 런타임은 업로드가 특정 시간대에 몰리는 작업이라 처음에 GCP Cloud Run 을 골랐고, 가볍고 cold start 가 짧은 golang 을 채택했다.
- 그런데 Cloud Run 은 HTTP/1 요청 크기가 32MB 로 제한된다. 청크가 작을수록 lock 잠금과 해제, HTTP 요청 응답 지연이 반복돼 업로드가 길어지고, 원본 영상은 수백 개 청크가 생겨 병합 후처리가 부담이 됐다.
- HTTP/2 에서는 제한이 없지만 golang fiber 의 core 인 fasthttp 가 http/2 를 지원하지 않아, 요청 크기를 늘릴 수 있는 GKE 로 옮기는 쪽을 택했다.
- 스트리밍 업로드를 위해 fiber 는 StreamRequestBody 옵션을 켜고, Kubernetes Ingress 에서는 nginx 의 proxy-request-buffering 어노테이션을 off 로 둬야 한다.
- 초기에는 사내 VPN 으로만 접근 가능한 internal ingress 로 노출했는데 대용량 업로드가 너무 느렸다. 상대 속도가 internal SSL 1, internal 비SSL 4, external SSL 10 수준이었고 VPN 터널링 지연이 원인이라 판단해 external ingress 로 바꾸고 Cloud Armor 로 접근을 제어했다.
- 마지막 병합도 병목이었다. GCS 의 compose 는 한 번에 청크 32개만 다룰 수 있어 수백 개면 수 분 이상 걸리고, 공개 코드는 업로드마다 GCS 에서 object 정보를 읽어와 청크가 늘수록 느려졌다. 병합을 병렬화하고 청크 정보를 Redis 에 캐싱해 해결했다.
- 남은 과제로 관리자가 업로드를 즉시 중단하는 기능, 업로드 현황 대시보드, 클라이언트와 Tus 서버만 아는 진행 상황을 CMS 백엔드에 이벤트로 알리는 기능을 꼽는다.
왜 읽나영상처럼 큰 파일을 다루는 운영툴과 업로드 서버를 처음 설계하는 팀에게 프레임워크 선정 기준부터 클라우드 런타임 제약까지 부딪힌 순서 그대로 보여주는 개발기.