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 으로 설정하면 줄을 주석 처리할 필요 없이 infodebug 호출의 비용이 사실상 0이 됩니다(메시지 문자열이 아예 만들어지지 않습니다). 그래서 logging영구적인 진단에 적합한 도구이며, 원시 print일회성 진단에 적합합니다.