같은 저장소에서 같은 것을 세 번 셌습니다. 세 번 다 답이 달랐습니다.
센 것은 이 시리즈 내내 배경에 깔려 있던 규칙 하나입니다. 테스트 이름은 한국어로 의도를 표현한다. 1편이 인용한 코딩 원칙 체크리스트에 한 항목으로 들어 있고, “기계로 셀 수 있는 규칙”의 대표 격입니다. 단언이 구체적인지는 사람이 읽어야 알지만, 메서드 이름에 한글이 있는지는 정규식이 답합니다. 그러니 몇 번을 세든 같은 답이 나와야 합니다.
| 측정 | 시각 | 테스트 메서드 | 한글 메서드명 | @DisplayName |
|---|---|---|---|---|
| 1차 | 오전 | 10,379 | 5,186 (50.0%) | 7,641 |
| 2차 | 오전 | 10,465 | 4,609 (44.0%) | 9,782 |
| 3차 | 14:20 | 11,062 | 5,590 (50.5%) | 10,312 |
3편은 원래 이 수치를 논거로 쓸 참이었습니다. 기계적으로 검사되는 규칙은 잘 지켜지고, 판단이 필요한 규칙은 안 지켜진다는 이야기입니다. 그런데 그 기계적인 검사가 검사기마다 다른 답을 냈습니다.
1편은 기준이 문서에 있는데 260곳 남짓에서 안 지켜진 이야기였고, 2편은 기준이 지켜졌는데 그 기준이 틀렸던 이야기였습니다. 이 글은 기준을 검사하는 쪽이 틀렸던 이야기입니다.
⚠️ 이 글에서
[재구성]표시가 붙은 것은 사내 저장소의 코드, 문서, 커밋 메시지의 구조와 요지만 남긴 것입니다. 이름은 중립적인 것으로 바꿨고 경로와 줄번호는 달지 않습니다.[원문]표시가 붙은 것은 이 블로그 저장소와 필자가 공개 배포하는 하네스 플러그인이라 원문 그대로이며 출처를 밝힙니다. 모듈 이름은 이 글 안에서만 통용되는 임의의 라벨입니다.
한 가지 배경을 먼저 밝혀 둡니다. 이 시리즈는 에이전트 여럿에게 나눠 맡겨 썼습니다. 1차 측정은 1편의 근거를 모은 리서치 에이전트가 냈고, 2차는 그 값이 미심쩍어 제가 직접 다시 센 것이며, 3차는 3편을 쓰면서 파서를 처음부터 새로 짜 낸 값입니다.1 규칙을 위임한 결과를 재려다가 위임의 실패를 먼저 만났습니다.
한쪽은 오차가 아니었습니다
@DisplayName부터 봅니다. 7,641과 9,782는 28% 차이입니다. 이 정도면 어느 한쪽이 틀렸다고 보는 게 자연스러운데, 3차 측정에서 부착 대상별로 나눠 세니 둘 다 맞는 답이었습니다.
| 질문 | 3차 측정값 | 선행 측정의 대응값 |
|---|---|---|
“테스트 메서드 몇 개가 @DisplayName을 갖는가” |
8,227 | 7,641 |
“파일 안에 @DisplayName이 몇 개 있는가” |
10,312 | 9,782 |
차이는 클래스에 붙은 @DisplayName 2,085개, 전체의 20.2%입니다. 비율로 보면 더 분명해집니다. 선행 두 측정의 비는 9,782 / 7,641 = 1.280이고, 3차 측정의 비는 10,312 / 8,227 = 1.254입니다.
즉 두 측정이 각각 정확하게 셌고, 서로 다른 것을 셌습니다. 한쪽은 테스트 메서드에 붙은 것만 셌고 다른 쪽은 파일에 있는 전부를 셌습니다. 어느 쪽도 실수하지 않았어요.
문제는 그 다음입니다. 어느 노트에도 무엇을 셌는지가 적혀 있지 않았습니다. 남은 것은 @DisplayName 7,641이라는 이름표뿐이고, 그 이름표는 자기가 무엇을 셌는지 말하지 않습니다. 그래서 두 숫자가 같은 이름을 달고 나란히 놓였고, 나란히 놓이는 순간 하나가 틀린 것처럼 보였습니다.
1편이 “이름이 단언보다 강한 명세면 이름이 거짓말한다”였는데, 여기서는 측정치의 이름표가 측정 기준보다 강한 명세였습니다. 그리고 노트에 남는 것은 언제나 이름표 쪽입니다.
다른 쪽은 버그였습니다
한글 이름 비율은 성격이 다릅니다. 50.0%와 44.0%는 6.0%p 차이인데, 모수는 거의 같습니다. 10,379와 10,465로 0.8% 차이입니다. 두 측정이 사실상 같은 파일 집합을 봤다는 뜻이죠. 그런데 분자만 12.5% 차이라면 파일 범위 문제가 아니라 이름 추출 문제입니다.
3차 측정 데이터 위에서 실패 시나리오를 하나씩 걸어 봤습니다. 맞는 값을 내는 시나리오는 하나였습니다. 애노테이션 블록에 중괄호 {가 들어 있으면 메서드 이름 추출이 실패하는 경우입니다.
| 시나리오 | 분자 | 모수 | 비율 |
|---|---|---|---|
| 정상 (3차 측정) | 5,590 | 11,062 | 50.5% |
| 중괄호 블록의 이름 추출 실패 | 4,876 | 11,062 | 44.1% |
| (참고) 중괄호 블록을 아예 제외 | 4,876 | 9,922 | 49.1% |
44.1%. 2차 측정값 44.0%와 0.1%p 차이입니다. 모수를 유지한 채 이름만 잃는 시나리오가 정확히 그 숫자를 만듭니다. 세 번째 줄이 보조 증거인데, 블록을 통째로 빼면 모수도 함께 줄어 49.1%가 나옵니다. 모수는 그대로고 분자만 빠지는 형태여야 44%가 됩니다.
다만 이건 원인을 지목한 게 아니라 역산한 것입니다. 선행 두 파서의 스크립트는 소실됐고, 저는 원 코드를 읽어 버그를 짚은 적이 없습니다.2 정확한 서술은 “원인을 특정했다”가 아니라 “이 원인을 넣으면 그 값이 재현된다”입니다.
그리고 이 계열의 버그는 이번이 처음이 아닙니다. 2편의 컨텍스트 집계 파서도 같은 자리에서 깨졌습니다. 클래스 선언 앞을 } 기준으로 자르면 @SpringBootTest(classes = {A.class, B.class})의 }에 걸려 애노테이션을 통째로 놓치는 것이었죠. 고친 뒤 대상 클래스가 775개에서 892개로 늘었습니다. 같은 계열의 버그가 이 시리즈에서 두 번 났고, 두 번 다 제가 만든 파서였습니다.
그 버그가 놓친 쪽
여기까지는 그냥 버그입니다. 중요한 건 그 버그가 무엇을 놓쳤는가입니다.
| 집단 | 개수 | 한글 이름 | 비율 |
|---|---|---|---|
애노테이션 블록에 {가 있는 테스트 메서드 |
1,140 | 704 | 61.8% |
| 전체 | 11,062 | 5,590 | 50.5% |
파서가 이름을 잃은 집단은 전체보다 규칙을 더 잘 지키는 집단이었습니다. 우연이 아닙니다. 애노테이션 종류로 쪼개면 이유가 나옵니다.
| 애노테이션 | 개수 | 한글 이름 | 비율 |
|---|---|---|---|
@Test (일반) |
8,407 | 3,335 | 39.7% |
@ParameterizedTest |
2,655 | 2,255 | 84.9% |
중괄호는 파라미터화 테스트에 몰려 있습니다. 다만 목록은 정확히 읽어야 합니다. 중괄호가 있는 블록에 함께 실린 애노테이션 상위는 @MethodSource 454건, @Order 247건, @ValueSource 158건, @SqlGroup 79건, @NullSource 59건, @CsvSource 52건 순입니다. 그런데 이 목록이 말하는 것은 “같은 블록에 있었다”이지 “중괄호를 만들었다”가 아닙니다. @MethodSource와 @Order는 인자에 중괄호를 쓰지 않습니다. 중괄호를 실제로 만드는 것은 @ParameterizedTest(name = "…{0}…")의 이름 템플릿과 @ValueSource, @CsvSource처럼 배열을 받는 인자이고, 둘 다 파라미터화 테스트에만 붙습니다.
그러니까 파서의 사각지대는 파라미터화 테스트였고, 파라미터화 테스트는 한글 이름 준수율이 85%로 일반 테스트(40%)의 두 배가 넘습니다. 사각지대가 측정 대상과 상관돼 있었습니다.
결과적으로 그 버그는 잡음을 더한 게 아니라 준수율을 체계적으로 낮춰 보고했습니다. 무작위 오차라면 비율이 위아래로 흔들리기만 하는데, 이 오차는 한쪽으로만 밀었어요. 그리고 밀린 거리가 서술을 바꿉니다. 44%는 “절반도 안 지킨다”로 읽히고 50%는 “절반은 지킨다”로 읽힙니다.
측정 오차가 측정 대상과 상관되면 그건 잡음이 아니라 가짜 신호입니다. 이게 이 글의 중심입니다. 규칙을 기계에게 넘기면 사각지대도 함께 넘기게 되고, 그 사각지대는 거의 무작위가 아닙니다. 검사기가 못 보는 자리는 대체로 코드의 어떤 성질과 붙어 있고, 그 성질이 준수율과 무관할 이유는 없습니다.
여기서 아무도 실패하지 않았다는 점이 중요합니다. 파서는 예외를 던지지 않았고, 결과 표는 멀쩡하게 출력됐고, 숫자는 그럴듯했습니다. 1편의 항진명제도, 2편의 null이 된 트래커 팩토리도 같은 모양이었습니다. 전부 초록불인 채로 답만 달라졌습니다.
44%를 그대로 받았다면
이 글은 다르게 쓰였을 겁니다. “기계로 검사되는 규칙조차 절반도 안 지켜진다”가 주제문이 됐겠죠. 그 문장은 틀렸을 뿐 아니라 그럴듯하기까지 합니다.
제가 배포하는 하네스 플러그인에 이 실패를 겨냥한 항목이 있습니다.3 원문 그대로입니다.
인지적 굴복 (Cognitive Surrender): 자기 의견을 세우기 전에 AI 답변을 그대로 수용하는 것 — 특히 debug·verify에서 “왜 이게 원인인지”를 스스로 재구성한 뒤 채택한다
3차 측정에서 실제로 한 일이 저 문장의 뒷부분입니다. 두 값 중 하나를 고른 게 아니라 왜 이 값이 이 값인지를 처음부터 다시 만들었고, 만들고 나니 원인이 대상이 아니라 검사기에 있었습니다.
그래서 논지를 고쳐 세웁니다
검사가 검사기마다 다른 답을 냈다면, “기계적으로 검사 가능한 규칙”을 전제로 세운 논지는 무너지는 걸까요. 저는 아니라고 봅니다. 근거 셋입니다.
첫째, 갈린 것은 준수율이 아니라 측정 기준입니다. @DisplayName 쪽은 애초에 오차가 아니었고, 한글 이름 쪽은 원인 시나리오를 넣으니 0.1%p 안에서 재현됐습니다. 두 측정 다 사후에 재구성할 수 있었다는 것 자체가 “기계적으로 검사 가능”의 증거입니다. 판단이 필요한 규칙에서는 이런 규명이 아예 불가능합니다. “이 단언이 충분히 구체적인가”를 세 사람이 다르게 답했을 때, 어느 답이 왜 나왔는지를 소수점까지 되짚을 방법은 없습니다.
둘째, 1편의 근거 노트가 이 규칙에 걸었던 대조는 흔들리지 않았습니다. 1편이 약한 단언으로 가장 심하게 지목한 그 모듈은 3차 측정에서도 한글 이름 준수율 95.6%로 소수점까지 재현됐고, 금융 프로토콜 모듈들의 0%도 그대로였습니다. 둘 다 1편 본문에는 없고 노트에만 있는 수치입니다. 흔들린 것은 전체 평균이라는 뭉뚱그린 수치였습니다.
셋째, 그리고 새로 드러난 것이 있습니다. “기계적으로 검사 가능”이 공짜가 아닙니다. 한글 이름 규칙 하나를 세려면 주석과 문자열 리터럴을 분리하고, 애노테이션 블록의 괄호 짝을 맞춰 건너뛰고, 이어지는 선언이 클래스인지 메서드인지 판별해야 합니다. grep으로 되는 일이 아니에요. 실제로 두 번의 시도가 그 지점에서 깨졌습니다.
검사 가능성은 이분법이 아니라 비용의 스펙트럼입니다. 2편의 결론은 “검사하기 쉬운 기준이 하나 있으면 그게 유일한 기준인 것처럼 작동한다”였습니다. 3편은 그 결론을 물리는 게 아니라, 그 “쉬움”조차 등급이 있다는 것을 보탭니다. 그러니 3편이 쓰려던 명제 쪽을 이렇게 고쳐 세웁니다.
기계적으로 검사 가능한 규칙은 지켜지고 판단이 필요한 규칙은 안 지켜진다.규칙마다 검사 비용이 다르고, 검사 비용이 준수율을 만든다. 그리고 검사기를 만드는 순간 검사기의 사각지대가 새 변수로 들어온다.
두 가지를 더 재 봤고, 둘 다 음성이었습니다
3편의 주제상 자연스럽게 떠오르는 가설이 있습니다. “에이전트가 쓴 코드는 체크리스트를 더 잘 지키지 않을까.” 커밋 본문의 Co-authored-by: Claude 트레일러 비율로 재 봤습니다. [재구성]
| 모듈 | 커밋 수 | 에이전트 공동 작성 | 한글 이름 준수율 |
|---|---|---|---|
| 모듈 A | 183 | 0% | 95.6% |
| 모듈 E | 93 | 56% | 99.7% |
| 모듈 H | 84 | 17% | 97.4% |
| 모듈 I | 40 | 0% | 98.6% |
| 모듈 B | 390 | 32% | 84.0% |
| 재구축 트랙 | 108 | 0% | 4.8% ~ 94.1% |
상관이 없습니다. 공동 작성 0%인 모듈이 95.6%와 98.6%를 찍고, 56%인 모듈이 99.7%를 찍고, 역시 0%인 재구축 트랙이 4.8%도 찍습니다. 그리고 모듈마다 도메인, 시기, 작성자가 전부 다르니 반대 방향 주장도 할 수 없습니다. 이 데이터로는 판별되지 않습니다. 음성 결과지만 적어 둡니다. 3편이 “에이전트에게 맡기면 규칙을 더 잘 지킨다”로 흐를 자리가 여기였으니까요.
두 번째로 잰 것은 저장소 드리프트입니다. 6% 격차가 그냥 시간 차이일 수도 있으니까요. 세션 동안 세 번 재니 테스트 파일이 2,099개에서 2,100개로, 메서드가 11,052에서 11,063을 거쳐 11,062로 갔습니다. 마지막 1분 사이에 하나가 줄어든 셈이죠. 실제로 14:13, 14:15, 14:16, 14:19에 테스트 파일 수정이 있었습니다. 커밋 기준으로도 오전 10시 시점과 지금 사이에 테스트 애노테이션 줄이 122줄(1.1%) 늘었습니다.4 드리프트는 1% 수준이라 6% 격차를 설명하지 못하지만, 사실 하나가 남습니다. 저장소 수치는 사실이 아니라 타임스탬프가 붙은 관측입니다. 1편의 3.4%도, 2편의 892클래스 213컨텍스트도, 이 글의 50.5%도 그날 그 시각의 스냅숏입니다.
같은 규칙이 두 판본으로 존재합니다
여기서부터는 검사기가 아니라 규칙 자체를 봅니다. 2편이 다룬 세 신호는 지금 서로 다른 두 곳에 적혀 있습니다.
- 사내 프로젝트 로컬 스킬: 한 저장소의
.claude/skills/아래에 컨벤션 스킬 7종.[재구성] - 공개 하네스 스킬:
sr-harness의dev-testing-strategy.[원문]
시간 순서가 이렇습니다.
| 날짜 | 일 |
|---|---|
| 2026-08-09 23:41 | 사내 로컬 스킬 신설 |
| 2026-08-10 13:45 | 공개 하네스에 dev-testing-strategy 추가 |
| 2026-08-10 18:16 | 사내 로컬 스킬 갱신 |
| 2026-08-11 09:07 | 사내 로컬 스킬 재갱신 |
규칙은 프로젝트에서 태어나 하루 뒤 하네스로 올라갔습니다. 그리고 올라간 뒤에도 프로젝트 쪽은 계속 자랍니다.
규칙이 이런 순서로 자라는 이유가 커밋 하나에 적혀 있습니다. 규칙 문서와 코드를 같은 PR에서 함께 고친 커밋인데, 요지만 옮기면 이렇습니다. [재구성]
코드만 고치면 다음에 같은 판단을 처음부터 다시 하게 되므로, 이번에 확정한 규약을 스킬 문서에 남긴다.
1편의 진단은 “기준은 있었는데 안 지켜졌다”였고, 그 진단의 자연스러운 처방은 “기준을 더 강하게 요구하자”입니다. 이 커밋은 다른 걸 합니다. 판단이 확정된 바로 그 자리에서 규칙을 갱신합니다. 규칙을 미리 완성해 두고 지키게 하는 게 아니라, 결정이 날 때마다 규칙을 자라게 하는 방식이에요. 위 타임라인의 사흘 세 번 갱신이 그 방식이 실제로 굴러간 흔적입니다.
두 판본의 본문을 기계적으로 세면 일반화의 대가가 그대로 나옵니다.
| 항목 | 공개 판본 [원문] |
사내 판본 [재구성] |
|---|---|---|
| 본문 분량 | 67줄 / 3,303자 | 103줄 / 9,681자 |
| 파일 경로 | 0 | 19 |
파일:줄번호 |
0 | 9 |
| 날짜 (YYYY-MM-DD) | 0 | 3 |
| “실측” / “확정” 표기 | 0 | 3 |
| 예시 클래스 실명 | 0 | 43 |
| 설정 키 실값 | 0 | 15 |
| 이슈 번호 | 0 | 1 |
살아남은 것은 세 신호의 판단 기준, 신호 3의 코드 예시, 스코프 원칙, 프로퍼티 값 관리 원칙, 체크리스트입니다. 사라진 것은 그 규칙을 만든 사건, 사건의 날짜, 사건이 일어난 파일과 줄, 검증이 “실측”인지 “확정”인지의 구분, 대표 사례 클래스 43개, 실제 설정 키와 값입니다.
사라진 것은 근거만이 아니었습니다
사라진 것을 뭉뚱그리면 “구체성”인데, 실제로는 성격이 다른 셋입니다.
첫째, 판정의 근거. 사내 판본은 각 기준 옆에 그 기준을 만든 사건이 붙어 있습니다. 2편이 다룬 그 null 사건도 날짜와 함께 적혀 있습니다. 공개 판본에는 결론만 남았습니다. “특히 BeanPostProcessor를 등록하는 자동설정이 얽히면 조용히 깨진다”는 문장은 있는데, 어떻게 알았는가는 없습니다.
둘째, 반례를 만드는 대표 사례. 사내 판본은 기준마다 “이 클래스가 대표 사례”라고 지목합니다. 판단이 안 서면 그 파일을 열어 보면 돼요. 공개 판본에서 판단이 안 서면 처음부터 다시 생각해야 합니다.
셋째, 이 규칙이 아직 자라고 있다는 표시. 날짜가 그 역할을 합니다. “2026-08-09 실측”, “2026-08-10 확정”이 붙어 있으면 이 규칙이 어제 만들어졌고 오늘 하나가 추가됐다는 게 보입니다. 공개 판본은 날짜가 없어서 완성된 것처럼 보입니다. 2편이 다룬 사건, 그러니까 기준이 하루 만에 하나에서 셋이 된 그 일은 공개 판본에 흔적이 없습니다.
셋 중 마지막이 가장 덜 자명하고 가장 값어치가 있다고 봅니다. 일반화는 근거만 지우는 게 아니라 “이건 아직 미완성”이라는 신호도 함께 지웁니다. 그리고 미완성 표시가 사라진 규칙은 반박하기 어려워집니다. 근거가 없으니 반증할 대상도 없거든요.
균형을 위해 반대편도 적습니다. 공개 판본이 얻은 것이 분명히 있습니다. 다른 프로젝트에 붙고, 읽는 시간이 3분의 1이고(3,303자 대 9,681자), 핵심 경고가 살아남았습니다. 규칙은 결정 직전에 읽혀야 효력이 있는데, 길면 그 자리에서 안 읽힙니다.
그러니 정확한 명제는 “일반화가 나쁘다”가 아닙니다. 재사용성과 근거는 같은 축의 양 끝이고, 어느 쪽으로 가든 잃는 것이 정해져 있으며, 무엇을 잃는지 알고 가는 것과 모르고 가는 것이 다릅니다.
한 가지 덧붙이면, 이 두 판본을 대조하는 이 글 자체가 그 축 위에 있습니다. 저는 공개 판본만 원문으로 옮길 수 있고 사내 판본은 재구성해야 합니다. 일반화된 쪽만 인용 가능하다는 사실 자체가 왜 일반화가 일어나는지를 설명합니다.
가장 잘 정리된 판본이 한 번도 읽히지 않았습니다
공개 판본은 자기 활성화 조건을 명시합니다. 원문 그대로입니다.5
> **활성화 방법**: 프로젝트 `CLAUDE.md`에 아래 한 줄을 추가한다.
> ```
> execute 실행 시 `dev-testing-strategy` 스킬을 로드하고 Spring 테스트 컨텍스트 구성 전략을 구현에 적용한다.
> ```
같은 문장이 frontmatter의 description에도 들어 있습니다. “프로젝트 CLAUDE.md에 선언하여 활성화.”
그래서 제 작업 디렉터리 아래 CLAUDE.md를 전수 조사했습니다.
| 항목 | 값 |
|---|---|
CLAUDE.md 파일 수 |
83 |
| “execute 실행 시” 활성화 문장이 있는 파일 | 0 |
dev-testing-strategy를 언급하는 파일 |
0 |
| 활성화 선언을 안내하는 스킬 4종 중 어느 하나라도 언급하는 파일 | 0 |
83개 중 0개입니다. 방금 대조한 그 판본, 근거를 걷어내고 43개 예시를 지우면서까지 재사용 가능하게 만든 그 판본은 한 번도 로드된 적이 없습니다.
반대로 사내 로컬 스킬 7종은 선언 없이도 발동합니다. 프로젝트 저장소의 .claude/skills/ 아래 있는 것만으로 후보에 오르고, 무엇이 발동시키는지는 frontmatter의 description이 정합니다. 그리고 두 판본의 description이 끝나는 방식이 다릅니다.
- 사내 판본의 마지막 설명 문장은 언제 쓰는지입니다. “테스트 코드에 단언문을 추가하거나 수정할 때 사용” 같은 식이에요.
[재구성] - 공개 판본의 마지막 설명 문장은 어떻게 활성화하는지입니다. “프로젝트 CLAUDE.md에 선언하여 활성화.”
[원문]
일반화가 지운 목록에 하나가 더 있습니다. 발동 조건입니다. 사내 판본은 “이 상황이면 나를 읽어라”라고 적고, 공개 판본은 “누가 나를 등록해 주면 읽힌다”고 적습니다. 그리고 등록은 83개 중 0개에서 일어났습니다.
1편은 기준이 문서에 있는데 260곳에서 안 지켜진 이야기였습니다. 여기서는 한 단계 앞입니다. 지켜지고 말고 이전에 읽히지도 않았습니다. 규칙은 적히는 순간이 아니라 로드되는 순간에 존재합니다.
규칙의 비용은 쓰는 비용이 아니라 나르는 비용입니다
그러면 항상 로드되게 하면 될까요. 같은 사내 저장소의 git 이력에 이 문제를 정면으로 다룬 자리가 있습니다. [재구성]
2026-07-28에 설정 클래스 컨벤션을 CLAUDE.md에서 프로젝트 스킬로 옮기는 커밋이 있고, 사유는 요지만 옮기면 이렇습니다.
이 가이드는 해당 종류의 클래스를 작성할 때만 필요하므로 매 세션 항상 로드될 필요가 없다. 스킬로 옮기고
CLAUDE.md에는 포인터 한 줄만 남긴다.
흥미로운 건 그 다음입니다. 이 커밋은 10분 뒤 되돌려졌고, 5분 뒤 다시 커밋됐습니다. 22:33 커밋, 22:43 revert, 22:48 재커밋. 15분 사이의 왕복입니다.
CLAUDE.md에 적힌 규칙은 매 세션 컨텍스트에 상주하고, 스킬에 적힌 규칙은 필요할 때만 로드됩니다. 어느 쪽이 나은지는 규칙의 성격에 달렸고, 그 판단이 15분 만에 두 번 뒤집힐 만큼 미묘합니다.
두 실패는 같은 축의 양 끝입니다. 항상 나르면 자리를 차지하고, 부를 때만 부르면 안 불릴 수 있습니다. 83개 중 0개가 후자의 실패이고, 이 revert 왕복이 전자의 부담입니다. 2편이 “좁힘과 공유는 대립하지 않는다”로 정리했던 그 구조와 같은 모양입니다. 대립하는 두 선택지처럼 보이지만, 실제로 갈리는 것은 “어디에 적는가”입니다.
체크리스트가 자기 오용법을 적어 둔 자리
일반화 과정에서 43개 클래스명과 19개 경로가 전부 사라졌는데, 살아남은 문장이 하나 있습니다. 원문 그대로입니다.5
주의: 신호 1(프로퍼티 공유 여부)만 기계적으로 확인하고 판단하지 않는다. 프로퍼티는 완전히 고정 공유하는데 신호 3에 걸려 있는 테스트를 신호 1만 보고
@Autowired로 옮기면 조용히 잘못된 선택이 된다.
사내 판본도 같은 취지의 문장을 갖고 있습니다. 무엇을 남길지 고를 때 이 문장이 두 판본 모두에서 살아남았다는 사실 자체가 재료입니다. 이건 규칙이 아니라 그 규칙을 오용하는 법이고, 항목이 자기를 부정하는 조항을 품고 있는 셈입니다.
같은 성격의 장치가 더 있습니다. 검증 스킬 쪽이 가장 강합니다. 원문 그대로입니다.6
## ⛔ Iron Law
이번 메시지에서 직접 실행한 명령의 출력 없이는 어떤 항목도 ✅로 표시할 수 없다.
명령을 안 돌렸으면 그 항목은 ⬜(미검증)이고, ⬜가 하나라도 있으면 submit 불가.
**규칙의 문구를 어기는 것은 규칙의 정신을 어기는 것이다.**
그리고 「Red Flags (이 생각이 들면 멈추고 ⬜ 처리)」 표가 이어집니다. 원문 그대로입니다.
| 떠오른 생각 | 현실 |
|---|---|
| “테스트는 아마 통과할 것” | 미검증. 추측은 증거가 아니다 — ⬜. |
| “방금 전에 돌렸으니 또 안 돌려도 됨” | 마지막 수정 이후 출력이 이번 메시지에 없으면 ⬜. |
| “변경이 작으니 안전” | 변경 크기와 검증은 무관. 돌려서 확인한다. |
| “execute에서 통과하는 거 봤음” | 그건 다른 메시지다. verify는 이번 메시지의 출력으로만 통과. |
이건 규칙이 아니라 특정 실행자의 특정 실패 모드를 겨냥한 방어입니다. 왼쪽 열은 전부 에이전트가 스스로에게 할 법한 변명이고, 오른쪽 열은 그 변명 각각을 미리 기각해 둔 것입니다. 그리고 표 뒤에 붙는 마지막 줄은 아예 자문 형식입니다. 원문은 「✅를 적기 전에 자문한다: “이번 메시지에 이 명령의 출력이 실제로 있는가?”」입니다.
반면 같은 하네스의 코딩 원칙 스킬과 아키텍처 스킬에는 이런 장치가 없습니다. 그리고 1편에서 260곳 남짓 어겨진 규칙(“단언은 구체적으로”)이 있는 곳이 바로 코딩 원칙 스킬입니다.
여기서 인과를 주장하고 싶어지는데, 그건 성급합니다. 그 규칙이 안 지켜진 이유는 1편이 이미 다르게 진단했습니다. 단언을 구체적으로 만드는 것은 규율이 아니라 질문이라는 진단이었습니다. 자기 경고가 있었으면 지켜졌으리라는 근거는 없습니다.
말할 수 있는 건 이겁니다. 체크리스트가 자기 오용법을 아는 경우와 모르는 경우가 실제로 갈리고, 아는 쪽은 “이 항목만 보고 판단하지 마라”를 항목 안에 넣어 뒀습니다. 1편이 가장 중요한 칸이라고 했던 그 표의 빈칸(—), 그러니까 “이 레이어는 이 불변식에 대해 할 말이 없다”를 명시한 칸과 같은 발상입니다. 규칙이 자기가 못 보는 자리를 자기 안에 적어 두는 것이죠.
그래서 제 게이트를 봤습니다
여기까지는 규칙을 적는 쪽 이야기였으니 이제 규칙을 돌리는 쪽을 엽니다. 이 블로그에는 계약 테스트가 있고, CI가 push와 PR마다 돌립니다. 1편에서 이미 인용했던 그 파일입니다.
| 파일 | 줄 수 | 단언 |
|---|---|---|
test/site_output_test.rb |
222 | 61 |
test/search_test.js |
59 | 12 |
워크플로 실행 이력을 전수 조회했습니다.7
| 항목 | 값 |
|---|---|
| 총 실행 | 101 |
| 성공 | 101 |
| 실패 | 0 |
| 기간 | 2026-07-13 ~ 2026-08-11 |
이 게이트는 한 달 동안 한 번도 빨간불이 된 적이 없습니다.
두 가지로 읽힙니다. 낙관적으로는 “아무것도 안 깨졌다”이고, 회의적으로는 “작동을 증명한 적이 없다”입니다. 그리고 1편이 이미 이 파일 안에서 change detector 세 건을 지목했으니, 회의적인 쪽을 그냥 무시할 수가 없어요.
정확히 해 두면, 101회 초록불이 게이트가 무용하다는 근거는 아닙니다. 실패가 없었던 이유가 (a) 계약이 안 깨져서인지 (b) 계약이 깨질 만한 변경이 없어서인지 (c) 단언이 깨질 수 없어서인지를 저는 구분하지 못했습니다. 다만 (c)에 해당하는 사례가 이력에 남아 있습니다.
change detector의 영수증
2026-08-09, PR #85. 알고리즘 패널을 3열에서 2열로 넓히면서 계약 테스트에 이 단언을 추가했습니다. 원문 그대로입니다.
# [원문] 커밋 28cbbfb가 test/site_output_test.rb에 추가한 줄
# 2열 그리드로 넓혀 패널 하나의 실제 렌더 폭을 키운다 (277px → 420px, 960px 컬럼 기준)
assert_match(/\.rl-algo-embed \.rl-grid\{\s*display:grid;grid-template-columns:repeat\(2,1fr\)/,
rate_limiter_post, "알고리즘 패널이 2열 그리드여야 한다")
같은 날, PR #88. 피드백을 받고 3열로 되돌리면서 같은 단언을 다시 고쳤습니다. 원문 그대로입니다.
# [원문] 커밋 3386bc3의 diff. 위 3줄을 지우고 아래 4줄로 바꿨다
# 2열로 넓혔더니 패널 하나에 세로 여백이 과해 보인다는 피드백으로 3열로 되돌렸다.
# 6개 패널이 2행에 다 들어와 스크롤도 줄어든다. (#87)
assert_match(/\.rl-algo-embed \.rl-grid\{\s*display:grid;grid-template-columns:repeat\(3,1fr\)/,
rate_limiter_post, "알고리즘 패널이 3열 그리드여야 한다")
잡은 결함 0건, 편집 2회. 그 단언은 태어나서 한 번도 실패한 적 없이, 다음 디자인 결정과 함께 고쳐졌습니다. 1편이 인용한 Google의 판정, 그러니까 change detector는 결함을 못 잡으면서 유지보수 비용만 늘리므로 negative value라는 것의 실물 영수증입니다. 같은 커밋 계열의 다른 단언들(메모리 인디케이터 30개, 지브라 스트라이프 알파값, 상태색 4개)도 전부 디자인과 함께 태어났습니다.
1편에서 이 단언들을 비판할 때는 “이론적으로 이런 비용이 생긴다”였습니다. git 이력에는 그 비용이 이미 지불돼 있었습니다.
실패를 확인한 단언 하나
그런데 같은 파일에 성격이 완전히 다른 단언이 하나 있습니다. 2026-07-17, 이슈 #18을 닫은 커밋의 메시지입니다. 원문 그대로입니다.
_config.yml의 exclude에 test가 없어 계약 테스트 파일이 그대로 발행되고 있었다 (_site/test/site_output_test.rb→https://seokrae.github.io/blog/test/...).이 문제를 테스트가 못 잡은 이유는 IGNORED_ENTRIES가 test를 복사 대상에서 빼고 빌드하기 때문이다 — 테스트 환경에 test/가 아예 없으니 재현 자체가 안 됐다. fixture로 test/를 만들어 넣어 계약을 잠근다. exclude를 빼면 실패하고 넣으면 통과하는 것을 확인했다.
세 겹으로 좋습니다.
- 테스트 스위트가 자기 자신을 프로덕션에 유출시켰습니다. 계약 테스트 파일이 라이브 사이트에 그대로 발행되고 있었습니다.
- 테스트가 못 잡은 이유가 테스트 하네스의 최적화였습니다. 빌드를 빠르게 하려고
test/를 복사 대상에서 뺐고, 그래서 그 버그는 테스트 환경에 존재할 수조차 없었습니다. 검사기가 자기 최적화 때문에 자기를 못 봤습니다. - 고칠 때 단언이 실패하는 것을 실제로 확인했습니다. “exclude를 빼면 실패하고 넣으면 통과하는 것을 확인했다.”
1편의 주제 질문은 “무엇이 바뀌면 이게 실패해야 하는가”였습니다. 이 커밋은 그 질문에 답한 게 아니라 실행해 봤습니다. 그리고 61개 단언 중에서 자기가 빨간불이 될 수 있다는 걸 실제로 보인 것은 이 하나뿐입니다.
나머지가 어떻게 만들어졌는지도 이력에 있습니다. 1편이 다룬 한국어 검색 0건 사건(#12)도, 유령 서비스 워커 사건(#34)도, 방금 본 #18도 전부 프로덕션에서 먼저 발견되고 그 다음에 테스트가 추가됐습니다. 101회 초록불은 그렇게 만들어진 것입니다.
사후에 잠근 계약은 과거를 고정하는 데는 확실합니다. 미래를 잡을 수 있다는 증명은 따로 해야 하고, 저는 한 번 했습니다.
무엇을 배웠는가
첫째, 검사기의 사각지대는 거의 무작위가 아닙니다. 파서가 이름을 잃은 집단은 하필 규칙 준수율이 85%인 파라미터화 테스트였고, 전체 평균은 40%대였습니다. 그래서 오차가 비율을 흔든 게 아니라 한쪽으로 밀었습니다. 측정 오차가 측정 대상과 상관되면 그건 잡음이 아니라 가짜 신호입니다. 규칙을 기계에게 넘길 때 사각지대도 함께 넘어갑니다.
둘째, 세는 기준을 안 적으면 그게 기본값이 됩니다. @DisplayName 28% 차이는 오차가 아니라 정의 차이였습니다. 두 측정이 각각 정확했고 서로 다른 것을 셌는데, 노트에 남은 건 숫자와 이름표뿐이었습니다. 측정치의 이름표는 측정 기준보다 강한 명세입니다.
셋째, 검사 가능성은 이분법이 아니라 비용의 스펙트럼입니다. 한글 이름 규칙 하나를 세는 데도 주석 제거와 괄호 짝 맞추기가 필요했고, 두 번의 시도가 그 지점에서 깨졌습니다. 2편이 “검사하기 쉬운 기준이 하나 있으면 그게 유일한 기준인 것처럼 작동한다”였다면, 여기에 한 줄을 보탭니다. 그 쉬움에도 등급이 있습니다.
넷째, 일반화는 근거만 지우는 게 아닙니다. 경로 19개가 0개, 날짜 3개가 0개, 예시 43개가 0개가 되면서 “이 규칙이 아직 자라고 있다”는 신호와 발동 조건까지 함께 사라졌습니다. 그 결과 가장 잘 정리된 판본이 83개 프로젝트 중 0개에서 로드됐습니다. 규칙은 적히는 순간이 아니라 로드되는 순간에 존재합니다.
다섯째, 규칙의 비용은 쓰는 비용이 아니라 나르는 비용입니다. 항상 나르면 자리를 차지하고 부를 때만 부르면 안 불릴 수 있습니다. 그 판단이 15분 만에 두 번 뒤집힌 커밋 이력이 남아 있습니다.
여섯째, 좋은 규칙은 자기를 부정하는 조항을 갖고 있습니다. 43개 예시가 사라지는 일반화에서도 “이 항목만 보고 기계적으로 판단하지 마라” 한 줄은 살아남았습니다. 검증 스킬의 Red Flags 표는 아예 실행자가 할 법한 변명 넷을 미리 적어 두고 각각을 기각합니다.
일곱째, 한 번도 빨간불이 된 적 없는 게이트는 작동을 증명한 적이 없습니다. 101회 초록불 중 실패 가능성이 실증된 단언은 하나뿐이고, 그 하나는 프로덕션 사고를 잠그면서 “빼면 실패하는지” 직접 돌려 본 경우였습니다.
세 편이 같은 것을 물었습니다
1편은 기준이 안 지켜진 이야기, 2편은 기준이 틀렸던 이야기, 3편은 기준을 검사하는 쪽이 틀렸던 이야기입니다.
그런데 세 편이 물은 질문은 하나였습니다. 초록불이 무엇을 뜻하는가. 그리고 세 번 다 답은 같은 모양이었습니다. 그 초록불을 만든 쪽이 무엇을 안 보기로 했는가.
1편에서는 isNotNull()이 “결과의 내용”을 안 보기로 했고, 커버리지 리포트가 “실행됐는가 너머”를 안 보기로 했습니다. 2편에서는 컨텍스트 구성 방식이 “쓸 수 있는 단언의 집합”을 미리 정해 나머지를 안 보기로 했습니다. 3편에서는 파서가 중괄호 안쪽을 안 보기로 했고, 게이트가 61개 중 60개에 대해 “이게 빨간불이 될 수 있는가”를 안 묻기로 했습니다.
이건 도구를 탓하는 이야기가 아닙니다. 모든 검사기는 무언가를 안 봅니다. 안 보는 게 검사기의 정의예요. 위임할 수 있는 것은 검사의 실행이고, 위임할 수 없는 것은 “이 검사가 무엇을 안 보는지”를 아는 일입니다.
외부 근거 하나만 붙이겠습니다. DORA 2025 리포트가 이 시리즈 세 편을 요약하는 문장을 갖고 있습니다.8
“AI accelerates software development, but that acceleration can expose weaknesses downstream.”
이 글의 근거는 실측이고, 이 인용은 그 실측이 혼자가 아니라는 확인입니다. 1편이 단언의 약점, 2편이 컨텍스트 구성의 약점, 3편이 검사기와 규칙 배선의 약점을 봤는데 셋 다 downstream이었습니다. 없던 문제가 생긴 게 아니라, 원래 있었는데 안 보이던 문제입니다.
마지막으로 덧붙일 게 하나 있습니다. 이 글이 인용한 그 「인지적 굴복」 항목도 83개 CLAUDE.md 중 어디에도 선언돼 있지 않습니다. 자기 규칙이 자기에게 연결돼 있지 않은 상태로 이 글을 썼습니다.
규칙을 넘기기 전에 물어볼 것
1편이 단언을 쓰기 전에, 2편이 컨텍스트를 띄우기 전에 물어볼 목록으로 끝났으니 이 편도 목록으로 닫겠습니다.
- 이 규칙을 검사하는 비용이 얼마인가.
grep한 줄인지, 괄호 짝을 맞춰야 하는지, 사람이 읽어야 하는지. 비용이 준수율을 만듭니다. - 그 검사기가 못 보는 집단은 무엇인가. 그리고 그 집단이 준수율과 상관될 이유가 있는가. 있으면 오차가 아니라 가짜 신호입니다.
- 무엇을 셌는지 숫자 옆에 적었는가. 안 적으면 남는 건 이름표뿐이고, 이름표는 자기가 무엇을 셌는지 말하지 않습니다.
- 이 규칙을 일반화하면서 무엇을 버리는가. 근거, 대표 사례, 그리고 “아직 미완성”이라는 표시. 버리는 건 괜찮지만 무엇을 버리는지는 알고 버려야 합니다.
- 이 규칙은 무엇이 있으면 발동하는가. 누가 등록해 줘야 읽히는 규칙이라면, 등록되지 않은 채로 얼마나 오래 있었는지 먼저 세어 봅니다.
- 이 규칙이 자기 오용법을 적고 있는가. “이 항목만 보고 판단하지 마라”가 항목 안에 있는지.
- 이 게이트가 실제로 빨간불이 된 적이 있는가. 없다면, 하나라도 일부러 깨뜨려 실패를 확인해 봅니다. 답을 아는 것과 실행해 보는 것은 다릅니다.
이 글로 3부작을 마칩니다. 세 편이 전부 답이 아니라 질문 목록으로 끝난 데는 이유가 있습니다. 초록불이 무엇을 뜻하는지는 그 초록불을 만든 쪽에서만 셀 수 있습니다.
-
3차 측정 기준은 이렇다. 경로에
/src/test/java/를 포함하고/build/를 포함하지 않는.java파일 전부(2,100개)를 읽고, 문자열 리터럴을 보존하며 주석을 공백으로 치환한 뒤 파싱했다. 테스트 메서드는 애노테이션 블록에@Test,@ParameterizedTest,@RepeatedTest,@TestFactory,@TestTemplate중 하나 이상이 있고 뒤에 메서드 선언이 오는 것을 블록당 1개로 셌다. 한글 이름은 메서드 식별자에[가-힣ㄱ-ㆎ]가 하나라도 있으면 참으로 보며@DisplayName문자열과 본문은 보지 않는다.@DisplayName은 부착 대상별(테스트 메서드 / 클래스)로 나눠 셌다. 측정 시각은 2026-08-11 14:20이다. 한계: 정규식과 문자 단위 파싱이라 정식 Java 파서가 아니고, 메타 애노테이션으로@Test를 합성한 커스텀 애노테이션은 놓친다. 절대 수치는 근사치로 읽어야 하며 그룹 간 대조(파라미터화 84.9% 대 일반 39.7%)가 절대값보다 견고하다. 1편, 2편과 같은 한계다. 그리고 이번 편의 사내 수치는 전부 정적 분석이며 테스트를 실행하지 않았다. 한글 이름 준수율이 곧 품질이라는 근거는 없다. 금융 프로토콜 모듈들의 0%는 1편이 “도메인 어휘가 영어”로 해석했고 이번에도 그 해석을 검증하지는 않았다. ↩ -
선행 두 파서의 스크립트는 둘 다 소실됐다. 본문의 규명은 “어떤 파서 설정이 그 숫자를 낳는가”를 3차 데이터 위에서 역산한 것이지 원 코드를 읽고 버그를 지목한 것이 아니다. 44.1%가 44.0%와 0.1%p 차이라는 게 근거인데, 다른 원인이 우연히 같은 값을 낼 가능성을 배제하지는 못한다. 마찬가지로
@DisplayName의 정의 차이가 선행 두 측정에서 “의도된 것”이었는지도 확인할 수 없다. 두 측정 모두 무엇을 셌는지 기록하지 않았으므로, 본문은 결과가 그 정의 차이와 정합적이라는 것까지만 말한다. ↩ -
sr-harness0.23.0의skills/ownership-principles/SKILL.md:26. 2026-08-02 추가됐다. 같은 스킬:8의 머리말은 “에이전트가 실행을 대신할수록, 사람이 유지해야 하는 것은 “무엇을 했는가”가 아니라 “왜 그것을 승인했는가”다.”이고,:12-15가 inner loop(에이전트 실행, 위임 가능)와 outer loop(결정, 검증, 승인, 책임, 위임 불가)를 가른다. 출처: SeokRae/sr-harness ↩ -
42개 하위 git 저장소 각각에서
2026-08-11 10:00이전 마지막 커밋과 현재 HEAD의 테스트 애노테이션 줄 수를git grep -c로 비교했다. 10,902에서 11,024로 122줄(1.1%) 늘었고 변동은 두 저장소에서만 발생했다. 재현하려는 사람을 위해 함정을 적어 둔다.git grep -E는 POSIX ERE라\b가 동작하지 않으므로([^A-Za-z]|$)로 대체해야 하고,-o는 리비전을 지정하면 기대대로 동작하지 않아-c(줄 수)로 비교했다. ↩ -
이 스킬 파일은 사내 문서가 아니라 필자가 GitHub 마켓플레이스로 배포하는 공개 하네스 플러그인의 일부라 원문을 그대로 옮긴다.
sr-harness0.23.0의skills/dev-testing-strategy/SKILL.md다. 활성화 방법은:10-13이고:3frontmatter의description도 설명 문장을 “프로젝트 CLAUDE.md에 선언하여 활성화.”로 맺은 뒤Keywords:목록을 붙인다. 자기 오용 경고는:34다. 두 판본 밀도 대조는 두SKILL.md의 frontmatter를 제외한 본문에 정규식 9종을 적용해 셌다. 공개 판본에서 클래스명 정규식이 걸리는 것은 전부SpringBootTest라는 애노테이션 이름이라 예시 클래스는 실제로 0건이다. 출처: SeokRae/sr-harness ↩ ↩2 -
sr-harness0.23.0의skills/verify/SKILL.md. Iron Law는:11-15, Red Flags 표는:91아래, 자문 문장은:100이다. 모두 원문 그대로다. 같은 하네스의dev-monitoring-design/SKILL.md:61에도 같은 계열의 항목이 있다.[ ] 비중 감소를 "개선"으로 오독할 여지(기계적 희석)를 주석으로 막았는가?반면dev-coding-principles와dev-architecture에는 이런 자기 경고가 없다(전수 grep으로 확인). 출처: SeokRae/sr-harness ↩ -
gh run list --workflow=site-check.yml --limit 300 --json conclusion,createdAt으로 전수 조회했다. 워크플로 최초 실행일이 이 저장소의 CI 도입일과 일치하므로 101건이 전수다. 워크플로는 push와 PR마다bundle exec ruby test/site_output_test.rb와node test/search_test.js를 순서대로 돌린다. 단언 수 61개의 내역은assert_match27,assert_includes11,refute_match8,assert_equal5,refute_includes4,assert4,refute2다. ↩ -
“2025 DORA Report: State of AI-Assisted Software Development”, Google Cloud (DORA Research Program), 2025-09-24. 기술 인력 약 5,000명 설문과 100시간 이상의 정성 데이터가 표본이다. 인용문의 전체 문장은 “This confirms our central theory - AI accelerates software development, but that acceleration can expose weaknesses downstream. Without robust control systems, like strong automated testing, mature version control practices, and fast feedback loops, an increase in change volume leads to instability.”다. 같은 리포트는 채택률을 “90% of survey respondents report using AI at work. More than 80% believe it has increased their productivity.”로, 처리량과 안정성의 관계를 “Unlike last year, we observe a positive relationship between AI adoption on both software delivery throughput and product performance.”와 “However, AI adoption does continue to have a negative relationship with software delivery stability.”로 적는다. 그리고 “AI doesn’t fix a team; it amplifies what’s already there.”라는 요약을 붙인다. 출처: Google Cloud Blog ↩