방어적 트레이스 락 (Defensive Trace Lock), 엔지니어링 에디션 — 5가지 아티팩트 상세 분석
요약
AI와 페어링하여 코드를 작성할 때 발생하는 거버넌스 문제를 해결하기 위한 '방어적 트레이스 락(Defensive Trace Lock)'의 5가지 엔지니어링 아티팩트를 상세히 분석합니다. 마크다운 기반의 레지스트리, 앵커 SSOT, 트레이스 노드 등의 개념을 통해 코드의 신뢰성을 확보하는 방법을 다룹니다.
핵심 포인트
- 마크다운을 활용한 사람이 읽고 스크립트로 파싱 가능한 레지스트리 설계
- 단일 진실 공급원(SSOT)인 앵커를 통한 트레이스 계약 관리
- 데이터 흐름의 모든 노드와 진입점을 추적하여 변경 영향도 파악
- 정규 표현식을 이용한 구조화된 데이터 추출 및 거버넌스 자동화
2026년 5월 · 시리즈 "Trace Lock — 코드를 작성하기 위해 AI와 페어링하며 얻은 거버넌스 노트" · 9개 포스트 중 6번째
이 포스트는 A1 Defensive Trace Lock의 엔지니어링 버전입니다.
A1에서는 "왜(why)"와 "5가지 아티팩트(artifacts)의 상위 수준 모습이 어떠한가"를 다루었습니다. 이 포스트에서는 각 아티팩트를 상세히 풀어냅니다: 코드 형태, 왜 그렇게 설계되었는지, 그리고 무엇을 조정할 수 있는지에 대해 설명합니다.
거버넌스 규칙 (governance rules), 피닝 테스트 (pinning tests), 그리고 코드형 설정 (config-as-code)이 무엇인지 이미 알고 있는 독자들을 위해 작성되었습니다. 엔지니어링 배경 지식이 없다면 A1이 더 적합합니다.
나의 스택: Vue 3 + Vite + Vitest + Supabase (PostgreSQL) + Node.js 스크립트. 하지만 아래 5가지 아티팩트에 적용된 패턴은 프론트엔드 프레임워크와 무관합니다. 이는 주로 파일 시스템 컨벤션 (filesystem conventions)과 정규 표현식 파싱 (regex parsing)에 의존합니다.
5가지 아티팩트 한눈에 보기
| # | 아티팩트 (Artifact) | 역할 | 파일 유형 |
|---|---|---|---|
| 1 | 레지스트리 (Registry) | 트레이스 디렉토리, 수동 작성된 마크다운 (markdown) | 文檔/data-source-registry.md |
| ... |
총 코드 라인 수 약 600줄 (레지스트리 파서 + 2가지 거버넌스 규칙 + 스킬 마크다운). 첫 설정에는 약 4시간이 소요됩니다. 이후 각 트레이스에는 30~45분이 소요됩니다.
아티팩트 1: 레지스트리 마크다운 형식
레지스트리는 마크다운 (markdown) 파일입니다. YAML도, JSON도, 데이터베이스도 아닙니다. 그 이유는: 사람이 유지 관리할 수 있어야 함과 동시에 스크립트로 파싱 (parseable) 가능해야 하기 때문입니다. 마크다운은 양쪽 방향 모두에서 수용 가능합니다.
각 트레이스는 ### T-{id}: 제목 블록입니다. 필드는 **필드 이름**: 값 컨벤션을 사용하며, 이는 정규 표현식 (regex)으로 쉽게 추출할 수 있게 해줍니다. 다음은 실제 T-001의 축약 버전입니다:
### T-001: 볶은 원두 재고 그램 수 → POS 변형 주문 가능 수량
- **Type**: data-flow trace
...
필드를 이런 방식으로 나누는 이유
**앵커 SSOT (Anchor SSOT)**는 이 트레이스에 대한 유일하게 신뢰할 수 있는 계산 엔트리 (compute entry)입니다. 모든 엔트리 포인트 (Entry points)는 반드시 이 앵커로 수렴해야 합니다. 만약 앵커가 변경되면, 트레이스 계약 (trace contract)이 변경됩니다. 이는 주요 변경 사항으로 간주됩니다.
**트레이스 노드 (Trace nodes)**는 쓰기 측(DB 컬럼, 트리거)부터 표시 측(UI 컴포넌트)까지의 모든 노드를 순서대로 나열합니다. 각 노드는 "이 부분을 건드리면 트레이스 동작에 영향을 줄 수 있음"이라고 판단되는 후보 파일입니다.
**진입점 (Entry points)**은 동일한 앵커 SSOT(단일 진실 공급원)로 향하는 여러 진입 경로를 의미합니다. 예를 들어, T-001의 앵커인 variantInventory.js는 ProductVariantModal(사용자가 POS에서 변형을 선택할 때)과 AdminPOS(결제 전 장바구니 안전 점검 시) 모두에 의해 호출됩니다. 진입점은 "이 SSOT가 실제로 사용되어야 하는 모든 곳에서 사용되고 있는가?"를 확인하기 위한 감사 체크리스트를 형성합니다.
**최종 수정일 (Last edited)**은 AI가 읽기 위한 용도입니다. 각 트레이스 노드 수정 후, 작업자는 이 필드를 업데이트합니다. 다음에 AI가 확인했을 때, "누군가 최근에 이것을 건드렸으므로 컨텍스트가 어긋났을 수 있음"을 인지하게 됩니다.
설계 트레이드오프 (Design tradeoff): 왜 YAML이나 JSON이 아닌가
YAML을 고려했으나 거부했습니다. 이유는 다음과 같습니다:
- 마크다운 (Markdown) 링크와 코드 참조가 내장되어 있음. YAML은 "이것은 파일 경로임"을 표현하기 위해 별도의 스키마가 필요합니다.
- 인간의 유지보수 비용. YAML의 엄격한 들여쓰기는 작은 수정 작업을 수행하기에 친숙하지 않습니다. 마크다운은 자유롭게 줄바꿈을 하고 주석을 달 수 있습니다.
- GitHub 및 VS Code 렌더링. 마크다운은 링크를 인라인으로 보여주지만, YAML은 가공되지 않은 텍스트로 보여줍니다.
그 대가로 파싱(parsing)이 YAML보다 약 30% 더 까다롭습니다 (yaml.parse보다 정규 표현식(regex)이 오류가 발생하기 쉽습니다). 하지만 레지스트리는 프로젝트당 약 30개의 트레이스라는 실질적인 상한선이 있으므로, 파싱 복잡성 또한 제한적이라는 점을 고려하여 이 트레이드오프를 수용합니다.
아티팩트 2: 트레이스 테스트 5개 섹션 구조
T-001 트레이스 테스트는 5개의 describe 블록을 포함합니다. 각 섹션은 트레이스의 하나의 의미론적 측면을 고정합니다:
import { describe, expect, it } from 'vitest'
import {
getVariantWeightPerPack,
...
각 섹션이 고정하는 것
섹션 1, 계약 (Contract). 트레이스가 다루는 "기본 계약"을 선언합니다. T-001의 경우, 계약은 "각 변형(variant) 형식은 팩당 특정 순중량에 매핑됨"입니다. 이 섹션은 상태(state)가 없는 순수한 명세(spec) 매핑을 테스트합니다.
섹션 2, 인시던트 피닝 (Incident pinning). 이 트레이스(trace)를 생성하게 만든 트리거인 고객 이벤트(customer-event)를 테스트 케이스로 기록합니다. T-001의 경우, San Agustin 드립백 사고(244g 배럴 → 0.5파운드 1개 또는 드립백 2팩)가 이에 해당합니다. 이 섹션은 역사적 기억(historical memory) 역할을 합니다. 이 섹션이 없다면, 트레이스는 다시 깨진 상태로 퇴보(rot)할 수 있습니다.
섹션 3, 엣지 케이스 (Edge cases). null, undefined, 0, 음수, 누락된 필드 등을 다룹니다. 어떤 것도 예외(throw)를 발생시켜서는 안 되며, 모두 안전한 폴백(fallback)으로 넘어가야 합니다. 이 섹션은 향후 AI가 헬퍼(helper)를 수정할 때 엣지 케이스가 회귀(regress)하는 것을 방지하기 위해 존재합니다.
섹션 4, 역방향 해상 (Reverse resolution). 트레이스가 "진입점(entry point)에서 앵커(anchor)가 이해할 수 있는 형식으로 역방향으로 해상"해야 하는 경우, 이 섹션은 역방향 해상을 테스트합니다. T-001의 경우, resolveCartItemVariantFormat은 장바구니 아이템(cart item) 객체로부터 변형(variant) 형식을 도출합니다.
섹션 5, 교차 진입점 일관성 (Cross-entry consistency). N개의 진입점에 전달된 동일한 입력은 반드시 동일한 결과를 반환해야 합니다. 이 섹션은 "진입점들이 중복된 구현이 아니라 실제로 하나의 경로를 공유하는지"를 테스트합니다.
모든 트레이스에 5개 섹션이 모두 필요한 것은 아닙니다. 최소 구성은 섹션 1 + 섹션 2(계약(contract) + 인시던트(incident))입니다. 섹션 3~5는 트레이스의 형태에 따라 추가됩니다.
앵커(anchor) 임포트가 필수인 이유
아티팩트 4(거버넌스 규칙 B)는 테스트 파일의 첫 번째 import가 실제로 레지스트리(registry)에 선언된 앵커 경로를 가리키고 있는지 확인합니다.
이유: 만약 트레이스 테스트가 앵커를 임포트하지 않는다면, 테스트 대상은 SSOT(단일 진실 공급원)가 아니라 "테스트 파일 내부에 복사하여 붙여넣은 로직 스니펫(logic snippet)"이 됩니다. 나중에 앵커가 변경되더라도 테스트는 이를 인지할 수 없습니다. 테스트는 통과(green) 상태를 유지하지만, 트레이스는 부패(rot)하게 됩니다.
이 체크는 단순하지만 매우 중요한 역할을 합니다. 디버깅 과정에서 레지스트리에 등록되지 않았던 하나의 트레이스가 정확히 이런 방식으로 부패한 적이 있습니다. 테스트는 통과(green) 상태였지만, 실제 로직은 다른 PR(Pull Request)에 의해 재작성되어 있었습니다.
아티팩트 3 & 4: 거버넌스 규칙 코드 워크스루 (walkthrough)
두 가지 거버넌스 규칙(governance rules)은 scripts/governance-guard.mjs에 존재하며, 모든 pre-push 또는 CI 사이클에서 실행됩니다. 핵심은 마크다운(markdown)을 객체 배열로 추출하는 parseTraceRegistry()입니다:
function parseTraceRegistry() {
const registryPath = path.join(repoRoot, '文檔/data-source-registry.md')
if (!fs.existsSync(registryPath)) return []
...
몇 가지 엔지니어링 세부 사항:
.*? 대신 [\s\S]*?를 사용합니다. JS 정규 표현식(regex)은 기본적으로 .을 사용하여 줄바꿈(newline)을 매칭하지 않습니다. 마크다운 블록은 여러 줄에 걸쳐 있으므로 [\s\S]가 필요합니다.
종료 조건으로 (?=\n##\s|$)를 사용합니다. 전방 탐색(lookahead)을 통해 다음 ## 헤딩(heading)이나 파일 끝(end-of-file)에서 파싱이 중단되도록 보장합니다. 이것이 없다면 파서가 관련 없는 섹션까지 모두 긁어모으게 됩니다.
.slice(1)은 서문(prelude)을 건너뜁니다. split을 통해 얻은 첫 번째 청크는 ## Critical Traces 헤딩과 첫 번째 ### 사이의 콘텐츠입니다. 이는 섹션 서문이며 트레이스(trace)가 아닙니다.
isSqlOnly 분리(carveout). T-021 (FIFO consumption trace)은 sql-only입니다. 해당 테스트는 .js가 아닌 .sql 파일입니다. Type 필드는 sql-only-trace로 표시되며, 규칙 B는 임포트(import) 확인을 건너뜁니다 (SQL 테스트에는 임포트 개념이 없기 때문입니다).
규칙 A: 모든 트레이스는 대응하는 테스트 파일을 가집니다
function checkTraceRegistryTestCoverage(violations) {
const traces = parseTraceRegistry()
for (const trace of traces) {
...
두 가지 실패 모드(failure modes)를 차단합니다: (a) 레지스트리에 선언되었으나 테스트 경로(test path) 필드가 없는 트레이스, (b) 테스트 경로가 선언되었으나 파일이 존재하지 않는 경우(경로 오타 또는 아직 생성되지 않음).
규칙 B: 트레이스 테스트는 선언된 앵커(anchor)를 임포트합니다
function checkTraceTestImportsAnchor(violations) {
for (const trace of traces) {
if (!trace.testPath || !trace.anchorPath) continue
...
앵커의 베이스네임(basename)에 대해 느슨한 매칭(loose match, path-relative-flexible)을 수행하므로, import { ... } from '...variantInventory(.js)' 또는 require(...) 중 어느 것이든 통과합니다. String.raw를 사용하여 백슬래시 이스케이프(backslash escape) 지옥을 방지합니다.
두 규칙을 합치면 약 60줄의 코드입니다.
아티팩트 5: AI 리마인더 스킬 (the AI reminder skill)
이 스킬은 마크다운 (markdown) 파일입니다. 프론트매터 (frontmatter)의 description은 트리거 조건을 선언합니다. 본문에는 5단계 감사 체크리스트 (audit checklist)가 포함됩니다. Claude Code는 작업 설명 (task description)이 다음 내용과 일치할 때 이를 자동으로 로드합니다:
---
name: trace-lock-modify
description: "文檔/data-source-registry.md > Critical Traces 의 Anchor SSOT / Trace nodes / Entry points에 나열된 파일을 수정할 때 사용하십시오. 교차 계층 트레이스 가드레일 (Cross-layer trace guardrail). AI는 반드시 트레이스의 전체 체인을 나열하고 + 베이스라인 (baseline)을 고정하기 위해 트레이스 테스트를 실행해야 하며 + 수정 후 재실행해야 하고 + 사용자에게 Last edited를 업데이트하도록 상기시켜야 합니다."
...
몇 가지 엔지니어링 세부 사항:
description은 반드시 "Use when ..."으로 시작해야 합니다. Claude Code는 트리거 조건을 판단할 때 이 문구를 사용합니다. 다른 문구들은 측정 가능한 수준으로 낮은 트리거율을 보였습니다 (4번의 스킬 재작성 과정에서 관찰됨).
특정 파일 경로를 나열하십시오. description에는 세 가지 카테고리(Anchor SSOT / Trace nodes / Entry points)가 직접 나열되어 있습니다. AI가 이 경로들을 언급하는 작업 설명을 볼 때, 매칭률이 높습니다.
5단계 체크리스트는 본문에 위치합니다. 트리거된 후, AI는 실제로 SKILL.md 본문을 읽습니다. 본문의 단계들은 실행 가능해야 합니다 (grep 명령, vitest 명령, "수정 후 재실행" 등).
5가지 단계가 강제하는 사항
- 파일이 어떤 트레이스에 속하는지 확인 (레지스트리에서 grep 실행)
- 사용자에게 트레이스의 전체 체인을 나열 (AI가 이미 알고 있더라도 사용자는 기억하지 못할 수 있음)
- 수정 전 트레이스 테스트 실행 (베이스라인 기록)
- 수정 후 트레이스 테스트 재실행 (아무것도 망가지지 않았음을 확인)
- 레지스트리의
Last edited필드 업데이트
2단계가 중요합니다. AI는 사용자가 트레이스 세부 정보를 기억하고 있다고 가정해서는 안 됩니다. 매번 체인을 나열함으로써 사용자와 AI의 멘탈 모델 (mental models)을 동기화하도록 강제합니다.
엔지니어링 고려 사항
파서 허용 오차 (Parser tolerance)
정규 표현식 (Regex) 파싱에는 세 가지 일반적인 실패 모드가 있습니다:
- Markdown 링크와 일반 텍스트의 공존.
**Anchor SSOT**: [path](url)와**Anchor SSOT**: path를 모두 인식해야 합니다. 제 정규 표현식 (Regex)은\[?/\]?를 사용하여 두 경우를 모두 허용합니다. - 필드 값 뒤의 주석 (Annotations). 필드 이름 뒤에 이모지나 마커가 붙을 수 있습니다.
/**Type**:\s*에?를 추가하면**Type**: data-flow ⚠️와 같은 사례를 수용할 수 있습니다. - 빈 필드 (Empty fields). 트레이스 (Trace)에서 필드가 누락되었을 때 오류를 발생시키지 마세요.
null을 반환하고, 거버넌스 규칙 (Governance rule)이 사용자에게 명확한 메시지와 함께 "이 필드를 채워주세요"라는 메시지를 출력하도록 합니다.
크로스 플랫폼 경로 (Cross-platform paths)
Node.js 스크립트는 Windows, Mac, Linux에서 실행됩니다:
const repoRoot = path.dirname(fileURLToPath(import.meta.url)).replace(/scripts$/, '')
const registryPath = path.join(repoRoot, '文檔/data-source-registry.md')
절대로 '/'나 '\'를 하드코딩하지 마세요. 항상 path.join을 사용해야 합니다. CJK (한중일) 폴더 이름은 Windows의 UTF-8 환경에서 작동하지만 (Node.js fs가 자동으로 처리함), Windows의 콘솔 출력은 cp950 인코딩에 주의가 필요합니다 (chcp 65001 + LC_ALL=C.UTF-8).
CI 통합 (CI integration)
scripts/governance-guard.mjs는 독립적인 엔트리 (Entry)로 작성되었습니다. Pre-push hook과 GitHub Actions 모두 이를 실행합니다:
# pre-push hook
node scripts/governance-guard.mjs || exit 1
violations 배열이 모든 위반 메시지를 수집합니다. 마지막에 이를 출력하고 종료 코드 (Exit code) 1로 종료합니다. 핵심 사항: 모든 트레이스 규칙은 BLOCKER 티어 (Tier)입니다 (권고 사항이 아님). 이 규칙들은 푸시 (Push)를 차단합니다. 이유는 트레이스가 설계상 "이미 안정적이어야 하는 관계"이기 때문입니다. 권고 (Advisory) 수준은 강제성이 없음을 의미합니다.
점진적 비용 (Incremental cost)
각각의 새로운 트레이스 (첫 번째가 아니라, 프레임워크가 이미 구축된 상태에서 추가되는 새로운 트레이스)에 대해:
- 레지스트리 (Registry) 블록 작성: 약 10분
- 트레이스 테스트 (5개 섹션) 작성: 20-30분
- 검증을 위한 두 가지 거버넌스 규칙 실행: 30초
Last edited업데이트: 30초
트레이스당 총 30-45분이 소요됩니다. 이는 A1에서의 추정치와 일치합니다.
프로젝트 간 이식성 (Cross-project portability)
각 아티팩트 (Artifact)를 다른 프로젝트로 복사할 때의 이식성은 다음과 같습니다:
| Artifact | Portability | Why |
|---|---|---|
| Registry markdown format | ★★★★★ | Pure convention, project-independent |
| ... | ||
| In other words: 프레임워크 계층은 약 80% 이식 가능하고; 콘텐츠 계층은 0% 이식 가능하다. 이 비율은 C1 Offense + Defense combined에서 제시된 '프레임워크 80% 이식, 콘텐츠는 재구축 필요' 추정치와 일치합니다. |
상세한 크로스 프로젝트 재사용 분석은 C2 Cross-project reuse matrix에서 확인하실 수 있습니다 (동일 시리즈 브랜치의 다음 게시물).
이 방식이 효과적이지 않은 경우
이 5가지 아티팩트를 통째로 가져오는 것이 가치가 없을 때는 다음과 같습니다:
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기