검증 도구가 거짓 신호를 준 세 가지 — 테스트·표본·집계 단위
블로그 자동화 도구를 고치면서 하루에 측정 실수 세 개를 연달아 겪었다. 세 가지 모두 코드는 정상적으로 돌았고 예외도 나지 않았다. 숫자도 정확했다. 틀린 것은 그 숫자가 무엇을 뜻하는지였다.
1. 테스트가 거짓으로 실패했다
lint 규칙에 회귀 테스트를 붙였다. 특정 제목에 대해 경고 메시지가 출력되는지 확인하는 내용이었다.
const WARN = "제목에 고유명사(지명·단지명·기관명)가 없다";
test("순수 개념어 제목은 경고한다", () => {
assert.match(lint("공동임차인 보증금반환, 왜 나눠서 못 받나"), new RegExp(WARN));
});
테스트가 실패했다. 그런데 출력을 직접 보니 메시지는 분명히 들어 있었다. 규칙을 의심하며 정규식과 지명 목록을 한참 뒤졌는데, 원인은 규칙이 아니라 테스트 코드 쪽이었다.
const msg = '제목에 고유명사(지명·단지명·기관명)가 없다';
const out = '⚠️ ' + msg + ' — 실측상 …';
out.includes(msg); // true
new RegExp(msg).test(out); // false ← 같은 문자열인데
new RegExp는 문자열을 패턴으로 해석한다. 괄호가 캡처그룹이 되므로 실제로 검사되는 것은 이런 패턴이다.
제목에 고유명사(지명·단지명·기관명)가 없다
↓ 실제 의미
제목에 고유명사 + [캡처그룹: 지명·단지명·기관명] + 가 없다
즉 제목에 고유명사지명·단지명·기관명가 없다를 찾고 있었다. 괄호 문자 자체는 사라진다.
메타문자를 이스케이프하면 통과한다.
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
new RegExp(esc(msg)).test(out); // true
하지만 더 나은 답은 애초에 정규식을 쓰지 않는 것이다.
const has = (out, needle) =>
assert.ok(out.includes(needle), `출력에 "${needle}"가 없다:\n${out}`);
부분 문자열이 있는지만 보고 싶었으니 includes가 의도를 정확히 표현한다. 실패 메시지에 실제 출력을 붙여 두면 다음에 같은 일이 생겨도 원인이 즉시 보인다.
이 버그가 나쁜 이유는 방향이 위험한 쪽이라는 것이다. 거짓 실패는 그나마 눈에 띈다. 하지만 같은 실수가 거짓 통과로 나타날 수도 있다 — 기대 문자열에 .이나 ?가 들어 있으면 패턴이 느슨해져서, 실제로는 다른 출력인데도 통과한다.
2. 43건으로 상관계수를 냈다
광고 수익 데이터를 처음 받아 일자별 조회수와 대조했다. 결과가 흥미로웠다.
조회수 ↔ 수익 상관 -0.31
조회수 ↔ 클릭 상관 -0.15
클릭 ↔ 수익 상관 +0.87
조회수가 가장 높은 날의 수익이 78원이고, 가장 낮은 날이 7,326원이었다. “트래픽이 수익을 끌지 못한다”는 해석이 바로 떠올랐고, 실제로 그렇게 쓸 뻔했다.
멈춘 이유는 원자료를 다시 봤기 때문이다. 7일간 총 클릭이 43건이었다.
클릭당 단가는 78원에서 916원까지 12배로 흔들렸다. 하루 클릭이 1건인 날도 있었다. 이 정도 표본에서 일자별 상관계수는 한두 건의 클릭이 어디에 떨어졌는지에 따라 부호가 바뀐다. 상관계수 함수는 표본이 몇 건이든 성실하게 숫자를 돌려주고, 그 숫자가 믿을 만한지는 말해 주지 않는다.
그래서 결론을 미루고 대신 도구를 고쳤다.
if (clicks < 100) {
console.log(`\n ※ 총 클릭 ${clicks}건 — 100건은 넘어야 유형별 비교가 의미 있다.`);
console.log(` 지금 수치로 "어떤 주제가 돈이 되는가"를 결론내면 과적합이다.`);
}
포인트는 임계값 100이 아니다. 다음에 이 데이터를 보는 사람(나 자신을 포함해)이 43건짜리 상관을 확정된 사실로 읽지 않도록, 경고를 기억이 아니라 출력에 두는 것이다. 문서에 적으면 잊히고, 주석에 적으면 안 읽는다.
3. 합계를 개별과 비교했다
가장 오래 헤맨 것이다.
같은 날 같은 공고를 두 가지 방식으로 썼던 기록이 있었다. 하나는 대상을 여섯 개로 나눠 여섯 편으로, 다른 하나는 여섯 개를 한 편에 묶어서.
나눠 쓴 6편 합계 : 786회
묶어 쓴 1편 : 51회
15배 차이다. “묶으면 손해”라는 결론이 자연스러워 보였고, 그래서 초안 검사 도구에 규칙을 넣었다. 본문에 서로 다른 대상이 셋 이상 나오면 쪼개라고 경고하는 것이었다.
규칙을 만들었으니 실제 데이터에 돌려 봤다. 발행된 100편 전부에 적용한 결과가 이랬다.
경고가 붙은 24편 : 평균 105회
전체 100편 : 평균 60회
경고가 붙은 글이 오히려 평균보다 높았다. 게다가 1위 글(856회)에도 경고가 붙었다.
제목에 묶음 표현이 있는지로 판정을 바꿔 다시 재도 결과는 같았다. 어떤 정의를 써도 “묶은 글이 개별 글보다 나쁘다”는 신호가 나오지 않았다.
돌아가서 원래 근거를 보니 문제가 보였다. 786은 여섯 편의 합계이고 51은 한 편의 값이다. 편당으로 고치면 131 대 51이 되어 차이가 15배에서 2.6배로 줄고, 그마저도 표본이 둘뿐이다.
더 근본적인 문제는 근거의 단위와 규칙의 단위가 다르다는 것이었다. 합계가 크다는 사실이 뜻하는 바는 “여섯 개의 검색 질의를 각각 잡았다”이다. 이것은 무엇을 몇 편으로 쓸지에 대한 근거다. 그런데 내가 만든 규칙은 글 하나를 놓고 좋다 나쁘다를 판정하는 lint였다. 포트폴리오 구성의 근거를 개별 품질 검사에 갖다 쓴 셈이다.
규칙을 지웠다. 지운 이유를 코드가 있던 자리에 남겨 뒀다 — 근거가 없어서 지웠다는 사실을 적어 두지 않으면, 다음에 같은 15배 숫자를 본 사람이 똑같은 규칙을 다시 만든다.
세 가지의 공통점
전부 실행은 정상이었다.
- 테스트는 초록이거나 빨강일 뿐, “이 비교가 의미 있는가”를 묻지 않는다.
- 상관계수 함수는 표본이 3건이든 3만 건이든 숫자를 돌려준다.
- lint는 규칙이 실제 데이터에서 맞는지 검사하지 않는다.
검증 도구는 자기 자신을 검증하지 않는다. 그래서 도구를 만든 다음에 그 도구를 데이터에 대조해 보는 단계가 따로 필요하다. 세 번째 사례에서 규칙을 100편에 돌려 본 것이 그 단계였고, 그 덕분에 틀린 규칙이 코드베이스에 남지 않았다.
실용적인 습관 세 가지로 정리하면 이렇다.
기대값에 메타문자가 있으면 정규식을 쓰지 않는다. 부분 문자열 검사에는 includes가 정확하고, 실패 메시지에 실제 출력을 붙인다.
표본 수를 지표 옆에 항상 같이 출력한다. 사람이 기억하기를 기대하지 말고 도구가 매번 찍게 한다.
규칙을 만들면 기존 데이터 전체에 돌려 본다. 규칙이 잡는 것과 잡지 않는 것의 실제 성과를 비교해서, 그 규칙이 예측력이 있는지 확인한 다음에 커밋한다.
자주 묻는 질문
왜 assert.match 대신 includes를 쓰나요?
기대 문자열에 정규식 메타문자가 들어 있으면 new RegExp가 그것을 패턴으로 해석하기 때문입니다. 괄호는 캡처그룹, 점은 임의 문자, 물음표는 수량자가 됩니다. 부분 문자열이 있는지만 보고 싶다면 includes가 의도를 정확히 표현하고 오해의 여지가 없습니다. 정규식이 꼭 필요하면 메타문자를 이스케이프해야 합니다.
표본이 몇 건이면 충분한가요?
정답은 지표마다 다르고, 이 글의 요점은 특정 숫자가 아니라 그 기준을 도구가 직접 출력하게 만드는 것입니다. 저는 클릭 단위로 크게 튀는 지표라 100건을 임계로 잡고, 그 미만이면 '지금 수치로 결론내면 과적합이다'라는 문장을 도구가 매번 찍도록 했습니다. 사람의 기억이 아니라 출력이 방어선이 됩니다.
합계와 개별을 비교하면 왜 안 되나요?
6편의 합계가 1편보다 크다는 것은 6개의 검색 질의를 각각 잡았다는 뜻이지, 글 하나하나가 더 낫다는 뜻이 아닙니다. 전자는 포트폴리오 구성에 대한 근거이고 후자는 개별 글 품질에 대한 근거인데, 제가 만든 규칙은 글 하나를 보고 판정하는 lint였습니다. 근거의 단위와 규칙의 단위가 어긋난 것입니다.
세 가지의 공통점은 무엇인가요?
전부 실행은 정상이었고 예외도 나지 않았다는 점입니다. 테스트는 초록이거나 빨강일 뿐 '이 비교가 의미 있는가'를 묻지 않고, 상관계수 함수는 표본이 몇 건이든 숫자를 돌려주며, lint는 규칙이 실제 데이터에서 맞는지 검사하지 않습니다. 그래서 검증 도구 자체를 데이터에 대조해 보는 단계가 따로 필요합니다.