리팩터링 도구나 YAGNI 감사에서 가장 매력적인 숫자는 호출자 0이다.
호출자가 없으면 지워도 될 것처럼 보인다.
실제로 많은 경우 그렇다.
하지만 항상 그런 것은 아니다.
어떤 코드는 호출되지 않는 것이 정상이고, 오히려 그 사실을 증명하기 위해 존재한다.
이 글에서는 이런 성격의 코드를 편의상 Negative-Space Guard라고 부르겠다. 표준화된 분류명이라기보다, “정상 상태에서는 호출되지 않지만 금지된 상태를 명시하는 코드”를 설명하기 위한 표현이다.
이 성격을 이해하지 못하면 코드 줄 수는 줄지만 시스템의 안전성도 같이 줄어드는 리팩터링을 할 수 있다.
1. “사용되지 않는다”와 “도달하면 안 된다”는 다르다
예를 들어 상태 enum에 “정상 경로에서는 절대 만들어지면 안 되는 값”이 하나 있다고 하자.
그런데 값 자체는 정의돼 있다.
그리고 테스트는 이렇게 말한다.
이 값은 정상 경로에서 절대 나타나면 안 된다.
만약 나타나면 즉시 실패해야 한다.호출자 수만 보면 죽은 코드다.
하지만 의미는 정반대다.
이 타입은 금지된 상태를 표현하는 안전장치다.
2. Negative-Space Guard란
일반 코드는 “무엇이 일어나야 하는가”를 표현한다.
Negative-space guard는 반대다.
이 타입은 생성되면 안 된다.
이 분기는 도달하면 안 된다.
도달하면 즉시 실패해야 한다.
이 값은 노출되면 안 된다.즉 존재 이유가 실행이 아니라 금지된 상태를 명시적으로 표현하는 것이다.
3. 왜 자동 감사가 잘못 분류하는가
Knip, ts-prune, Vulture, ESLint의 no-unused-vars 같은 도구가 보는 지표는 대체로 다음과 같다.
- caller count
- reference count
- coverage
- duplication
- complexity
Negative-space guard는 이 모든 지표에서 불리하다.
호출자가 적다
정상 경로에서는 실행되지 않는다
coverage가 낮을 수 있다그래서 자동으로 “삭제 후보”에 올라온다.
4. 테스트를 먼저 읽어야 한다
삭제 후보가 올라왔을 때 가장 먼저 볼 것은 호출 그래프가 아니라 테스트일 수 있다.
특히 이런 테스트는 강한 신호다.
| 언어·도구 | “도달하면 안 된다”를 적는 방법 |
|---|---|
| TypeScript | assertNever(x) — 남은 케이스가 있으면 타입 검사에서 걸린다 |
| Python 3.11+ | typing.assert_never(x) |
| Rust | unreachable!() |
| Java | default: throw new AssertionError(x) |
| pytest | pytest.raises(...) — 실패하는 것이 정상인 경로 |
이런 테스트는 “있어야 한다”가 아니라 “없어야 한다”를 검증한다. 도달 불가를 타입
검사기가 아는 형태로 적어 두면(assertNever 계열) 케이스가 늘어났을 때 빌드가
먼저 알려준다 — 런타임에 터지기 전에.
5. 보안 경계에서도 자주 나타난다
Negative-space guard는 보안 코드에서 특히 중요하다.
예를 들면:
민감정보가 로그에 나타나면 안 된다.
특정 상태에서 권한이 상승하면 안 된다.
금지된 값이 처리 대상에 들어오면 안 된다.이런 invariant는 정상 운영에서는 실행 흔적이 거의 없다.
그래서 단순 usage 기반 감사로 보면 불필요해 보인다.
6. 삭제 전 질문
호출자 0이라는 이유로 지우기 전에 다음을 확인한다.
[ ] impossible state를 표현하는가
[ ] fail-loud 역할을 하는가
[ ] security boundary인가
[ ] forbidden path를 타입으로 표현하는가
[ ] 테스트가 "없어야 함"을 검증하는가
[ ] 삭제하면 회귀 검증 능력이 사라지는가7. YAGNI 규칙에 추가할 예외
YAGNI 감사에서는 다음 범주를 별도로 분류하는 것이 좋다.
negative-space guard
impossible-state representation
fail-closed branch
security boundary
forbidden-path representation
invariant enforcement이런 코드는 “얼마나 쓰이는가”가 아니라 “무엇을 막는가”를 봐야 한다.
결론
호출자 0은 강한 신호지만 판정은 아니다.
Dead code와 negative-space guard는 외형이 매우 비슷하다.
차이는 실행 빈도가 아니라 설계 의미다.
리팩터링할 때는 항상 한 번 더 물어야 한다.
이 코드를 없애면 어떤 잘못된 상태가 더 이상 명시적으로 금지되지 않는가?