streamId라는 필드 이름이 같다고 각 서비스가 같은 대상을 가리키는 것은 아닙니다. 미디어 서비스는 방송 한 회차를 식별하고 싶었고, 기존 소비자는 그 값을 실제 녹화 조각을 찾는 경로로 사용했습니다. 생산자에서 더 적절해 보이는 ID로 바꾸기만 하면, 소비자에서는 정상 요청의 조회 결과가 0행이 될 수 있었습니다.
저는 방송 회차를 분리하자는 제안을 내면서 Spring 소비자가 그 값으로 어떤 조회를 하는지 확인했습니다. 팀원의 답변을 받은 뒤 물리 경로를 ingestStreamId로 따로 남기고, 소비자를 먼저 배포한 다음 새 의미의 streamId를 발행하기로 했습니다. 하나의 필드를 바꾸는 일에 데이터 조회와 배포 순서가 함께 들어온 이유를 설명합니다. Spring 소비자의 주 구현은 팀원이 맡았습니다.
송출 경로와 방송 회차는 수명이 다르다
송출 키와 실제 인제스트 경로는 여러 날에 걸쳐 재사용할 수 있습니다. 반면 월요일 방송과 화요일 방송은 각각의 시작·종료, 원본, 클립을 갖는 별도 회차입니다. 재사용되는 경로를 회차의 유일한 ID로 쓰면 같은 계정의 여러 방송을 구분하기 어렵습니다.
그래서 이벤트의 streamId를 session_id, 즉 논리적인 방송 회차로 정의하려 했습니다. 그러나 기존 소비자는 물리 경로와 시간 범위로 조각을 조회하고 있었습니다. “필드의 의미를 바로잡자”는 생산자의 제안만으로는 해결되지 않았습니다. 실제로 어떤 조회가 실행되는지, 이전 데이터의 NULL 행과 회차 앞부분을 어떻게 읽는지까지 함께 고려해야 했습니다.
| 필드 | 계약에서 정한 의미 |
|---|---|
streamId |
논리적인 방송 회차인 session_id |
ingestStreamId |
실제 녹화·조각 조회·재생 경로 |
streamerId |
사용자 ID |
eventId |
전달된 이벤트 한 건의 중복 식별 |
sequence |
같은 방송 회차 안에서의 순서 |
필드를 늘리는 비용은 생깁니다. 하지만 하나의 이름에 물리 경로와 논리 회차를 동시에 담는 비용보다 명시적인 두 필드가 낫다고 판단했습니다. 소비자가 파일을 찾는 데 필요한 좌표를 없애지 않으면서, 회차의 의미를 별도로 고정할 수 있기 때문입니다.
소비자의 답변을 받고 필드와 배포 순서를 함께 바꿨다
2026년 9월 3일의 협의는 변경 영향 공유에서 시작했습니다. 기존 조각 조회가 깨질 수 있음을 먼저 적고 소비자 측 대응을 확인했습니다. 팀원은 물리 경로와 시간 범위를 유지해야 한다고 답했고, 이를 별도 필드로 수용했습니다.
| 시각 | 확인한 내용 |
|---|---|
| 15:10 | 회차 ID로 바꾸면 기존 조각 조회에 영향이 있음을 공유하고 대응 요청 |
| 16:06 | 팀원이 물리 경로+시간 범위 유지, 회차 앞부분·기존 NULL 행·배포 순서 고려를 답변 |
| 16:19 | ingestStreamId를 추가하고 소비자 배포 완료 뒤 새 값을 발행하기로 확정 |
팀원의 답변으로 제가 바꾼 것은 이름만이 아니었습니다. 기존 NULL 행과 회차 앞부분도 조회해야 하므로, 소비자의 물리 경로·시간 범위 조건을 유지할 좌표가 필요했습니다. 이를 ingestStreamId에 남기고 새 회차 식별자와 분리한 계약을 위키 PR #113에 반영했습니다. 당시 확인 요청과 답변 기록에서도 이 변경 과정을 확인할 수 있습니다.
값을 먼저 바꾸고 소비자를 나중에 고치는 순서는 피했습니다. 새 필드를 이해하는 소비자가 배포된 뒤 생산자가 새 의미의 값을 발행해야 합니다. 필드 정의와 배포 순서를 한 변경으로 본 이유입니다. 계약 문서만 맞아도 전환 순간의 실제 조합이 맞지 않으면 조회는 여전히 깨질 수 있습니다.
종료 판정도 소비자마다 반복하지 않게 했다
ID의 의미를 분리해도 방송이 언제 끝났는지 서비스마다 다르게 판단하면 상태가 어긋납니다. SRT 연결이 끊기거나 runOnOffline이 호출된 사실만으로 사용자 종료 버튼과 일시 단절을 구분할 수는 없습니다. 의도적인 종료와 무신호 타임아웃을 구분하고, 마지막 파일의 꼬리 정산을 마친 뒤 ended를 확정하는 방향으로 계약을 정리했습니다.
미디어 경로가 회차의 시작·종료를 확정하고 소비자는 이를 상태로 반영합니다. 이벤트 한 건은 eventId로, 회차 안의 순서는 sequence로 다룹니다. 이것이 중복 전달이나 역순 도착이 없다는 보장은 아닙니다. 오히려 중복과 순서를 소비자가 판단할 정보를 계약에 포함한 것입니다. ended가 먼저 오는 경우의 종료 표식·placeholder도 이 상태 계약의 검토 대상에 두었습니다.
이 계약을 실어 나를 경로로는 SNS FIFO에서 서비스별 SQS FIFO·DLQ로 분기하는 구조를 설계했습니다. 각 서비스가 독립적으로 이벤트를 받게 하려는 선택입니다. 이 글에서 확인한 완료 범위는 식별자·생명주기·전환 순서의 합의이며, 전체 생산자와 소비자의 운영 배선은 각 구현에서 별도로 확인해야 합니다.
호환성은 생산자의 새 의미만으로 정해지지 않는다
처음 제안은 재사용되는 송출 경로에서 방송 회차를 분리하는 데 초점을 뒀습니다. 소비자와 대조하니 그 경로가 이미 파일 조회의 좌표로 쓰이고 있었습니다. 저는 이 의존을 없다고 가정하는 대신 새 필드로 보존했고, 소비자가 두 의미를 구분할 준비가 된 뒤 생산자를 바꾸는 순서까지 합의했습니다.
필드의 타입과 이름이 같아도 호환성이 유지되는 것은 아니었습니다. 그 필드로 실행하는 쿼리와 기존 데이터, 전환 순간에 함께 떠 있을 버전까지 맞아야 했습니다. 이번 계약 변경은 새 의미를 추가하면서도 기존 조회를 잃지 않는 데서 시작했습니다.
이 글은 PokeClip·MediaMTX 기술 기록 모음의 일부입니다. 포트폴리오 전체본 19쪽에서 연결된 사례도 함께 볼 수 있습니다.