실수를 막는 방법 — 기계가 확인하게 한다¶
소규모 팀 기준이다. 사람이 더 조심하는 것으로는 안 된다는 것이 이 문서의 전제다.
왜 이 문서가 있나¶
2026-09-27 하루 동안 같은 형태의 실수가 여섯 번 났다. 전부 "확인할 수 있는 것을 확인하지 않고 단정한 것" 이다.
| 무엇 | 어떻게 드러났나 |
|---|---|
| 영어 주석 정책을 정한 직후 새 모듈을 한국어로 썼다 | 래칫이 막았다 (builder#711) |
| 스크립트 가드를 방금 삽입한 텍스트로 검사해 세 번 오판했다 | 직접 재확인 |
src/·e2e/ 만 훑고 __tests__/ 를 빠뜨렸다 |
CI 가 막았다 (studio#429) |
| "세 저장소의 변경 빈도가 다르다" 를 재보지 않고 썼다 | 사람이 지적했다 |
i18n 잔량을 "0건" 이라 보고했다 (검사기가 main 에 없었다) |
나중에 재측정 |
| 감사 표에서 9를 8이라고 반복 보고했다 | 기계적 재계산 |
막힌 것은 전부 기계가 막았고, 빠져나간 것은 전부 기계가 없던 자리다. 조심함의 차이가 아니었다.
규칙¶
1. 숫자가 들어간 문장은 명령에서 나온다¶
같은 호흡에 그 숫자를 만든 명령이 있어야 한다. 없으면 숫자를 쓰지 않는다.
✗ "세 저장소의 변경 빈도가 다르다"
✓ git log --since='60 days ago' --oneline | wc -l
→ 253 / 323 / 278 — 거의 같다. 위 문장은 틀렸다
기억에서 꺼낸 수치는 수치가 아니다.
2. 전수 조사는 git ls-files 로 한다¶
경로를 손으로 고르면 빠뜨린다. 저장소가 무엇을 가졌는지는 저장소에 묻는다.
__tests__/ 가 빠진 이유는 그 디렉터리를 몰랐던 것이 아니라 목록을 손으로 썼기
때문이다.
3. 게이트 없는 규칙은 희망이다¶
AGENTS.md 는 처음부터 "주석은 영어" 라고 적고 있었다. 그 사이에 한국어 주석이
9,298건 쌓였다(kpubdata 3,275 · builder 3,878 · studio 2,145).
래칫을 넣은 날 그것이 멈췄고, 같은 날 나를 두 번 막았다.
새 규칙을 정할 때 같이 만드는 것:
- 규칙을 검사하는 명령
- 그 명령을 CI 에 연결
- 그 명령이 실패하는 것을 보여주는 테스트
세 번째가 빠지면 작동하는지 아무도 모른다.
4. 기존 부채는 동결하고 새 부채만 막는다 — 래칫¶
전부 고치고 시작하려면 아무것도 시작되지 않는다. 9,298건을 한 번에 번역할 수 없다.
--update 는 내리는 방향으로만 쓴다. 이 구조라야 부채를 갚는 동안 새 부채가 0으로
유지된다.
5. 게이트는 로컬에서 돌아야 한다¶
CI 가 첫 리뷰어가 되면 되돌이가 10초에서 10분이 된다. 소규모 팀에서는 그 차이가 "돌려보고 고친다" 와 "올려보고 고친다" 의 차이다.
studio#429 가 실패한 이유가 이것이다 — 이 저장소의 node_modules 가 비어 있어서
테스트를 로컬에서 돌릴 수 없었고, 그래서 CI 가 처음 보는 사람이 됐다.
원인은 게으름이 아니었다. npm config get registry 가 사내 프록시 피드를 가리키고
있고 그 피드에 w3c-xmlserializer 가 없다. 환경 문제가 검증 능력을 조용히
없애고 있었다.
그래서 새 저장소나 새 기계에서 처음 하는 일은 게이트를 로컬에서 한 번 돌려 보는 것이다. 안 돌면 그것이 첫 번째 고칠 것이다.
6. "했다" 는 명령의 출력으로 말한다¶
완료 보고에는 그것을 확인한 명령의 출력이 있어야 한다. 테스트가 실패했으면 실패 출력을 그대로 싣는다. 건너뛴 단계는 건너뛰었다고 적는다.
7. 대조 대상을 스스로 만들지 않는다¶
치환 목록·매핑·기준값을 손으로 쓰면 그 목록이 틀릴 수 있다. 가능하면 이미 있는 것에서 뽑아낸다.
두 번째가 첫 번째를 고친 방법이다.
8. 치환에는 가장 짧은 항목의 가드가 필요하다¶
매핑을 diff 에서 뽑는 것(7절)만으로는 부족하다. 가장 짧은 항목이 어디에 걸리는지를 따로 막아야 한다.
브랜드 개명에서 실제로 난 일이다. locale diff 에서 뽑은 102건 중 하나가 맨 문자열
Kubi 였고, 4자짜리라 라벨뿐 아니라 식별자와 모듈 경로에도 걸렸다.
35개 파일 455곳. 그리고 증상이 파싱 실패라서 "35개 파일이 깨졌다" 로 보였다 — 원인이 치환 한 번이라는 것과 거리가 멀었다.
가드는 둘 중 하나다.
- 단어 경계 — 앞뒤가 문자면 치환하지 않는다
- 최소 길이 — 짧은 값은 아예 대상에서 뺀다
같은 저장소의 check-stale-ui-literals.mjs 는 이미 8자 미만을 거부하고 있었다.
치환이 그 판단을 빌려 쓰지 않았을 뿐이다.
9. 없는 체크는 실패가 아니라 정지다¶
게이트가 실패하는 것만 생각하면 한 가지를 놓친다. 필수로 걸어둔 체크가 아예 보고되지 않는 경우다.
kpubdata-studio#413 이 매트릭스에서 Node 20 을 뺐다. 보호 설정은 여전히
Lint, type check, test, build (20) 을 요구했다.
GitHub 은 없는 체크를 실패로 처리하지 않는다. 기다린다. PR 은 붉어지지도 않고 그냥 영영 BLOCKED 에 머문다. 볼 로그도 없고 재실행할 잡도 없다.
그래서 사람은 --admin 으로 지나간다. 그런데 --admin 은 나머지 체크도 전부
건너뛴다. 한 칸이 막힌 대가로 전부가 열린 셈이고, 그 세션의 모든 병합이 그렇게
들어갔다. 전부를 막는 규칙은 아무것도 지키지 않는다.
두 가지를 함께 해야 한다.
- 집계 게이트 하나만 required 로 건다. 그 잡이 매트릭스를
needs하면, 버전을 더하거나 빼도 보호 설정은 손댈 일이 없다. 매트릭스 이름을 required 에 적는 순간 매트릭스 수정이 곧 보호 설정 수정이 되고, 잊는 쪽이 이 장애다. - 집계 게이트는
always()아래에서 돈다. 업스트림이 실패했다고 건너뛰어지면, 그 게이트 자체가 방금 말한 "없는 체크" 가 된다.
확인은 scripts/check_required_checks.py 가 한다. 매트릭스 잡을 실제로 생기는
컨텍스트로 펼쳐서 비교하고, --workflows 로 다른 저장소 체크아웃도 본다.
CI 가 아니라 로컬에서 돈다 — 보호 설정을 읽으려면 admin 토큰이 필요하고, 그
토큰을 든 워크플로는 이 스크립트가 잡을 드리프트보다 큰 위험이다.
지금 있는 게이트¶
| 저장소 | 게이트 | 무엇을 막나 |
|---|---|---|
| builder | scripts/check_korean_comments.py |
새 한국어 주석·docstring |
| builder | scripts/check_version_consistency.py |
선언·CHANGELOG·태그 불일치 발행 |
| kpubdata | scripts/verify_guard.py |
도구가 자기 산출물을 검증하는 것 |
| kpubdata | scripts/check_fixture_authorship.py |
손으로 쓴 fixture |
| kpubdata | scripts/junit_summary.py |
실패한 run 을 PASS 로 보고 |
| 3곳 | 브랜치 보호 + enforce_admins |
CI 를 건너뛴 병합 |
| kpubdata | scripts/check_required_checks.py |
아무도 만들지 않는 required 체크 (3곳 모두 본다) |
| 3곳 | CI 의 CI gate 잡 |
매트릭스 수정이 보호 설정을 깨는 것 |
없어서 만들어야 하는 것은 BACKLOG.md 와 관련 이슈에 있다.
관련¶
- POLICY.md — 18절 Verification 수준, 19절 규칙
- BACKLOG.md
kpubdata-builder#710·kpubdata-studio#427— 주석 부채kpubdata-studio#430— backend·frontend 버전 정합성