선택 모듈 — 오디오를 실제로 흘려보내기

메타데이터와 집계만 있던 파이프라인에 실제 재생 경로를 붙였다. 제약: 기존 코드 무변경 — streaming/ 패키지와 audio_asset 테이블만 추가.

원본 음원은 ffmpeg 합성 사인파다. 저작권 문제가 없고, 길이를 track.duration_ms 와 정확히 일치시킬 수 있어 검증이 깨끗하며, 저장소엔 바이너리 대신 재현 방법을 버전관리한다. 트랙마다 주파수를 trackId 에서 유도해서 “다른 트랙인데 같은 소리” 같은 배선 오류를 귀로 잡을 수 있게 했다.

HLS 구조 — RFC 8216 을 코드에 대응시키기

media-store/{trackId}/
├─ master.m3u8          마스터 플레이리스트: "무엇을 고를 수 있나" (EXT-X-STREAM-INF)
├─ v64k/playlist.m3u8   미디어 플레이리스트: "어떤 순서로 재생하나" (EXTINF, ENDLIST)
├─ v64k/seg_000.ts ...  MPEG-TS 세그먼트 (AAC)
└─ v128k/...

variant 인코딩·분할은 ffmpeg 에 맡기고 마스터 플레이리스트는 직접 썼다BANDWIDTH(비트레이트 + TS 오버헤드)와 CODECS(“mp4a.40.2” = AAC-LC)가 어디서 오는지 코드에 남는다. 플레이어는 이 두 값으로 다운로드 전에 variant 를 고른다. ABR 의 선택 근거가 마스터에 선언돼 있다는 게 HLS 구조의 핵심이다.

검증은 ffprobe 교차 검증 — 우리가 만든 산출물을 우리가 아닌 도구가 “AAC + MPEG-TS, 길이 N초”로 읽는지 확인한다. 생성기와 검증기가 같으면 순환 논증이다.

Range Request(206) 와 HLS 의 관계

같은 문제(“파일 일부만 전송”)를 자르는 주체가 다르다.

Range (프로그레시브) HLS
자르는 주체 클라이언트, 바이트 단위 서버, 미디어 단위(미리)
탐색 Range: bytes=N- 재요청 다른 세그먼트 요청
화질 전환 표현 불가 variant 의 같은 시점 세그먼트
CDN 부분 요청이라 비효율 세그먼트 = 완결된 정적 파일

Spring 은 ResponseEntity<Resource> 를 반환하면 Range 를 자동 처리한다 — 세그먼트에 bytes=0-99 를 보내면 206 + Content-Range 가 온다. HLS 플레이어는 안 쓰지만 두 방식의 차이를 설명하는 살아 있는 데모가 된다.

사전 생성 vs 온디맨드 — 측정이 가설을 기각한 네 번째 사례

패키징을 입수 후 미리 할 것인가, 첫 재생 요청 때 할 것인가. 측정을 위해 audio_asset 테이블에 packaging_ms/storage_bytes 컬럼을 스키마 단계에 넣었다.

실카탈로그 21트랙(평균 220초) 측정:

지표
패키징 시간 평균 5.4초 (2.6~7.9초)
스토리지 5.51 MB/트랙 (비트레이트 역산과 일치)
5만 트랙 외삽 269 GB, 8워커 병렬 약 9.4시간

가설은 “온디맨드도 1초쯤이면 쓸 만하다”였다. 실측 5.4초로 기각. 음악 재생의 기대 반응시간은 백 ms 대다. Zipf 분포로 “롱테일은 안 들으니 아끼자”는 온디맨드 옹호 논리도 뒤집으면 “롱테일 청취자만 5초 페널티”다. 사전 생성을 기본으로, 온디맨드는 같은 준비 경로를 쓰므로 한계 비용 0 으로 보조·복구용으로 남겼다.

재사용된 패턴들

새 도메인인데 새 개념이 거의 없었다.

  • 동시 준비 선점: UNIQUE(track_id) INSERT 원자성 (M1 재처리 claim 패턴)
  • 상태 머신: PREPARING → READY | FAILED (ingest_failure 와 동일)
  • ffmpeg 실행 중 트랜잭션을 잡지 않는다 (M1 IngestService 와 동일 — 외부 프로세스가 커넥션 풀을 고갈시킨다)
  • path traversal 방어: normalize + startsWith (M1 입수 API 와 동일)

패턴의 재사용이 곧 설계 일관성이라는 게 이 모듈의 결론이다.

상세: ADR-0006, HLS 구현 노트