pdb 를 이용한 Python 코드 디버깅
F-Lab : 상위 1% 개발자들의 멘토링
안녕하세요. F-Lab 'Python Backend' 과정 멘토 Jacob 입니다. 저는 센드버드 출신의 엔지니어링 조직 문화와 생산성에 관심이 많은 개발자입니다. 오늘은 디버깅 도구로써 pdb에 대해 소개하고 어떤 방식으로 pdb를 사용할 수 있는지 함께 알아보려고 합니다.
개요
오래된 코드를 이해하기 위해서 print 함수를 여기저기 넣으면서 무슨 일이 일어나고 있는지 파악하려고 애쓴 적이 있으신가요? 경험이 많건 적건 개발자라면 이런 경험을 한번쯤은 해 보았을것 같습니다. 코드가 많아질수록, 그리고 상태값이 늘어날수록 디버깅은 어려워집니다. 디버깅을 할 때 가장 쉬운 접근 방식은 단계별로 프로그램의 내부 상태를 읽어내는 것이라고 생각합니다.
이를 위해서 print 함수나 logger 등으로 내부 상태를 외부에 노출시키고 경우에 따라서는 input 함수를 이용해서 프로그램 흐름을 조작 하기도 합니다. 하지만 이러한 접근 방법은 프로그램에 대한 수정이 필요하고 수정된 영역에 대해서만 제한적으로 내부 상태를 파악할수 있다는 단점이 있습니다.
만약 상태값을 들여다보고 프로그램 흐름을 조작할 수 있는 도구가 있다면 매번 프로그램을 수정하고 흐름을 처음부터 시작하는 번거로운 과정을 간단하게 만들 수 있을 것 같습니다. 프로젝트 규모가 커질수록 코드를 읽고 이해하는 시간이 코드를 작성하는 시간보다 많아지기 때문에 어느정도 성숙한 프로그래밍 언어 생태계에서는 위에서 언급된 기능을 제공하는 도구를 필연적으로 제공하게 됩니다. Python 생태계에서는 pdb 를 필두로 한 여러가지 python debugger 들이 그 예시로 꼽힐 수 있습니다.
pdb 란?
pdb 는 Python 인터프리터에 standard library 로 포함되어 있는 debugger module 입니다. pdb는 Python 의 아버지인 Guido van Rossum 에 의해서 만들어져서 1991년 Python 0.9.0 버전부터 소개되었으며, 그 이후로 지속적으로 업데이트 되어 현재의 형태를 갖추게 되었습니다.
pdb는 커맨드 라인 툴 또는 Python 모듈로써 import 되어서 사용될 수 있습다. pdb는 python interpreter와 상호작용하며 사용자가 프로그램 흐름을 조작하고 프로그램의 상태를 확인하고 수정할수 있게 해 줍니다.
* pdb (파이썬 디버거) : https://docs.python.org/ko/3/library/pdb.html
디버깅 명령어
pdb는 다양한 디버깅 명령어를 제공합니다. 명령어들은 모두 위에서 언급한 "상태값을 들여다보고" "프로그램 흐름을 조작하는" 일을 여러가지 방식으로 하기 위해서 존재합니다. 특정한 디버깅 상황에서 유용합니다. 이제 각 명령어와 그 사용 사례에 대해 살펴보겠습니다:
- h 또는 help - 도움말을 표시합니다. h [command] 와 같이 명령어를 지정하여 특정 명령어에 대한 도움말을 확인할 수 있습니다.
- l 또는 list - 현재 읽고 있는 줄 주변의 소스 코드를 출력합니다.
- w 또는 where - 현재 스택 프레임의 콜 스택을 출력합니다.
- s 또는 step - 한 줄씩 코드를 실행합니다. 현재 읽고 있는 줄에서 함수 호출이 일어난 경우 해당 함수의 첫 줄로 이동합니다.
- n 또는 next - 한 줄씩 코드를 실행합니다. 현재 읽고 있는 줄에서 함수 호출이 일어난 경우 함수를 실행하고 다음 줄로 이동합니다.
- b 또는 break - breakpoint를 생성합니다. b [filename:lineno] 와 같은 형식으로 호출할 경우 해당되는 줄에 breakpoint를 생성합니다. b [function name] 와 같은 형식으로 호출할 경우 함수 시작점에 breakpoint를 생성합니다. 별도의 명령 없이 호출될 경우 존재하는 모든 breakpoint 들을 출력합니다.
- c 또는 continue - breakpoint 가 발견될때까지 프로그램을 실행합니다.
- cl 또는 clear - breakpoint를 제거합니다. cl [filename:lineno] 와 같은 형식으로 호출할 경우 해당되는 줄에 존재하는 breakpoint를 모두 제거합니다. cl [breakpoint number] 와 같은 형식으로 호출할 경우 숫자에 해당되는 breakpoint들을 제거합니다.
- r 또는 return - 현재 함수의 실행을 완료하고 호출한 곳으로 돌아갑니다.
명령어들의 동작을 보면 크게 현재 실행되고 있는 줄 주변의 소스코드(1)나 콜스택을 출력하기 위한 명령(2)과 소스 코드를 한 줄, 또는 여러 줄 실행하기 위한 명령(3)으로 나뉘는 것을 알 수 있습니다. pdb에서 제공하는 다양한 명령어들이 위에서 언급된 "상태값을 들여다보고" "프로그램 흐름을 조작하는" 기능을 세분화한 것이라는 것을 알 수 있습니다. 그러면 위에서 배운 명령어들을 실습하기 위해서 간단한 파이썬 프로그램을 예시로 들어 보겠습니다.
0. 실습
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n-1)
def main():
num = 5
print("Factorial of", num, "is", factorial(num))
if __name__ == "__main__":
main()
위 코드는 재귀적으로 팩토리얼을 계산하는 함수를 포함하고 있습니다. python -m pdb factorial.py
와 같은 방식으로 pdb에게 Python 파일을 실행하도록 명령할 수 있습니다. 만약 프로그램 내부에서 pdb 의 시작점을 지정해야 한다면 `pdb` 모듈을 import 하고 `pdb.set_trace()` 와 같이 시작점을 지정해주는것도 가능합니다.
> /Users/Jacob/factorial.py(1)<module>()
-> def factorial(n):
pdb 로 위 프로그램을 실행하면 위와 같이 함수 정의 부분에서 멈추는 것을 확인할 수 있습니다. 이것은 Python 이 인터프리터 언어이고 함수의 정의도 프로그램 실행 시간 (런타임) 에 만들어지기 때문입니다.
1. l 또는 list 명령어
(Pdb) l
1 -> def factorial(n):
2 if n == 0:
3 return 1
4 else:
5 return n * factorial(n-1)
6
7 def main():
8 num = 5
9 print("Factorial of", num, "is", factorial(num))
10
11 if __name__ == "__main__":
l 명령어로 현재 pdb 가 코드의 어느 줄을 실행하고 있는지 확인할 수 있습니다.
2. b 또는 break 명령어
(Pdb) b 5
Breakpoint 1 at /Users/Jacob/factorial.py:5
(Pdb) c
> /Users/Jacob/factorial.py(5)factorial()
-> return n * factorial(n-1)
(Pdb) l
1 def factorial(n):
2 if n == 0:
3 return 1
4 else:
5 B-> return n * factorial(n-1)
6
7 def main():
8 num = 5
9 print("Factorial of", num, "is", factorial(num))
10
11 if __name__ == "__main__":
b 명령어로 5번 줄에 breakpoint 를 생성하고 c 명령어로 breakpoint 를 만날 때 까지 프로그램을 실행했습니다. l 명령어를 이용해서 현재 위치를 확인하면 breakpoint (B) 를 확인할 수 있습니다.
(Pdb) print(n)
5
pdb의 강력한 기능 중 하나는 현재 스택 프레임에 대해서 Python expression을 실행하고 결과를 출력할 수 있다는 점입니다. 위에서는 print 함수를 이용해서 n 이라는 이름의 변수가 어떤 값을 가지고 있는지 확인해봤습니다. 변수의 값을 읽어들이는것 외에도 임의의 값을 할당하는것도 가능합니다.
3. c 또는 continue 명령어
(Pdb) c
> /Users/Jacob/factorial.py(5)factorial()
-> return n * factorial(n-1)
(Pdb) print(n)
4
c 명령어를 한번 더 실행하면 동일하게 breakpoint 에서 실행을 멈추는 것을 확인할 수 있습니다. 이때 n 의 값은 4이며 이를 통해서 재귀 호출이 일어났음을 짐작할 수 있습니다.
4. cl 또는 clear 명령어
(Pdb) cl 1
Deleted breakpoint 1 at /Users/Jacob/factorial.py:5
(Pdb) l
12 main()
13
[EOF]
cl 명령어를 이용해서 breakpoint 를 제거합니다.
5. w 또는 where 명령어, r 또는 return 명령어
(Pdb) w
/Users/Jacob/.pyenv/versions/3.8.12/lib/python3.8/bdb.py(580)run()
-> exec(cmd, globals, locals)
<string>(1)<module>()
/Users/Jacob/factorial.py(12)<module>()
-> main()
/Users/Jacob/factorial.py(9)main()
-> print("Factorial of", num, "is", factorial(num))
/Users/Jacob/factorial.py(5)factorial()
-> return n * factorial(n-1)
> /Users/Jacob/factorial.py(5)factorial()
-> return n * factorial(n-1)
(Pdb) r
--Return--
> /Users/Jacob/factorial.py(5)factorial()->24
-> return n * factorial(n-1)
(Pdb) r
--Return--
> /Users/Jacob/factorial.py(5)factorial()->120
-> return n * factorial(n-1)
(Pdb) r
Factorial of 5 is 120
--Return--
> /Users/Jacob/factorial.py(9)main()->None
-> print("Factorial of", num, "is", factorial(num))
w 명령어를 이용해서 현재 콜 스택을 확인하면 재귀 호출이 두번 일어났음을 확인할 수 있습니다. r 명령어를 두번 실행해서 factorial 함수의 호출을 빠져나오고 r 명령어를 한번 더 실행하면 `Factorial of 5 is 120` 라는 출력을 확인할 수 있습니다.
마무리
위의 예시를 이용한 실습에서 pdb를 이용하면 별도의 print 함수나 logger 호출 없이 Python 프로그램의 내부 상태를 들여다보고 현재 스택 프레임의 상태를 조작할 뿐만 아니라 실행을 원하는 대로 진행시키거나 멈추어서 원하는 시점에 프로그램의 상태를 들여다보는 것이 가능하다는 것을 알 수 있었습니다.
이렇듯, pdb는 파이썬 프로그램의 디버깅을 간편하게 도와주는 강력한 도구입니다. pdb 는 단번에 이해하기 어려운 코드를 순차적으로 실행시킴으로써 코드 흐름을 이해하는데 도움을 주고 내부 상태를 직접 조작함으로써 버그의 원인을 파악하고 고치는 데 들어가는 노력을 덜어줍니다. 독자 여러분이 디버깅을 하다가 벽에 부딪혔을때 이 블로그 글을 참고해서 도움을 받을 수 있으면 좋겠습니다.
이 컨텐츠는 F-Lab의 고유 자산으로 상업적인 목적의 복사 및 배포를 금합니다.