← 뉴스레터

2026-07-28

다 돌렸습니다, 라는 말을 못 하게 만들었다

“검증 전량 통과했습니다.” 이 문장이 문제였다. 틀린 문장은 아니었다. 다만 ‘전량’이 무엇을 뜻하는지가 사람마다 달랐다.

열일곱 개는 전량이 아니었다

어떤 변경이 올라왔다. 작성자는 이름 패턴에 맞는 테스트 열일곱 개를 돌렸고, 전부 통과했고, 그렇게 보고했다. 머지됐다.

며칠 뒤 다른 테스트 하나가 깨져 있는 게 발견됐다. 그 열일곱 개의 패턴 바깥에 있던 테스트였다.

작성자를 탓하기 어려웠다. 검증 절차 문서에 이렇게 쓰여 있었기 때문이다 — 영향받는 테스트를 실행한다. 무엇이 영향받는지는 작성자가 판단한다. 이름이 비슷한 것들을 골랐고, 그게 합리적으로 보였다.

그런데 실제 계약은 처음부터 전량을 요구하고 있었다. 문서 한 구절이 그 계약을 다르게 읽히게 만든 것이다.

문서를 고치는 걸로는 부족했다

당연히 문서를 고쳤다. “전량이다. 이름 글롭도 아니고 영향면만도 아니다. 부분 실행은 계약 미이행이다.”

그런데 이걸로 끝내면 같은 일이 또 난다. 문서는 읽는 사람에 따라 다르게 읽히고, 시간이 지나면 안 읽힌다. 문장으로 막은 것은 문장으로 뚫린다.

진짜 문제는 다른 데 있었다. “다 돌렸습니다”가 작성자의 주장이라는 것. 주장은 검증할 방법이 없다. 받는 쪽은 믿거나 안 믿거나 둘 중 하나고, 매번 안 믿고 직접 돌려볼 수는 없다.

실행 기록을 코드에 묶었다

그래서 이렇게 바꿨다.

검증을 돌리면 실행 기록이 나온다. 몇 개를 돌렸고, 몇 개가 통과했고, 몇 개가 실패했고, 언제 어느 기계에서 돌았는지. 그리고 그 기록에는 그 시점 코드의 지문이 함께 찍힌다.

머지 게이트가 이 기록을 기계적으로 대조한다. 기록의 지문과 지금 코드의 지문이 다르면 — 그러니까 기록을 낸 뒤에 코드를 한 줄이라도 더 고쳤으면 — 그 기록은 낡은 것으로 거부된다.

이제 “다 돌렸습니다”는 말이 필요 없다. 기록이 있거나 없거나, 맞거나 낡았거나다.

실제로 걸리기 시작했다

도입하고 얼마 안 돼서 한 건이 걸렸다. 게이트가 실패 판정을 냈는데, 원인이 예상 밖이었다.

지문을 넘길 때 짧은 형태로 넘긴 것이다. 기록에는 전체 형태가 찍혀 있는데 대조할 때 앞부분만 넘기니 문자열이 달라서 불일치가 났다. 기록이 낡아서가 아니라 길이가 달라서.

이게 왜 함정이냐면, 발행 로그가 지문을 짧게 줄여서 보여준다. 사람이 그걸 복사해서 쓰면 바로 걸린다. 그리고 걸린 사람은 “기록이 낡았나 보다” 하고 10분에서 30분짜리 전량 검사를 헛돌린다.

지금은 경고만 뜨는 관찰 모드라 실제 머지가 막히진 않는다. 나중에 차단 모드로 올리면 이 함정이 진짜 비용을 청구할 것이다. 그래서 함정 자체를 문서에 박아 뒀다.

못 돌리는 건 못 돌린다고 적는다

한 가지 더 필요했던 게 있다. 정당하게 못 돌리는 검사를 어떻게 다룰 것인가.

기계마다 환경이 다르다. 어떤 노드에는 특정 검사 도구가 아예 없다. 그 노드에서 그 검사를 요구하면 영원히 통과할 수 없다.

그렇다고 조용히 건너뛰게 하면 도루묵이다. 그래서 사유와 함께 건너뛰기를 선언하게 했다. 사유 없는 건너뛰기는 게이트가 거부한다.

우회가 아니라 기록이다. “이 노드에는 이 도구가 없어서 이 검사는 다른 노드에서 교차검증이 필요하다” 가 기록으로 남는다. 나중에 누가 봐도 무엇이 검증됐고 무엇이 안 됐는지 알 수 있다.

배운 것

같은 사고를 문장으로 막으면 문장으로 뚫린다. 재발방지는 규칙 문장 추가가 아니라 게이트나 테스트를 하나 더 붙이는 것이어야 한다. 문장은 사람이 지키고, 게이트는 코드가 지킨다.

그리고 “검증했다”는 서술은 서술한 시점에만 참이다. 코드가 움직이면 그 서술도 같이 낡아야 하는데, 서술은 스스로 낡지 않는다. 그래서 서술 대신 코드 상태에 결속된 기록으로 바꿨다.

마지막으로, 사고의 뿌리가 사람인 경우는 생각보다 드물다. 이번 건도 작성자가 게을렀던 게 아니라 문서가 그렇게 읽혔던 거다. 사람을 고치려 들면 재발하고, 읽히는 방식을 고치면 안 재발한다.


주장은 낡는다. 지문에 묶인 기록만 스스로 낡았다고 말한다.

— 강대종 / @ssamssae