버그 진단하기 (Diagnosing Bugs)
어려운 버그를 위한 규율입니다. 명시적으로 정당화되는 경우에만 단계를 건너뛰세요.
코드베이스를 탐색할 때는 CONTEXT.md(존재하는 경우)를 읽어 관련 모듈의 명확한 멘탈 모델을 얻고, 작업 중인 영역의 ADR을 확인하세요.
편집 (Redact)
이 스킬은 명령어, 출력 및 캡처된 아티팩트를 보여주도록 요구합니다. 먼저 모든 비밀을 편집하세요: 해당 위치에 <REDACTED>라고 작성하세요. 환경 변수에 대해 루프를 구축하여 자격 증명이 표시되는 내용이 아닌 환경에 남아 있도록 하세요. 캡처된 아티팩트에는 인증 헤더가 포함됩니다: 신호를 전달하는 줄만 인용하세요.
편집된 출력만으로 버그를 진단하기에 충분하지 않다면, 그렇게 말하고 사용자에게 물어보세요.
1단계: 피드백 루프 구축 (Phase 1: Build a feedback loop)
이것이 이 스킬의 핵심입니다. 나머지는 모두 기계적인 작업입니다. 버그에 대한 빠듯한 통과/실패 신호(이 버그 에 대해 빨간색으로 표시되는 신호)가 있다면, 원인을 찾을 수 있을 것입니다; 이분법, 가설 테스트, 계측 모두 이 신호를 소비할 뿐입니다. 그러한 신호가 없다면, 아무리 코드를 들여다봐도 소용없습니다.
여기에 불균형적으로 노력을 쏟으세요. 적극적이 되세요. 창의적이 되세요. 포기하지 마세요.
대략 이 순서대로 신호를 구성하는 방법
- 실패하는 테스트 (Failing test) - 버그에 도달하는 어떤 지점(단위, 통합, E2E)에서든.
- Curl / HTTP 스크립트 - 실행 중인 개발 서버에 대해.
- CLI 호출 - 픽스처 입력으로, 표준 출력을 알려진 양호한 스냅샷과 비교.
- 헤드리스 브라우저 스크립트 (Headless browser script) (Playwright / Puppeteer) - UI를 구동하고 DOM/콘솔/네트워크를 어설션.
- 캡처된 트레이스 재생 (Replay a captured trace) - 실제 네트워크 요청/페이로드/이벤트 로그를 디스크에 저장하고, 격리된 코드 경로를 통해 재생.
- 일회용 하네스 (Throwaway harness) - 버그 코드 경로를 단일 함수 호출로 실행하는 최소한의 시스템 하위 집합(하나의 서비스, 모의된 종속성)을 가동.
- 속성/퍼즈 루프 (Property / fuzz loop) - 버그가 "때때로 잘못된 출력"인 경우, 1000개의 무작위 입력을 실행하고 실패 모드를 찾음.
- 이분법 하네스 (Bisection harness) - 버그가 두 개의 알려진 상태(커밋, 데이터셋, 버전) 사이에 나타난 경우, "상태 X에서 부팅, 확인, 반복"을 자동화하여
git bisect run으로 실행할 수 있도록 함. - 차등 루프 (Differential loop) - 동일한 입력을 이전 버전과 새 버전(또는 두 구성)에 대해 실행하고 출력을 비교.
- HITL 배시 스크립트 (HITL bash script) - 최후의 수단. 사람이 클릭해야 하는 경우,
scripts/hitl-loop.template.sh로 그들을 이끌어서 루프가 구조화되도록 함. 캡처된 출력은 다시 당신에게 피드백됨.
올바른 피드백 루프를 구축하면 버그는 90% 해결된 것입니다.
루프를 더 빠듯하게 만들기 (Tighten the loop)
루프를 제품처럼 대하세요. 일단 하나의 루프가 생기면, 더 빠듯하게 만드세요:
- 더 빠르게 만들 수 있나요? (설정 캐싱, 관련 없는 초기화 건너뛰기, 테스트 범위 좁히기)
- 신호를 더 선명하게 만들 수 있나요? ("충돌하지 않음" 대신 특정 증상을 어설션)
- 더 결정론적으로 만들 수 있나요? (시간 고정, RNG 시드 고정, 파일 시스템 격리, 네트워크 고정)
30초짜리 불안정한 루프는 루프가 없는 것보다 거의 나을 것이 없습니다; 2초짜리 결정론적 루프는 빠듯하며, 디버깅의 슈퍼 파워입니다.
비결정론적 버그 (Non-deterministic bugs)
목표는 깔끔한 재현이 아니라 더 높은 재현율입니다. 트리거를 100번 반복하고, 병렬화하고, 스트레스를 가하고, 타이밍 윈도우를 좁히고, 슬립을 주입하세요. 50% 확률로 발생하는 버그는 디버깅 가능합니다; 1%는 그렇지 않습니다. 따라서 디버깅 가능해질 때까지 재현율을 계속 높이세요.
루프를 정말로 만들 수 없을 때 (When you genuinely cannot build a loop)
멈추고 명시적으로 그렇게 말하세요. 시도한 것을 나열하세요. 사용자에게 다음을 요청하세요: (a) 버그가 재현되는 환경에 대한 접근 권한, (b) 편집된 캡처 아티팩트(HAR 파일, 로그 덤프, 코어 덤프, 타임스탬프가 있는 화면 녹화), 또는 (c) 임시 프로덕션 계측을 추가할 수 있는 권한. 루프 없이 가설을 세우는 것을 진행하지 마세요.
완료 기준: 빨간색으로 표시되는 빠듯한 루프 (Completion criterion: a tight loop that goes red)
1단계는 루프가 빠듯하고 빨간색 표시가 가능할 때 완료됩니다: 당신이 이미 최소한 한 번 실행한 (호출과 그 출력을 보여주고, 편집된) 하나의 명령어(스크립트 경로, 테스트 호출, curl)를 지목할 수 있어야 하며, 다음 조건을 만족해야 합니다:
- 빨간색 표시 가능 (Red-capable): 실제 버그 코드 경로를 구동하고 사용자의 정확한 증상을 어설션하여, 이 버그에 대해 빨간색이 되고 수정되면 초록색이 될 수 있습니다. "에러 없이 실행됨"이 아닙니다; 이 특정 버그를 잡을 수 있어야 합니다.
- 결정론적 (Deterministic): 실행할 때마다 동일한 판결(불안정한 버그의 경우: 고정된 높은 재현율, 위 참조).
- 빠름 (Fast): 분 단위가 아닌 초 단위.
- 에이전트 실행 가능 (Agent-runnable): 무인으로 실행할 수 있습니다; 사람의 개입은
scripts/hitl-loop.template.sh를 통해서만 가능.
이 명령어가 존재하기 전에 코드를 읽으며 이론을 세우고 있다면, 멈추세요: 가설로 바로 뛰어드는 것은 이 스킬이 방지하고자 하는 정확한 실패입니다. 빨간색 표시 가능 명령어가 없으면 2단계도 없습니다.
2단계: 재현 및 최소화 (Phase 2: Reproduce + minimise)
루프를 실행하세요. 버그가 나타나면서 빨간색으로 변하는 것을 지켜보세요.
확인:
- 루프가 사용자가 설명한 실패 모드를 생성하는지, 근처에 있는 다른 실패가 아닌지. 잘못된 버그 = 잘못된 수정.
- 실패가 여러 번 실행에 걸쳐 재현 가능한지(또는 비결정론적 버그의 경우, 디버깅할 수 있을 만큼 높은 비율로 재현 가능한지).
- 정확한 증상(오류 메시지, 잘못된 출력, 느린 타이밍)을 캡처하여 이후 단계에서 수정이 실제로 문제를 해결하는지 확인할 수 있는지.
최소화 (Minimise)
빨간색이 확인되면, 재현을 여전히 빨간색인 가장 작은 시나리오로 축소하세요. 한 번에 하나씩 입력, 호출자, 구성, 데이터 및 단계를 잘라내고, 각 잘라내기 후에 루프를 다시 실행하고, 실패에 영향을 미치는 것만 유지하세요.
왜 해야 하는가: 최소한의 재현은 3단계에서 가설 공간을 줄이고(의심할 부분이 적어짐) 5단계에서 깔끔한 회귀 테스트가 됩니다.
남아 있는 모든 요소가 영향을 미칠 때 완료됩니다: 그중 하나라도 제거하면 루프가 초록색으로 변합니다.
재현 및 최소화를 완료할 때까지 진행하지 마세요.
3단계: 가설 수립 (Phase 3: Hypothesise)
가설을 테스트하기 전에 3~5개의 순위가 매겨진 가설을 생성하세요. 단일 가설 생성은 첫 번째 그럴듯한 아이디어에 고정됩니다.
각 가설은 반증 가능해야 합니다: 그것이 만드는 예측을 명시하세요.
형식: "만약 가 원인이라면, <Y를 변경>하면 버그가 사라질 것입니다 / <Z를 변경>하면 악화될 것입니다."
예측을 명시할 수 없다면, 가설은 그냥 느낌일 뿐입니다: 버리거나 다듬으세요.
테스트하기 전에 순위가 매겨진 목록을 사용자에게 보여주세요. 그들은 종종 즉시 순위를 다시 매기는 도메인 지식("우리가 방금 #3에 변경을 배포했습니다")을 가지고 있거나 이미 배제한 가설을 알고 있습니다. 저렴한 체크포인트, 큰 시간 절약. 사용자가 자리 비움 시 순위를 계속 진행하세요.
4단계: 계측 (Phase 4: Instrument)
각 프로브는 3단계의 특정 예측에 매핑되어야 합니다. 한 번에 하나의 변수만 변경하세요.
도구 선호도:
- 디버거 / REPL 검사 - 환경이 지원하는 경우. 중단점 하나가 로그 열 개보다 낫습니다.
- 타겟 로그 - 가설을 구분하는 경계에서.
- "모든 것을 로깅하고 grep"은 절대 하지 마세요.
모든 디버그 로그에 고유한 접두사를 태그하세요(예: [DEBUG-a4f2]). 마지막 정리는 단일 grep이 됩니다. 태그가 없는 로그는 유지되고, 태그가 있는 로그는 제거됩니다.
성능 브랜치 (Perf branch). 성능 회귀의 경우 로그는 일반적으로 틀립니다. 대신: 기준 측정(타이밍 하네스, performance.now(), 프로파일러, 쿼리 플랜)을 설정한 다음 이분법을 수행하세요. 먼저 측정하고, 그 다음에 수정하세요.
5단계: 수정 및 회귀 테스트 (Phase 5: Fix + regression test)
수정 전에 회귀 테스트를 작성하되, 이를 위한 올바른 지점이 있는 경우에만 작성하세요.
올바른 지점은 테스트가 호출 사이트에서 발생하는 실제 버그 패턴을 실행하는 지점입니다. 사용 가능한 유일한 지점이 너무 얕은 경우(버그에 여러 호출자가 필요할 때 단일 호출자 테스트, 버그를 트리거한 체인을 복제할 수 없는 단위 테스트), 그곳의 회귀 테스트는 잘못된 확신을 줍니다.
올바른 지점이 없다면, 이것 자체가 발견입니다. 그것을 기록하세요. 코드베이스 아키텍처가 버그를 고정시키는 것을 방해하고 있습니다. 다음 단계를 위해 플래그를 지정하세요.
올바른 지점이 있다면:
- 최소화된 재현을 해당 지점에서 실패하는 테스트로 전환하세요.
- 테스트가 실패하는 것을 확인하세요.
- 수정을 적용하세요.
- 테스트가 통과하는 것을 확인하세요.
- 원래(최소화되지 않은) 시나리오에 대해 1단계 피드백 루프를 다시 실행하세요.
6단계: 정리 (Phase 6: Cleanup)
완료를 선언하기 전에 필수:
- 원래 재현이 더 이상 재현되지 않음(1단계 루프 재실행)
- 회귀 테스트 통과(또는 지점 부재가 문서화됨)
- 모든
[DEBUG-...]계측 제거됨(접두사grep) - 일회용 프로토타입 삭제됨(또는 명확하게 표시된 디버그 위치로 이동됨)
- 올바른 것으로 판명된 가설이 커밋/PR 메시지에 명시되어, 다음 디버거가 배울 수 있도록 함