공격적 감사(Offensive audit) — 전체 비즈니스 흐름을 주기적으로 감사하는 방법 (독학 개발자를 위한 가이드)
요약
비즈니스 흐름 전반의 취약점을 선제적으로 찾아내는 '공격적 감사(Offensive audit)' 방법론을 소개합니다. 반복되는 버그를 임시방편으로 수정하는 대신, 전체 프로세스를 점검하여 보호되지 않은 노드를 식별하고 근본적으로 해결하는 가이드를 제공합니다.
핵심 포인트
- 반복되는 버그는 개별 수정보다 전체 흐름 감사가 우선되어야 함
- 테스트, 거버넌스 규칙, 비즈니스 계약의 존재 여부를 기준으로 노드 검토
- 보호되지 않은 체인 노드를 식별하여 잠재적 손실을 방지
- Claude를 활용한 비즈니스 흐름 스케치 및 감사 전략 수립
2026년 5월 · 시리즈 "Trace Lock — 코드를 작성하기 위해 AI와 페어링하며 얻은 거버넌스(Governance) 노트" · 9개 포스트 중 3번째 포스트
이전 포스트(A1 Defensive)에서는 "알려진 계층 간 관계를 잠그는 것"(방어적, 반응적)에 대해 다루었습니다. 이번 포스트는 그 반대 방향을 다룹니다: 전체 비즈니스 흐름 전반에 걸쳐 보호되지 않은 체인 노드(chain nodes)를 찾기 위해 선제적으로 감사(audit)를 수행한 다음, 이를 한꺼번에 수정하는 것(공격적, 선제적).
두 가지 사고 방식은 상호 보완적입니다: A1은 "발을 헛디딘 후에 잠그는 것"이라면, 이 포스트는 "불발탄을 찾기 위해 정기적으로 순찰하는 것"입니다.
이 포스트는 독학 개발자들을 위한 평이한 언어 버전입니다. 엔지니어링 세부 사항(6가지 수정 패턴 템플릿, 거버넌스 규칙 작성법, SQL 테스트 실행 방법 등)은 B2(엔지니어 대상)에서 다룹니다.
시작점: 버튼이 멈춘 고객의 스크린샷
5월 25일 정오. 고객의 스크린샷: "로스팅 스테이션의 '완료(complete)' 버튼이 멈췄고, '로스트 레벨 불일치(roast level mismatch)' 오류가 표시됩니다."
나는 30초 동안 화면을 응시했습니다. 평소처럼 수정해야 할까요? 코드 한 줄, RAISE 문을 주석 처리하면 끝입니다.
하지만 이것은 지난 6개월 동안 발생한 5번째 "계층 간 계약 드리프트(cross-layer contract drift)" 버그였습니다. 각각 35시간이 걸렸고, 총 1525시간이 소요되었습니다. 수정할 때마다 "이번이 마지막이어야 한다"고 생각했지만, 다음 달에 또 다른 버그가 나타났습니다.
나는 Claude에게 한 가지 질문을 던졌습니다:
"지난 6개월 동안 이런 버그를 5번 수정했습니다. 잠시 멈추고 **동일한 형태의 모든 누수(leaks)**를 찾아 한꺼번에 수정해야 할까요?"
Claude는 다음과 같이 제안했습니다: 먼저 전체 비즈니스 흐름을 감사하는 데 1시간을 할애하세요. 어떤 체인 노드들이 보호되지 않고 있는지 목록을 만드세요. 그런 다음 무엇을 먼저 수정할지 결정하세요.
1시간 동안의 감사가 실제로 수행한 일
Claude와 나는 "주문에서 배송까지"의 비즈니스 흐름을 스케치하며, 데이터에 영향을 미치는 고객 주문부터 배송된 패키지까지의 모든 단계인 11개의 체인 노드를 나열했습니다.
각 노드에 대해 나는 3가지 질문을 던졌습니다:
- 이 노드에 상응하는 테스트가 있는가? (노드가 고장 났을 때 감지될 수 있는가)
- 이 노드에 상응하는 거버넌스 규칙 (governance rule)이 있는가? (누군가 잘못 수정했을 때 감지될 수 있는가)
- 이 노드의 "비즈니스 계약 (business contract)"이 문서화되어 있는가? (왜 다른 로직이 아닌 이 로직이어야 하는가)
검토 결과:
- 🟢 완전히 보호됨: 5개 노드 (테스트 + 거버넌스 + 계약 모두 존재)
- 🟡 부분적으로 보호됨: 2개 노드 (한두 가지가 누락됨)
- 🔴 완전히 보호되지 않은 차단 요소 (BLOCKERs): 4개 노드
4개의 차단 요소 (BLOCKERs)는 다음과 같습니다:
- 주문 취소 / 환불 흐름 (현금 우선 환불 vs 보너스 우선 환불에 대한 명세 없음)
- 로스팅 완료 단계 ("로스팅 불일치 → 차단 또는 경고"에 대한 명세 없음)
- FIFO (선입선출) 원재료 소비 (동일한 커피의 어떤 봉투가 먼저 사용되는지에 대한 명세 없음)
- 패키징 스테이션 작업 상태 ("건너뜀 (skipped)"이 유효한 상태인지에 대한 명세 없음)
각각은 "다음 고객이 여기서 발을 헛디디면 나에게 3~5시간의 손실을 입힌다"라는 지뢰와 같았습니다.
의사결정 고정 (Decision Pinning) = 감사의 가장 중요한 의식
차단 요소들을 나열하면서, 놓치기 쉬운 한 가지를 발견했습니다. 이 차단 요소들이 차단 요소인 이유는 코드가 고장 나서가 아니라, "비즈니스가 어떻게 작동해야 하는지"가 문서화되어 있지 않기 때문입니다.
"환불을 현금으로 먼저 해야 하는가, 아니면 보너스로 먼저 해야 하는가"를 예로 들어보겠습니다:
- 내 머릿속에는 답이 있습니다 (현금 우선. 왜냐하면 원래 차감 시 보너스를 우선시했으므로, 현금 우선 취소가 균형을 유지하기 때문입니다)
- 하지만 이 답은 어디에도 적혀 있지 않습니다.
- 3개월 후, 나는 코드를 보고 "이거 이상한데, 정리 좀 해야겠다"라고 생각할 수도 있습니다.
- AI 페어 프로그래밍 도구는 이를 보고 자신의 직관에 따라 행동할 수도 있습니다.
따라서 감사의 첫 번째 결과물은 코드를 수정하는 것이 아닙니다. 그것은 바로 이 4가지 비즈니스 의사결정을 공식적으로 문서화하는 것입니다.
- 환불 지갑 주문 (Refund wallet order): 원래 공제액 $50 보너스 + $30 현금 → 환불 시 현금 $30를 먼저 채우고, 나머지는 보너스 $50를 채움
- 로스팅 완료 로스팅 불일치 (Roasting completion roast mismatch): RAISE 트리거 → 비고(notes) 컬럼에 경고(warning)로 재작성하고, 완료 프로세스 계속 진행 (운영 스테이션이 멈추는 것을 방지)
- FIFO 원재료 소비 (FIFO ingredient consumption):
created_at ASC순서대로 동일한 커피 소비; 재고 불변성 (inventory invariant): 소비 전 총량 - 소비 후 총량 = 소비량 - 포장 작업 상태 머신 (Packaging task machine): 순수하게 선형적인 pending → in_progress → completed/cancelled 구조, 건너뛰기(skipped) 또는 부분 완료(partial) 없음
일단 기록하고 나면, 이러한 결정들은 "나의 개인적인 선호"에서 "시스템 계약 (system contract)"으로 변모합니다. 이를 변경하려는 누구라도 (미래의 나, AI 포함) 이 결정들을 확인할 수 있게 됩니다.
나는 이 단계에 작업 명칭을 붙였습니다: "결정 고정 (Decision Pinning)" (가칭, 나만의 명칭).
6단계 수정 패턴: 모든 차단 요소(BLOCKER)는 동일한 레시피를 따릅니다
4가지 차단 요소(BLOCKER)를 수정할 때, 각각은 동일한 6단계 템플릿을 적용했습니다:
- 순수 함수 헬퍼 (Pure-function helper): 흩어져 있는 로직을 하나의 파일로 추출하고, 모든 호출자가 이를 통하도록 함
- 퓨즈 테스트 (Fuse test, 추적 테스트): 헬퍼의 현재 올바른 동작을 고정 (결정 고정(Decision Pinning)을 위한 사고 사례 고정 포함)
- 레지스트리 항목 (Registry entry): A1에서 언급된 레지스트리에 이 추적(trace)을 추가
- 거버넌스 규칙 (Governance rule): 헬퍼를 거치지 않는 호출자를 자동 스캔하여 커밋(commit)을 차단
- 호출자 예외 목록 (Caller exemption list): 헬퍼를 사용하지 않을 정당한 이유가 있는 파일들을 명시적으로 나열 (영구적인 오류 표시 방지)
- 반복 로그 (Iteration log): 무엇이 수정되었는지, 계약(contract)이 무엇인지, 다음 감사(audit)에서 놓치지 말아야 할 것이 무엇인지 기록
첫 번째 차단 요소를 해결하는 데는 템플릿에 익숙해지느라 2시간이 걸렸습니다. 두 번째는 1.5시간, 세 번째는 1시간, 네 번째는 45분이 걸렸습니다 (새로운 카테고리를 위한 한 가지 특별한 세부 사항 제외). 한계 비용(Marginal cost)이 반복할수록 감소합니다.
스프린트 요약 작성을 포함하여 4개의 BLOCKER(차단 요소)를 해결하는 데 총 7시간이 소요되었습니다. "한 번에 하나씩 수정하는" 모드(예상 1220시간)와 비교했을 때, **직접적인 시간 대비 ROI(투자 수익률)는 약 23배**입니다. 하지만 감사(audit) 경로를 통해 N개의 미래 버그를 예방할 수 있으므로, 제가 추산하는 동등 ROI는 5~8배입니다.
역검증 의식(Reverse verification ritual): 잠금 장치가 실제로 작동하는지 증명하기
각 BLOCKER를 수정한 후, 저는 한 가지를 수행했습니다. 도우미(helper)를 의도적으로 고장 내어 테스트가 실제로 실패(red)하는지 확인하고, 호출자 예외(caller exemption)를 제거하여 거버넌스 규칙이 실제로 커밋을 차단하는지 확인하는 것입니다.
중복처럼 들리나요? 이 과정 덕분에 "너무 느슨한 잠금 장치"를 작성하는 실수를 두 번이나 잡아낼 수 있었습니다.
- 한 번은 테스트를 깨뜨렸을 때 단 2개의 케이스만 실패했는데, 이는 많은 테스트가 분포 알고리즘(distribution algorithm)을 실행하지 않는 단일 입력을 사용했기 때문이었습니다.
- 또 다른 때는 거버넌스 예외를 제거했음에도 차단되지 않았는데, 예외 정규식(regex)이 너무 느슨하여 모든 파일이 통과되었기 때문이었습니다.
역검증(Reverse verification)은 잠금 장치를 위한 잠금 장치입니다. 역검증이 없는 보호는 "테스트는 하지만 실제로는 테스트하지 않는" 상태가 될 수 있으며, 이는 테스트가 없는 것보다 더 나쁩니다(잘못된 확신을 주기 때문입니다).
4개의 BLOCKER를 통해 배운 7가지 사항
4개를 모두 수정하는 과정에서 "다음 감사 시 밟지 말아야 할 것들"도 드러났습니다.
- 6개 조각 패턴(6-piece pattern)은 첫 번째에는 2시간, 두 번째에는 1.5시간, 네 번째에는 45분이 걸립니다.
- 순수 DB 로직은 자체적인 테스트 방법이 있습니다 (프론트엔드 단위 테스트 프레임워크가 아닌, Supabase CLI를 사용하여 SQL을 실행하십시오). 저는 여기에
sql-only-trace라는 카테고리 이름을 붙였습니다. - 역검증은 선택 사항이 아닌 필수 의식입니다.
- 중국어 정규식(Chinese regex)에는 함정이 있습니다 (
\S{5,}가 반드시 5개의 연속된 중국어를 매칭하는 것은 아닙니다). - 거버넌스 독스트링(Governance docstring) 스캔은 오탐(false-positive)이 발생하기 쉽습니다 (RPC 이름을 언급하는 도우미가 호출자로 잘못 분류될 수 있습니다).
- 컨텍스트 윈도우(Context window)는 능동적인 추정이 필요합니다 (각 BLOCKER 이후에 "내가 얼마나 많은 컨텍스트를 소모했는지" 스스로 추정해 보십시오).
- Postgres 함수 시그니처(function signature)를 변경할 때는 기본 NULL 매개변수를 추가하기 전에 이전 버전을 DROP(삭제)해야 합니다. 그렇지 않으면 오버로드 충돌(overload conflict)이 발생합니다.
저는 다음 비즈니스 흐름 감사(business-flow audit)에서 이를 직접 재사용할 수 있도록 이 7가지 사항을 스프린트 요약(sprint summary)에 작성했습니다.
전이 가능한 제안 (A transferable suggestion)
만약 당신이 AI와 함께 작업하는 1인 개발자(solo dev)라면:
- 가장 중요한 5가지 비즈니스 흐름을 나열하세요 (결제(checkout) / 주문(order) / 재고(inventory) / 회원(member) / 결제(payment)...)
- 한 달에 하나씩 선택하여, 1시간 동안 감사(audit)를 수행하세요 (11개 체인 노드 × 3가지 질문)
- 차단 요소(BLOCKERs)를 찾아내어 → 6단계 패턴(6-piece pattern)으로 수정하세요 (각 수정에는 1~2시간이 소요되며, 점차 빨라집니다)
- 모든 차단 요소(BLOCKER)에 대해 의사결정 고정(Decision Pinning)을 수행하세요 (이것이 코드 수정이 아닌, 감사의 진정한 핵심입니다)
- 매년, 핵심 비즈니스 흐름에 대해 전체 감사(full audit pass)를 한 번 실시하세요
다음 버그가 나타날 때까지 기다리지 마세요. 버그가 나타날 때쯤이면 고객은 이미 그 버그에 걸려 넘어졌을 것입니다. 감사는 고객이 발견하기 전에 문제를 포착합니다.
왜 A1(방어적) + B1(공격적)을 함께 사용해야 하는가
A1(알려진 흔적에 대한 반응적 잠금(reactive lock-down))만 사용하면, 보호 조치는 항상 버그보다 뒤처지게 됩니다. 기껏해야 본전치기일 뿐입니다.
B1(선제적 감사(proactive audit))만 사용하면, 찾아낸 차단 요소(BLOCKERs)가 흔적 잠금(trace locks)으로 전환되지 않을 경우 부패하게 됩니다. 다음 달에 다시 나타나게 됩니다.
함께 사용할 경우:
- B1이 보호되지 않은 체인 노드(chain nodes)를 찾아냅니다.
- A1이 이를 잠급니다(locks them down).
- 다음 라운드의 B1이 새로운 대상들을 찾아냅니다.
이것이 C1 '공격 + 방어 결합(Offense + Defense combined)'에서 다루는 "이중 칼날(dual-blade)" 접근 방식입니다.
관련 포스트
- A1 · 방어적 흔적 잠금(Defensive Trace Lock) — 계층 간 관계를 잠그는 방법 (시리즈 이전 글)
- C1 · 공격 + 방어 결합(Offense + Defense combined) — 두 방식이 짝을 이루는 방법 (시리즈 다음 글)
- 中文版
이 포스트에 대하여
이 포스트는 2026년 5월 동안 제가 Claude(AI 페어 프로그래밍 도구)와 나누었던 대화들을 정리한 기록입니다. 저는 나중에 참고할 가치가 있는 몇 가지 패턴을 발견했고, Claude에게 이를 글로 구조화하도록 요청했습니다.
제가 주장하지 않는 몇 가지 사항은 다음과 같습니다:
- 이 포스트에서 사용된 용어들(공격적 감사 (Offensive audit) / 결정 핀닝 (Decision Pinning) / 6단계 수정 패턴 (6-piece fix pattern) / sql-only-trace)은 제가 직접 붙인 임시 명칭이며, 업계 표준 용어가 아닙니다.
- 저의 시스템은 특정한 형태(1인 유지보수, 많은 계층 간 의존성, 모호한 비즈니스 계약)를 가지고 있습니다. 이러한 패턴은 귀하의 상황에는 적용되지 않을 수 있습니다.
- 저는 소프트웨어 엔지니어가 아니라, AI와 협업하여 코드를 작성하는 바리스타일 뿐입니다.
만약 전문 엔지니어께서 오용 사례를 발견하시거나, 이 개념들 중 이미 더 표준적인 명칭이 있다면, 진심으로 수정을 환영합니다.
본문은 제 블로그에 게재되었습니다: Offensive audit — How to periodically audit a whole business flow (for self-taught developers)
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기