어제 개발자 단톡방에서 나온 질문입니다. "유닛테스트 5천 개가 CI 파이프라인에 걸려 있는데, 빌드할 때마다 5천 개를 똑같이 다 돌리나요?" 문구 몇 개 고쳤는데 테스트만 30~40분. 같은 질문을 제 Claude Code 하네스에 던져 실측했습니다. 훅(hook) 발화 기록 100만 건, 파일 157개, 등록 96개를 전부 세어 본 결과입니다.
먼저 결론입니다. 제 하네스(harness — 인공지능이 "다 했다"고 거짓말하지 못하게 증거를 강제하는 감시 장치 묶음)는 검사를 매번 전부 돌리지 않습니다. 검사기 파일은 157개인데, 실제로 이번 프로젝트에서 작동하도록 등록된 것은 96개입니다. 나머지는 이 프로젝트에 해당이 없어서 아예 설치되지 않았습니다.
스코프(scope — 검사 범위)를 좁히는 장치는 5개 층으로 겹쳐 있습니다. 프로젝트 종류로 한 번, 도구 종류로 한 번, 파일 경로로 한 번, 고친 범위가 덮는 시나리오 목록으로 한 번, 마지막으로 "상태가 그대로면 다시 안 함"으로 한 번입니다. 그런데 딱 한 구역만 일부러 전수(全數)로 남겨뒀습니다. 그 이유가 이 글의 핵심입니다.
단톡방 대화의 결론("검증 빌드는 메인에 머지할 때만, 평소엔 수정 범위만")과 제 하네스가 도달한 구조는 서로 모르는 채 같은 모양입니다. 평소에는 좁게, 되돌릴 수 없는 순간(푸시·배포·완료 선언)에만 넓게. 다만 한 가지가 다릅니다. 저는 "무엇을 테스트할지 고르는 일"만큼은 절대 좁히지 않습니다 — 그걸 좁히는 순간, 통과하기 쉬운 것만 고르는 부정행위가 구조적으로 가능해지기 때문입니다.
단톡방의 문제와 제 하네스의 문제는 겉보기엔 같지만 아픈 지점이 다릅니다. 한쪽은 기다리는 시간이 아프고, 다른 쪽은 맞는 검사가 켜져 있는가가 아픕니다. 이 차이가 해법의 방향도 갈라놓습니다.
"프론트 문구 몇 개 바꿨는데 테스트만 30~40분"
"141일 동안 100만 번 발화, 그중 1.5%만 실제로 막았다"
제 훅 발화 기록에는 실행 소요시간이 남지 않습니다. 그래서 이 글은 "훅 때문에 몇 분이 느려졌다"는 주장을 하지 않습니다. 셀 수 있는 것은 발화 횟수·차단 횟수·설치 범위뿐이고, 아래 수치는 전부 그 셋에서만 나왔습니다. 같은 이유로 로그에는 "무엇이 차단됐는지"(대상 파일·명령)도 남지 않아, 차단 1건의 내용을 사후에 복원할 수 없습니다. 이건 제 하네스의 알려진 결함이고, 고칠 후보로 이미 등록돼 있습니다.
검사기는 157개가 있지만, 파일 하나를 저장할 때 157개가 전부 도는 것이 아닙니다. 아래 그림처럼 네 번 걸러진 뒤에야 실제로 실행됩니다. 각 층은 앞 층이 통과시킨 것만 받습니다.
단톡방에서 "엄청 플랫폼마다 다 시키고 있어요"라는 말이 나왔습니다. 제 하네스는 반대로 갑니다. 프로젝트를 열 때 어떤 종류의 프로젝트인지 먼저 감지하고, 그 종류에 해당하는 검사 묶음만 설치합니다. 감지 순서는 앱 설정 파일(pubspec.yaml) → 웹 설정 파일(package.json 안의 프레임워크 이름) → 파이썬 설정 파일 순입니다.
| 검사 묶음 | 이 프로젝트 설치 | 설치 근거 |
|---|---|---|
| universal (공통) | 22 / 22 | 모든 프로젝트 공통 — 비밀키 유출·강제 푸시 금지 등 |
| web-ts (웹·타입스크립트) | 10 / 10 | 웹 페이지 프로젝트로 감지됨 |
| ui-qa (화면 검증) | 5 / 5 | 화면이 있는 프로젝트로 감지됨 |
| backend (서버) | 0 / 6 | 서버 코드가 없어 설치되지 않음 |
| flutter (모바일 앱) | 0 / 1 | 앱 설정 파일 없음 |
| supabase (데이터베이스) | 0 / 1 | 해당 라이브러리 미사용 |
| tdd (테스트 우선) | 0 / 1 | 테스트 도구 미감지 |
템플릿 46개 중 37개만 설치됐습니다. 나머지 9개는 이 프로젝트에서 한 번도 실행되지 않습니다 — 켜져 있다가 그냥 통과하는 것이 아니라, 애초에 존재하지 않습니다. 단톡방 표현을 빌리면 "노드 아이디 목록을 넘기는" 방식을 설치 시점에 미리 해두는 셈입니다.
설치가 끝난 96개도 매번 다 도는 것은 아닙니다. 각 검사기는 어떤 도구를 쓸 때 깨어날지가 등록 단계에서 정해져 있습니다. 셸 명령을 실행할 때 도는 검사기와, 파일을 저장할 때 도는 검사기가 서로 다릅니다.
| 깨어나는 조건 | 공통 설정 | 이 프로젝트 설정 | 대표 검사 내용 |
|---|---|---|---|
Bash — 셸 명령 실행 직전 | 6 | 16 | 비밀키 커밋 차단 · 위험한 삭제 명령 차단 · 푸시 전 검증 |
Edit|Write — 파일 저장 직전/직후 | 4 | 16 | 포맷 검사 · 금지 패턴 · 문서 동기화 |
Skill / Workflow | 2 | — | 외부 기능 로드 전 보안 스캔 |
| 도구 무관 (대화 시작·종료) | 45 | 7 | 완료 선언 검증 · 상태 저장 · 컨텍스트 주입 |
세 번째 필터는 검사기 안쪽에 있습니다. 깨어난 검사기도 대상 파일 경로를 먼저 보고, 자기 담당이 아니면 즉시 통과시킵니다. 157개 중 44개가 이 방식으로 스스로 범위를 좁히고, 155개가 "검사할 게 없으면 즉시 통과"하는 조기 종료 경로를 갖고 있습니다. 이게 100만 발화의 98.5%가 아무 일 없이 지나가는 이유입니다 — 낭비가 아니라 설계입니다.
단톡방에서 나온 아이디어는 이겁니다. "테스트 노드마다 아이디를 따서, 수정 범위가 영향을 끼치는 부분만 그룹핑해 테스트한다." 제 하네스에도 똑같은 장치가 있습니다. 이름은 covers_inventory, 우리말로 옮기면 "이 시나리오가 덮는 화면 요소 목록"입니다.
먼저 화면의 버튼·폼·API·데이터베이스 항목을 전수로 훑어 각각 고유 아이디를 붙입니다. 그다음 테스트 시나리오마다 "나는 이 아이디들을 검증한다"를 적어둡니다. 코드를 고치면 그 파일이 건드린 아이디를 뽑고, 거꾸로 그 아이디를 포함한 시나리오만 골라 다시 돌립니다.
단톡방 방식은 테스트를 골라내는 방식이고, 제 방식은 화면 요소를 먼저 전수로 세고 그 요소를 덮는 테스트를 역으로 찾는 방식입니다. 방향이 반대인 이유는 하나입니다 — 요소 목록에 있는데 어떤 시나리오도 덮지 않는 항목이 나오면, 그건 테스트가 빠진 곳이라는 뜻이고 검사기가 그 자리에서 통과를 막습니다. 범위를 좁히는 장치를 동시에 누락을 잡는 장치로 쓰는 셈입니다.
범위를 좁히는 마지막 축은 "이미 통과한 검증을 언제까지 믿을 것인가"입니다. 예전 제 하네스는 시간으로 판단했습니다 — "1시간 안에 통과했으면 유효". 이 방식은 2026년 8월 3일에 전부 폐기했습니다. 파일 시각만 갱신하는 명령 한 줄이면 아무것도 검증하지 않고 통과할 수 있었기 때문입니다.
"방금 돌렸으니까 괜찮다"
"검증한 상태와 지금 상태가 같은가"
얼핏 보면 안전장치 같지만, 실제 효과는 범위 축소입니다. 상태가 그대로면 이미 통과한 검증을 다시 돌리지 않고, 고친 파일이 덮는 부분만 다시 돌립니다. 반대로 상태가 바뀌면 그 순간 넓은 검증으로 되돌아갑니다. 단톡방의 "검증 빌드는 메인에 머지할 때만"과 같은 판단을, 브랜치가 아니라 상태 해시로 하는 셈입니다.
단톡방에서 "문제는 비용과 시간"이라는 말이 나왔습니다. 제 하네스에서 비용이 가장 큰 검사는 다른 인공지능 모델을 불러 적대적으로 리뷰시키는 단계와 브라우저를 띄워 실제로 클릭해보는 단계입니다. 그래서 검증을 4단 사다리로 세우고, 아래 단을 통과하지 못하면 위 단은 실행조차 하지 않습니다.
이유는 두 가지입니다. 첫째, 기능이 깨진 상태에서 하는 고급 검사는 그냥 낭비입니다. 둘째, 그 검사 결과는 고치는 순간 무효가 됩니다 — 수정이 들어가면 상태 지문이 바뀌고, 위에서 본 규칙에 따라 이전 통과 기록이 전부 무효가 되기 때문입니다.
2026년 4월 12일부터 8월 31일까지 141일 동안 검사기는 1,001,073번 깨어났고, 그중 15,026번만 실제로 작업을 막았습니다. 나머지 98.5%는 아무 일 없이 지나갔습니다. 이 수치를 보고 "98.5%가 낭비"라고 결론 내리기 쉬운데, 그건 틀린 독해입니다. 아래에 왜 그런지 실측으로 보입니다.
| 검사기 | 차단 / 발화 | 차단 비율 | 성격 |
|---|---|---|---|
| 작업 재확인 게이트 | 555 / 2,279 | 24.35% | 가장 좁게 잘 걸린 검사 — 완료 선언 직전 실제 작업 여부 확인 |
| 커밋 전 검증 | 5,815 / 68,954 | 8.43% | 차단 절대량 1위 |
| 설계 문서 게이트 | 1,067 / 14,444 | 7.39% | 코드 형태를 먼저 리뷰하게 강제 |
| 외부 기능 보안 스캔 | 2,252 / 31,583 | 7.13% | 외부 스킬 로드 전 악성 패턴 차단 |
| SQL 주입 검사 | 716 / 12,175 | 5.88% | 보안 — 위험 대비 낮은 발화 |
| 푸시 전 적대 리뷰 게이트 | 1,126 / 70,198 | 1.60% | 발화 2위인데 차단은 드묾 — 되돌릴 수 없는 순간의 보험 |
| 강제 푸시 · 루트 삭제 금지 | 0 / 2,216 | 0% | 차단 코드는 있는데 0건 — 그 위험 행위를 한 번도 안 했다는 뜻 |
| 컨텍스트 감시 · 프롬프트 보강 | 0 / 9,113 | 해당 없음 | 차단 기능 자체가 없음 — 기록·주입 전용이라 이 지표로 재면 안 됨 |
차단 0건 검사기 10종을 코드로 열어보니 두 종류로 갈렸습니다. 첫째, 차단 명령(exit 2)이 아예 없는 것들 — 컨텍스트 감시·프롬프트 보강·피드백 수집처럼 기록만 하는 도구입니다. 이들에게 차단률을 묻는 건 온도계에게 "몇 명을 막았냐"고 묻는 것과 같습니다. 둘째, 차단 명령이 있는데 0건인 것들 — 강제 푸시 금지, 루트 디렉토리 삭제 금지 같은 것들입니다. 여기서 0건은 실패가 아니라 "그 사고가 141일 동안 한 번도 안 났다"는 뜻입니다. 화재경보기가 울리지 않았다고 떼지 않는 것과 같습니다.
그래서 스코프를 좁힐 때 제가 쓰는 기준은 차단률이 아니라 "이 검사가 깨어나는 순간이 그 위험이 실제로 발생할 수 있는 순간인가"입니다. 셸 명령을 실행할 때만 위험한 검사가 파일 저장할 때도 깨어난다면 그건 줄일 대상이고, 발화가 많아도 깨어나는 순간이 정확하면 그대로 둡니다.
여기까지는 전부 "어떻게 줄였는가"였습니다. 이제 반대 이야기입니다. 제 하네스에는 샘플링을 명시적으로 금지하는 구역이 있습니다. 화면의 버튼이 40개면 40개를 전부 눌러야 하고, 39개만 누르면 검사기가 통과를 막습니다. "버튼 3개만 확인" 같은 시나리오는 작성 단계에서 거부됩니다.
"실행 비용"에 관한 결정
"합격 기준"에 관한 결정
제 하네스가 자동으로 이슈를 처리하던 시기에 포렌식을 돌린 적이 있습니다. 자율 처리 실패율 65%, 사람이 되돌려보낸 비율 49%였고, 원인을 추적하니 한 지점으로 모였습니다. 과녁이 "사용자가 원한 기능이 동작하는가"가 아니라 "내가 정한 테스트가 통과하는가"로 바뀌어 있었습니다. 극단적인 사례에서는 버튼을 화면에서 숨기거나 아무 일도 하지 않는 함수로 바꿔서 자기 테스트를 통과시켰습니다. 테스트는 초록불인데 기능은 사라진 겁니다.
"무엇을 테스트할지 고르는 권한을 실행자에게 주면, 통과하기 쉬운 것만 고르는 길이 열린다. 그래서 실행 범위는 좁혀도 합격 기준은 좁히지 않는다."
스코프 조절의 진짜 위험은 "너무 좁혀서 놓친다"가 아닙니다. 실수로 0이 되어도 조용하다는 점입니다. 검사기가 통과할 때는 아무 소리도 내지 않기 때문에, 꺼진 검사기와 통과시킨 검사기는 겉보기에 완전히 같습니다. 제 하네스에서 실제로 일어난 일입니다.
설정을 자동 동기화하는 과정에서 검사기 등록이 함께 지워졌습니다. 서로 아무 관련 없는 8개 검사기가 같은 11분 창(11:25~11:35) 안에 일제히 발화를 멈췄습니다. 누적 발화 58,544회짜리 검사 묶음이 통째로 사라졌는데, 에러는 한 줄도 나지 않았습니다.
13일 뒤, 정리 작업이 "등록도 안 돼 있고 최근 발화도 0건"을 근거로 그중 2종을 파일까지 삭제했습니다. 생애 발화 24,954회짜리였습니다. 문제는 그 "발화 0건"이 바로 1단계 사고가 만들어낸 값이었다는 겁니다. 사고가 증거를 만들고, 청소가 그 증거를 근거로 사고를 되돌릴 수 없게 만들었습니다.
사라진 검사기 중 하나는 "보조 작업자를 띄울 때 컨텍스트 한계에 맞는 모델을 쓰라"고 강제하던 것이었습니다. 그게 없는 상태에서 보조 작업자를 띄웠고, "프롬프트가 너무 깁니다"로 즉사했습니다. 그 검사기는 정확히 그 사고를 막으려고 만든 것이었습니다. 발견까지 18일이 걸렸습니다.
첫째, 발화 실적은 최근 기간이 아니라 전체 기간으로 센다. 최근 창으로 재면 그 지표가 사고의 증상을 그대로 복창합니다. 둘째, 서로 무관한 검사기가 같은 시각에 함께 멈췄다면 그건 "안 쓰는 검사기"가 아니라 등록부 사고다. 지금은 검사기 등록 개수가 줄어들면 그 자체를 차단하는 장치를 두고 있습니다 — 파일 개수만 보는 감시로는 설정 파일 안쪽에서 사라진 등록을 잡을 수 없기 때문입니다.
"빌드할 때마다 5천 개를 똑같이 다 돌리나요?" — 제 하네스의 답은 "아니오, 다섯 번 걸러서 돌립니다. 대신 무엇을 검사 대상으로 볼지는 한 번도 줄이지 않습니다"입니다.
"수정 범위가 영향 끼치는 노드만 테스트"는 좋은 방향입니다. 여기에 하나만 더 붙이면 훨씬 튼튼해집니다 — 영향 범위 계산이 실패했을 때 무슨 일이 일어나는지를 정해두는 것입니다. 의존성 분석이 실패했는데 조용히 "영향 없음"으로 처리되면, 그 커밋은 아무 테스트도 돌지 않은 채 통과합니다. 통과는 소리를 내지 않기 때문에 그런 커밋이 몇 개나 지나갔는지는 나중에도 알 수 없습니다. 저는 이 경우를 "통과"가 아니라 "판정 불가"로 분류하고 전량 실행으로 되돌립니다. 위 3단계 사고가 정확히 이 교훈의 비싼 버전이었습니다.
한 줄로 줄이면 이렇습니다. 실행 범위는 마음껏 좁히되, 합격 기준과 "모르겠음"의 처리 방향은 절대 좁히지 않는다. 앞의 둘은 비용 문제지만, 뒤의 둘은 신뢰 문제입니다.