서버가 정상적으로 떠 있어도 종료가 끝나지 않으면 재시작과 배포가 막힌다. MediaMTX의 인증서 로더에서 만난 문제는 조금 특이했다. 인증서 파일을 저장할 수 없는 환경에서도 서버는 실행됐지만, 종료할 때는 존재하지 않는 감시 작업을 기다렸다.
이 글은 개인 오픈소스 기여인 PR #6427의 재현 조건과 수정 근거를 정리한다. 작업은 AI 에이전트와 협업했다. 코드 조사·구현·실험 실행과 리뷰에는 에이전트의 도움을 받았고, 나는 실행 재현을 수정의 전제로 정하고 저장 성공·실패 두 경우를 테스트에 포함하도록 결정했으며, 상류 관례와 게시 내용을 확정했다. 아래 결과는 당시 실험 기록이며 이 글을 위해 새로 실행한 벤치마크는 아니다.
감시 작업의 종료를 기다리는 이유
CertLoader는 TLS 인증서를 읽고, 파일이 바뀌면 다시 불러오는 구성 요소다. Go의 고루틴은 동시에 실행되는 작업이고, 여기서 watch() 고루틴은 인증서 파일 변경을 감시한다.
종료 요청을 보냈다는 사실만으로 그 고루틴이 이미 끝났다고 볼 수는 없다. 그래서 #6417은 Close()가 done 채널을 기다리도록 바꿨다. watch()가 끝나며 이 채널을 닫으면 대기가 풀린다. 닫힌 채널에서의 수신이 더 이상 기다리지 않는다는 성질은 Go 언어 명세에 정의돼 있다.
문제는 watch()를 시작하지 않고 초기화에 성공하는 경로도 있었다는 점이다.
자동 생성한 인증서를 디스크에 저장하지 못하면 로더는 메모리의 인증서를 사용하되 파일 감시는 시작하지 않는다. 이 no-watch 경로는 #5815의 커밋에 이미 있었다. 여기에 무조건적인 완료 대기가 추가되면서 두 동작의 계약이 어긋났다.
| 초기화 결과 | watch() 시작 |
수정 전 done을 닫는 주체 |
Close() 결과 |
|---|---|---|---|
| 자동 인증서 저장 성공 | 시작함 | watch() |
감시 작업 종료 후 반환 |
| 자동 인증서 저장 실패 | 시작하지 않음 | 없음 | 끝나지 않는 대기 |
이는 작업 속도가 느린 문제가 아니었다. 대기 조건을 만족시킬 실행 경로가 없었다.
권한 조건을 고정해 회귀를 확인했다
서버 재현에서는 새 작업 디렉터리에 설정 파일만 두고 인증서 파일을 쓸 수 없게 했다. 읽기 전용 마운트를 사용하거나, 일반 사용자로 실행하면서 디렉터리 권한을 555로 제한하는 방식이다. root로 실행하면 쓰기 권한 검사 결과가 달라질 수 있으므로 실행 사용자도 조건에 포함해야 했다.
다음은 PR에 사용한 mediamtx.yml이다. 재현 대상인 MoQ(Media over QUIC)만 켜고 다른 서버는 껐다. 같은 숫자의 HTTP/2·HTTP/3 포트는 각각 TCP·UDP를 사용한다.
# mediamtx.yml — 재현용 새 디렉터리에 이 파일만 배치한다.
logLevel: debug
api: no
rtsp: no
rtmp: no
hls: no
webrtc: no
srt: no
moq: yes
moqHTTP2Address: :18892
moqHTTP3Address: :18892
moqQUICAddress: :18893
인증서 저장 실패 로그가 발생한 것을 확인하고 SIGINT를 보냈다. 2026년 10월 26~27일 기록에서 회귀가 있는 커밋 9540cb6c는 [MoQ] closing 이후에도 10초 넘게 실행 중이었다. 읽기 가능한 코드만 보고 원인을 단정하지 않도록, 같은 종료 동작을 네 조건으로 비교했다.
| 비교 조건 | 당시 관찰 |
|---|---|
9540cb6c, 읽기 전용 디렉터리 |
10초 초과 대기, 강제 종료 필요 |
9540cb6c, 쓰기 가능한 디렉터리 |
1초 이내 정상 종료 |
#6417 이전 b5df4626, 읽기 전용 디렉터리 |
1초 이내 정상 종료 |
| 수정 적용, 읽기 전용 디렉터리 | 1초 이내 정상 종료 |
이 시간은 해당 재현에서 종료 여부를 구분한 관측값이다. 일반적인 종료 성능이나 모든 환경의 최대 지연을 보장하는 수치는 아니다. 회귀는 v1.21.3에서도 존재했다.
종료 함수에 예외를 늘리지 않고 완료 상태를 맞췄다
수정은 Initialize()의 no-watch 분기에서 close(cl.done)을 호출하는 한 줄이었다. 감시 작업을 시작하지 않는다고 결정한 시점에, 기다릴 작업이 없다는 상태를 완료 채널에도 반영했다. 정확한 변경은 머지된 diff에서 확인할 수 있다.
Close()에서 별도 조건으로 대기를 건너뛰는 방법도 생각할 수 있다. 하지만 여기서는 초기화 단계가 이미 감시 작업의 존재 여부를 알고 있었다. 그 분기에서 완료 신호를 정리하면 종료 함수의 순서와 기존 감시 경로를 유지할 수 있었다.
한 줄 수정에도 두 경로가 필요했다. TestCloseAutoCert는 saved와 not saved를 표 기반 하위 테스트로 실행한다. 저장 실패 사례에서는 권한만 바꾸는 대신 auto.key라는 이름의 디렉터리를 먼저 만든다. 파일을 저장할 자리를 디렉터리가 차지하게 해 root로 실행하는 CI에서도 저장 실패를 유도한다.
각 사례는 임시 디렉터리에서 로더를 초기화하고, Close()가 반환하면 테스트용 완료 채널을 닫는다. 5초 안에 반환하지 않으면 테스트가 실패한다. 이 5초는 테스트의 실패 판정 상한이며 제품 코드에 추가한 종료 타임아웃이 아니다.
수정 전 코드에 같은 테스트를 적용하면 not saved 사례가 실패하고, 수정 후에는 두 사례가 통과했다. 저장 실패만 통과시키는 것으로 끝내지 않고, 정상 감시 경로도 계속 종료되는지 함께 확인한 것이다.
이전 종료 PR과 이번 수정은 구분한다
이전에 제안했던 #6261은 감시 고루틴이 끝날 때까지 기다리도록 하는 별도 작업이었다. 그 PR은 머지되지 않고 닫혔으며 메인테이너의 #6417로 대체됐다. 기존 인증서 watcher 종료 글은 그 제안의 기록이다.
이번 #6427은 #6417 이후 no-watch 경로에서 생긴 교착을 수정한 PR이다. 두 PR의 테스트 결과나 머지 성과를 합치면 기여 내용이 달라진다. #6427은 한국 시각 2026년 10월 27일 05:03에 065f94c2로 머지됐다. 10월 28일 확인한 최신 릴리스는 v1.21.3이므로, 이 글에서는 상류 main 머지까지를 결과로 기록한다.
영향 범위도 좁혀야 했다. 당시 코드에서 자동 인증서를 허용하는 AllowAuto: true를 사용하는 실제 서버 경로는 MoQ였다. 범용 HTTP 서버 전체에 같은 장애가 있었다고 확대하지 않는다. 또한 이 변경은 인증서 저장 실패를 해결하거나 파일시스템을 쓰기 가능하게 만드는 수정이 아니다. 저장하지 못한 인증서를 사용하는 경로에서도 종료가 완료되도록 한다.
종료 대기를 추가할 때 확인할 것은 정상 작업의 완료뿐만이 아니었다. 작업을 시작하지 않는 성공 경로까지 포함해, 대기하는 쪽과 완료를 알리는 쪽이 모든 분기에서 짝을 이루는지 확인해야 했다. 이번에는 그 계약을 초기화 분기에 한 줄로 표현하고, 저장 성공·실패 두 사례로 남겼다.