소스와 테스트를 grep 한 줄로 연결하기: @tested / @covers 양방향 마커 시스템
코드베이스가 커지면 "이 파일에 테스트가 있나?" "이 테스트가 뭘 커버하나?"를 추적하는 비용이 커집니다. JSDoc에 @tested와 @covers 마커를 넣어 소스·테스트 간 양방향 링크를 구축하면, grep 한 줄로 커버리지 맵을 얻을 수 있습니다. 실제 Obsidian 플러그인 프로젝트에 적용한 사례.
1. 이 글의 위치
이 글은 양방향 링크 시스템 시리즈의 세 번째 글입니다.
- AI 코딩 어시스턴트 시대의 문서화: 양방향 링크 시스템 구축기 —
@handbook/@code도입 - 양방향 링크 시스템을 테스트 커버리지에 확장하기: @tested / @covers 마커 패턴 — 개념 설계 + CI 자동 검증 스크립트
- 소스와 테스트를 grep 한 줄로 연결하기: @tested / @covers 양방향 마커 시스템 ← 현재 글
이전 글에서 @tested / @covers 마커의 설계와 CI 검증을 다뤘습니다. 이 글은 Obsidian 플러그인 프로젝트(Auto Note Importer)에 실제로 적용한 경험 — 21 + 22 파일 규모의 사례, 5가지 grep 쿼리 패턴, 그리고 "왜 외부 메타파일이 아니라 주석인가"의 트레이드오프 분석을 다룹니다.
2. 핵심 결정: 왜 외부 파일이 아니라 주석인가
이 시스템을 설계할 때 가장 중요한 결정은 "소스↔테스트 관계를 어디에 저장할 것인가" 였습니다. 후보는 세 개였습니다.
2.1 대안 1: 외부 메타파일 (.coverage-map.json)
{
"src/core/config-instance.ts": [
"tests/core/config-instance.test.ts",
"tests/e2e/run-e2e.mjs"
]
}
장점: 구조화돼 있어서 프로그래밍적으로 쉽게 다룰 수 있음.
단점: 파일이 이동하거나 리네임될 때마다 수동으로 갱신해야 함. PR에서 까먹으면 drift가 쌓이고, 1년 뒤에는 "아무도 믿지 않는 파일"이 됩니다. Single source of truth가 코드 밖에 있는 게 근본 문제입니다.
2.2 대안 2: 파일명 규약
src/foo.ts → tests/foo.test.ts처럼 위치·이름으로 암묵적 매핑.
장점: 아무것도 안 해도 됨.
단점: N:M 관계를 표현할 수 없음. E2E 파일이 7개 소스를 커버한다는 정보는 어디에도 저장할 수 없습니다. airtable-client-view.test.ts가 airtable-client.ts의 하위 기능을 테스트하는 것도 암묵적 추측에 의존합니다.
2.3 대안 3: 주석 기반 마커 (선택)
장점:
- 코드 옆에 있음. 파일이 움직여도 관계가 따라갑니다 (git blame이 따라가는 것과 같은 원리).
- IDE의 자동완성 없이도 파일 상단만 보면 관계가 드러납니다.
- N:M 관계를 자연스럽게 표현합니다.
- grep 한 줄이면 쿼리 가능. 특수 도구 불필요.
- Parser-friendly. 나중에 진짜 tooling을 만들고 싶다면 JSDoc 형식을 파싱하면 됩니다.
단점:
- 파일 이동 시 경로 문자열이 stale해질 수 있음 (다만 grep으로 cross-check 가능).
- IDE의 "find all references"가 문자열 검색을 해주지 않으면 navigation이 불편함.
결정적으로 이 단점은 "grep으로 검증 가능"이라는 장점이 대부분 상쇄해 줍니다. 외부 메타파일의 drift 문제와 비교하면 주석 기반은 훨씬 self-healing합니다.
3. 마커 문법 요약
자세한 정의는 이전 글에 있습니다. 이 글에서 활용할 형식만 간단히 요약하면:
소스 파일
// src/core/config-instance.ts
/**
* ConfigInstance owns the full service stack for one config entry.
*
* @handbook 9.2-service-initialization-order
* @tested tests/core/config-instance.test.ts
* @tested e2e:tests/e2e/run-e2e.mjs
*/
테스트 파일
// tests/core/config-instance.test.ts
/**
* @covers src/core/config-instance.ts
*/
형식 규약 3개가 전부입니다.
- 경로는 프로젝트 루트 기준 상대 경로로 씁니다.
src/...,tests/... - 한 라인에 하나의 관계만 적습니다. 리스트로 쓰지 않습니다.
- E2E 테스트는
@tested쪽에서만e2e:접두사를 붙입니다.@covers는 접두사 없음(테스트 파일 자체가 E2E라는 걸 이미 알고 있으므로).
실제 적용 규모
실제 프로젝트(Obsidian 플러그인 Auto Note Importer)에 적용한 결과:
- 소스 파일에
@tested: 21개 파일 (38개 마커 — 일부 파일은 unit + E2E 두 개) - 테스트 파일에
@covers: 22개 파일 (28개 마커 — E2E 파일이 여러 소스를 커버) - 총 마커: 약 60개
이 60개 라인으로 "누가 누구를 테스트하는가" 지도가 완성됩니다.
4. grep 한 줄이 도구 — 5가지 쿼리 패턴
이 시스템의 진짜 가치는 특별한 tooling이 필요 없다는 점입니다. grep -r만 있으면 됩니다.
4.1 "이 소스의 테스트는 뭐야?"
grep "@tested" src/core/config-instance.ts
# @tested tests/core/config-instance.test.ts
# @tested e2e:tests/e2e/run-e2e.mjs
파일 하나 열어도 JSDoc 블록에 바로 보이지만, 수십 개 파일을 한 번에 검색하고 싶으면 grep이 효율적입니다.
4.2 "이 테스트가 뭘 커버해?"
grep "@covers" tests/e2e/run-e2e.mjs
# @covers src/core/sync-orchestrator.ts
# @covers src/core/config-manager.ts
# @covers src/core/config-instance.ts
# @covers src/core/conflict-resolver.ts
# @covers src/services/airtable-client.ts
# @covers src/builders/bases-file-generator.ts
# @covers src/main.ts
E2E 스위트가 어떤 소스 파일들의 실제 동작에 의존하는지 한눈에 보입니다. E2E가 실패했을 때 "어느 쪽 변경 때문인가" 를 추적하는 출발점이 됩니다.
4.3 "이 소스를 참조하는 모든 테스트는?"
grep -r "@covers.*config-instance" tests/
# tests/core/config-instance.test.ts: * @covers src/core/config-instance.ts
# tests/e2e/run-e2e.mjs: * @covers src/core/config-instance.ts
소스 파일 쪽 @tested와 테스트 파일 쪽 @covers가 서로 일치하는지 이 쿼리 하나로 교차 검증할 수 있습니다. Drift가 생기면 한 쪽에만 마커가 있게 되고, 이 grep으로 즉시 감지됩니다.
4.4 "테스트가 아예 없는 소스 파일은?"
# 모든 소스 파일 목록
find src -name "*.ts" | sort > /tmp/src-files.txt
# @tested 마커가 있는 파일만
grep -l "@tested" src/**/*.ts 2>/dev/null | sort > /tmp/tested-files.txt
# 차집합
comm -23 /tmp/src-files.txt /tmp/tested-files.txt
테스트가 없는 파일을 골라내서 "의도적으로 테스트가 없는지(예: 타입 정의 파일, barrel export)" 아니면 "그냥 빠뜨린 건지"를 검토할 수 있습니다.
4.5 "이 PR에서 바꾼 파일과 관련된 테스트 목록"
Git diff와 결합하면 수정된 파일이 어떤 테스트와 연관되는지 뽑을 수 있습니다.
# 현재 브랜치에서 수정된 소스 파일
git diff --name-only main..HEAD | grep '^src/' \
| xargs -I {} grep "@tested" {} \
| awk '{ print $3 }' | sort -u
이 출력을 그대로 테스트 실행 커맨드에 파이프하면 수정된 파일과 관련된 테스트만 돌릴 수 있습니다. 거칠지만 효과적입니다.
5. Triple Link — 문서까지 양방향으로 묶기
이 프로젝트는 ENGINEERING_HANDBOOK이라는 아키텍처 문서를 별도로 관리합니다. 똑같은 양방향 마커 시스템을 문서와 코드 사이에도 적용했습니다.
// 소스 → 문서
/**
* @handbook 9.2-service-initialization-order
* @handbook 4.4-provider-abstraction
* @tested tests/core/config-instance.test.ts
*/
<!-- 문서 → 소스 (핸드북 내부) -->
### 9.2 Service Initialization Order
<!-- @code src/core/config-instance.ts -->
<!-- @code src/main.ts -->
Services are initialized in this order: ...
세 개의 축이 서로 link되어 있습니다: 소스 ↔ 테스트 ↔ 문서.
@tested
┌──────────────────┐
│ ▼
┌─────────┐ ┌──────────┐
│ source │◄──────│ test │
└────┬────┘ @covers└──────────┘
│
│ @handbook
▼
┌─────────┐
│ docs │
└─────────┘
│ @code
▲
└── (reverse link)
Grep 쿼리 하나로 이 트라이앵글의 어느 꼭짓점에서 다른 두 꼭짓점을 찾을 수 있습니다. 특히 문서 섹션 하나가 참조하는 모든 파일을 찾는 일은 handbook의 "architecture refactor 영향 범위"를 파악할 때 매우 유용합니다.
6. 실제 적용 경험 — 21 + 22 파일에 마커를 박은 과정
6.1 일괄 적용보다 점진적 적용
마커를 한 번에 전부 달지 않았습니다. 새로 작성하거나 수정하는 파일부터 시작해서 PR 단위로 점진적으로 확산시켰습니다. 다음 규칙을 지켰습니다.
- 새 파일은 처음부터 마커 달고 시작. 안 달면 PR 리뷰에서 지적.
- 기존 파일은 수정할 때 함께 마커 달기. 한 번에 수십 개 파일을 건드리는 PR은 만들지 않음.
- 가끔 "일괄 마커 달기" PR 하나. 3~6개월에 한 번, 방치된 파일들에 마커를 달면서 현재 상태 snapshot을 취함.
이 3번째 PR이 실제로 45 파일에 283줄 추가(마커 + vitest coverage plugin 의존성 추가)로 한 번에 정리했습니다.
6.2 @vitest/coverage-v8 추가의 이유
이 PR에 @vitest/coverage-v8를 dev dependency로 추가했는데, 이건 마커 시스템과 직접적인 관계는 없습니다. 마커가 인간이 "관계"를 추적하는 수단이라면, vitest coverage plugin은 "실제 실행 시 어느 줄이 실행되었나"를 기계적으로 측정하는 수단입니다. 두 개는 서로 보완적입니다.
- 마커: "이 파일이 어떤 테스트로 검증되는가"에 답함. Intent 기반.
- Coverage: "이 파일의 어느 라인이 실행되었는가"에 답함. Reality 기반.
마커가 "connection이 존재함"을 선언하고, coverage가 "connection이 실제로 얼마나 깊게 실행되는지" 수치화합니다. 둘 다 있으면 "마커는 있는데 coverage가 0%"인 파일을 찾을 수 있습니다 — 이건 테스트가 의도한 것과 다른 경로만 걷고 있다는 신호입니다.
6.3 한 주 뒤에 나온 진짜 가치
마커를 단 직후에는 "조금 장식적이다"라는 느낌이 있었습니다. 기존에도 폴더 구조만 보면 대부분 관계는 알 수 있었기 때문입니다. 진짜 가치는 Provider Abstraction 리팩토링(PR #49) 같은 대규모 변경을 할 때 드러났습니다.
airtable-client.ts를 수정할 때@tested에airtable-client.test.ts,airtable-client-view.test.ts,e2e:run-e2e.mjs가 바로 보이니 리팩토링 대상 테스트의 전체 목록이 한 번에 떠올랐습니다.- E2E 스위트가 어떤 소스 7개를 커버하는지
@covers목록에 바로 적혀 있어서, "E2E가 갑자기 실패하면 원인 후보가 이 7개" 라는 것을 빠르게 좁힐 수 있었습니다. - 핸드북의
§4.4 Provider Abstraction섹션에<!-- @code ... -->마커가 있어서, 문서 기준으로 "이 섹션이 영향받는 소스 파일" 을 찾을 수 있었습니다.
7. 체크리스트
양방향 마커 시스템을 도입할 때 권장 절차:
- [ ] 형식 결정:
@tested/@covers두 축(3축이면@handbook/@code추가). 경로는 프로젝트 루트 기준 - [ ] 파일당 한 블록: 파일 상단 JSDoc에 모든 마커를 모으기. 중간에 흩뿌리지 말기
- [ ] E2E 구분:
e2e:접두사로 unit/integration과 시각적 분리 - [ ] 점진적 적용: 새 파일과 수정 파일부터. 대규모 일괄 PR은 3~6개월에 한 번
- [ ] Cross-check grep:
@tested와@covers가 대칭적인지 검증 (2편의 CI 스크립트 참고) - [ ] 문서에도 확장: 핸드북 같은 아키텍처 문서가 있다면
@handbook/@code쌍도 추가 - [ ] IDE 설정: 가능하면 JSDoc 마커에 clickable link가 되는 플러그인 설치 (VS Code의 몇몇 extension이 지원)
8. FAQ
Q: 왜 @tested 하나로 만들지 않고 @covers도 따로 두나요?
A: 양방향이 필요하기 때문입니다. "이 소스의 테스트는?" 질문은 @tested로만 답할 수 있고, "이 테스트는 뭘 커버하나?" 질문은 @covers로만 답할 수 있습니다. 한 쪽만 있으면 항상 두 쿼리 중 하나가 느려집니다 (모든 소스 파일을 grep해야 하는 식으로). 양쪽에 두면 어느 방향이든 대칭적으로 빠릅니다.
Q: 타입 정의 파일이나 barrel export에도 마커를 달아야 하나요?
A: 달 필요 없습니다. 대신 파일 상단에 @tested none — types only나 @tested none — barrel export 같이 "의도적으로 없음"을 명시하면 "빠뜨린 건지 의도인지"를 구분할 수 있습니다. 저는 보통 주석 없이 두는데, 대신 테스트 부재를 체크하는 쿼리에서 src/**/index.ts와 src/types/**를 제외하는 방식으로 운영합니다.
Q: @handbook/@code와 @tested/@covers를 섞어 쓰면 혼란스럽지 않나요?
A: 혼란스럽기보다는 시각적으로 그룹핑됩니다. 파일 상단 JSDoc에 마커를 쓸 때 순서 규약을 두면 됩니다: 먼저 @handbook(아키텍처 문서), 그 다음 @tested(테스트). 한 블록 안에서 @handbook → @tested → @covers → 기타 순서를 지키면 읽기 쉽습니다.
Q: 이 시스템을 tool이 없는 언어(Python, Go 등)에도 적용할 수 있나요?
A: 네. 주석 규약은 언어 중립적이고, grep은 어디에나 있습니다. Python은 docstring에 @tested/@covers를 넣으면 되고, Go는 package doc에 넣습니다. 핵심은 "grep으로 추출 가능한 고유한 접두사"만 유지하는 것입니다.
Q: 마커 대신 describe('src/core/foo') 같은 네이밍 규약으로 test suite를 쓰면 어떨까요?
A: 가능하지만 두 가지 문제가 있습니다. 첫째, 소스 쪽에서 "내가 어떤 테스트로 커버되는가"를 알 수 없습니다. 테스트 파일을 열어야만 알 수 있고, 소스 파일 상단만 보면 모릅니다. 둘째, 문자열 describe 이름이 경로와 동기화되지 않으면 drift가 생기는데, 이건 주석 방식과 같은 문제입니다. 주석 방식이 더 명시적이라고 판단했습니다.
9. 참고 자료
- JSDoc @see tag — 비슷한 패턴의 공식 JSDoc 태그
- Vitest Coverage — 실행 기반 coverage
- Obsidian Bidirectional Links — 원래 영감을 받은 노트 링킹 패턴
10. 다음 단계
여기까지가 "마커 시스템을 어떻게 구축하고 적용하는가"의 기술적 이야기였습니다. 시리즈의 마지막 글은 "언제 이 도구가 필요하지 않은가" 라는 반대 방향 질문을 다룹니다. 같은 마커 시스템을 훨씬 작은 사이드 프로젝트(chrome-secret)에 적용했을 때, CI 검증도, 45파일 규모의 일괄 적용도, @vitest/coverage-v8도 전부 필요 없었다는 회고입니다.
시리즈 목차:
- AI 코딩 어시스턴트 시대의 문서화: 양방향 링크 시스템 구축기
- 양방향 링크 시스템을 테스트 커버리지에 확장하기: @tested / @covers 마커 패턴
- 소스와 테스트를 grep 한 줄로 연결하기: @tested / @covers 양방향 마커 시스템 ← 현재 글
- 양방향 링크 시스템 회고: 프로젝트 스케일에 따른 도구화 결정