Cloudflare Stream 도입기 4: 영상 버전과 단계별 운영 전환

정기창·

영상 제공자를 바꾸는 작업에는 플레이어 교체 외에도 데이터와 배포 순서가 얽혀 있었습니다. 기존 진도와 수료증의 의미를 유지하면서 Stream 전환을 구현한 과정을 정리합니다.

앞선 글에서는 영상 전달 경로, 서명 URL, 시청 구간 계산을 다뤘습니다. 마지막 글은 운영 중인 애플리케이션에 그 구현을 넣는 문제입니다. 핵심은 어떤 데이터를 유지하고, 어떤 순간부터 새 정책을 적용할지 명확히 정하는 것이었습니다.

영상 ID를 바꾸면 기존 진도는 무엇을 뜻할까요

강의 제목이 같아도 영상이 바뀌면 시청 기록의 의미가 달라집니다. 10분짜리 첫 영상을 모두 본 학생이 새 20분 영상을 모두 본 것으로 처리되면 안 됩니다. 제공자를 옮겼다는 이유만으로 원본이 같은지 자동으로 증명할 수도 없습니다. 따라서 이번 전환에서는 영상 제공자·ID·길이의 변경을 새 영상 버전으로 취급했습니다.

기존 진도 기록은 지우지 않고 이전 버전에 남깁니다. 현재 강의의 진도와 수료 판정은 현재 버전을 사용합니다. 반면 제목, 소개, 정렬 순서, 썸네일만 바꿀 때는 같은 영상으로 보고 버전을 유지합니다. 관리자가 어떤 필드를 수정했는지에 따라 학습 기록에 미치는 영향을 나눈 것입니다.

변경 항목영상 버전진도 처리
제공자·영상 ID·길이증가이전 기록 보존, 새 버전은 새로 기록
제목·소개·썸네일·정렬유지현재 기록 유지
강의 비공개미디어 변경과 별개신규 재생과 갱신에서 공개 상태 검사

이 선택에는 사용자 영향이 있습니다. 같은 콘텐츠를 옮긴 경우에도 새 버전의 진도는 처음부터 시작합니다. 기존 진도를 이관하려면 콘텐츠 동일성을 확인한 뒤 별도의 승인된 절차를 설계해야 합니다. 이번 구현에는 그런 자동 이관을 넣지 않았습니다.

스키마는 먼저 확장하고, 재생 정책은 명시적으로 바꿉니다

기존 데이터베이스는 영상 ID와 길이를 저장했습니다. 여기에 제공자 필드를 추가하고, Stream UID가 들어갈 수 있도록 ID 컬럼 길이를 넓혔습니다. 기존 행의 제공자 기본값은 YouTube로 유지합니다. 이 단계에서는 과거 ID를 Stream UID처럼 해석하거나 임의의 값으로 바꾸지 않습니다.

새 관리자 화면에서는 Stream 영상만 등록하도록 하고, 기존 YouTube 행에는 교체가 필요하다는 상태를 표시합니다. 학생 화면에서는 기존 제공자의 재생을 허용하지 않습니다. 즉, 데이터 보존과 기존 재생 유지 여부는 별도의 결정입니다. 이번 전환에서는 새 코드에서 YouTube 재생을 차단하고, 기존 데이터와 감사 근거는 남기기로 했습니다.

이 방식은 흔히 스키마를 먼저 확장하는 배포 전략과 닿아 있습니다. 다만 컬럼을 추가했다고 애플리케이션 전체의 하위 호환성이 저절로 보장되지는 않습니다. 새 코드의 필수 컬럼, 기능 플래그, 실제 배포된 서버 버전을 함께 확인해야 합니다.

관리자 입력과 공급자가 확인한 값을 구분합니다

관리자는 관리자 화면에서 원본 파일을 선택합니다. 서버가 짧은 유효기간의 업로드 주소를 발급하고, 브라우저가 tus 프로토콜로 Cloudflare에 직접 전송하는 흐름으로 확장했습니다. 파일 전송이 끝났다고 곧바로 강의에 연결하지 않고, 인코딩이 끝난 뒤 관리자가 저장해야 반영합니다. 기존 영상은 그때까지 유지합니다.

서버는 관리자가 입력한 주소를 따라가는 대신 공급자가 발급한 영상 ID로 정해진 API를 호출합니다. 서명 필수·허용 호스트·업로드 제한을 확인하고, 강의 길이는 공급자의 처리 결과로 결정합니다. 업로드 예약에 사용하는 최대 길이와 수료 기준에 사용하는 실제 길이는 다른 값입니다. 전송 중 끊기면 같은 주소로 재개하고, 인코딩이 완료된 미연결 영상은 보관함에서 다시 선택하도록 했습니다. 설계에 사용한 계약은 Cloudflare Direct Creator 업로드 문서에서 확인할 수 있습니다.

재생 토큰을 발급할 때도 제공자의 보호 설정과 저장된 길이를 확인합니다. 이 검증은 잘못된 상태를 조기에 발견하지만 외부 API 지연과 장애의 영향을 받습니다. 나중에 캐시를 넣는다면 얼마 동안 오래된 설정을 허용할 것인지까지 정해야 합니다. 호출 수를 줄이는 최적화가 접근 제어의 허용 범위를 바꿀 수 있기 때문입니다.

수료증은 발급 당시의 근거를 보관합니다

강의 목록은 바뀔 수 있지만 이미 발급한 수료증의 내용을 같은 방식으로 덮어쓰면 곤란합니다. 이번 구조에서는 발급 시점의 회차 ID·영상 버전·길이를 모아 커리큘럼 해시를 만들고, 수료 정책 버전과 함께 발급 근거를 저장합니다. 생성한 PDF 자체도 보관합니다.

해시는 커리큘럼의 내용이 달라졌는지 구분하는 식별 수단입니다. 그 값 자체가 수료를 증명하는 것은 아닙니다. 발급 직전 현재 공개 회차의 시청 구간과 수강권을 검사하는 절차가 따로 있어야 합니다. 이후 회차가 추가되면 새 기준의 발급 여부를 다시 판단하고, 기존 원본은 발급 당시 기록으로 유지합니다.

테스트마다 확인할 수 있는 범위가 다릅니다

외부 영상 서비스가 포함되면 모든 검증을 실제 운영 영상으로 돌리고 싶어질 수 있습니다. 하지만 권한 거절, 네트워크 지연, 만료와 로그아웃의 겹침은 재현 가능한 환경이 있어야 반복해서 확인하기 쉽습니다. 이번에는 로컬 DB와 모의 Cloudflare 응답, 모의 플레이어 SDK를 사용했습니다.

검증확인 범위그 검증만으로 알 수 없는 내용
단위 테스트서명·claim·구간 병합·입력 검증실제 CDN의 토큰 수락
로컬 DB 통합 테스트수강권·버전·순번·동시 요청·수료증실제 제공자 장애 특성
브라우저 테스트저장·탐색·갱신·실패 복구실제 영상의 화질·음성·인코딩
실제 영상 검수 — 예정실제 SDK·CDN·기기에서의 재생모든 예외 상황의 회귀 보장

로컬과 CI에서 모의 환경의 회귀 검증은 통과했습니다. 실제 강의 영상으로 하는 검수는 이 글을 쓰는 시점에 아직 남아 있습니다. 테스트 통과와 외부 서비스에서의 실제 재생 확인을 구분해 기록하는 이유입니다.

배포 순서도 기능의 일부로 취급합니다

이번 배포 절차는 필요한 스키마를 먼저 확인하고 적용한 뒤, 서버 전용 환경변수를 연결하고, 해당 변경이 포함된 커밋으로 백엔드와 화면을 배포하는 순서로 잡았습니다. 마이그레이션 전에는 기존 스키마와 실행 기록을 백업하고, 적용 후에는 컬럼·인덱스·외래 키를 기대한 정의와 비교합니다.

스키마 준비 플래그는 실제 마이그레이션 완료를 대신하지 않습니다. 비공개 API 토큰과 서명 개인키는 런타임 서버 설정에만 두고 프론트 빌드에 전달하지 않습니다. 마지막에는 배포 도구가 성공했다고 표시한 것과 별개로, 실제 올라간 커밋과 서비스 상태를 확인하도록 했습니다.

장애가 나면 무조건 이전 코드를 배포하면 된다고 가정하지도 않습니다. 컬럼 확장처럼 이전 코드가 읽을 수 있는 변경과, 되돌릴 수 없는 데이터 변경은 복구 절차가 다릅니다. 특히 중간에 실패한 DDL은 현재 상태를 확인하고 이어가야 합니다. 전체 스크립트를 무심코 다시 실행하면 이미 적용된 단계와 충돌할 수 있습니다.

학습 기록과 방문 분석은 목적에 맞게 나눕니다

전환 작업과 함께 주요 기능의 사용·완료·오류와 수강 신청 클릭 같은 행동을 분석 이벤트로 추가했습니다. 이 데이터는 서비스 이용 흐름을 보기 위한 것입니다. 학습 진도와 수료증 발급 근거는 서버 DB에 남기고, 분석 도구의 이벤트가 결제나 수료 여부를 결정하지 않도록 했습니다.

사용자가 입력한 원문과 학생·세션 식별자는 GA4로 보내지 않습니다. 이벤트 이름과 제한된 범주의 값만 사용하고, 분석 동의가 있을 때만 전송합니다. 동의 전에 시작한 행동이 동의 후에 끝났다고 해서 이전 행동을 소급 전송하지 않도록 경계도 검사했습니다. 실제 수집 활성화는 운영 설정과 고지 준비를 확인한 뒤 진행하는 별도 단계로 두었습니다.

영상 플랫폼 전환에서 오래 남을 코드는 플레이어 API 호출보다 상태의 경계를 정한 부분이라고 생각합니다. 영상 버전, 시청 구간, 발급 근거, 배포 상태를 따로 표현하면 나중에 제공자나 상품 구성이 바뀌어도 무엇을 다시 판단해야 하는지 찾기 쉬워집니다.

Cloudflare Stream데이터베이스배포GA4