Python 디버깅

PythonBeginner
지금 연습하기

소개

컴퓨터 프로그래밍에서 디버깅 (debugging) 은 프로그램이 올바르게 작동하지 못하게 하는 결함 또는 "버그 (bug)"를 찾아내고 해결하는 필수적인 과정입니다. 버그는 소프트웨어 개발의 불가피한 부분이지만, Python 은 효율적으로 문제를 진단하고 수정하는 데 도움이 되는 강력한 도구 세트를 제공합니다.

본 랩에서는 Python 의 몇 가지 기본적인 디버깅 기술을 학습합니다. 다음 사항들을 다룰 것입니다:

  • 오류 발생 시 프로그램 흐름을 제어하기 위해 예외 (exception) 를 수동으로 발생시키고 처리하는 방법.
  • 로깅 목적으로 상세한 오류 추적 (traceback) 을 문자열로 캡처하는 방법.
  • 논리적 오류를 조기에 포착하기 위해 어설션 (assertion) 을 건전성 검사 (sanity check) 로 사용하는 방법.
  • 프로그램의 실행 흐름을 추적하고 흐름을 중단하지 않고 문제를 진단하기 위한 유연한 로깅 시스템을 구현하는 방법.

예외 발생시키기 (Raising Exceptions)

이 단계에서는 raise 문을 사용하여 수동으로 예외를 발생시키는 방법을 배웁니다. 예외는 오류가 발생했음을 알리는 방법입니다. raise 문은 raise 키워드, Exception() 함수 호출, 그리고 Exception()에 전달되는 유용한 오류 메시지 문자열로 구성됩니다.

함수의 전제 조건 (preconditions) 이 충족되지 않았을 때 함수 내에서 예외를 발생시키고, 이후 해당 함수를 호출하는 코드에서 try 및 except 블록을 사용하여 이러한 예외를 처리하는 것이 일반적인 관행입니다.

참고: WebIDE(VS Code 와 유사) 사용이 익숙하지 않다면, 이 실습에서 먼저 학습하십시오.

예제를 통해 실습해 보겠습니다.

  1. 왼쪽 파일 탐색기에서 raise_exception.py 파일을 엽니다.
  2. 파일에 다음 코드를 추가합니다. 이 코드는 잘못된 인수에 대해 예외를 발생시키는 box_print 함수를 정의한 다음, 유효한 입력과 유효하지 않은 입력을 모두 사용하여 이 함수를 호출하고 예외를 포착합니다.
def box_print(symbol, width, height):
    if len(symbol) != 1:
      raise Exception('Symbol must be a single character string.')
    if width <= 2:
      raise Exception('Width must be greater than 2.')
    if height <= 2:
      raise Exception('Height must be greater than 2.')
    print(symbol * width)
    for i in range(height - 2):
        print(symbol + (' ' * (width - 2)) + symbol)
    print(symbol * width)

## Handle exceptions when calling the function
for sym, w, h in (('*', 4, 4), ('O', 20, 5), ('x', 1, 3), ('ZZ', 3, 3)):
    try:
        box_print(sym, w, h)
    except Exception as err:  ## Catch exception and print error message
        print('An exception happened: ' + str(err))
  1. 이제 터미널에서 스크립트를 실행하여 예외가 어떻게 발생하고 처리되는지 확인합니다.
python3 raise_exception.py

다음과 같은 출력을 보게 될 것입니다. box_print에 대한 처음 두 번의 호출은 성공하지만, 마지막 두 번의 호출은 실패하여 except 블록이 트리거되고 사용자 정의 오류 메시지가 출력됩니다.

****
*  *
*  *
****
OOOOOOOOOOOOOOOOOOOO
O                  O
O                  O
O                  O
OOOOOOOOOOOOOOOOOOOO
An exception happened: Width must be greater than 2.
An exception happened: Symbol must be a single character string.

Traceback 을 문자열로 얻기

처리되지 않은 예외가 발생하면 Python 은 오류를 유발한 함수 호출에 대한 자세한 보고서인 트레이스백 (traceback) 을 출력합니다. 때로는 예외를 우아하게 처리하면서도 나중에 디버깅할 수 있도록 전체 트레이스백을 기록하고 싶을 수 있습니다. traceback 모듈을 사용하면 이 작업을 수행할 수 있습니다.

이 단계에서는 traceback.format_exc()를 사용하여 트레이스백을 문자열로 캡처하고 파일에 저장하는 방법을 배웁니다.

  1. 파일 탐색기에서 get_traceback.py 파일을 엽니다.
  2. 다음 코드를 추가합니다. 이 스크립트는 traceback 모듈을 가져온 다음, try...except 블록을 사용하여 예외를 포착합니다. except 블록 내에서 전체 트레이스백을 errorInfo.txt라는 파일에 씁니다.
import traceback

try:
    raise Exception('This is the error message.')
except:
    with open('errorInfo.txt', 'w') as error_file:
        error_file.write(traceback.format_exc())  ## Write traceback to file
    print('The traceback info was written to errorInfo.txt.')
  1. 터미널에서 스크립트를 실행합니다.
python3 get_traceback.py

스크립트는 확인 메시지를 출력합니다.

The traceback info was written to errorInfo.txt.
  1. 이제 새로 생성된 errorInfo.txt 파일의 내용을 확인합니다. 파일 탐색기에서 열거나 터미널에서 cat 명령어를 사용할 수 있습니다.
cat errorInfo.txt

파일에는 다음과 유사한 전체 트레이스백이 포함됩니다.

Traceback (most recent call last):
  File "/home/labex/project/get_traceback.py", line 4, in <module>
    raise Exception('This is the error message.')
Exception: This is the error message.

이 기술은 프로그램을 충돌시키지 않으면서 프로덕션 애플리케이션에서 오류를 로깅하는 데 매우 유용합니다.

Assertion 사용하기

어설션 (Assertion) 은 코드가 명백히 잘못된 작업을 수행하고 있지 않은지 확인하기 위한 건전성 검사 (sanity check) 입니다. assert 문은 특정 조건이 참인지 확인합니다. 조건이 거짓이면 AssertionError를 발생시키는데, 이는 이상적으로는 프로그램이 중단되도록 해야 합니다. 이러한 "빠른 실패 (fail-fast)" 접근 방식은 버그의 근본 원인을 더 빨리 식별하는 데 도움이 됩니다.

assert 문은 assert 키워드, 확인할 조건, 그리고 선택적인 오류 메시지로 구성됩니다.

이 단계에서는 어설션을 사용하여 프로그램의 상태를 검증해 보겠습니다.

  1. 파일 탐색기에서 use_assertions.py 파일을 엽니다.
  2. 다음 코드를 추가합니다. 이 코드는 pod_bay_door_status의 상태를 확인합니다. 첫 번째 어설션은 조용히 통과하지만, 두 번째 어설션은 실패하여 AssertionError를 발생시킵니다.
## assert statement: check condition, raise AssertionError if False
pod_bay_door_status = 'open'
assert pod_bay_door_status == 'open', 'The pod bay doors need to be "open".'  ## Passes

pod_bay_door_status = 'I\'m sorry, Dave. I\'m afraid I can\'t do that.'
assert pod_bay_door_status == 'open', 'The pod bay doors need to be "open".'  ## Raises AssertionError
  1. 터미널에서 스크립트를 실행합니다.
python3 use_assertions.py

두 번째 어설션이 실패했기 때문에 프로그램이 중단되고 사용자 정의 메시지와 함께 AssertionError가 표시됩니다.

Traceback (most recent call last):
  File "/home/labex/project/use_assertions.py", line 6, in <module>
    assert pod_bay_door_status == 'open', 'The pod bay doors need to be "open".'
AssertionError: The pod bay doors need to be "open".

예외와 달리, 어설션은 try...except로 처리해서는 안 됩니다. AssertionError는 수정이 필요한 프로그램의 버그를 나타냅니다. 또한, 어설션은 Python 을 -O (최적화) 플래그와 함께 실행하여 전역적으로 비활성화될 수 있으므로, 항상 강제되어야 하는 입력 유효성 검사에는 사용해서는 안 된다는 점에 유의해야 합니다.

로깅 구현하기

디버깅을 위해 print() 문을 사용할 수 있지만, 더 강력하고 유연한 접근 방식은 Python 의 내장 logging 모듈을 사용하는 것입니다. 로깅을 사용하면 프로그램 실행 중 이벤트를 추적하고 심각도별로 메시지를 분류할 수 있습니다.

이 단계에서는 버그 진단을 돕기 위해 프로그램에 로깅을 추가해 보겠습니다.

  1. 먼저 파일 탐색기에서 factorial_log.py 파일을 엽니다.
  2. 다음 코드를 추가합니다. 이 프로그램은 숫자의 팩토리얼을 계산하려고 시도하지만 버그가 포함되어 있습니다. 각 단계에서 변수 값을 추적하기 위해 logging.debug() 호출을 추가했습니다. logging.basicConfig() 함수는 로깅 시스템을 구성하여 DEBUG 레벨 이상의 메시지를 표시하도록 설정합니다.
import logging
logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(levelname)s - %(message)s')
logging.debug('Start of program')

def factorial(n):
    logging.debug('Start of factorial(%s)' % (n))
    total = 1
    for i in range(1, n + 1):
        total *= i
        logging.debug('i is ' + str(i) + ', total is ' + str(total))
    logging.debug('End of factorial(%s)' % (n))
    return total

print(factorial(5))
logging.debug('End of program')
  1. 터미널에서 스크립트를 실행합니다.
python3 factorial_log.py

프로그램의 일반 출력 사이에 상세한 로그 출력이 표시됩니다.

 %(asctime)s - DEBUG - Start of program
 %(asctime)s - DEBUG - Start of factorial(5)
 %(asctime)s - DEBUG - i is 1, total is 1
 %(asctime)s - DEBUG - i is 2, total is 2
 %(asctime)s - DEBUG - i is 3, total is 6
 %(asctime)s - DEBUG - i is 4, total is 24
 %(asctime)s - DEBUG - i is 5, total is 120
 %(asctime)s - DEBUG - End of factorial(5)
120
 %(asctime)s - DEBUG - End of program

(참고: 원본 마크다운에는 예제 코드에 버그가 있었습니다. 더 나은 학습 경험을 위해 수정된 코드가 여기에 제공됩니다. 원래 버그는 range(n + 1)이 0 부터 시작하여 total 이 항상 0 이 되게 하는 것이었습니다. 수정 사항은 range(1, n + 1)입니다.)

로그 메시지를 검토함으로써 루프의 각 반복에서 i와 total의 값을 명확하게 볼 수 있으며, 이는 프로그램의 흐름을 이해하고 버그를 찾는 강력한 방법입니다.

로깅 동작 구성하기

logging 모듈은 구성 가능성이 매우 높습니다. 표시할 메시지의 심각도 수준을 제어하거나, 로깅을 완전히 비활성화하거나, 로그 메시지를 화면 대신 파일로 리디렉션할 수 있습니다.

로깅 레벨 (Logging Levels)

다섯 가지 표준 로깅 레벨이 있으며, 각각 해당 함수가 있습니다.

레벨 (Level) 로깅 함수 (Logging Function) 설명 (Description)
DEBUG logging.debug() 문제 진단 시에만 유용한 세부 정보용.
INFO logging.info() 일반적인 이벤트를 기록하거나 예상대로 작동하는지 확인용.
WARNING logging.warning() 프로그램 실행을 중단시키지는 않지만 잠재적인 문제가 있음을 나타냄.
ERROR logging.error() 특정 작업 실패를 유발한 오류 기록용.
CRITICAL logging.critical() 프로그램 종료를 유발하려는 치명적인 오류용.

basicConfig()에서 로깅 레벨을 설정하면 Python 은 해당 레벨 및 그보다 높은 모든 레벨의 메시지를 표시합니다. 예를 들어, level=logging.INFO로 설정하면 DEBUG 메시지는 표시되지 않고 INFO, WARNING, ERROR, CRITICAL 메시지가 표시됩니다.

로깅 비활성화하기 (Disabling Logging)

디버깅 후에는 코드에서 logging 호출을 제거하지 않고 모든 로그 메시지를 끄고 싶을 수 있습니다. 이는 logging.disable()을 사용하여 수행할 수 있습니다. 이 함수는 인수로 레벨을 받아 해당 레벨 이하의 모든 로그 메시지를 억제합니다. 예를 들어, logging.disable(logging.CRITICAL)은 모든 로그 메시지를 억제합니다.

파일로 로깅하기 (Logging to a File)

로그를 콘솔에 출력하는 대신, logging.basicConfig()에 filename 키워드 인수를 전달하여 파일에 저장할 수 있습니다.

실습해 보겠습니다.

  1. configure_logging.py 파일을 엽니다.
  2. 다음 코드를 추가합니다. 이 코드는 DEBUG 레벨 이상의 모든 메시지를 myProgramLog.txt라는 파일에 기록하도록 로깅을 구성합니다.
import logging

## Configure logging to write to a file
logging.basicConfig(filename='myProgramLog.txt', level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')

logging.debug('This is a debug message.')
logging.info('This is an info message.')
logging.warning('This is a warning message.')
  1. 터미널에서 스크립트를 실행합니다.
python3 configure_logging.py

이번에는 터미널에 아무런 출력도 보이지 않을 것입니다.

  1. myProgramLog.txt 파일의 내용을 확인합니다.
cat myProgramLog.txt

모든 로그 메시지가 타임스탬프와 심각도 수준과 함께 파일에 기록된 것을 확인할 수 있습니다.

2023-10-27 10:30:00,123 - DEBUG - This is a debug message.
2023-10-27 10:30:00,123 - INFO - This is an info message.
2023-10-27 10:30:00,123 - WARNING - This is a warning message.

이로써 기본적인 Python 디버깅 기술에 대한 소개를 마칩니다.

요약

본 실습 (lab) 에서는 필수적인 여러 Python 디버깅 기술을 학습하고 연습했습니다. 먼저 raise 문을 사용하여 사용자 정의 예외 (custom exceptions) 를 생성하고 발생시키고, 이를 try...except 블록으로 처리하는 방법을 배웠습니다. 그런 다음, 로깅 목적으로 전체 예외 추적 정보 (traceback) 를 문자열로 캡처하는 traceback.format_exc() 사용법을 익혔습니다.

다음으로, 프로그램이 예상치 못한 상태에 진입했을 때 논리적 검사 (sanity checks) 를 수행하고 "빠르게 실패 (fail fast)"하는 방법으로 assert 문을 탐구했습니다. 마지막으로, logging 모듈을 깊이 있게 다루면서, 메시지를 표시하도록 구성하는 방법, 다양한 로깅 레벨로 출력을 제어하는 방법, 그리고 영구적인 기록을 위해 로그를 파일에 쓰는 방법을 배웠습니다.

이러한 기술들은 견고하고 (robust), 유지보수하기 쉬우며 (maintainable), 디버깅이 용이한 Python 애플리케이션을 작성하는 데 기본이 됩니다.