공격적 감사, 엔지니어링 에디션 — 6단계 수정 패턴 상세 분석
요약
AI와 페어링하여 코드의 격차를 발견하고 수정하는 '6단계 수정 패턴'을 상세히 분석합니다. 파일 시스템 컨벤션, 정규 표현식 파서, CI 강제 실행을 활용하여 비즈니스 로직을 구조화하고 부패를 방지하는 엔지니어링 방법론을 다룹니다.
핵심 포인트
- 비즈니스 로직을 순수 함수로 추출하여 부작용을 방지하는 6단계 패턴 제시
- 파일 시스템 컨벤션과 CI를 활용한 코드 거버넌스 강화 방법
- A2(방어)와 B2(수정) 프로세스의 상호 보완적 관계 설명
- 단일 진실 공급원(SSOT) 확보를 위한 엔지니어링 워크플로우
2026년 5월 · 시리즈 "Trace Lock — 코드를 작성하기 위해 AI와 페어링하며 얻은 거버넌스 노트" · 9개 중 7번째 포스트
이 포스트는 B1 Offensive audit의 엔지니어링 버전입니다.
B1에서는 "왜 전체 체인을 감사해야 하는가"와 "전반적인 흐름이 어떻게 되는가"를 다루었습니다. 이 포스트에서는 감사를 통해 드러난 각 격차(gap)를 실제로 수정하는 데 사용되는 6단계 수정 패턴(6-piece fix pattern, 임시 명칭)을 상세히 풀어냅니다. 즉, 각 구성 요소의 코드 형태, 그렇게 설계된 이유, 그리고 조정 가능한 부분들을 다룹니다.
거버넌스 규칙(governance rules), 피닝 테스트(pinning tests) 또는 사고 기반 테스트(incident-driven tests)에 이미 익숙한 엔지니어를 위해 작성되었습니다. 엔지니어링 배경이 없다면 B1이 더 적합합니다.
나의 스택: Vue 3 + Vite + Vitest + Supabase (PostgreSQL) + Node.js 스크립트. 하지만 아래의 6단계 패턴은 프론트엔드 프레임워크와 거의 무관합니다. 이는 파일 시스템 컨벤션 (filesystem conventions) + 정규 표현식 파서 (regex parsers) + CI 강제 실행 (CI-enforced execution) (pre-push hook + GitHub Actions)에 의존합니다.
6단계 요약
| # | 구성 요소 (Piece) | 역할 (Role) | 파일 유형 (File type) |
|---|---|---|---|
| 1 | 순수 함수 헬퍼 (Pure-function helper) | 비즈니스 로직을 부작용이 없는 (side-effect-free) 함수로 추출 | frontend-app/src/lib/business-rules/<feature>Logic.js |
| ... | |||
| 이 세트는 다음 파이프라인에 대응합니다: a. 공격이 격차를 드러냄 → b. 비즈니스 계약 동결 → c. 순수 함수 추출 → d. 동작 피닝 (pin behavior) → e. 호출자 강제 (force callers) → f. 결정 기록. |
A2의 5가지 아티팩트(artifacts)와의 매핑
A2의 5가지 아티팩트 (레지스트리 + 트레이스 테스트 + 2가지 거버넌스 규칙 + AI 리마인더 스킬)는 "단일하고 알려진 트레이스에 대한 부패 방지 (rot protection)"를 해결합니다.
B2의 6단계는 "제로 상태에서 시작하여, 한 번의 스프린트 내에 N개의 격차를 발견하고 수정"하는 문제를 해결합니다. 차이점은 다음과 같습니다:
- A2는 헬퍼(helper)가 필요하지 않습니다 (트레이스(trace)에 이미 단일 진실 공급원(SSOT)이 있는 경우가 많음)
- B2는 반드시 헬퍼를 추가해야 합니다 (격차(gaps)가 존재하는 이유는 아직 SSOT가 존재하지 않기 때문이며, 로직이 분산되어 있음)
- A2는 반복 로그(iteration log)가 필요하지 않습니다 (단일 트레이스, 일회성 설정)
- B2는 반드시 반복 로그를 추가해야 합니다 (다중 격차 수정 프로세스는 에스컬레이션(escalation), 역검증(reverse verification), 셀프 체크(self-checks)를 기록해야 함)
두 세트는 상호 보완적입니다. B2는 새로운 격차를 드러내고 고정하며, A2는 이후의 부패(rot)를 방지합니다.
하나라도 누락될 경우 발생하는 문제
| 누락된 요소 | 결과 |
|---|---|
| 순수 함수 헬퍼 (Pure-function helper) | 비즈니스 로직이 분산된 상태로 유지되며, 다음 호출자가 이를 다시 구현하게 됨 |
| ... |
어느 한 요소만으로는 충분하지 않습니다. 6가지 요소가 모두 존재할 때만 폐쇄 루프(closed loop)가 형성됩니다.
요소 1 (순수 함수 헬퍼): 추출 로직 (extraction logic)
비즈니스 로직이 Vue 컴포넌트, 컴포저블(composable), 또는 RPC 핸들러에 임베드(embedded)되면, 사이드 이펙트(side effects; DB 읽기/쓰기, 스토어 뮤테이션(store mutations), emit)를 수반하게 됩니다. 이 경우 트레이스 테스트(trace test)는 깨끗한 테스트 대상을 확보할 수 없습니다.
순수 함수 헬퍼를 위한 설계 컨벤션(design conventions):
// frontend-app/src/lib/business-rules/cancelOrderLogic.js
/**
...
몇 가지 엔지니어링 세부 사항입니다.
사이드 이펙트 제로 + 의존성 주입 (dependency injection)
헬퍼는 useUserStore(), supabase.from(...), 또는 window.localStorage를 직접 호출하지 않습니다. 모든 의존성은 파라미터를 통해 전달됩니다:
// 반면 사례 (스토어에 결합됨)
export function calculateRefundAllocation(refundAmount) {
const userStore = useUserStore()
...
이유: 트레이스 테스트는 스토어나 Supabase를 모킹(mocking)하지 않고도 피스처(fixture) 데이터를 사용하여 헬퍼를 호출할 수 있습니다. 테스트 비용이 10배(an order of magnitude) 가량 절감됩니다.
composables/가 아닌 lib/business-rules/ 아래에 배치
composables/는 Vue의 컨벤션입니다. 해당 디렉토리의 함수들은 컴포넌트에 ref / reactive를 반환합니다. 반면 lib/business-rules/는 순수 JS이며, Vue와 완전히 디커플링(decoupled)되어 있습니다.
만약 향후에 다음과 같은 작업을 원한다면:
- Edge Function (Deno 런타임, Vue를 임포트할 수 없음)에서 헬퍼 (helper) 재사용
- SSR (window 객체 없음)에서 재사용
- 단위 테스트 (unit tests) 작성 (Vue test utils를 실행할 필요 없음)
올바른 위치에 배치하면 재사용이 매우 수월해집니다.
명명 규칙 (Naming convention): <feature>Logic.js
- ✅
cancelOrderLogic.js/roastingLifecycle.js/packagingTaskState.js - ❌
cancelOrderUtils.js/refundHelper.js/roastingService.js
Logic 접미사를 사용하면 단 한 번의 grep 명령으로 모든 SSOT (Single Source of Truth, 단일 진실 공급원) 헬퍼를 찾아낼 수 있습니다. Utils / Helper / Service는 너무 일반적이어서 감사 (audit)하기 어렵습니다.
헬퍼를 추출하지 말아야 할 때
- 로직이 단 한 곳에서만 호출되는 경우. 추출하면 독자의 주의를 분산시킵니다.
- 로직이 순수하게 읽기 전용이거나 분기(branch)가 없는 경우. 인라이닝 (Inlining) 해도 괜찮습니다.
- 로직이 주로 DB에 존재하는 경우 (예: FIFO 소비). sql-only-trace 변형 방식을 사용하세요 (나중에 다룹니다).
두 번째 조각 (Trace test): 5개 섹션 구조
트레이스 테스트 (trace test)는 vitest 스펙이지만, 그 구조는 일반적인 단위 테스트보다 더 엄격합니다. 각 describe 블록은 의미론적 측면 (semantic facet)에 대응합니다. 다음은 T-019 주문 취소 / 환불의 골격입니다:
// frontend-app/src/__tests__/traces/T019-cancel-refund.trace.test.js
import { describe, expect, it } from 'vitest'
...
각 섹션의 용도
섹션 1 (비즈니스 계약, 인시던트 고정)
비즈니스 계약을 확정 지었던 논의 내용을 테스트 케이스로 작성하세요. 각 차단 요소 (BLOCKER gap)에 대한 결정은 사용자가 그 자리에서 내린 것입니다 (예: "환불은 현금을 먼저 채운다"). 이 섹션은 해당 결정을 직접적으로 고정 (pin)합니다. 3개월 후, 알고리즘을 변경하는 사람은 테스트가 실패(red)하는 것을 보게 되며, 결정의 맥락을 반드시 확인해야만 합니다.
각 트레이스 테스트는 여기서 최소 1개의 케이스를 가지며, 종종 5~10개(해피 패스 (happy paths) + 경계값 (boundaries) 포함)를 가집니다.
섹션 2 (정상 경로 (Legal paths))
전체 해피 패스의 변형들입니다. 예를 들어, T-022 패키징 작업 상태 머신 (state machine)은 pending → in_progress → completed 및 pending → cancelled라는 정상적인 전이 (transitions)를 가집니다. 이 섹션은 4~6개의 정상 경로를 테스트합니다.
섹션 3 (비정상 경로 (Illegal paths) / 엣지 케이스 (edge cases))
null / undefined / 빈 배열 (empty array) / 누락된 필드 (missing fields) / 음수 (negatives) / 매우 큰 숫자 (very large numbers). 이 섹션은 향후 AI 편집으로 인해 엣지 케이스 (edge cases)가 퇴보하는 것을 방지합니다.
이 섹션을 작성할 때는 다음과 같이 적극적으로 질문해야 합니다: "도우미 함수 (helper)가 지저분한 입력값 (dirty input)을 받았을 때 무엇을 하는가? 예외를 던져야 (throw) 하는가?" 정답은 보통 "안전한 대체값 (safe fallback)을 반환하고, 예외를 던지지 마라"입니다 (예외를 던지면 UI catch 체인이 터져버리기 때문입니다). 하지만 이를 테스트에서 명확히 고정(pin)해 두어야 합니다.
섹션 4 (역할 / 권한 / 비즈니스 규칙 제한 (Role / permission / business rule limits))
만약 차이점 (gap)이 역할 제한과 관련이 있다면 (예: T-022에서 "롤백 완료(rollback completed) → 취소(cancelled)는 admin/super_admin만 가능"이라고 명시한 경우), 이 섹션은 역할별 허용/거부 매트릭스 (allow/deny matrix)를 테스트합니다.
모든 트레이스 (trace)에 이 섹션이 필요한 것은 아닙니다. 순수 계산 로직 (pure calculation logic)은 이 섹션을 건너뛸 수 있습니다.
섹션 5 (계층 간 구조적 계약 (Cross-layer structural contract))
도우미 함수 (helper)와 DB 스키마 (DB schema), 다른 도우미 함수, 또는 다른 앵커 (anchors) 사이의 접점 (contact surface)입니다. 예를 들어, T-022 상태 문자열은 DB 내 packaging_jobs.tasks JSONB의 유효한 값과 일치해야 합니다. 이 섹션은 도우미 함수가 알고 있는 상태 집합이 DB의 기대치와 일치함을 고정합니다.
이 섹션은 "도우미 함수가 DB 스키마로부터 벗어나는 현상 (helper drifted from DB schema)"을 잡아냅니다.
테스트가 반드시 앵커를 임포트(import)해야 하는 이유
트레이스 테스트의 첫 번째 줄은 lib/business-rules/<feature>Logic에서 함수를 임포트합니다. 거버넌스 규칙 B (A2에서 다룸)는 모든 트레이스 테스트가 레지스트리 (registry)에 선언된 앵커 경로를 실제로 임포트하는지 확인합니다.
앵커를 임포트하지 않는 트레이스 테스트는 로직을 복사해서 붙여넣은 복사본을 테스트하는 것입니다. 앵커가 이동해도 테스트는 이를 인지하지 못하며, 테스트는 여전히 통과(green)되지만 트레이스는 부패하게 됩니다.
트레이스당 15개 이상의 케이스 목표
fulfillment-chain-fix 스프린트의 사례: T-019 / T-020 / T-022는 평균 3038개의 케이스를 가졌습니다. 섹션 1에서 최소 1개의 고정 (pinning) 케이스를 포함하고, 나머지 섹션에서 각각 35개의 케이스를 포함합니다.
15개 미만인 경우는 대개 비즈니스 계약 (business contract)이 충분히 명확하지 않음을 의미합니다. 감사 맵 (audit map)으로 돌아가서 정교화하십시오.
피스 3 (거버넌스 규칙 (Governance rule)): 코드 템플릿
각 차이점 (gap)은 scripts/governance-guard.mjs에 거버넌스 규칙을 가집니다. pre-push hook과 GitHub Actions 모두 이를 실행합니다.
M5 (Cancel/Refund Helper Coverage)의 축소 버전:
// scripts/governance-guard.mjs
// M5 Cancel/Refund Helper Coverage (2026-05-25, T-019)
//
...
설계 포인트 (Design points)
헤더 독스트링 (Header docstring)에는 반드시 3가지가 포함되어야 합니다: 생성 날짜 + 해당 트레이스 ID (trace ID) + 동기 (motivation). 동기는 "CI를 통과하기 위해"가 아닌, 비즈니스적 동기여야 합니다.
RPC_PATTERN에 논리합 정규식 (disjunction regex) 사용: src.includes('fn_a') || src.includes('fn_b') 대신 여러 RPC 이름을 |로 결합하세요. 더 깔끔하고 빠릅니다.
__tests__ / dist 건너뛰기: 트레이스 테스트 자체도 헬퍼 (helper)를 임포트(import)하고 RPC 이름을 언급하므로, 건너뛰지 않으면 오탐 (false-positive)이 발생합니다. dist는 빌드 결과물이며, 스캔할 필요가 없는 낭비입니다.
hasHelperImport를 느슨하게 작성: 호출자 (caller)가 모듈 전체를 임포트하거나 (import * as logic), 구조 분해 할당을 사용할 수 있습니다 (import { calculateRefundAllocation }). 논리합 (disjunction)을 사용하면 두 경우를 모두 매칭할 수 있습니다.
@cancel-refund-ok는 \S{5,}를 만족해야 함: 빈 사유는 금지됩니다 (아래에서 함정으로 다룸).
차단 (BLOCKER) vs 권고 (advisory)
스프린트 경험상: 트레이스 잠금 (trace-lock) 관련 규칙은 모두 차단 (BLOCKER) 항목입니다 (pre-push 시 exit 1을 반환하며 푸시를 차단함). 이유는 다음과 같습니다:
- 트레이스는 설계상 "가급적 안정적인" 관계여야 합니다.
- 권고 (Advisory)는 강제되지 않음을 의미합니다. 3개월 뒤면 무시될 것입니다.
- 차단 (BLOCKER) 항목의 오탐은 예외 주석 (exemption comments)을 통해 처리됩니다 (비용이 낮음).
예외: 순수 텍스트 / 문서 / 전환 기간의 린트 (lint) 규칙은 권고 사항일 수 있습니다. 트레이스 잠금은 해당 카테고리에 속하지 않습니다.
파트 4 (호출자 예외 주석): 두 가지 함정
예외 주석은 거버넌스 규칙을 우회하는 정당한 탈출구 (escape hatch)입니다. 형식은 다음과 같습니다:
// useOrders.js
// @cancel-refund-ok: thin-wrapper-only — 이 컴포저블 (composable)은 RPC 결과를 UI로 전달만 합니다
export async function cancelOrder(orderId) {
...
예외는 소수여야 합니다. 주요 두 가지 카테고리는 다음과 같습니다:
- 씬 래퍼 (Thin wrapper): 호출자가 결과만 전달할 뿐, 비즈니스 로직이 없는 경우
- 독스트링 언급 (Docstring mention): 헬퍼 JSDoc이 RPC 이름을 참조하지만, 실제로 호출하지는 않는 경우
함정 A: \S{5,} 정규식과 중국어 텍스트
예외 정규식 @xxx-ok\s*:\s*\S{5,}은 콜론(:) 뒤에 5개의 연속된 공백이 아닌 문자(non-whitespace characters)를 요구합니다. "此 helper 只透傳"과 같은 중국어 텍스트는 첫 번째 문자 뒤에 공백이 있어, 정규식이 한 글자를 읽은 후 끊어지게 되어 매칭에 실패합니다.
수정 사항:
// 실패 — 중국어 문자 + 공백
// @cancel-refund-ok: 此 helper 只透傳
...
실제로 스프린트(sprint)에서는 대시(-)로 연결된 영어를 채택했습니다. 짧은 구문은 파싱(parse)이 쉽고, audit:all 출력이 잘리더라도 가독성이 유지되며, 정규식에서 모호함이 없습니다.
함정 B: RPC 이름 언급으로 인한 docstring 오탐(false-positive)
헬퍼(helper)의 JSDoc이 (IDE의 정의 이동 기능을 돕기 위해) 관련 RPC 이름을 언급할 수 있지만, 실제로 헬퍼가 해당 RPC를 호출하지는 않을 수 있습니다. 거버넌스(governance)의 RPC_PATTERN 정규식은 이러한 의미론적 차이를 구분하지 못해 오탐을 발생시킵니다.
예시:
/**
* cancelOrderLogic
*
...
수정: 헬퍼 파일 상단에 이유를 docstring-mention-only로 명시하여 예외 주석을 추가합니다. 거버넌스는 이 예외를 확인하고 건너뜁니다.
스프린트 통계: 4개의 갭(gap)에서 총 5개의 예외(thin wrapper × 3 + docstring-mention × 2)를 사용했으며, 이는 전체 호출자(caller)의 10% 미만입니다. 예외가 일반적인 관행이 되어서는 안 됩니다. 대부분의 호출자는 헬퍼를 임포트(import)해야 합니다.
5단계 (레지스트리 항목): 기존 형식과 일치시키기
文檔/data-source-registry.md의 ## Critical Traces 아래에 새로운 트레이스(trace) 블록을 추가합니다. 형식은 기존의 T-001(A2에서 다룸)과 일치하며, 스프린트에서 발견된 몇 가지 보충 사항이 포함됩니다:
### T-019: 주문 취소 / 환불 SSOT
- **Type**: data-flow trace
...
A2의 T-001과 비교하여 추가된 두 개의 필드
갭 수정(Gap-fix)으로 생성된 트레이스는 A2의 T-001 형식과 비교하여 두 개의 필드가 추가되었습니다.
Governance: 이 트레이스에 대한 거버넌스 규칙 ID입니다. 레지스트리의 ID M5 / M6 / M7 / M8은 governance-guard.mjs와 정확히 일치해야 합니다. 향후 감사(audit) 시 레지스트리를 스캔하여 어떤 규칙이 어떤 트레이스와 쌍을 이루는지 즉시 확인할 수 있습니다.
Frozen business contract (동결된 비즈니스 계약): 사용자가 즉석에서 고정한 결정 사항들을 불렛 리스트(bullet list)로 정리한 것입니다. 이는 A2의 5가지 아티팩트(artifacts)와 비교했을 때 가장 큰 부가가치를 제공합니다. 모든 동결된 결정 사항이 영구적으로 추적(trace)되기 때문입니다.
Trace nodes (트레이스 노드) 작성하기
RPC (DB 쓰기 측면) + 헬퍼 (순수 함수 SSOT) + 호출자 (프론트엔드 컴포저블 / 페이지)를 포함합니다. 순서는 대략 "DB → UI"를 따르지만, 실제로는 서로 뒤섞여 나타납니다. 이 필드는 주로 AI에게 어떤 파일이 이 트레이스(trace)에 속하는지를 알려주는 역할을 합니다. 순서의 엄격함은 A2의 T-001보다 덜 엄격합니다.
sql-only-trace 변형
갭(gap)에 프론트엔드 헬퍼가 없는 경우 (예: T-021 FIFO 소비와 같은 순수 DB 로직):
- **Type**: sql-only-trace
- **Trace test (SQL)**: [`scripts/integration-test/T021-fifo-consume-test.sql`](../path)
Type은 sql-only-trace가 됩니다. Trace test는 Trace test (SQL)가 됩니다. parseTraceRegistry는 두 마커를 모두 확인하고 임포트(import) 체크를 건너뜁니다 (이후 단계에서 처리됨).
6단계 (Iteration log): 5개 섹션 구조
이테레이션 로그(iteration log)는 "왜 이 갭(gap)이 이런 방식으로 수정되었는가"를 기록합니다. 3개월 후, 미래의 당신은 커밋 메시지(commit messages)가 보여줄 수 없는 맥락을 여기서 확인할 수 있습니다. 파일명 형식은 NN_Gap-N_<topic>.md이며, 文檔/iteration-logs/<sprint>/ 아래에 위치합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기