10번 조각은 업로드 완료, 11번은 아직 대기, 12번은 완료인 상태를 생각해 봅니다. 가장 큰 완료 seq가 12라고 해서 목록을 그곳까지 공개하면 플레이어는 아직 없는 11번 파일을 요청합니다. 반대로 11번을 조용히 빼 버리면 영상이 점프합니다. 목록 문법이 맞는 것과 공개할 범위가 맞는 것은 다른 문제였습니다.

저는 연속으로 확정된 구간까지만 공개하는 규칙을 먼저 고정했습니다. 그리고 DB·S3·HTTP·파일 접근을 제외한 부품에서 경계와 HLS 문장을 계산하도록 나눴습니다. 발행이 실패한 것과 잘못된 목록을 만든 것을 따로 조사할 수 있게 하기 위해서입니다. 구현·검증은 AI와 협업했고 공개 규칙과 검증 범위는 제가 정했습니다. 2026년 9월 25일 PR #196으로 병합됐습니다.

출력 규칙을 고정한 다음 비용을 봤습니다. 10만 행 원장에서 최근 900조각 목록을 만드는 전체 렌더는 0.280ms였습니다. 그러나 과거 원장을 보관하는 heap은 11.58MiB로 늘었습니다. 공개할 창의 계산과 원장을 얼마나 오래 들고 있을지는 별개의 문제였습니다.

최신 행이 아니라 연속으로 확정된 앞부분

창의 최신 경계인 머리는 컷오프부터 연속으로 상태가 확정된 구간의 끝입니다. 10번 uploaded 뒤에 11번 pending이 있으면 12번 uploaded까지 넘어가지 않습니다. 반면 11번을 확정 GAP으로 판정했다면 파일이 없는 구간이라는 상태를 목록에 남길 수 있습니다. “아직 기다리는 중”과 “없는 것으로 확정”을 구별해야 공개 경계가 결정됩니다.

행 상태 경계 계산의 의미
uploaded 공개 가능한 조각
확정 GAP 없는 구간으로 확정, 상태를 보존
pending 뒤의 완료 조각까지 경계를 전진하지 않음
미확정 seq 구멍 원인을 판정할 때까지 경계를 멈춤

꼬리는 원장 첫 행이 아닙니다. 공개 가능한 머리에서 거꾸로 최근 1시간을 채울 가장 늦은 행을 구해야, 오래 방송할수록 목록이 계속 커지지 않습니다. 실제 판정은 경계 계산 코드에 있습니다.

창이 밀려도 불연속 번호는 같은 규칙이어야 합니다

렌더에서 DISCONTINUITY 태그를 붙이는 기준과, 오래된 행을 축출할 때 DISC-SEQ를 늘리는 기준이 다르면 플레이어가 전후 목록을 다르게 읽을 수 있습니다. HasDiscontinuityTag라는 행 단위 판정을 두 곳에서 공유했습니다. 같은 행이 어느 창에서 빠지든 같은 누적값이 나와야 합니다.

구성은 경계 계산, 렌더·검사·봉인, 캐시, 부팅 장부 읽기로 나눴습니다. 순수 부품은 외부 저장소에 직접 닿지 않습니다. 목록 공개 전 S1~S7 검사를 적용하고 종료용 ENDLIST를 붙이는 부품도 분리했습니다. 지원하지 않는 델타 기능을 광고하지 않도록 HLS VERSION은 6으로 선택했습니다. HLS 규격의 문법을 따르는 것과 제품의 공개 규칙을 함께 검사한 셈입니다.

출력 바이트와 파서 수용을 각각 확인했습니다

공개 규칙과 불연속 누계를 구현한 뒤에는 테스트 기대값을 같은 렌더 함수로 만들지 않았습니다. 900조각의 기대 목록을 독립 Python 생성기로 만들고 실제 출력 바이트와 대조했습니다. 같은 잘못된 규칙을 기대값 생성까지 재사용하지 않기 위해서입니다. 이어 hls.js 1.5.15·1.6.0·1.7.3 파서에서 구문 오류 0을 확인해 출력 내용과 파서 수용을 따로 봤습니다.

골든, 경계, 결함 주입, race, PostgreSQL 읽기 검증도 포함했습니다. 다만 파서가 받는다는 사실은 브라우저가 모든 미디어를 끝까지 재생했다는 뜻이 아닙니다. PG 통합 검증도 부팅 재구성 SELECT까지입니다. S3 발행 루프, 조건부 PUT, 종료 전이를 호출하는 연결은 다음 계층입니다.

10만 행이 있어도 보여 줄 창은 900조각입니다

2026년 10월 6일에는 합성 원장에서 최근 1시간, 4초 조각 900개를 고정한 채 보유 원장만 늘렸습니다. 조건별 1초씩 10회 측정했습니다. 최초 계산과 직전 tail을 이용한 갱신은 동일 구현의 서로 다른 작업입니다.

원장 행 수 최초 계산 tail 갱신 보유 heap
900 3.933μs 4.162μs 0.106MiB
5,400 21.324μs 3.646μs 0.680MiB
21,600 71.739μs 2.855μs 2.393MiB
100,000 320.410μs 2.952μs 11.58MiB

10만 행 원장에서 최근 900조각 목록을 만드는 전체 렌더는 0.280ms, 약 507KB/회 할당이었습니다. RowsFrom·경계 계산의 0할당을 전체 렌더 0할당이라고 쓰지 않습니다. 초기 320.410μs와 갱신 2.952μs도 수정 전후가 아니므로 개선 배수로 만들지 않습니다. 측정 표에 각 범위를 분리했습니다.

처음 계산은 원장이 커질수록 3.933→320.410μs로 늘었지만, 직전 tail을 쓰는 갱신은 10만 행에서도 2.952μs였습니다. 최근 창을 다시 찾는 작업 범위는 작게 유지할 수 있었습니다. 그렇다고 원장 보관 비용이 사라지지는 않았습니다. heap은 0.106→11.58MiB로 증가했으므로, 계산 경로를 줄이는 결정과 과거 행의 보관 수명은 따로 다뤄야 합니다.

다음 발행 계층에 넘길 수 있게 된 규칙

미해결 구멍, 공개 GAP, 목록 복사의 독립성도 반례로 검사했습니다. 이 부품에서 확정한 것은 11번이 미완료면 12번을 먼저 공개하지 않는 규칙, 그리고 같은 상태에서 같은 목록을 만드는 규칙입니다. 벤치마크에는 DB·네트워크 요청이 없고 main 서비스 연결도 아직 남아 있어 운영 처리량이나 시청자 수로 환산할 수는 없습니다.

이제 다음 발행 계층에서는 올바른 목록을 만들었는지와 그 목록이 최신 상태로 공개됐는지를 나눠 확인할 수 있습니다. 늦은 writer가 새 목록을 덮는 문제는 경계 계산을 다시 최적화해서 해결되지 않습니다. 공개할 내용을 먼저 고정한 뒤 세대·권한과 조건부 쓰기로 이어 가는 순서를 택한 이유입니다.


이 글은 PokeClip의 개발·검증 기록을 2026년 10월 15일 기준으로 정리한 CASE 27입니다. 전체 기술 기록에서 연결되는 결정을 함께 읽을 수 있습니다.