Debugging Guide
디버깅 접근법
과학적 디버깅
1. 문제 정의: 무엇이 잘못되었는가?
2. 가설 수립: 원인이 무엇일 수 있는가?
3. 실험 설계: 가설을 어떻게 검증할 것인가?
4. 실험 실행: 테스트 수행
5. 결과 분석: 가설이 맞았는가?
6. 반복 또는 수정
이분 탐색 디버깅
# 문제 범위를 절반씩 좁혀가기
# 1. 중간 지점에 로그 추가
function processOrder(order):
validate(order)
print("DEBUG: after validate") # 여기까지 왔나?
calculate(order)
print("DEBUG: after calculate")
save(order)
print("DEBUG: after save")
# 2. 문제 지점 발견 후 해당 함수 내부로 이동
# 3. 반복
고무 오리 디버깅
문제를 말로 설명하면서 해결책 발견
1. 문제를 처음부터 설명한다
2. 코드가 무엇을 해야 하는지 설명한다
3. 코드가 실제로 무엇을 하는지 설명한다
4. 차이점을 발견한다
에러 유형별 접근
Null/Undefined 에러
# 증상
TypeError: Cannot read property 'x' of undefined
# 디버깅
1. 스택 트레이스에서 라인 확인
2. 해당 변수가 어디서 오는지 추적
3. 경계 조건 확인 (빈 배열, null 응답 등)
# 방어적 코딩
user = getUser(id)
if not user:
throw UserNotFoundError(id)
name = user?.profile?.name ?? "Unknown"
비동기 에러
# 증상
- 순서가 예상과 다름
- 가끔만 발생
- 콜백이 실행되지 않음
# 디버깅
async function fetchData():
print("1. 시작")
response = await fetch(url)
print("2. 응답 받음")
data = await response.json()
print("3. 파싱 완료")
return data
# 일반적인 실수
# Bad: await 누락
data = fetchData() # Promise 객체가 반환됨
# Good
data = await fetchData()
상태 관련 버그
# 증상
- 특정 순서로만 발생
- 재현이 어려움
- "새로고침하면 됨"
# 디버깅
1. 상태 변경 지점 로깅
function setState(newState):
print("State changed:", oldState, "→", newState)
print("Call stack:", getStackTrace())
2. 타임라인 만들기
[0ms] 초기 상태: {count: 0}
[100ms] 버튼 클릭 → {count: 1}
[150ms] API 응답 → {count: 0} # 문제!
3. 불변성 확인
# Bad: 직접 수정
state.items.push(newItem)
# Good: 새 객체 생성
state = {...state, items: [...state.items, newItem]}
성능 문제
# 증상
- 느림
- 메모리 사용량 증가
- CPU 사용량 높음
# 디버깅 도구
- 프로파일러
- 메모리 스냅샷
- 네트워크 탭
# 측정 코드
start = performance.now()
expensiveOperation()
duration = performance.now() - start
print(f"Operation took {duration}ms")
로깅 베스트 프랙티스
로그 레벨
DEBUG # 개발 중 상세 정보
INFO # 정상 동작 기록
WARN # 잠재적 문제
ERROR # 실제 에러
FATAL # 시스템 중단 에러
# 예시
logger.debug("Processing item", {itemId, step: 1})
logger.info("Order completed", {orderId, total})
logger.warn("Retry attempt", {attempt: 3, maxAttempts: 5})
logger.error("Payment failed", {orderId, error})
구조화된 로깅
# Bad: 문자열 연결
print("User " + userId + " purchased " + productId)
# Good: 구조화된 로그
logger.info("Purchase completed", {
userId: userId,
productId: productId,
amount: amount,
timestamp: now(),
correlationId: requestId
})
컨텍스트 정보
# 요청 추적
function middleware(request, next):
requestId = generateId()
logger.setContext({requestId, userId: request.user?.id})
logger.info("Request started", {
method: request.method,
path: request.path
})
response = next(request)
logger.info("Request completed", {
status: response.status,
duration: getDuration()
})
return response
에러 추적
에러 경계
# 에러를 적절한 수준에서 처리
# Bad: 모든 에러 무시
try:
doSomething()
catch:
pass
# Good: 적절한 처리
try:
result = riskyOperation()
catch ValidationError as e:
logger.warn("Validation failed", {error: e})
return BadRequest(e.message)
catch NetworkError as e:
logger.error("Network error", {error: e})
raise ServiceUnavailable()
catch Exception as e:
logger.error("Unexpected error", {error: e})
alertOncall(e)
raise
에러 컨텍스트 추가
# Bad: 원본 에러만
throw error
# Good: 컨텍스트 추가
throw new Error("Failed to process order", {
cause: error,
context: {
orderId: orderId,
step: "payment",
attempt: retryCount
}
})
스택 트레이스 읽기
Error: Cannot read property 'name' of undefined
at getFullName (user.js:25:15) ← 실제 에러 위치
at formatUser (formatter.js:42:10) ← 호출한 곳
at processUsers (processor.js:18:5) ← 그 전 호출
at main (index.js:10:1) ← 시작점
읽는 방법:
1. 첫 줄: 에러 메시지
2. 두 번째 줄: 에러 발생 위치
3. 나머지: 호출 스택 (위에서 아래로 따라가기)
재현 가능한 버그 만들기
최소 재현 케이스
# 원래 코드: 1000줄
# 버그가 발생하는 최소 코드 찾기
# Step 1: 코드 절반 제거 → 버그 발생?
# Step 2: 반복
# Step 3: 최소 재현 케이스 도출
# 예시: 최소 재현
const data = null
data.toString() // TypeError
테스트로 재현
test "reproduces the bug":
# Given: bug condition
user = createUser(age=17)
# When: trigger bug
result = purchaseAlcohol(user)
# Then: expected behavior (currently fails)
assert result.error == "Age verification required"
디버깅 체크리스트
시작 전
진행 중
해결 후
관련 스킬
testing: 버그 재현 테스트 작성
code-quality: 에러 처리 원칙
performance: 성능 문제 디버깅