- Published on
언어별 디버깅 실전 가이드: Python · JavaScript/TypeScript · Go · Java에서 브레이크포인트, 실행, 프로파일링까지
- Authors

- Name
- Youngju Kim
- @fjvbn20031
이 글은 디버깅 실전 시리즈 5편 중 1편이다.
왜 언어별 디버깅 전략이 달라야 할까
디버깅의 본질은 같지만(재현 → 관찰 → 가설 → 검증), 언어마다 런타임/메모리 모델/툴 체인이 달라서 접근이 달라진다.
- Python: 동적 타입 + 인터프리터 중심. 런타임에 거의 모든 것을 들여다볼 수 있지만, 타입 관련 버그가 런타임까지 숨어 있다.
- JavaScript/TypeScript: 이벤트 루프/비동기 콜스택. Promise 체인에서 에러 원인이 사라지기 쉽다.
- Go: 고루틴/채널/간결한 런타임. 동시성 버그(race condition, 데드락)가 주요 난관이다.
- Java: JVM 기반, 프로파일링 생태계 성숙. 메모리 누수와 GC 튜닝이 주요 과제다.
아래는 실무에서 바로 쓰는 방식만 압축해서 정리했다.
언어별 디버깅 도구 비교
| 항목 | Python | JavaScript/TS | Go | Java |
|---|---|---|---|---|
| 기본 디버거 | pdb / breakpoint() | Chrome DevTools / --inspect | Delve (dlv) | JDWP (jdb) / IDE 내장 |
| 프로파일러 | cProfile, py-spy | --cpu-prof, Chrome Perf | pprof | JFR, async-profiler |
| 메모리 분석 | memory-profiler, objgraph | --heapsnapshot, Chrome Memory | pprof heap | VisualVM, Eclipse MAT |
| 비동기 추적 | asyncio debug mode | Async Stack Traces (DevTools) | goroutine dump, GODEBUG | Thread dump, Virtual Threads |
| 정적 분석 | mypy, pylint | tsc --strict, ESLint | go vet, staticcheck | SpotBugs, Error Prone |
언어별 흔한 버그 패턴과 진단법
| 언어 | 흔한 버그 패턴 | 진단 방법 | 예방 전략 |
|---|---|---|---|
| Python | TypeError/AttributeError (동적 타입), N+1 쿼리 | breakpoint()로 타입 확인, django-debug-toolbar | 타입 힌트 + mypy strict, select_related |
| JS/TS | Unhandled Promise rejection, 메모리 누수 (클로저) | async stack trace, heap snapshot 비교 | eslint-plugin-promise, WeakRef 활용 |
| Go | race condition, goroutine 누수, 채널 데드락 | -race 플래그, pprof goroutine, stack dump | go vet, context 기반 취소, errgroup |
| Java | NPE, OOM (메모리 누수), 스레드 데드락 | exception breakpoint, heap dump 분석, jstack | Optional 활용, try-with-resources, Virtual Threads |
1) Python 디버깅
실행 방법
# 기본 실행
python app.py
# pdb로 즉시 디버그 모드 실행
python -m pdb app.py
# pytest에서 실패 지점 진입 (첫 실패에서 멈추고 pdb 열기)
pytest -x --pdb
# asyncio 디버그 모드 활성화
PYTHONASYNCIODEBUG=1 python app.py
브레이크포인트
def calculate_total(items):
subtotal = sum(i.price for i in items)
breakpoint() # Python 3.7+ 기본 내장
return subtotal
조건부 브레이크포인트
def process_order(order):
# 특정 조건에서만 디버거 진입 — 대량 루프에서 유용
if order.user_id == "U-9999":
breakpoint()
# 또는 pdb 조건부 설정 (pdb 프롬프트에서)
# (Pdb) b process_order, order.status == "FAILED"
result = validate(order)
return result
실무 팁:
- 루프 안 브레이크포인트는 반드시 조건부로 제한한다. 만 건 루프에서 무조건 걸면 작업이 멈춘다.
- 데이터 크기 큰 객체는
len()/핵심 필드만 확인한다.print(huge_dict)한 번이면 터미널이 멈출 수 있다. PYTHONBREAKPOINT=0으로 프로덕션에서 breakpoint를 무시시킬 수 있다.
메모리 누수 탐지
import tracemalloc
# 메모리 추적 시작
tracemalloc.start()
# --- 의심 구간 실행 ---
process_large_dataset()
# --- 끝 ---
# 현재 메모리 할당 스냅샷 생성
snapshot = tracemalloc.take_snapshot()
# 메모리를 가장 많이 사용하는 상위 10개 위치 출력
top_stats = snapshot.statistics('lineno')
for stat in top_stats[:10]:
print(stat)
# objgraph로 순환 참조 탐지
import objgraph
# 가장 많이 생성된 객체 타입 상위 20개
objgraph.show_most_common_types(limit=20)
# 특정 타입의 참조 그래프 시각화
objgraph.show_backrefs(
objgraph.by_type('MyLeakingClass')[:3],
filename='refs.png'
)
프로파일링
CPU
# cProfile로 함수 단위 CPU 시간 측정
python -m cProfile -o out.prof app.py
python -m pstats out.prof
# 운영 환경 친화적 샘플링 (py-spy — 프로세스 중단 없음)
py-spy top --pid <PID>
py-spy record -o profile.svg --pid <PID>
# line_profiler로 함수 내부 라인별 시간 측정
pip install line-profiler
kernprof -l -v slow_function.py
메모리
pip install memory-profiler
python -m memory_profiler app.py
# 데코레이터 방식으로 특정 함수만 측정
# @profile 데코레이터를 함수 위에 붙이면 라인별 메모리 사용량 출력
2) JavaScript/TypeScript (Node.js) 디버깅
실행 방법
# 디버그 포트 오픈 (기본 9229)
node --inspect src/index.js
# 첫 줄에서 멈춤 — 초기화 과정 디버깅에 유용
node --inspect-brk src/index.js
# TypeScript (source map 활성화 상태 전제)
node --inspect-brk dist/index.js
# ts-node로 직접 디버깅 (개발환경)
node --inspect-brk -r ts-node/register src/index.ts
브레이크포인트
async function syncUser(id: string) {
const user = await repo.findById(id)
debugger // DevTools/IDE 연결 시 중단
return user
}
조건부 브레이크포인트 (Chrome DevTools)
// Chrome DevTools에서 브레이크포인트 우클릭 → "Edit breakpoint" 선택
// 조건 예시:
// user.role === "admin"
// items.length > 100
// error.code === "TIMEOUT"
// 코드에서 조건부로 debugger 사용
function handleRequest(req: Request) {
// 특정 헤더가 있을 때만 디버거 진입
if (req.headers['x-debug'] === 'true') {
debugger
}
// ...
}
비동기 디버깅 팁
// 나쁜 예: Promise 체인에서 에러 원인 추적 불가
fetchUser(id)
.then((user) => fetchOrders(user.id))
.then((orders) => process(orders))
.catch((err) => console.log(err)) // 어디서 실패했는지 알 수 없음
// 좋은 예: async/await로 콜스택 유지
async function syncUserOrders(id: string) {
try {
const user = await fetchUser(id) // 실패 시 이 라인에서 멈춤
const orders = await fetchOrders(user.id) // 여기서 멈추면 원인 명확
return await process(orders)
} catch (err) {
// 스택트레이스에 원인 라인이 남음
console.error('syncUserOrders failed:', err)
throw err
}
}
// 이벤트 루프 블로킹 감지
// --prof 플래그로 V8 프로파일 생성
// node --prof dist/index.js
// node --prof-process isolate-*.log > processed.txt
// 또는 Clinic.js Doctor로 자동 진단
// npx clinic doctor -- node dist/index.js
메모리 누수 탐지
// Heap snapshot 3-snapshot 기법
// 1) 초기 상태 snapshot
// 2) 의심 작업 반복 수행
// 3) 작업 후 snapshot
// DevTools Memory 탭에서 Comparison 뷰로 증가한 객체 확인
// 코드에서 강제 GC 후 메모리 확인
// node --expose-gc dist/index.js
if (global.gc) {
global.gc()
}
console.log(process.memoryUsage())
// { rss, heapTotal, heapUsed, external, arrayBuffers }
프로파일링
# CPU profile 파일 생성
node --cpu-prof dist/index.js
# Heap snapshot (SIGUSR2 시그널로 트리거)
node --heapsnapshot-signal=SIGUSR2 dist/index.js
kill -USR2 <PID>
# Clinic.js로 종합 진단
npx clinic doctor -- node dist/index.js
npx clinic flame -- node dist/index.js
npx clinic bubbleprof -- node dist/index.js
Chrome DevTools에서:
- Performance 탭: CPU 병목, 이벤트 루프 지연 확인
- Memory 탭: Heap snapshot 비교로 누수 추적
- Sources 탭: Async Stack Traces 활성화하여 비동기 콜스택 확인
3) Go 디버깅
실행 방법
go run ./cmd/api
Delve 디버거:
# 디버그 모드로 실행
dlv debug ./cmd/api
# 테스트 디버깅
dlv test ./...
# 실행 중인 프로세스에 연결
dlv attach <PID>
# 원격 디버깅 (헤드리스 모드)
dlv debug ./cmd/api --headless --listen=:2345 --api-version=2
브레이크포인트
(dlv) break main.main
(dlv) break service/user.go:42
(dlv) continue
(dlv) print userID
조건부 브레이크포인트
# 특정 조건에서만 중단
(dlv) break service/order.go:55
(dlv) condition 1 order.Status == "FAILED"
# hit count 조합 — 100번째 호출에서만 멈춤
(dlv) condition 1 hitcount % 100 == 0
# 고루틴 확인
(dlv) goroutines # 전체 고루틴 목록
(dlv) goroutine 15 # 특정 고루틴으로 전환
(dlv) bt # 현재 고루틴 스택 트레이스
비동기(고루틴) 디버깅 팁
# race detector — 동시성 버그의 첫 관문
go test -race ./...
go run -race ./cmd/api
// 고루틴 누수 탐지: runtime.NumGoroutine() 모니터링
import "runtime"
func debugGoroutines() {
// 주기적으로 고루틴 수 확인
ticker := time.NewTicker(5 * time.Second)
for range ticker.C {
log.Printf("Active goroutines: %d", runtime.NumGoroutine())
}
}
// 데드락 진단: SIGQUIT로 전체 고루틴 스택 덤프
// kill -QUIT <PID> 또는 Ctrl+\ 로 트리거
// GOTRACEBACK=all 환경변수로 모든 고루틴 포함
// context 기반 타임아웃으로 데드락 예방
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
select {
case result := <-ch:
return result, nil
case <-ctx.Done():
return nil, fmt.Errorf("operation timed out: %w", ctx.Err())
}
프로파일링 (pprof)
import _ "net/http/pprof"
import "net/http"
go func() {
_ = http.ListenAndServe(":6060", nil)
}()
# CPU 30초 프로파일링
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# Heap 메모리 분석
go tool pprof http://localhost:6060/debug/pprof/heap
# 고루틴 덤프 — 누수 확인에 필수
go tool pprof http://localhost:6060/debug/pprof/goroutine
# 웹 UI로 시각화
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/profile?seconds=30
4) Java 디버깅
실행 방법
./gradlew bootRun
# 또는
java -jar app.jar
원격 디버그 포트:
# JDK 9+ 원격 디버그 설정
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -jar app.jar
# Docker 컨테이너에서 디버그 포트 노출
# docker run -p 5005:5005 -e JAVA_TOOL_OPTIONS="-agentlib:jdwp=..." app
브레이크포인트
// IntelliJ에서 조건부 브레이크포인트 설정
// 브레이크포인트 우클릭 → Condition 입력:
// orderId.equals("ORD-12345")
// items.size() > 100
// user != null && user.getRole().equals("ADMIN")
// Logpoint (중단 없이 로그만 출력)
// Evaluate and log: "Order processed: " + orderId + ", total=" + total
- 예외 브레이크포인트(NullPointerException, IllegalStateException 등) 적극 사용
- 조건부 브레이크포인트로 대량 트래픽 중 특정 케이스만 포착
- logpoint로 서비스 중단 없이 값 관찰
메모리 누수 탐지
# Heap dump 생성
jcmd <PID> GC.heap_dump /tmp/heapdump.hprof
# OOM 발생 시 자동 heap dump
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/ -jar app.jar
# Eclipse MAT (Memory Analyzer Tool)로 분석
# Leak Suspects Report → Dominator Tree → Retained Size 기준 정렬
// WeakReference로 캐시 누수 예방
import java.lang.ref.WeakReference;
import java.util.WeakHashMap;
// 나쁜 예: static Map에 계속 추가 → 메모리 누수
static Map<String, Object> cache = new HashMap<>();
// 좋은 예: WeakHashMap 사용 → GC가 필요시 수거
static Map<String, Object> cache = new WeakHashMap<>();
프로파일링
JFR (Java Flight Recorder)
# 프로덕션에서 120초 프로파일링 (오버헤드 1% 미만)
jcmd <PID> JFR.start name=onprod settings=profile duration=120s filename=app.jfr
jcmd <PID> JFR.stop name=onprod
# JDK Mission Control (JMC)로 .jfr 파일 분석
# Hot Methods → Call Tree → 상위 핫스팟 함수 확인
async-profiler
# CPU 프로파일링 (30초, flamegraph SVG 출력)
./profiler.sh -d 30 -f cpu.svg <PID>
# 메모리 할당 프로파일링
./profiler.sh -e alloc -d 30 -f alloc.svg <PID>
# Lock contention 프로파일링
./profiler.sh -e lock -d 30 -f lock.svg <PID>
# wall-clock 프로파일링 (I/O 대기 포함)
./profiler.sh -e wall -d 30 -f wall.svg <PID>
공통 디버깅 루틴 (언어 무관)
- 재현 조건 고정: 입력/버전/환경 변수 캡처. 재현 안 되면 디버깅 불가능하다.
- 관측 지점 최소화: 브레이크포인트 남발 금지. 경계 지점에만 배치한다.
- 가설 1개씩 검증: 한 번에 하나만 바꿔보기. 두 가지 동시에 바꾸면 원인을 못 잡는다.
- 프로파일링 우선순위: CPU → I/O → 메모리 순으로 의심 영역을 좁힌다.
- 사후 문서화: 원인/탐지 신호/재발 방지 기록. 같은 버그를 두 번 겪지 않기 위해 필수다.
팀 운영 체크리스트
- PR 템플릿에 "재현 방법" 필드 추가
- 장애 리포트에 flamegraph/JFR 첨부
- 디버깅 세션에서 시간 제한(예: 30분) 후 접근 전환
- 로컬/스테이징 디버그 설정 템플릿 공유
종합 트러블슈팅 체크리스트
재현 및 환경 확인
- 문제가 로컬에서 재현되는가?
- 재현 조건(입력값, 환경변수, 버전)을 정확히 기록했는가?
- 최근 배포/변경 사항과의 연관성을 확인했는가?
- 다른 환경(스테이징, 프로덕션)에서도 동일하게 발생하는가?
관측 및 분석
- 에러 로그/스택 트레이스를 확인했는가?
- 관련 메트릭(CPU, 메모리, 응답시간)에 이상이 있는가?
- 브레이크포인트를 경계 지점(입력/출력/외부 호출)에 배치했는가?
- 조건부 브레이크포인트로 범위를 좁혔는가?
프로파일링
- CPU 프로파일을 캡처했는가? (py-spy / --cpu-prof / pprof / JFR)
- 메모리 프로파일(heap dump/snapshot)을 확인했는가?
- I/O 병목(네트워크, 디스크, DB)을 확인했는가?
- flamegraph에서 상위 핫스팟 함수를 식별했는가?
해결 및 후속
- 근본 원인(root cause)을 식별했는가?
- 수정 사항에 대한 회귀 테스트를 작성했는가?
- 원인/탐지 신호/재발 방지를 문서화했는가?
- 모니터링/알림 규칙을 추가 또는 개선했는가?
- 팀에 postmortem을 공유했는가?
증상별 빠른 진단 표
현장에서 증상을 보고 어떤 언어·도구를 먼저 의심해야 하는지 즉시 찾을 수 있는 표다.
| 증상 | Python | JavaScript/TS | Go | Java |
|---|---|---|---|---|
| 응답 느림 (CPU 바운드) | py-spy top, cProfile | --cpu-prof, Clinic flame | pprof profile | JFR, async-profiler |
| 메모리 단조 증가 | tracemalloc, objgraph | Heap Snapshot 비교 | pprof heap | jcmd GC.heap_dump, MAT |
| 간헐적 행(hang) | PYTHONASYNCIODEBUG=1 | Async Stack Traces | goroutine dump (SIGQUIT) | jstack, Thread dump |
| 동시성 버그 | threading.enumerate() | unhandledRejection 핸들러 | go test -race | Deadlock detection (IntelliJ) |
| 타입/Null 오류 | mypy strict + breakpoint() | tsc --strict | go vet, staticcheck | SpotBugs, Exception BP |
| N+1 쿼리 | django-debug-toolbar | Prisma query logging | sqlx logging | Hibernate SQL + p6spy |
디버깅 환경 셋업 원라이너
새 프로젝트에서 디버깅 환경을 빠르게 구성하는 명령 모음이다.
# Python — 디버깅 + 프로파일링 도구 일괄 설치
pip install debugpy py-spy line-profiler memory-profiler objgraph ipdb
# Node.js — 진단 도구 설치
npm install -D @clinic/doctor why-did-you-render
# Go — Delve 디버거 + 정적 분석 도구
go install github.com/go-delve/delve/cmd/dlv@latest && go install honnef.co/go/tools/cmd/staticcheck@latest
# Java — async-profiler 다운로드 (Linux x64)
curl -L https://github.com/async-profiler/async-profiler/releases/latest/download/async-profiler-3.0-linux-x64.tar.gz | tar xz
마무리
좋은 디버거는 "툴을 많이 아는 사람"이 아니라 문제를 빠르게 좁히는 사람이다. 언어별 도구는 달라도, 핵심은 같다:
관찰 가능한 상태를 만들고, 근거 기반으로 한 단계씩 좁혀라.
각 언어의 런타임 특성을 이해하고, 해당 생태계에서 검증된 도구를 능숙하게 쓸 수 있으면 대부분의 버그는 30분 안에 원인을 찾을 수 있다. 도구를 배우는 데 드는 시간은 디버깅에서 절약되는 시간으로 몇 배 이상 돌아온다.
다음 글에서는 프레임워크별 디버깅 실전을 다룬다. 언어에 프레임워크가 얹어지면 실행 흐름이 어떻게 바뀌고, 브레이크포인트를 어디에 찍어야 하는지 정리했다.