테스트를 통과시켰지만 근거가 틀렸습니다 — 리뷰가 잡아낸 두 건
남은 일은 셋이었습니다. 워크플로에 기본 셸을 지정하는 한 줄, 경로를 문자열로 단정하던 테스트 다섯 건, 화면 좌표를 1픽셀도 안 틀리게 단정하던 테스트 두 건.
쉬워 보였습니다. 실제로 반나절이면 끝날 분량이었습니다. 그런데 리뷰가 제 수정 중 두 건의 근거를 무너뜨렸습니다. 결과물이 아니라 근거가 무너졌다는 게 이 편의 이야기입니다.
첫 번째 — 검색 결과의 첫 줄을 근거로 삼았습니다
문제의 테스트는 영상 처리에 넘길 명령을 만드는 코드를 검증하는 것이었습니다. 다른 테스트들은 전부 "코드가 옳고 테스트가 리눅스를 전제했다"는 모양이었는데, 이 하나만 방향이 반대로 보였습니다. 프로덕션 코드가 슬래시를 직접 박아 쓰고 있는 것처럼 보였거든요.
그래서 저는 파일명을 검색했습니다. 결과가 나왔고, 그 자리에 정말로 슬래시가 박혀 있었습니다. 가설과 증거가 맞아떨어졌습니다. 저는 테스트 쪽 기대값을 프로덕션에 맞추는 방향으로 고치고 넘어갔습니다.
리뷰가 짚은 것은 이랬습니다. 제가 찾은 그 슬래시는 제가 찾던 경로가 아니었습니다. 같은 파일명이 코드에 두 군데 등장하는데, 하나는 파일을 저장소에 올릴 때 쓰는 저장 키이고 다른 하나가 실제 디스크 경로입니다. 저장 키는 원래 슬래시로 씁니다. 운영체제와 아무 상관이 없습니다.
그리고 진짜 디스크 경로는 그냥 넘어가지도 않았습니다. 명령 문법상 경로에 특수문자가 들어 있으면 앞에 백슬래시를 붙여줘야 해서, 이스케이프 함수를 한 번 거칩니다. 그 함수가 처리하는 문자 목록에는 콜론과 백슬래시도 들어 있습니다.
윈도우 절대 경로: C:\작업폴더\captions.ass
이스케이프 통과 후: C\:\\작업폴더\\captions.ass
그러니까 정답은 제가 쓴 슬래시 버전도 아니고, 다른 테스트들처럼 경로 결합 함수만 쓴 버전도 아니었습니다. 프로덕션이 실제로 내놓는 문자열은 세 번째 모양입니다. 제 수정은 테스트를 통과시키지 못했고, 정확히 말하면 실패하는 모양만 바꿨습니다.
덧붙이자면 이 함수에 대해 그때 제가 갖고 있던 인상 하나도 나중에 틀린 것으로 드러납니다. 그 이야기는 다음 편에서 하겠습니다.
같은 이름은 어느 쪽인지 알려주지 않습니다
이 건에서 제가 배운 것은 검색을 조심하라는 정도의 이야기가 아닙니다.
저는 가설을 세우고, 그 가설을 확인해줄 검색어를 던지고, 첫 히트를 근거로 채택했습니다. 5편에서 무선 랜을 범인으로 지목했을 때와 완전히 같은 모양입니다. 그때도 저는 답을 정해두고 그것을 확인해줄 데이터를 찾으러 갔고, 성공적으로 찾아냈습니다.
다른 점은 그때는 재측정이 저를 구했고 이번에는 리뷰가 구했다는 것뿐입니다. 스스로 알아차린 적은 두 번 다 없었다는 생각이 들었습니다.
두 번째 — 동작은 맞았는데 이유가 틀렸습니다
두 번째 지적은 더 미묘했습니다.
앞 편에서 서버가 못 뜨던 문제, 그러니까 포트 번호가 글자 그대로 넘어가던 그 문제를 저는 워크플로에 기본 셸을 지정한 덕분에 해결됐다고 기록해뒀습니다. 문제가 있었고, 셸을 바꿨고, 해결됐으니 자연스러운 서술로 보였습니다.
그런데 그 문제는 워크플로에 적힌 명령이 아니라 개발 스크립트 안에 있었습니다. 테스트 도구가 서버를 띄우고, 그 서버가 패키지 매니저를 부르고, 그 패키지 매니저가 스크립트를 실행합니다. 문제의 문법이 확장되는 자리는 손자뻘 프로세스이고, 워크플로에 지정한 셸은 거기까지 닿지 않습니다.
실제 해결책은 앞 편에 적은 그 기계의 설정 파일 두 번째 줄이었습니다. 두 조치를 같은 날 넣었더니 결과만 보고 공을 엉뚱한 쪽에 준 것입니다.
이 종류가 더 위험하다는 생각이 들었습니다. 첫 번째 건은 테스트가 빨간불로 남아 있어서 언젠가는 드러납니다. 그런데 이건 결과가 초록불이고 기록만 틀렸습니다. 아무도 검증하지 않습니다. 그리고 나중에 누군가 그 기록을 믿고 "셸 지정만 하면 되겠네"라고 판단하면 그때 다시 막힙니다. 틀린 인과는 결과가 맞는 동안 조용히 살아남습니다.
부끄럽지만 하나 더 있습니다. 저는 커밋 메시지에 "이 문법을 쓰는 단계가 일곱 군데"라고 적었는데, 세어보니 실제로는 두 군데였습니다. 검색 결과 줄 수를 셌더니 주석에 적힌 같은 문자열까지 함께 세어졌던 것입니다. 숫자를 자동으로 세면 정확할 것 같지만, 무엇을 세고 있는지는 여전히 사람이 확인해야 합니다.
2픽셀은 허용오차로 덮지 않기로 했습니다
남은 두 건은 성격이 달랐습니다. 화면 요소가 1픽셀도 움직이지 않아야 한다고 단정하는 테스트인데, 윈도우에서는 글꼴이 미세하게 달라 2픽셀이 움직입니다.
가장 쉬운 해결은 허용오차를 주는 것입니다. 몇 픽셀까지는 봐주자고 하면 모든 환경에서 통과합니다.
고르지 않았습니다. 그러면 맥과 hosted 러너에서도 단정이 함께 느슨해지기 때문입니다. 이 테스트의 존재 이유가 "레이아웃이 흔들리는 것을 잡는 것"인데, 러너 한 대를 편입하려고 나머지 전부의 감시망을 낮추는 것은 손해입니다.
대신 윈도우에서만 건너뛰게 했습니다. 그 러너는 예비용이라 어쩌다 한 번 도는데, 상시로 도는 환경에서는 검사가 그대로 유지됩니다. 완벽하진 않지만, 느슨해지는 범위를 예비 러너 한 대로 가둔 셈입니다.
켤 때를 알려주는 계기판
이제 그 노트북은 무거운 작업까지 받을 수 있게 됐습니다. 그런데 마지막 문제가 하나 남습니다. 그것을 켤 방법이 없습니다.
4편에서 원격으로 깨우는 데 실패했고, 그 뒤에 확인해보니 그 노트북은 BIOS에 전원 관련 설정 자체가 없는 기종이었습니다. 소프트웨어 문제가 아니라 그 경로가 존재하지 않는 것이니 더 시도할 것이 없습니다.
그래서 자동화의 마지막 한 칸은 사람에게 넘겼습니다. 대시보드가 "지금 켜세요"라고 알려주게 만들었습니다. 조건은 이렇습니다.
자가 러너 모드로 전환됨 AND
빈 자리 없음 AND
대기 중인 작업 3개 이상 AND
윈도우 러너 꺼져 있음
첫 줄이 핵심입니다. 이것을 빼면 평상시에도 경보가 울립니다. hosted는 동시에 20개까지 돌기 때문에 애초에 줄이 생기지 않는데, 그 사실을 조건에 넣지 않으면 "대기 작업 3개 이상"이 정상 상태에서도 참이 될 수 있습니다. 경보는 울리지 않는 것보다 쓸데없이 울리는 쪽이 더 빨리 무시당합니다.
이 조건이 실제로 그렇게 동작하는지는 판정 함수를 그대로 꺼내 다섯 가지 상황에 넣어보고 확인했습니다. 맥북이 한가하면 조용하고, 두 자리가 다 찬 채로 넷이 밀려 있으면 울리고, 셋 미만이면 "포화"만 표시하고, 노트북을 켜면 저절로 꺼집니다. 판정식을 다시 구현해서 검증하면 그건 판정식이 아니라 제 이해를 검증하는 것이라, 코드를 옮기지 않고 그대로 불러 썼습니다.
켜기만 하면 러너 프로그램이 자동으로 등록되고, 끄면 GitHub이 알아서 배정에서 제외합니다. 3편에서 라벨로 폴백을 설계해둔 것이 여기서 또 배당금을 냈습니다.
다만 라벨을 붙이면서 하나를 더 알았습니다. 라벨은 등급이 아니라 정확한 교집합 매칭입니다. 작업이 "가벼운 일"을 요구하는데 러너에게 "무거운 일" 라벨만 달아주면, 상위 호환처럼 보이지만 그 작업을 아예 못 받습니다. 맥북이 양쪽을 다 소화하던 것도 성능이 좋아서가 아니라 라벨을 둘 다 갖고 있었기 때문이었습니다. 그래서 기존 라벨을 유지한 채 하나를 추가했습니다.
3편에서 저는 라벨이 폴백의 의미를 표현한다고 썼습니다. 조금 더 정확히는 의미가 아니라 집합을 표현합니다. 사람이 읽기에 자연스러운 위아래 관계를 라벨은 갖고 있지 않습니다.
아직 하나가 남아 있었습니다
이렇게 마무리됐다고 생각했는데, 수정하는 동안 발견해놓고 별건으로 미뤄둔 것이 하나 있었습니다. 같은 이스케이프 누락이 다른 자리에 하나 더 있었습니다.
작은 후속 작업으로 볼 일이었고 실제로 반나절이면 끝날 분량이었는데, 거기서 리뷰가 저를 또 두 번 세웠습니다. 한 번은 한쪽만 고치면 남는 비대칭 때문이었고, 다른 한 번은 그 함수의 주석이 하고 있던 주장이 사실이 아니었기 때문입니다. 바로 앞에서 제가 미뤄둔 그 이야기입니다.
다음 편에서 그것을 재보고, 시리즈를 마칩니다.
관련 글
껐던 노트북을 다시 켰습니다 — 막고 있던 것은 성능이 아니었습니다
윈도우 러너에 무거운 작업을 맡길 수 없다고 믿었는데, 막고 있던 것은 성능이 아니라 코드베이스가 리눅스를 전제하고 쓴 문법이었습니다. 그리고 그날 내린 보류 결정은 지적 한 줄에 그날 무너졌습니다.
주석이 거짓말을 하고 있었습니다 — 이스케이프가 실제로 막는 범위
경로를 감싸는 함수가 콜론을 이스케이프하고 있었습니다. 그래서 윈도우 경로를 막아준다고 주석에 적혀 있었는데, 재보니 콜론만 여전히 실패했습니다. 이스케이프하는 것과 지켜주는 것은 다른 말이었습니다.
다 만들어놓고 전부 껐습니다 — 쿼터를 태운 범인은 macOS 10배 배수였습니다
사용량을 총량으로만 보면 원인이 안 보입니다. 저장소별 SKU로 분해하자 raw 99.4분짜리 macOS 작업이 994분으로 청구돼 쿼터의 절반을 태우고 있었습니다. 2주간 만든 러너 함대는 그래서 전부 껐습니다.