Harness Analysis — Test Scope Control

5천 개를 매번 다 돌릴 것인가,
고친 곳만 돌릴 것인가

어제 개발자 단톡방에서 나온 질문입니다. "유닛테스트 5천 개가 CI 파이프라인에 걸려 있는데, 빌드할 때마다 5천 개를 똑같이 다 돌리나요?" 문구 몇 개 고쳤는데 테스트만 30~40분. 같은 질문을 제 Claude Code 하네스에 던져 실측했습니다. 훅(hook) 발화 기록 100만 건, 파일 157개, 등록 96개를 전부 세어 본 결과입니다.

답: 이미 좁히고 있다. 단 한 곳만 빼고

먼저 결론입니다. 제 하네스(harness — 인공지능이 "다 했다"고 거짓말하지 못하게 증거를 강제하는 감시 장치 묶음)는 검사를 매번 전부 돌리지 않습니다. 검사기 파일은 157개인데, 실제로 이번 프로젝트에서 작동하도록 등록된 것은 96개입니다. 나머지는 이 프로젝트에 해당이 없어서 아예 설치되지 않았습니다.

1,001,073검사 발화 (141일)
157 → 96검사기 파일 → 실제 등록
46 → 37템플릿 → 설치된 것
1.501%실제 차단 비율

스코프(scope — 검사 범위)를 좁히는 장치는 5개 층으로 겹쳐 있습니다. 프로젝트 종류로 한 번, 도구 종류로 한 번, 파일 경로로 한 번, 고친 범위가 덮는 시나리오 목록으로 한 번, 마지막으로 "상태가 그대로면 다시 안 함"으로 한 번입니다. 그런데 딱 한 구역만 일부러 전수(全數)로 남겨뒀습니다. 그 이유가 이 글의 핵심입니다.

Key Insight

단톡방 대화의 결론("검증 빌드는 메인에 머지할 때만, 평소엔 수정 범위만")과 제 하네스가 도달한 구조는 서로 모르는 채 같은 모양입니다. 평소에는 좁게, 되돌릴 수 없는 순간(푸시·배포·완료 선언)에만 넓게. 다만 한 가지가 다릅니다. 저는 "무엇을 테스트할지 고르는 일"만큼은 절대 좁히지 않습니다 — 그걸 좁히는 순간, 통과하기 쉬운 것만 고르는 부정행위가 구조적으로 가능해지기 때문입니다.

같은 질문, 다른 비용 구조

단톡방의 문제와 제 하네스의 문제는 겉보기엔 같지만 아픈 지점이 다릅니다. 한쪽은 기다리는 시간이 아프고, 다른 쪽은 맞는 검사가 켜져 있는가가 아픕니다. 이 차이가 해법의 방향도 갈라놓습니다.

CI Pipeline — 단톡방 사례

유닛테스트 5,000개 / 전량 실행

"프론트 문구 몇 개 바꿨는데 테스트만 30~40분"

  • 실행 단위: 테스트 케이스 5,000개
  • 트리거: 모든 빌드 · 모든 플랫폼
  • 비용: 벽시계 시간 + 실행 비용 (직접 체감)
  • 한 번 실행에 수십 분 → 개발 리듬이 끊김
  • 해법: 테스트 노드에 식별자를 붙이고, 고친 범위가 영향 주는 노드만 골라 실행
Claude Code Harness — 내 환경

검사 훅 157개 / 도구 호출마다 발화

"141일 동안 100만 번 발화, 그중 1.5%만 실제로 막았다"

  • 실행 단위: 훅(hook) 스크립트 — 파일 저장·명령 실행 직전에 끼어드는 검사기
  • 트리거: 도구 호출 1건마다 (파일 수정, 셸 명령, 작업 종료 등)
  • 비용: 개별 실행은 짧지만 모든 작업 앞에 붙음
  • 진짜 위험: 느려지는 것보다 맞는 검사가 안 걸려 있는 것
  • 해법: 5개 층으로 범위를 좁히되, 되돌릴 수 없는 순간엔 전량으로 되돌림
측정의 한계 — 먼저 밝힙니다

제 훅 발화 기록에는 실행 소요시간이 남지 않습니다. 그래서 이 글은 "훅 때문에 몇 분이 느려졌다"는 주장을 하지 않습니다. 셀 수 있는 것은 발화 횟수·차단 횟수·설치 범위뿐이고, 아래 수치는 전부 그 셋에서만 나왔습니다. 같은 이유로 로그에는 "무엇이 차단됐는지"(대상 파일·명령)도 남지 않아, 차단 1건의 내용을 사후에 복원할 수 없습니다. 이건 제 하네스의 알려진 결함이고, 고칠 후보로 이미 등록돼 있습니다.

157개가 어떻게 걸러지는가

검사기는 157개가 있지만, 파일 하나를 저장할 때 157개가 전부 도는 것이 아닙니다. 아래 그림처럼 네 번 걸러진 뒤에야 실제로 실행됩니다. 각 층은 앞 층이 통과시킨 것만 받습니다.

검사 범위 깔때기 — 파일 157개에서 실제 판정까지 검사기 파일 전체 — 후보 집합 ~/.claude/hooks 에 존재하는 모든 검사 스크립트 157 축 1 · 설치 스코프 — 프로젝트 종류로 거름 웹인가 앱인가 서버인가를 감지해 해당 묶음만 설치 96 템플릿 9종 미설치 앱·서버·DB 전용 축 2 · 매처 — 도구 종류로 거름 셸 명령용·파일 저장용 검사기가 서로 다르게 등록됨 6종 해당 도구 아님 발화 자체가 없음 축 3 · 경로 — 고친 파일로 거름 내 담당 파일이 아니면 즉시 통과 44 경로 불일치 즉시 통과 처리 실제 판정 — 통과 or 차단 100만 발화 중 15,026건 차단 (1.501%) 98.499% 통과 막을 것이 없었음
파일 157개가 프로젝트 종류(축 1) → 도구 종류(축 2) → 파일 경로(축 3) 순으로 걸러진 뒤에야 실제 판정에 도달한다. 오른쪽으로 빠지는 점선이 각 층에서 걸러지는 몫이다. 수치는 이 프로젝트 기준 실측값.
"플랫폼마다 다 시킨다"의 반대편

단톡방에서 "엄청 플랫폼마다 다 시키고 있어요"라는 말이 나왔습니다. 제 하네스는 반대로 갑니다. 프로젝트를 열 때 어떤 종류의 프로젝트인지 먼저 감지하고, 그 종류에 해당하는 검사 묶음만 설치합니다. 감지 순서는 앱 설정 파일(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 — 셸 명령 실행 직전616비밀키 커밋 차단 · 위험한 삭제 명령 차단 · 푸시 전 검증
Edit|Write — 파일 저장 직전/직후416포맷 검사 · 금지 패턴 · 문서 동기화
Skill / Workflow2외부 기능 로드 전 보안 스캔
도구 무관 (대화 시작·종료)457완료 선언 검증 · 상태 저장 · 컨텍스트 주입
Axis 3 — 파일 경로

세 번째 필터는 검사기 안쪽에 있습니다. 깨어난 검사기도 대상 파일 경로를 먼저 보고, 자기 담당이 아니면 즉시 통과시킵니다. 157개 중 44개가 이 방식으로 스스로 범위를 좁히고, 155개가 "검사할 게 없으면 즉시 통과"하는 조기 종료 경로를 갖고 있습니다. 이게 100만 발화의 98.5%가 아무 일 없이 지나가는 이유입니다 — 낭비가 아니라 설계입니다.

"노드 아이디 목록만 넘긴다"와 같은 구조

단톡방에서 나온 아이디어는 이겁니다. "테스트 노드마다 아이디를 따서, 수정 범위가 영향을 끼치는 부분만 그룹핑해 테스트한다." 제 하네스에도 똑같은 장치가 있습니다. 이름은 covers_inventory, 우리말로 옮기면 "이 시나리오가 덮는 화면 요소 목록"입니다.

먼저 화면의 버튼·폼·API·데이터베이스 항목을 전수로 훑어 각각 고유 아이디를 붙입니다. 그다음 테스트 시나리오마다 "나는 이 아이디들을 검증한다"를 적어둡니다. 코드를 고치면 그 파일이 건드린 아이디를 뽑고, 거꾸로 그 아이디를 포함한 시나리오만 골라 다시 돌립니다.

역인덱스 재검증 — 고친 것이 덮는 시나리오만 고른다 코드 수정 발생 고친 파일이 건드린 화면 요소 아이디를 뽑는다 영향받은 요소 버튼 · but-a1b2 폼 · for-c3d4 API · api-e5f6 전체 시나리오 (예시 5건) TC-001 로그인 TC-002 글 등록 왕복 TC-003 알림 설정 TC-005 검색 결과 TC-007 프로필 수정 covers_inventory 역인덱스 재검증 2건 실행 · 나머지 3건 건너뜀
고친 파일이 건드린 요소 아이디(왼쪽)를 거꾸로 조회해 그 아이디를 덮는 시나리오(오른쪽 강조)만 다시 돌린다. 단톡방의 "노드 아이디 배열만 넘긴다"와 같은 구조를, 테스트 노드가 아니라 화면 요소 단위로 구현한 것.
차이점 하나

단톡방 방식은 테스트를 골라내는 방식이고, 제 방식은 화면 요소를 먼저 전수로 세고 그 요소를 덮는 테스트를 역으로 찾는 방식입니다. 방향이 반대인 이유는 하나입니다 — 요소 목록에 있는데 어떤 시나리오도 덮지 않는 항목이 나오면, 그건 테스트가 빠진 곳이라는 뜻이고 검사기가 그 자리에서 통과를 막습니다. 범위를 좁히는 장치를 동시에 누락을 잡는 장치로 쓰는 셈입니다.

"언제 다시 돌려야 하는가"의 답을 시간에서 상태로

범위를 좁히는 마지막 축은 "이미 통과한 검증을 언제까지 믿을 것인가"입니다. 예전 제 하네스는 시간으로 판단했습니다 — "1시간 안에 통과했으면 유효". 이 방식은 2026년 8월 3일에 전부 폐기했습니다. 파일 시각만 갱신하는 명령 한 줄이면 아무것도 검증하지 않고 통과할 수 있었기 때문입니다.

폐기 — 시간 기반

1시간 유효기간 방식

"방금 돌렸으니까 괜찮다"

  • 판정 기준: 검증 기록의 파일 시각
  • 구멍 1: 시각만 바꾸면 그냥 통과
  • 구멍 2: 무엇을 검증했는지와 무관
  • 50분 전 검증 + 그 사이 코드 30줄 수정 → 여전히 통과
  • 2026-08-03 전면 폐기
현행 — 상태 기반

상태 지문 결속

"검증한 상태와 지금 상태가 같은가"

  • 판정 기준: 커밋 위치 + 변경 목록 + 변경 내용 + 추적 안 되는 새 파일의 내용까지 합친 지문
  • 3일 전 검증이어도 상태가 같으면 유효
  • 1분 전 검증이어도 상태가 다르면 무효 → 재검증 강제
  • 검증 결과 캐시는 파일 내용 해시로 재사용 (안 고친 파일은 건너뜀)
  • 지문 계산 실패는 통과가 아니라 중단으로 처리
이 축이 스코프 조절인 이유

얼핏 보면 안전장치 같지만, 실제 효과는 범위 축소입니다. 상태가 그대로면 이미 통과한 검증을 다시 돌리지 않고, 고친 파일이 덮는 부분만 다시 돌립니다. 반대로 상태가 바뀌면 그 순간 넓은 검증으로 되돌아갑니다. 단톡방의 "검증 빌드는 메인에 머지할 때만"과 같은 판단을, 브랜치가 아니라 상태 해시로 하는 셈입니다.

깨진 것 위에 비싼 검사를 얹지 않는다

단톡방에서 "문제는 비용과 시간"이라는 말이 나왔습니다. 제 하네스에서 비용이 가장 큰 검사는 다른 인공지능 모델을 불러 적대적으로 리뷰시키는 단계브라우저를 띄워 실제로 클릭해보는 단계입니다. 그래서 검증을 4단 사다리로 세우고, 아래 단을 통과하지 못하면 위 단은 실행조차 하지 않습니다.

이유는 두 가지입니다. 첫째, 기능이 깨진 상태에서 하는 고급 검사는 그냥 낭비입니다. 둘째, 그 검사 결과는 고치는 순간 무효가 됩니다 — 수정이 들어가면 상태 지문이 바뀌고, 위에서 본 규칙에 따라 이전 통과 기록이 전부 무효가 되기 때문입니다.

검증 레벨 사다리 — 아래를 통과해야 위가 실행된다 한 단 안에서는 여러 검사를 동시에 돌리고, 단과 단 사이만 순서를 지킨다 레벨 4 · 운영 실제 배포 환경에서도 배포 URL 재검증 · 스모크 레벨 3 · 적대 깨뜨리려 해도 버티는가 타 모델 교차 리뷰 · 공격 프로브 레벨 2 · 휴리스틱 전문가가 보면 잡을 결함 빈 화면 · 경계값 · 모바일 375px 레벨 1 · 기능 요구한 대로 동작하는가 시나리오 전건 · 빌드 · 왕복 통과 후에만 통과 후에만 배포 있을 때만 어느 단에서든 실패하면 → 고친 뒤 레벨 1부터 다시 (이전 통과 기록은 무효)
한 단이라도 실패하면 위 단은 실행되지 않고, 수정 후에는 맨 아래부터 다시 오른다. 비싼 검사(다른 모델 리뷰·브라우저 실행)를 마지막에 배치해 실패 시 낭비를 줄이는 구조.
100만 번 중 1.5%만 실제로 막았다

2026년 4월 12일부터 8월 31일까지 141일 동안 검사기는 1,001,073번 깨어났고, 그중 15,026번만 실제로 작업을 막았습니다. 나머지 98.5%는 아무 일 없이 지나갔습니다. 이 수치를 보고 "98.5%가 낭비"라고 결론 내리기 쉬운데, 그건 틀린 독해입니다. 아래에 왜 그런지 실측으로 보입니다.

검사기차단 / 발화차단 비율성격
작업 재확인 게이트555 / 2,27924.35%가장 좁게 잘 걸린 검사 — 완료 선언 직전 실제 작업 여부 확인
커밋 전 검증5,815 / 68,9548.43%차단 절대량 1위
설계 문서 게이트1,067 / 14,4447.39%코드 형태를 먼저 리뷰하게 강제
외부 기능 보안 스캔2,252 / 31,5837.13%외부 스킬 로드 전 악성 패턴 차단
SQL 주입 검사716 / 12,1755.88%보안 — 위험 대비 낮은 발화
푸시 전 적대 리뷰 게이트1,126 / 70,1981.60%발화 2위인데 차단은 드묾 — 되돌릴 수 없는 순간의 보험
강제 푸시 · 루트 삭제 금지0 / 2,2160%차단 코드는 있는데 0건 — 그 위험 행위를 한 번도 안 했다는 뜻
컨텍스트 감시 · 프롬프트 보강0 / 9,113해당 없음차단 기능 자체가 없음 — 기록·주입 전용이라 이 지표로 재면 안 됨
차단률로 검사기를 지우면 안 되는 이유

차단 0건 검사기 10종을 코드로 열어보니 두 종류로 갈렸습니다. 첫째, 차단 명령(exit 2)이 아예 없는 것들 — 컨텍스트 감시·프롬프트 보강·피드백 수집처럼 기록만 하는 도구입니다. 이들에게 차단률을 묻는 건 온도계에게 "몇 명을 막았냐"고 묻는 것과 같습니다. 둘째, 차단 명령이 있는데 0건인 것들 — 강제 푸시 금지, 루트 디렉토리 삭제 금지 같은 것들입니다. 여기서 0건은 실패가 아니라 "그 사고가 141일 동안 한 번도 안 났다"는 뜻입니다. 화재경보기가 울리지 않았다고 떼지 않는 것과 같습니다.

그래서 스코프를 좁힐 때 제가 쓰는 기준은 차단률이 아니라 "이 검사가 깨어나는 순간이 그 위험이 실제로 발생할 수 있는 순간인가"입니다. 셸 명령을 실행할 때만 위험한 검사가 파일 저장할 때도 깨어난다면 그건 줄일 대상이고, 발화가 많아도 깨어나는 순간이 정확하면 그대로 둡니다.

한 구역만은 절대 좁히지 않는다

여기까지는 전부 "어떻게 줄였는가"였습니다. 이제 반대 이야기입니다. 제 하네스에는 샘플링을 명시적으로 금지하는 구역이 있습니다. 화면의 버튼이 40개면 40개를 전부 눌러야 하고, 39개만 누르면 검사기가 통과를 막습니다. "버튼 3개만 확인" 같은 시나리오는 작성 단계에서 거부됩니다.

좁혀도 되는 것

언제 · 무엇으로 검사할지

"실행 비용"에 관한 결정

  • 어떤 검사기를 설치할지 (프로젝트 종류로)
  • 어떤 도구를 쓸 때 깨어날지 (매처로)
  • 어떤 파일일 때 실제로 볼지 (경로로)
  • 무엇을 다시 볼지 (역인덱스·상태 지문으로)
  • 비싼 검사를 언제 부를지 (레벨 사다리로)
좁히면 안 되는 것

무엇이 검증 대상인가

"합격 기준"에 관한 결정

  • 화면 요소 목록 — 버튼·폼·API·DB 전수 수집
  • 모든 요소가 최소 하나의 시나리오에 포함될 것
  • 버튼은 전수 클릭 — 센 개수와 누른 개수가 같아야 통과
  • 요구사항에서 뽑은 합격 조건 전건 확인
  • "샘플링했습니다"는 통과 사유가 되지 않음
왜 이 선을 그었나 — 실제 사고에서 나온 규칙

제 하네스가 자동으로 이슈를 처리하던 시기에 포렌식을 돌린 적이 있습니다. 자율 처리 실패율 65%, 사람이 되돌려보낸 비율 49%였고, 원인을 추적하니 한 지점으로 모였습니다. 과녁이 "사용자가 원한 기능이 동작하는가"가 아니라 "내가 정한 테스트가 통과하는가"로 바뀌어 있었습니다. 극단적인 사례에서는 버튼을 화면에서 숨기거나 아무 일도 하지 않는 함수로 바꿔서 자기 테스트를 통과시켰습니다. 테스트는 초록불인데 기능은 사라진 겁니다.

"무엇을 테스트할지 고르는 권한을 실행자에게 주면, 통과하기 쉬운 것만 고르는 길이 열린다. 그래서 실행 범위는 좁혀도 합격 기준은 좁히지 않는다."

범위를 좁히다 0이 되면 아무도 모른다

스코프 조절의 진짜 위험은 "너무 좁혀서 놓친다"가 아닙니다. 실수로 0이 되어도 조용하다는 점입니다. 검사기가 통과할 때는 아무 소리도 내지 않기 때문에, 꺼진 검사기와 통과시킨 검사기는 겉보기에 완전히 같습니다. 제 하네스에서 실제로 일어난 일입니다.

1
8월 7일 — 11분 사이에 8종이 동시에 멈춤

설정을 자동 동기화하는 과정에서 검사기 등록이 함께 지워졌습니다. 서로 아무 관련 없는 8개 검사기가 같은 11분 창(11:25~11:35) 안에 일제히 발화를 멈췄습니다. 누적 발화 58,544회짜리 검사 묶음이 통째로 사라졌는데, 에러는 한 줄도 나지 않았습니다.

2
8월 20일 — "안 쓰는 것 같으니 지우자"

13일 뒤, 정리 작업이 "등록도 안 돼 있고 최근 발화도 0건"을 근거로 그중 2종을 파일까지 삭제했습니다. 생애 발화 24,954회짜리였습니다. 문제는 그 "발화 0건"이 바로 1단계 사고가 만들어낸 값이었다는 겁니다. 사고가 증거를 만들고, 청소가 그 증거를 근거로 사고를 되돌릴 수 없게 만들었습니다.

3
8월 25일 — 사라진 검사기가 막으려던 사고가 발생

사라진 검사기 중 하나는 "보조 작업자를 띄울 때 컨텍스트 한계에 맞는 모델을 쓰라"고 강제하던 것이었습니다. 그게 없는 상태에서 보조 작업자를 띄웠고, "프롬프트가 너무 깁니다"로 즉사했습니다. 그 검사기는 정확히 그 사고를 막으려고 만든 것이었습니다. 발견까지 18일이 걸렸습니다.

여기서 얻은 두 가지 규칙

첫째, 발화 실적은 최근 기간이 아니라 전체 기간으로 센다. 최근 창으로 재면 그 지표가 사고의 증상을 그대로 복창합니다. 둘째, 서로 무관한 검사기가 같은 시각에 함께 멈췄다면 그건 "안 쓰는 검사기"가 아니라 등록부 사고다. 지금은 검사기 등록 개수가 줄어들면 그 자체를 차단하는 장치를 두고 있습니다 — 파일 개수만 보는 감시로는 설정 파일 안쪽에서 사라진 등록을 잡을 수 없기 때문입니다.

단톡방 질문에 대한 답

"빌드할 때마다 5천 개를 똑같이 다 돌리나요?" — 제 하네스의 답은 "아니오, 다섯 번 걸러서 돌립니다. 대신 무엇을 검사 대상으로 볼지는 한 번도 줄이지 않습니다"입니다.

1
평소에는 좁게
프로젝트 종류 · 도구 종류 · 파일 경로로 세 번 거른다 (157 → 96 → 실제 발화)
2
고쳤을 때는 고친 곳이 덮는 만큼만
화면 요소 아이디를 거꾸로 조회해 관련 시나리오만 재검증 — 단톡방의 "노드 아이디 배열"과 같은 구조
3
되돌릴 수 없는 순간에는 전량으로
푸시 · 배포 · 완료 선언 직전 — 상태 지문이 바뀌었으면 레벨 1부터 다시 오른다
4
합격 기준은 언제나 전수
버튼 40개면 40개 전부. 샘플링은 통과 사유가 되지 않는다
단톡방 방식에 덧붙이고 싶은 한 가지

"수정 범위가 영향 끼치는 노드만 테스트"는 좋은 방향입니다. 여기에 하나만 더 붙이면 훨씬 튼튼해집니다 — 영향 범위 계산이 실패했을 때 무슨 일이 일어나는지를 정해두는 것입니다. 의존성 분석이 실패했는데 조용히 "영향 없음"으로 처리되면, 그 커밋은 아무 테스트도 돌지 않은 채 통과합니다. 통과는 소리를 내지 않기 때문에 그런 커밋이 몇 개나 지나갔는지는 나중에도 알 수 없습니다. 저는 이 경우를 "통과"가 아니라 "판정 불가"로 분류하고 전량 실행으로 되돌립니다. 위 3단계 사고가 정확히 이 교훈의 비싼 버전이었습니다.

한 줄로 줄이면 이렇습니다. 실행 범위는 마음껏 좁히되, 합격 기준과 "모르겠음"의 처리 방향은 절대 좁히지 않는다. 앞의 둘은 비용 문제지만, 뒤의 둘은 신뢰 문제입니다.