홈시리즈멘토링

© 2026 정기창. All rights reserved.

본 블로그의 콘텐츠는 CC BY-NC-SA 4.0 라이선스를 따릅니다.

☕후원하기소개JSON Formatter러닝 대기질개인정보처리방침이용약관

© 2026 정기창. All rights reserved.

콘텐츠: CC BY-NC-SA 4.0

☕후원하기
소개|JSON Formatter|러닝 대기질|개인정보처리방침|이용약관

주석이 거짓말을 하고 있었습니다 — 이스케이프가 실제로 막는 범위

정기창·2026년 8월 19일

앞 편에서 미뤄둔 것이 하나 있었습니다. 테스트를 고치다 발견한 같은 종류의 누락입니다. 영상에 자막을 얹을 때 넘기는 경로 하나가 이스케이프 함수를 거치지 않고 그냥 들어가고 있었습니다.

작은 후속 작업으로 봤습니다. 함수를 한 번 감싸면 끝나는 일이니까요. 그런데 그 반나절짜리 작업에서 리뷰가 저를 두 번 세웠고, 두 번째는 이 시리즈에서 가장 조용한 종류의 오류였습니다.

먼저, 왜 테스트가 이걸 못 잡았나

이상한 점이 하나 있었습니다. 그 코드에도 테스트가 있는데 통과하고 있었습니다.

이유는 단순했습니다. 테스트가 프로덕션이 만들어내는 문자열을 그대로 기대값으로 적어뒀기 때문입니다. 프로덕션이 이스케이프를 빠뜨리면 테스트도 이스케이프를 빠뜨린 값을 기대합니다. 둘이 사이좋게 틀려 있으면 초록불이 켜집니다.

테스트가 프로덕션의 출력을 그대로 베끼면 그것은 검증이 아니라 복사입니다. 출력이 바뀌었는지는 알려주지만 출력이 옳은지는 영원히 알려주지 않습니다. 이런 테스트는 있는 것이 없는 것보다 나쁠 수도 있겠다는 생각이 들었습니다. 지켜지고 있다는 인상을 주니까요.

첫 번째 지적 — 한쪽만 고치면 짝이 어긋납니다

자막을 얹는 경로는 두 갈래입니다. 최종 파일로 내보낼 때와, 확인용으로 미리 구워볼 때입니다.

이스케이프 함수는 내보내기 쪽 파일에 살고 있었습니다. 그런데 미리보기 쪽이 그 함수를 가져다 쓰려면 두 파일이 서로를 참조하는 모양이 되어버립니다. 그래서 미리보기 쪽은 처음부터 아무 처리 없이 경로를 그냥 쓰고 있었습니다.

제가 발견한 누락은 그 미리보기 쪽에 있었습니다. 거기서 한 줄만 고치면 어떻게 되는지 리뷰가 짚었습니다. 같은 갈래 안에서 한 종류의 자막은 처리되고 다른 종류는 안 되는 짝이 됩니다. 지금까지는 둘 다 처리 안 해서 최소한 일관되기라도 했는데, 한쪽만 고치는 순간 설명하기 더 어려운 상태가 됩니다.

그래서 함수를 아무것도 참조하지 않는 별도 파일로 분리해 두 갈래가 함께 쓰게 했습니다.

순수한 함수를 어디에 두느냐가 취향이 아니었습니다

여기서 예상 못 한 것을 하나 배웠습니다. 처음에는 굳이 새 파일을 만들 것 없이 미리보기 쪽 파일에 옮기면 되지 않나 생각했습니다. 해봤더니 테스트 세 건이 깨졌습니다.

그 파일은 외부 프로그램을 실제로 실행하는 모듈입니다. 테스트에서는 진짜로 실행하면 안 되니 모듈 전체를 가짜로 바꿔치기합니다. 그런데 바꿔치기는 파일 단위로 일어나므로, 같은 파일에 있던 계산만 하는 순수한 함수까지 함께 가짜가 됩니다. 그 함수를 부른 자리에는 경로 대신 undefined가 들어갔습니다.

돌이켜 생각해보면 이건 꽤 일반적인 규칙입니다. 바깥세상을 건드리는 모듈은 테스트에서 잘려나가는 경계이고, 순수한 헬퍼를 그 경계 안쪽에 두면 같이 잘립니다. 파일을 어떻게 나누느냐가 정리 취향이 아니라 테스트 가능성의 문제라는 것을 이때 처음 체감했습니다.

두 번째 지적 — 주석이 하던 주장이 사실이 아니었습니다

그리고 마지막 지적이 앞 편에서 미뤄둔 이야기입니다.

그 함수의 주석과 테스트는 "윈도우 드라이브 문자의 콜론을 막아준다"고 말하고 있었습니다. 처리하는 문자 목록에 콜론이 들어 있으니 자연스러운 서술로 보입니다. 앞 편을 쓸 때 제가 갖고 있던 인상도 정확히 그것이었습니다.

재봤습니다.

경로에 들어 있는 문자 이스케이프 없이 이 함수를 거친 뒤
, ; [ ] 실패 통과
: 실패 여전히 실패
\ ' 실패 여전히 실패

콜론만 다릅니다. 이유는 해석이 두 단계로 일어나기 때문입니다.

넘긴 문자열      C\:\작업폴더\captions.ass
1단계 (그래프)   백슬래시를 "다음 글자를 그대로 읽어라" 표시로 소비하고 버림
                 → C:\작업폴더\captions.ass
2단계 (옵션)     남은 콜론을 만나 거기서 잘라버림
                 → C

한 번 감싼 것은 첫 관문에서 소비되고, 두 번째 관문 앞에는 맨몸의 콜론이 도착합니다. 살아남으려면 두 번 감싸야 합니다.

그러니까 정확한 문장은 이렇습니다. 그 함수는 콜론을 이스케이프하지만, 콜론으로부터 지켜주지는 않습니다. 둘은 다른 말인데 코드만 봐서는 구분되지 않습니다. 목록에 콜론이 있다는 사실이 방어의 근거처럼 보일 뿐입니다.

동작이 아니라 주장을 고쳤습니다

실질 위험은 없습니다. 운영은 리눅스에서 돌고 경로에 특수문자가 들어갈 일이 없어서, 이 함수는 현재 입력에 대해 대체로 받은 것을 그대로 돌려줍니다. 이번에 바뀐 동작은 사실상 없습니다.

고친 것은 주석과 테스트가 하고 있던 주장입니다. 그래도 고칠 값어치가 있다고 봤습니다. 틀린 주석은 코드를 망가뜨리지 않는 대신 사람을 망가뜨립니다. 나중에 누군가 윈도우 경로를 다루면서 "이건 이미 막혀 있다"고 읽고 지나가면, 사고는 그 사람의 판단에서 납니다. 그리고 그때는 아무도 이 주석을 의심하지 않을 것입니다.

같은 계열의 것을 하나 더 발견했습니다. 배포 도구에 어떤 파일이 바뀌면 어떤 서비스를 배포할지를 적어둔 설정 항목이 있는데, 확인해보니 그 값을 읽는 코드가 어디에도 없었습니다. 실제 판단은 완전히 다른 곳에서 이뤄지고 있었습니다. 문제는 다른 문서가 이미 그 항목을 동작의 근거로 인용하고 있었다는 점입니다. 지우면 원래 의도까지 사라지므로, "이건 실제 동작의 근거가 아니다"라고 명시하는 쪽으로 정리했습니다.

죽은 설정과 틀린 주석은 같은 종류의 물건이라는 생각이 들었습니다. 둘 다 읽는 사람에게만 작동하는 코드입니다.

시리즈를 마치며

세어보니 이 시리즈에서 제가 내린 결론 중 나중에 정정되지 않은 것이 별로 없습니다.

편 그때의 결론 이후
2 결제 문제로 막혔다 쿼터 문제였음
5 범인은 무선 랜이다 같은 편에서 반증
5 범인은 파일시스템이다 절반만 맞음 (8편)
6 전부 끈다 한 대는 다시 켬 (9편)
9 무거운 작업은 보류한다 같은 날 뒤집힘
10 이렇게 고치면 된다 리뷰에서 근거가 무너짐
11 이건 이미 막혀 있다 재보니 안 막혀 있었음

이 표를 만들고 처음 든 생각은 부끄러움이었는데, 조금 더 보다 보니 다른 생각이 들었습니다. 정정된 결론들은 전부 그 시점의 최선이었고, 뒤집힌 계기는 예외 없이 둘 중 하나였습니다. 더 재봤거나, 남이 봤거나. 더 신중하게 생각해서 뒤집힌 적은 한 번도 없었습니다.

혼자 개발하면 재보는 일과 남이 보는 일이 둘 다 귀합니다. 1편에서 저는 혼자인데 CI가 왜 필요하냐고 물으면서 시작했습니다. 열한 편을 지나 도착한 답은 혼자이기 때문에 필요하다는 쪽입니다. 자기 결론을 자기가 뒤집는 일은 좀처럼 일어나지 않으니, 뒤집어줄 장치를 밖에 두는 수밖에 없습니다.

지금 그 노트북은 꺼져 있습니다. 6편을 쓸 때와 같은 상태처럼 보이지만 한 가지가 다릅니다. 그때는 켜면 동작한다는 것만 확인된 상태였고, 지금은 켜면 전부 받는다는 것까지 확인된 상태입니다. 그리고 켜야 할 순간이 오면 화면이 알려줍니다.

애석하게도 여기까지 오는 데 2주가 아니라 3주가 걸렸습니다. 다만 이번에는 아무것도 사지 않았고, 고친 것들은 예비 러너를 영영 끄더라도 코드에 남습니다. 경로를 문자열로 단정하지 않는 테스트, 전제가 바뀐 자리에서 꺼지는 캐시, 두 갈래가 함께 쓰는 이스케이프, 그리고 더 이상 거짓말하지 않는 주석이 그렇습니다.

임시로 만든 것 중에 무엇이 남는지가 그 작업의 진짜 성과였다는 생각이 듭니다. 러너 함대는 남지 않았지만, 그것을 만들려고 들여다본 자리마다 하나씩 남았습니다.

ffmpeg테스트코드 리뷰주석모듈 설계CI

관련 글

테스트를 통과시켰지만 근거가 틀렸습니다 — 리뷰가 잡아낸 두 건

경로를 잘못 단정하던 테스트를 고쳤는데, 리뷰가 제 수정의 근거를 무너뜨렸습니다. 검색 결과의 첫 히트를 근거로 삼았고, 그것은 제가 찾던 것과 이름만 같은 다른 값이었습니다.

관련도 97%

껐던 노트북을 다시 켰습니다 — 막고 있던 것은 성능이 아니었습니다

윈도우 러너에 무거운 작업을 맡길 수 없다고 믿었는데, 막고 있던 것은 성능이 아니라 코드베이스가 리눅스를 전제하고 쓴 문법이었습니다. 그리고 그날 내린 보류 결정은 지적 한 줄에 그날 무너졌습니다.

관련도 94%

혼자 개발하는데 CI가 왜 필요했나 — 편의 기능인 줄 알았는데 임계 경로였습니다

1인 개발자에게 CI는 사치일까요. 어느 날 CI가 통째로 멈추고 나서야, 그것이 테스트를 대신 돌려주는 편의 기능이 아니라 머지 자체를 막는 임계 경로였다는 것을 알게 됐습니다.

관련도 93%