VS Code로 파이썬 디버깅하기 — print 대신 중단점 쓰기
버그를 잡을 때 print(변수)를 넣고 돌리고, 지우고, 다시 다른 데 넣고 돌리기를 반복한 적 있으신가요. 저도 오래 그랬습니다. 그런데 이 방식은 한 번 실행할 때마다 궁금한 것 하나밖에 못 봅니다. 중단점은 그 순간의 모든 것을 한꺼번에 보여줍니다.
기본 — F5와 F9면 시작
중단점(breakpoint)은 "이 줄에 오면 프로그램을 잠시 멈춰라"는 표시입니다. 코드 줄 번호 왼쪽 여백을 클릭하거나 F9를 누르면 빨간 점이 찍힙니다.
F5로 실행하면 그 줄에서 멈추고, 왼쪽 패널에 그 시점의 모든 지역 변수가 펼쳐집니다. 딕셔너리도, 중첩 객체도 전부 열어볼 수 있습니다. print로는 미리 예상한 변수 하나만 보이지만, 중단점은 예상 못 한 변수까지 보여줍니다. 버그는 대개 예상 못 한 쪽에 있죠.
멈춘 뒤 조작은 네 개만 알면 됩니다.
- F10 스텝 오버 — 다음 줄로. 함수 호출은 통째로 실행하고 넘어감
- F11 스텝 인 — 함수 안으로 들어감
- Shift+F11 스텝 아웃 — 현재 함수를 마치고 호출한 곳으로
- F5 계속 — 다음 중단점까지 쭉 진행
launch.json — 실행 설정 고정
봇은 보통 인자나 환경변수를 받습니다. 매번 손으로 넣지 말고 .vscode/launch.json에 저장합니다.
{
"version": "0.2.0",
"configurations": [
{
"name": "봇 - 페이퍼 모드",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/bot.py",
"args": ["--mode", "paper", "--config", "config.ini"],
"env": {
"EXCHANGE_API_KEY": "${env:EXCHANGE_API_KEY}"
},
"console": "integratedTerminal",
"justMyCode": true
}
]
}
justMyCode는 기본값 true일 때 라이브러리 내부로는 들어가지 않습니다. 대개 이게 편하지만, 라이브러리 안에서 나는 에러를 추적할 땐 false로 바꿔야 합니다.
주의: env에 API 키를 직접 적지 마세요. 위처럼 ${env:...}로 참조하거나 envFile을 쓰고, .vscode/도 상황에 따라 .gitignore 대상입니다.
조건부 중단점 — 여기서 판이 바뀐다
이게 진짜 무기입니다. 봇이 1초에 수십 번 도는 루프에서 딱 문제가 나는 그 순간에만 멈추고 싶을 때가 있습니다. 그냥 중단점을 찍으면 수백 번 멈춰서 못 씁니다.
중단점을 우클릭 → 중단점 편집에서 조건을 겁니다.
// 예시 조건식 — 파이썬 표현식을 그대로 쓴다
gap_pct > 3.0 # 갭이 비정상적으로 클 때만
symbol == "TARGET_SYMBOL" # 특정 종목일 때만
order is None and retry_count >= 3 # 재시도 3회 후에도 실패했을 때
여기에 적중 횟수(hit count) 조건도 있습니다. >= 500으로 걸면 500번째 통과부터 멈춥니다. "한참 잘 돌다가 어느 순간부터 이상해진다" 같은 버그에 딱 맞습니다.
실전에서 이 기능 하나가 if 조건: import pdb; pdb.set_trace() 같은 임시 코드를 전부 대체합니다. 코드를 건드리지 않으니 지우는 걸 깜빡할 일도 없습니다.
로그포인트 — 멈추지 않는 print
실시간 봇은 멈추면 곤란한 경우가 많습니다. 호가가 계속 들어오는데 5초 멈추면 이미 상황이 달라져 있죠. 이때는 로그포인트(logpoint)를 씁니다.
중단점 우클릭 → 로그 메시지를 고르고 이렇게 적습니다.
진입 판단: {symbol} 갭={gap_pct:.3f} 잔고={balance}
중괄호 안이 실제 값으로 치환되어 디버그 콘솔에 출력됩니다. 프로그램은 멈추지 않습니다. print문의 효과를 내되 코드는 한 글자도 안 바뀝니다. 확인 끝나면 로그포인트만 지우면 되니, 디버그 코드가 커밋에 섞여 들어가는 사고도 없습니다.
조사식과 예외 중단
조사식(Watch) 패널에 표현식을 등록해두면 멈출 때마다 자동으로 재계산됩니다. 단순 변수뿐 아니라 계산식도 됩니다.
len(open_positions)
sum(p.qty * p.price for p in open_positions)
(ask - bid) / bid * 100
디버그 콘솔에서는 멈춘 시점의 컨텍스트로 아무 코드나 실행해볼 수 있습니다. 함수를 직접 호출해서 반환값을 확인하는 것도 가능합니다. 단, 주문 전송 함수처럼 부작용이 있는 코드는 절대 콘솔에서 호출하지 마세요. 실거래 계정이면 진짜로 주문이 나갑니다.
그리고 잘 안 쓰이는데 강력한 기능 하나 — 중단점 패널의 "Raised Exceptions" 체크박스입니다. 켜두면 예외가 발생하는 바로 그 줄에서 멈춥니다. try/except로 삼켜지는 예외까지 잡히기 때문에, "왜 조용히 실패하지?" 유형의 버그에 즉효입니다.
이미 돌고 있는 봇에 붙기
서버에서 며칠째 도는 봇에 이상 징후가 보일 때, 재시작하면 상태가 날아갑니다. 이럴 땐 원격 디버깅으로 붙습니다.
봇 코드에 이 부분을 미리 심어둡니다.
import os
if os.environ.get("DEBUG_ATTACH") == "1":
import debugpy
debugpy.listen(("127.0.0.1", 5678)) # 외부 노출 금지
print("디버거 대기 중... 포트 5678")
# debugpy.wait_for_client() # 시작부터 잡고 싶을 때만
그리고 launch.json에 접속 설정을 추가합니다.
{
"name": "돌고 있는 봇에 붙기",
"type": "debugpy",
"request": "attach",
"connect": { "host": "127.0.0.1", "port": 5678 },
"pathMappings": [
{ "localRoot": "${workspaceFolder}", "remoteRoot": "/app" }
]
}
보안 경고 하나. listen 주소는 반드시 127.0.0.1로 묶으세요. 0.0.0.0으로 열면 인터넷에서 누구나 붙어 봇 프로세스 안에서 임의 코드를 실행할 수 있습니다. 원격 서버라면 SSH 포트 포워딩으로 터널을 뚫어서 접근하는 게 정석입니다.
정리
print는 한 번에 하나, 중단점은 그 시점의 전부를 보여준다- 실행 인자·환경변수는
launch.json에 고정 (단, 키는 직접 적지 않기) - 조건부 중단점 + 적중 횟수가 반복 루프 디버깅의 핵심 무기
- 멈추면 안 되는 실시간 봇에는 로그포인트 — 코드 수정 없는 print
- "조용히 실패"하는 버그는 Raised Exceptions 중단으로 잡는다
- 돌아가는 봇에는
debugpyattach, 단127.0.0.1에만 바인딩 - 디버그 콘솔에서 주문 함수 호출 금지 — 실제로 나간다
중단점 사용법을 익히는 데 30분이면 충분합니다. 그 30분 뒤부터 디버깅 속도가 눈에 띄게 달라집니다. print를 지우다 커밋에 섞어 넣는 일도 함께 사라지고요.
댓글
댓글 쓰기