PokeClip의 재생목록을 정적 파일로 발행하려면 다음 경쟁을 막아야 합니다. 작업 A가 10번 조각까지 읽고 멈춘 사이 B가 11번까지 포함한 목록을 공개합니다. 뒤늦게 A의 PUT이 성공하면 목록이 다시 10번으로 돌아갑니다. 시청자는 방금 보던 조각을 잃을 수 있습니다.

저는 시청 요청마다 DB를 조회하는 대신 파일 상태가 바뀔 때 S3·CDN으로 목록을 발행하는 방향을 택했습니다. 같은 목록을 반복 계산하는 읽기 비용을 줄이는 대신, 누가 어느 세대를 공개할 권한이 있는지를 쓰기 경로에서 관리해야 했습니다. DB는 복구 기준으로, S3 목록은 그 장부에서 다시 만들 수 있는 결과물로 두었습니다.

2026년 10월 28일 갱신: 초기 설계에 10월 17일 실제 S3 발행 실측과 10월 19일 발행 계층 병합 결과를 추가했습니다. 발행 계층의 완료와 DVR 전체 시청 경로의 완료는 구분합니다. 구현·리뷰는 AI와 협업했으며, 저는 읽기 경로를 단순하게 만드는 대가로 어떤 쓰기 책임을 맡을지 정했습니다.

시청 부하를 줄이면 발행 쪽 책임이 늘어납니다

매 요청마다 목록을 합성하면 같은 방송을 보는 사람이 늘수록 비슷한 DB 조회와 계산을 반복합니다. 정적 발행은 변경할 때 계산하고 여러 시청자가 그 결과를 읽게 할 수 있습니다. 대신 DB 확정과 S3 PUT은 하나의 트랜잭션이 아니므로, 그 사이에 프로세스가 죽거나 작업 순서가 바뀌는 상황을 직접 다뤄야 합니다.

구성 기준으로 삼는 것 별도로 다뤄야 할 실패
요청 시 합성 요청 시점의 DB 상태 같은 목록의 반복 조회·계산
파일 변경 시 정적 발행 DB 장부에서 정한 공개 상태 늦은 쓰기, 중간 종료, 불명확한 commit

정적 발행을 택한 뒤에도 S3의 현재 파일만 보고 복구할 수는 없었습니다. 그 파일은 A가 뒤늦게 쓴 과거 목록일 수 있습니다. 무엇을 공개해야 하는지 다시 계산하려면 파일보다 앞선 확정 기록이 필요합니다. 그래서 DB 장부에서 공개 상태를 결정하고 S3 파일은 다시 만드는 방향으로 복구 책임을 모았습니다.

세대 번호 하나로는 모든 경합을 막지 못합니다

발행에는 서로 다른 질문이 있습니다. “이 작업자가 아직 쓸 권한이 있는가”, “다음에 만들 세대는 무엇인가”, “지금 덮을 S3 객체가 내가 예상한 버전인가”입니다. 이 질문을 한 카운터에 몰아넣지 않고 단계별로 구분했습니다.

장치 맡긴 판단
DB 장부 무엇을 공개하고 어디서 복구할지
fence 이전 작성자의 권한과 현재 권한 구분
manifest_gen + CAS 예상 DB 상태에서만 다음 세대 예약
S3 조건부 PUT 예상 객체 버전에서만 쓰기
확정·복구 절차 PUT 뒤 DB 확정이 불명확한 경우 재판정

앞의 A와 B에 적용하면 각 장치가 필요한 이유가 드러납니다. A가 옛 작성자라면 fence로 권한을 구분해야 하고, 유효한 작성자끼리 같은 다음 세대를 예약하려 하면 DB CAS로 예상 상태를 확인해야 합니다. 마지막 S3 쓰기에도 예상한 객체 버전이라는 조건이 필요합니다. 한 단계의 성공을 다른 단계의 권한으로 간주하지 않는 설계입니다.

Amazon S3 조건부 쓰기 문서의 객체 존재 여부·ETag 조건은 마지막 쓰기를 제한하는 도구입니다. DB의 작성 권한까지 검증해 주지는 않으므로, DB 예약 → 객체 갱신 → DB 확정 사이에서 관측과 재시도 규칙을 연결해야 합니다.

세대와 워터마크는 재생목록 본문에 임의 문법으로 넣지 않고 객체 메타데이터로 분리했습니다. 플레이어가 읽는 HLS 문장과 발행 시스템의 제어 정보를 섞지 않으려는 선택입니다. 이 설계의 단계와 남은 검증은 전체본 30쪽에 요약했습니다.

종료와 GAP도 같은 상태 전이를 거칩니다

종료 표시 ENDLIST만 별도 경로로 쓰면 일반 목록에 넣은 세대 검사가 무용해질 수 있습니다. 종료 목록이 늦은 쓰기에 덮이거나 이미 닫힌 목록이 다시 열릴 수 있기 때문입니다. 따라서 종료 발행도 같은 예약·확정 규칙을 거치도록 설계했습니다. 종료한 목록은 유지하고 새 방송은 새 회차 URL을 쓰는 방향입니다.

실패한 조각을 목록에서 조용히 빼는 방법도 피했습니다. 조각을 없애면 순서와 시각의 의미가 바뀔 수 있어, 확정된 GAP으로 남기도록 설계했습니다. DVR은 최근 1시간 창을 갱신하고 종료 뒤 VOD는 같은 미디어 URI를 재사용합니다. 동결 상태는 본문 전체를 DB에 쌓기보다 해시·키·ETag·창 기술자로 재현할 수 있게 남기는 방향입니다.

설계 당시 받아들인 비용과 통합 과제

2026년 9월 29일 정정한 설계 산식의 상한은 약 2,820 DB 왕복/시간/스트림입니다. 세대 예약·확정·fence 갱신 등이 늘어나는 비용을 계산한 값입니다. 트래픽을 흘려 측정한 DB 부하나 운영 처리량이 아니며, 이 수치로 지원 스트림 수를 산출하지 않습니다.

10월 12일의 설계 검토 시점에는 생산 계층 PR #192와 목록 렌더·경계 PR #196은 병합됐습니다. 당시 S3 발행 루프와 종료 전이를 연결하는 계층은 진행 중이었습니다. 오래된 writer, 성공 여부가 불명확한 DB commit, 종료 경쟁, 작성자 사망 뒤 회수는 통합 경로에서 검증할 과제로 남겼습니다.

이때 정한 통합 검증의 출발점도 첫 예시의 A와 B입니다. A를 멈춘 뒤 B가 새 목록을 공개하게 하고, A가 돌아왔을 때 목록이 뒤로 가지 않는지 확인해야 합니다. PUT은 성공했지만 DB commit 결과가 불명확한 경우와 종료 중 작성자가 죽는 경우도 이어서 다뤄야 합니다. 정적 파일의 읽기 경로를 택한 만큼, 이 쓰기·복구 경로까지 통과한 뒤에 발행 정합성의 성과를 말할 수 있습니다.

10월 17일: 실제 S3에서 재시작 전후 발행을 확인했습니다

발행 계층을 연결한 뒤 compose 환경에서 720p30 영상과 AAC 음성을 90초간 합성 송출했습니다. 목록은 실제 S3에 발행했고, 송출 중간에 사이드카를 재시작했습니다. 구현과 실험 실행은 AI와 협업했으며, 저는 확인할 조건과 외부 쓰기 범위를 정했습니다. 아래는 당시 기록을 정리한 결과로, 글 작성 시 재실행한 벤치마크가 아닙니다.

확인할 조건 관측 결과
시작·재시작 뒤 writer가 DB의 현재 권한과 일치 두 시점 모두 일치
목록 세대와 S3 객체가 갱신 7회 표본에서 세대 2→9, ETag 변화
정상 발행 중 실패 로그 발행 중단·컷오프 부재·작업 실패·ERROR 각 0
재시작 뒤 같은 URL로 발행 재개 세대 9→11→19, 미디어 순번 비역행
종료 전이 뒤 추가 갱신 멈춤 세대 22에서 ETag 5회 연속 동일

원본 조각 22개와 재포장 조각 22개, init 1개, 목록 1개로 총 46개 객체가 생성됐습니다. 실험 뒤 해당 객체를 정리했습니다. 여기서 ETag 고정은 관측한 종료 전이의 결과입니다. 모든 ENDLIST·종료 경합을 해결했다는 의미로 확장하지 않습니다.

발행·종료 전이 구현은 PR #289로 10월 19일 병합됐습니다. 따라서 초기 글의 “발행 계층 구현 중”은 이 결과로 갱신합니다. 다만 위 90초 실험은 모든 장애 조합의 검증이 아닙니다. DB commit 결과가 불명확한 창, 장시간 부하, CDN·제어면·플레이어를 연결한 전체 시청 경로는 별도 과제입니다.

정적 발행을 선택한 이유는 시청 요청마다 같은 목록을 계산하지 않기 위해서였습니다. 실제 S3 실험으로 확인한 성과는 사이드카 재시작 뒤에도 같은 URL의 세대가 되돌아가지 않고 발행을 이어간 것입니다. DB와 S3를 하나의 원자적 트랜잭션으로 만들었다거나, 전체 처리를 exactly-once로 보장했다는 주장은 하지 않습니다.

Live·DVR 분리와 이어지는 검증

이 발행 계층은 Live와 DVR의 역할을 나눈 구조의 일부입니다. Live 목록은 MediaMTX가 제공하고, DVR 목록은 장부를 기준으로 S3에 발행합니다. 웹 되감기 기본 연동은 PR #436으로 10월 27일 병합됐습니다. 같은 날 작업 기록에서 자동 핸드오프는 일부 구현·검수 중이며, 합성 송출에서 발견한 Live·DVR 시각 불일치의 수정 설계도 남아 있습니다. 따라서 발행 복구가 확인됐다는 사실과 두 경로 사이의 자동 전환이 완성됐다는 주장은 구분합니다.

URL을 나눈 이유와 자동 전환에서 지켜야 할 사용자 상태는 Live와 DVR을 나누고 다시 연결하는 조건에 이어 정리했습니다.


이 글은 PokeClip의 개발·검증 기록을 정리한 CASE 20이며, 10월 28일에 후속 결과를 보완했습니다. 전체 기술 기록에서 연결되는 결정을 함께 읽을 수 있습니다.