2.41. 디버깅#

카메라에서 실패하는 대부분의 스크립트는 세 가지 방식 중 하나로 실패합니다. 예외를 발생시키거나, 잘못된 값을 만들어내거나, 멈춰버립니다. 각 경우마다 사용할 수 있는 도구 모음이 다릅니다.

2.41.1. 트레이스백 읽기#

스크립트가 예외를 발생시켰는데 아무것도 그 예외를 처리하지 않으면, REPL이나 IDE가 트레이스백(traceback) 을 출력합니다. 이는 가장 바깥쪽 스크립트부터 예외가 발생한 줄까지 이어지는 호출 사슬의 기록입니다.

트레이스백은 아래에서 위로 읽습니다:

  • 맨 아래 줄은 예외 클래스와 그 메시지를 알려줍니다(ValueError: invalid literal for int()...).

  • 그 위의 각 File "...", line N, in <name> 블록은 프레임 입니다. 위로 올라갈수록 한 단계씩 더 깊은 호출입니다.

  • 맨 위 프레임은 스크립트가 시작된 곳이고, 맨 아래 프레임은 오류가 발생한 곳입니다.

먼저 맨 아래를 읽어 무엇이 잘못되었는지 파악한 다음, 위로 거슬러 올라가며 스크립트가 어떻게 거기에 이르렀는지 살펴봅니다. 줄 번호는 스크립트 내 정확한 소스 위치를 가리킵니다.

2.41.3. 객체 탐색하기#

두 가지 내장 함수가 “이 물건으로 무엇을 할 수 있을까”에 답해 줍니다:

  • dir() – 객체에 정의된 모든 이름의 목록을 반환합니다. 메서드, 속성, 던더(dunder) 등 전부입니다.

  • help() – 함수, 메서드, 클래스의 docstring(그리고 CPython에서는 시그니처)을 출력합니다.

둘을 함께 사용하세요. dir 로 이름을 찾고, help 로 그것이 무엇을 하는지 설명을 봅니다.

2.41.3.1. dir로 이름 찾기#

>>> dir([1, 2, 3])
['__add__', '__class__', '__contains__', '__delitem__',
 '__eq__', '__ge__', ..., 'append', 'clear', 'copy',
 'count', 'extend', 'index', 'insert', 'pop', 'remove',
 'reverse', 'sort']

목록의 첫 부분은 모든 객체로부터 상속된 던더 메서드입니다. 살펴볼 만한 이름들은 보통 그 뒤에 있습니다. dir 은 클래스, 인스턴스, 모듈, 내장 타입 등 무엇에든 동작합니다:

>>> import json
>>> dir(json)
['__name__', 'dump', 'dumps', 'load', 'loads']

그 두 번째 형식은 REPL을 벗어나지 않고도 모듈이 실제로 어떤 최상위 이름을 노출하는지 알아내는 방법입니다.

2.41.3.2. help로 찾아보기#

dir 이 후보를 드러내면 help 가 그것을 설명해 줍니다:

>>> help(str.split)
split(sep=None, maxsplit=-1)
    Return a list of the words in the string, ...

MicroPython에서 help 는 CPython보다 간소합니다. 때로는 시그니처만, 때로는 한 줄짜리 docstring만, 내장 C 함수의 경우 때로는 아무것도 없습니다. 그래도 IDE 툴팁이 가까이 없을 때 빠르게 상기시켜 주는 도구입니다.

2.41.4. 무언가 멈췄을 때#

반환하지 않는 스크립트는 예외를 발생시키는 스크립트보다 진단하기 어렵습니다. 흔한 원인은 다음과 같습니다:

  • 조건이 결코 거짓이 되지 않는 while 루프. 반복마다 루프 변수를 출력해 보세요. 값이 변하지 않는다면 루프 본문에 버그가 있는 것입니다.

  • 결코 도착하지 않는 입력을 기다리는 블로킹 호출. 비어 있는 큐로부터의 읽기, 끝나지 않는 sleep 같은 것입니다. 그 호출 앞뒤에 print를 두어 스크립트가 어느 줄에서 멈춰 있는지 확인하세요.

  • 무한 재귀. 결국 발생할 때의 트레이스백(RecursionError 와 함께)이 보통 바로 그 지점을 가리켜 줍니다.

멈춘 스크립트에 대한 가장 효과적인 복구 방법은 IDE의 stop 버튼입니다. 이 버튼은 USB를 통해 스크립트에 KeyboardInterrupt 를 보냅니다. 이 인터럽트는 현재 실행 중인 줄에서 트레이스백으로 나타나는데, 흔히 반환하지 않고 있는 바로 그 줄입니다.

참고

모든 진단을 거쳐도 멈춤 현상이 해결되지 않는다면(스크립트가 올바른 것 같고, 인터럽트 트레이스백이 사용자 스크립트가 아니라 내장 함수나 펌웨어 코드를 가리키거나, 동일한 코드가 이전 펌웨어 빌드에서는 잘 동작했다면) 원인은 스크립트 버그가 아니라 펌웨어 버그일 수 있습니다. 여전히 멈추는 가장 작은 재현 예제로 스크립트를 줄인 다음 OpenMV 포럼 에 보고하세요. 펌웨어 버전, 실행한 보드, 그리고 줄인 스크립트를 포함하세요.

2.41.5. 배포 전에 진단 코드를 제거하세요#

개발 중의 전략적인 print는 훌륭합니다. 하지만 운영용 스크립트에 백 개의 print 호출이 남아 있으면 출력이 어수선해지고, 실제 작업에 쓰일 수 있었던 힙을 잡아먹습니다. 버그를 고치고 나면 print를 제거하세요(또는 끌 수 있는 디버그 플래그 뒤에 가두세요).

코드 경로에 오래도록 남겨 두어야 하는 진단의 경우, print() 대신 logging 모듈로 전환하세요. 이 모듈은 각 메시지에 레벨 을 부여하고(debug, info, warning, error), 단일 설정만으로 운영 환경에서 조용한 메시지들을 침묵시킬 수 있습니다:

import logging

log = logging.getLogger("main")
log.info("starting up")
log.debug("loaded config: %s", config)
log.warning("falling back to defaults")

로거의 레벨을 logging.WARNING 으로 설정하면 줄을 주석 처리할 필요 없이 info 와 debug 호출의 비용이 사실상 0이 됩니다(메시지 문자열이 아예 만들어지지 않습니다). 그래서 logging 은 영구적인 진단에 적합한 도구이며, 원시 print 는 일회성 진단에 적합합니다.