MediaMTX에서 로거를 교체하는 동안 이전 훅이 종료 로그를 쓰는 경합을 조사했습니다. 저는 훅을 먼저 끝내고 로거를 교체하도록 순서를 바꿨습니다. 정한 테스트는 통과했지만, 3초 뒤 끝나는 훅에서는 RTSP 리스너 부재 중앙값이 0.248ms에서 3,014.39ms로 늘었습니다. 후속 리뷰에서는 이 기다림이 교착으로 이어질 수 있는 경로까지 지적됐습니다.

제안한 PR #6206은 2026년 10월 11일 병합 없이 닫혔습니다. 유지보수자 aler9가 PR #6326에서 로거를 닫지 않고 다시 읽는 방식으로 대체했습니다. 제가 만든 종료 순서 테스트가 무엇을 확인했고 무엇을 놓쳤는지, 왜 최종 접근이 달라져야 했는지 남깁니다. #6326의 구현은 유지보수자의 기여입니다.

종료 요청과 로거 사용 종료가 어긋났다

MediaMTX는 실행 중 로그 수준이나 목적지를 바꿀 수 있고, 동시에 외부 훅 명령도 실행합니다. 훅이 끝나면서 OnExit를 통해 Core.Log()를 호출하는데, 재설정 경로가 이미 로거를 닫고 교체 중이면 로거의 수명과 훅의 수명이 어긋납니다.

Cmd.Close()는 종료 신호를 보내고 반환하지만 실제 명령이 끝날 때까지 기다리는 함수는 아니었습니다. 반면 externalCmdPool.Close()는 훅 고루틴들의 종료를 기다릴 수 있었지만, 당시에는 shutdown에만 연결돼 있었습니다. 로거를 재생성하는 reload에서도 실행 중 훅이 남을 수 있다는 것이 문제의 경계였습니다.

초기에는 로거 접근을 보호하는 방향을 제안했습니다. 유지보수자는 매 로그 줄의 접근 보호 비용과 기존 구조와의 차이를 지적했고, 로거를 쓰는 구성 요소를 먼저 닫은 뒤 재생성하는 방향을 제안했습니다. 저는 그 의견을 반영해 훅 풀을 drain하는 방식으로 수정했습니다. 조사·구현·실측에는 AI 협업을 활용했고, 수정 방향의 전환과 검증 결과를 공개 PR에 반영했습니다.

수정한 순서가 실제로 지켜지는지 확인했다

두 번째 접근은 newConf == nil || closeLogger일 때 외부 명령 풀을 닫고 기다린 뒤 로거를 교체하는 것입니다. 로그 설정이 바뀌지 않는 reload에서는 훅을 건드리지 않고, Core.Log() 자체도 바꾸지 않았습니다.

재현에는 SIGINT를 받아도 release 파일이 만들어질 때까지 끝나지 않는 runOnInit 훅을 사용했습니다. 단순히 잠시 기다린 뒤 결과를 보는 대신, 실행 중인 훅의 완료 시점을 통제했습니다.

하위 테스트 확인하려는 계약
로거 변경 외부 명령 풀이 drain되는가
기존 훅과 다음 훅 이전 훅의 종료 뒤 다음 훅이 시작하는가
로거 불변 불필요하게 훅을 종료하지 않는가

수정 전 start → start → exit였던 기록은 start → exit → start로 바뀌었습니다. PR에는 macOS/arm64와 Linux 컨테이너에서 -race -count=20을 통과한 기록이 있습니다. 이 테스트는 제가 release 파일을 만들어 주면 훅이 끝나는 조건에서 이전 훅과 다음 훅의 순서를 확인했습니다. 그 조건은 이후 리뷰에서 드러난 한계를 이해하는 데도 중요합니다.

안전한 순서의 비용을 리스너 부재 시간으로 쟀다

훅 종료를 기다리는 동안 다른 구성 요소가 이미 닫혀 있다면 서버가 요청을 받을 수 없는 시간이 늘어납니다. 그래서 수정본의 정상 통과만 확인하지 않고 RTSP 리스너가 없는 구간을 별도로 측정했습니다.

측정값 M′는 비-race 빌드에서 로그 수준을 바꿔 reload를 유발하고, 구조화 로그의 [RTSP] closing부터 다음 started까지 추출한 값입니다. 총 50회 원시 기록을 조건별로 나눴습니다.

조건 전후 각각의 실행 수 수정 전 중앙값 제안 수정 후 중앙값
훅 없음 10회 0.229ms 0.239ms
SIGINT 뒤 3초 후 끝나는 훅 10회 0.248ms 3,014.39ms
기본 설정 대조 5회 0.201ms 0.194ms

훅이 없는 대조에서는 전후 모두 약 0.2ms였지만, 3초 뒤 끝나는 훅을 넣자 리스너가 없는 시간도 약 3초가 됐습니다. 종료 순서를 맞추기 위해 기다린 시간이 그대로 서비스 재개 경로에 들어온 것입니다. 저는 이 값을 수정의 부작용으로 남겼습니다. 훅이 끝나지 않는 경우까지 고려하면 대기 상한도 이 실험으로 정할 수 없었습니다.

기다리는 대상이 재설정을 일으키면 순환이 된다

후속 리뷰에서 유지보수자는 기다리는 외부 명령이 로거 재생성을 일으키는 경우를 지적했습니다. reload는 명령이 끝나기를 기다리고, 명령은 자신이 유발한 reload가 끝나기를 기다리면 순환이 됩니다. 앞의 release 파일 테스트에서는 완료를 외부에서 풀어줬기 때문에 이 관계를 만들지 못했습니다. 느린 훅의 3초 대기보다 더 근본적인 반례였습니다.

유지보수자의 최종 수정 #6326은 로거를 닫지 않고 다시 읽는 방향이었습니다. 제 drain 방식은 채택되지 않았고, 공개 기록도 “리뷰 중”에서 “미병합 종결”로 갱신했습니다.

제가 확인한 start → exit → start 순서는 필요한 조건이었지만 충분하지 않았습니다. 그 순서를 만들기 위해 기다리는 작업이 재설정의 완료에 의존하면 끝낼 수 없었습니다. 이후 수명 문제에서는 무엇을 먼저 끝낼지와 함께, 그 작업이 끝나려면 무엇이 먼저 완료돼야 하는지를 확인해야 합니다. 통과한 재현과 측정한 비용, 최종 반례를 함께 남겨야 다음 수정에서 같은 대기 구조를 반복하지 않을 수 있습니다.


이 글은 PokeClip·MediaMTX 기술 기록 모음의 일부입니다. 포트폴리오 전체본 44쪽에서 연결된 사례도 함께 볼 수 있습니다.