Installs into .claude/skills of the current project.
Are you the author of Debugging?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/insajin-debugging)
---
name: debugging
description: 체계적 디버깅 및 버그 수정 스킬
compatibility: omp
---
# Debugging Skill
버그를 체계적으로 재현하고 근본 원인을 분석하여 수정하는 스킬입니다.
## 핵심 원칙
**이 버그에 red가 되는 피드백 루프를 먼저 만든다.** 루프가 있으면 이분법·가설 검증·계측이
그것을 소비해 원인에 닿는다. 루프가 없으면 코드를 아무리 들여다봐도 못 찾는다. 루프의 대표
형태가 재현 테스트다: 수정 전 FAIL, 수정 후 PASS.
탐색할 때 `CONTEXT.md`(있으면)로 관련 모듈의 어휘를 잡고, 그 영역의 Lore(`auto why`)를 확인한다.
둘 다 없으면 대상 상수·설정의 doc comment와 Makefile/CI 주석을 Lore로 읽는다 — "예산을 키우는
방식은 세 번 실패했다" 같은 한 줄이 가설 순위를 바꾼다.
**비밀은 먼저 가린다.** 명령·출력·캡처를 보여 줄 때 credential은 `<REDACTED>`로 바꾼다. 루프는
env var를 읽게 만들어 값이 출력에 나오지 않게 한다. 가린 출력으로 진단이 안 되면 그렇다고 말하고
사용자에게 묻는다.
## 디버깅 프로세스
### 1단계: 피드백 루프 만들기
**이 단계가 스킬의 전부다.** 나머지는 기계적이다. 여기에 불균형하게 많은 노력을 쓴다.
공격적으로, 창의적으로, 포기하지 않는다.
루프를 만드는 방법(대략 이 순서로):
1. 버그에 닿는 seam에서의 **실패 테스트** — unit, integration, e2e
2. 개발 서버에 대한 **curl/HTTP 스크립트**
3. fixture 입력으로 **CLI 호출**, stdout을 known-good 스냅샷과 diff
4. UI를 몰고 DOM/console/network를 단언하는 **headless browser 스크립트**
5. 실제 요청/페이로드/이벤트 로그를 저장해 격리된 코드 경로로 **replay**
6. 시스템 최소 부분집합만 띄우는 **throwaway harness**
7. **이미 있는 flaky 테스트**라면 바이너리를 한 번 빌드(`go test -c`)하고 K개를 동시에
`-count=N`으로 돌려 부하를 재현한다. 라운드마다 바이너리/서브테스트 red 수를 표로 집계한다
8. "가끔 틀린다"면 무작위 입력 1000개로 **property/fuzz 루프**
9. 두 상태(커밋, 데이터셋, 버전) 사이에서 생겼다면 `git bisect run`이 가능한 **bisection harness**
10. 구버전 vs 신버전, 두 설정, 또는 **런타임을 뺀 같은 메커니즘**(셸 스크립트만으로 exec 비용
재현 등)에 같은 입력을 넣고 diff하는 **differential 루프**
11. 최후 수단: 사람이 클릭해야 하면 사람을 몰아가는 **HITL 셸 스크립트**
**루프를 조인다.** 하나 생기면 제품처럼 다듬는다: 더 빠르게(setup 캐시, 무관한 init 생략,
범위 축소), 더 날카롭게("죽지 않았다"가 아니라 정확한 증상을 단언), 더 결정적으로(시간 고정,
RNG seed, 파일시스템 격리, 네트워크 동결). 30초짜리 flaky 루프는 없는 것과 비슷하고, 2초짜리
결정적 루프는 **tight**하다.
**비결정적 버그**는 깨끗한 재현이 아니라 **재현율 상승**이 목표다. 트리거를 100번 반복,
병렬화, 스트레스, 타이밍 창 좁히기, sleep 주입. 50% 재현은 디버깅 가능하고 1%는 불가능하다.
코드를 못 고치는 단계에서는 **무수정 관찰**로 스톨 위치를 잡는다: 루프가 도는 동안 `ps`로
프로세스 상태·CPU를 샘플링하고, 시스템 로그와 데몬 부하(macOS의 XProtect/fseventsd 등)를 본다.
**루프를 정말 못 만들면** 멈추고 그렇다고 말한다. 시도한 것을 나열하고 (a) 재현 환경 접근,
(b) 가린 캡처(HAR, 로그 덤프, 코어 덤프, 타임스탬프 있는 화면 녹화), (c) 임시 운영 계측 허가
중 하나를 요청한다. 루프 없이 가설로 넘어가지 않는다.
**완료 기준**: 이미 최소 한 번 실행한(호출과 가린 출력을 보여 준) **명령 하나**를 댈 수 있고,
그 명령이
- [ ] **red-capable**: 실제 버그 코드 경로를 몰고 사용자의 정확한 증상을 단언한다
- [ ] **deterministic**: 매번 같은 판정. flaky면 **라운드 단위 ≥90% red**를 기준으로 삼고
바이너리/서브테스트 단위 재현율도 함께 적는다(단위마다 다르므로 하나만 쓰면 판정이 갈린다)
- [ ] **fast**: 분이 아니라 초
- [ ] **agent-runnable**: 무인 실행 가능
seam(어느 테스트·어느 단언)을 고르기 위해 테스트와 대상 코드를 읽는 것은 필요하다. 그러나
이 명령이 생기기 전에 **원인 이론**을 세우고 있다면 **멈춘다** — 가설로 바로 뛰는 것이 이 스킬이
막는 실패다.
### 2단계: 재현 + 최소화
루프를 돌려 red를 본다. 사용자가 말한 그 실패인지(근처의 다른 실패가 아닌지), 여러 번 재현되는지,
정확한 증상(에러 메시지, 잘못된 출력, 느린 시간)을 캡처했는지 확인한다.
red가 되면 **red가 유지되는 가장 작은 시나리오**로 줄인다. 입력·호출자·설정·데이터·단계를
하나씩 잘라내고 매번 루프를 다시 돌려, 실패에 필요한 것만 남긴다. 부하 의존 flake의 "입력"은
환경 축이다: 병렬도 K, `-race`, 파일 신선도, OS 상태를 하나씩 빼 본다. 남은 요소를 하나라도 빼면
green이 될 때 끝이다. 최소 재현은 3단계의 가설 공간을 줄이고 5단계의 회귀 테스트가 된다.
```go
// 버그 재현 테스트 — 수정 전 FAIL, 수정 후 PASS
func TestBug_IssueNumber_ReproductionTest(t *testing.T) {
// Given: 버그 발생 조건
// When: 버그 트리거 동작
// Then: 기대한 동작 (현재는 실패)
}
```
### 3단계: 가설
테스트하기 전에 **순위 매긴 가설 3~5개**를 만든다. 하나만 만들면 처음 그럴듯한 생각에 닻을
내린다. 각 가설은 **반증 가능**해야 한다 — 예측을 말한다:
> "X가 원인이면 Y를 바꿨을 때 버그가 사라진다 / Z를 바꾸면 더 나빠진다."
예측을 말할 수 없으면 가설이 아니라 감이다. 버리거나 벼린다. 가설마다 **이미 관측된 찬/반
증거**를 적어, 반증된 가설과 미검증 가설이 같은 모양으로 보이지 않게 한다. **순위 목록을 사용자에게 먼저
보여 준다** — 도메인 지식으로 즉시 재정렬되는 일이 많다. 사용자가 자리에 없으면 자기 순위로
진행한다.
### 4단계: 계측
각 probe는 3단계의 특정 예측에 대응한다. **한 번에 변수 하나만** 바꾼다.
도구 우선순위: (1) 디버거/REPL — breakpoint 하나가 로그 열 개보다 낫다, (2) 가설을 가르는
경계에만 두는 targeted log, (3) "전부 찍고 grep"은 하지 않는다.
**모든 디버그 로그에 고유 접두어를 붙인다**(예: `[DEBUG-a4f2]`). 정리가 grep 한 번이 된다.
태그 없는 로그는 살아남고 태그된 로그는 죽는다.
**성능 회귀**에는 로그가 대개 틀린 도구다. 기준 측정(타이밍 harness, profiler, query plan)을
먼저 만들고 bisect한다. 먼저 재고, 그다음 고친다.
도구 활용:
```bash
# Go: 레이스 컨디션 탐지
go test -race ./...
# 스택 트레이스 분석
go test -v -run TestBug 2>&1
# pprof 프로파일링
go test -cpuprofile cpu.out -memprofile mem.out
```
근본 원인 분류:
- **로직 오류**: 조건문, 계산 실수
- **경계 조건**: 빈 입력, 최대값, 최소값
- **동시성**: 레이스 컨디션, 데드락
- **타입/변환**: 오버플로우, nil 포인터
- **외부 의존성**: API 변경, 설정 오류
Caller/shared root-cause rule:
- 증상 위치(symptom location), owning function/path, caller 목록 또는 grep evidence를 기록합니다.
- caller와 shared root-cause path를 먼저 확인한 뒤 패치 위치를 선택합니다.
- 증상 위치만 패치하고 caller/shared root-cause evidence가 없으면 patch plan을 `revise-target`으로 표시합니다.
- evidence가 증상 위치 자체가 root cause이거나 affected caller가 하나뿐임을 보여줄 때만 focused patch를 허용합니다.
### 5단계: 수정 + 회귀 테스트
회귀 테스트를 **수정 전에** 쓴다 — 단, **올바른 seam**이 있을 때만. 올바른 seam은 버그가
호출 지점에서 실제로 일어나는 패턴을 그대로 밟는 자리다. 쓸 수 있는 seam이 너무 얕으면
(여러 호출자가 필요한 버그에 단일 호출자 테스트, 트리거 체인을 재현 못 하는 unit test) 그 자리의
회귀 테스트는 거짓 확신이다. **올바른 seam이 없다는 것 자체가 발견이다.** 기록하고 다음 단계로
넘긴다 — 코드 구조가 버그를 잠그지 못하게 막고 있다.
seam이 있으면: 최소 재현을 그 seam의 실패 테스트로 만든다 → 실패를 본다 → 수정 → 통과를
본다 → 1단계 루프를 원래(최소화 전) 시나리오로 다시 돌린다.
```
수정 원칙:
- 버그 수정에만 집중 (리팩토링 금지)
- 가능한 한 작은 변경
- 사이드 이펙트 최소화
```
### 6단계: 검증과 정리
```bash
# 재현 테스트 통과 확인
go test -run TestBug -v
# 회귀 테스트 실행
go test ./...
# 레이스 컨디션 재확인
go test -race ./...
```
## 체크리스트
- [ ] tight하고 red-capable한 피드백 루프 명령 하나를 실행해 보였다
- [ ] 사용자의 증상 그대로 재현하고 최소화했다
- [ ] 반증 가능한 가설 3~5개를 순위 매겨 사용자에게 보였다
- [ ] caller/shared root-cause path 확인 또는 `revise-target` 기록
- [ ] 회귀 테스트 작성(또는 올바른 seam 부재 기록) 및 수정 전 FAIL 확인
- [ ] 최소 수정 적용, 회귀 테스트 PASS, 원래 시나리오 루프 green
- [ ] `[DEBUG-...]` 계측 전부 제거(`grep` 접두어), throwaway harness 삭제
- [ ] 전체 테스트 스위트 통과
- [ ] 커밋 메시지에 이슈 번호와 **맞은 것으로 판명된 가설**을 남겨 다음 사람이 배우게 한다
1단계 루프 구성·조이기와 3~6단계 규율은 mattpocock/skills `diagnosing-bugs`(MIT)에서 가져와 다듬었다.