예외 처리 및 로깅

Beginner

This tutorial is from open-source community. Access the source code

소개

이 랩에서는 Python 에서 예외 처리 (exception handling) 를 구현하는 방법을 배우게 됩니다. 디버깅 및 코드 유지 관리에 필수적인 더 나은 오류 보고를 위해 로깅 모듈 (logging module) 을 사용하는 방법을 이해하게 됩니다.

또한 reader.py 파일을 수정하여 충돌 (crashing) 대신 오류를 적절하게 처리하는 연습을 할 것입니다. 이러한 실습 경험은 강력한 Python 프로그램을 작성하는 능력을 향상시킬 것입니다.

Python 의 예외 이해

이 단계에서는 Python 의 예외 (exception) 에 대해 배우겠습니다. 예외는 프로그래밍에서 중요한 개념입니다. 예외는 프로그램 실행 중에 발생할 수 있는 예기치 않은 상황을 처리하는 데 도움이 됩니다. 또한 현재 코드가 잘못된 데이터를 처리하려고 할 때 충돌하는 이유도 파악할 것입니다. 이를 이해하면 더 강력하고 신뢰할 수 있는 Python 프로그램을 작성하는 데 도움이 됩니다.

예외란 무엇인가요?

Python 에서 예외는 프로그램 실행 중에 발생하여 정상적인 명령 흐름을 방해하는 이벤트입니다. 고속도로의 길목과 같다고 생각하면 됩니다. 모든 것이 순조롭게 진행되면 프로그램은 정해진 경로를 따릅니다. 마치 맑은 도로를 달리는 자동차와 같습니다. 그러나 오류가 발생하면 Python 은 예외 객체를 생성합니다. 이 객체는 오류 유형 및 코드에서 오류가 발생한 위치와 같은 정보가 포함된 보고서와 같습니다.

이러한 예외를 적절하게 처리하지 않으면 프로그램이 충돌합니다. 충돌이 발생하면 Python 은 트레이스백 메시지 (traceback message) 를 표시합니다. 이 메시지는 오류가 발생한 코드의 정확한 위치를 보여주는 지도와 같습니다. 디버깅에 매우 유용합니다.

현재 코드 검토

먼저 reader.py 파일 구조를 살펴보겠습니다. 이 파일에는 CSV 데이터를 읽고 변환하는 데 사용되는 함수가 포함되어 있습니다. 편집기에서 파일을 열려면 올바른 디렉토리로 이동해야 합니다. 터미널에서 cd 명령을 사용합니다.

cd /home/labex/project

이제 올바른 디렉토리에 있으므로 reader.py의 내용을 살펴보겠습니다. 이 파일에는 몇 가지 중요한 함수가 있습니다.

  1. convert_csv(): 이 함수는 데이터 행을 가져와 제공된 변환기 함수를 사용하여 변환합니다. 이는 원자재 (데이터 행) 를 가져와 특정 레시피 (변환기 함수) 에 따라 다른 형태로 변환하는 기계와 같습니다.
  2. csv_as_dicts(): 이 함수는 CSV 데이터를 읽어 딕셔너리 목록으로 변환합니다. 또한 유형 변환 (type conversion) 을 수행합니다. 즉, 딕셔너리의 각 데이터 조각이 문자열, 정수 또는 부동 소수점과 같은 올바른 유형인지 확인합니다.
  3. read_csv_as_dicts(): 이것은 래퍼 함수 (wrapper function) 입니다. csv_as_dicts() 함수를 호출하여 작업을 완료하는 관리자와 같습니다.

문제 시연

코드가 잘못된 데이터를 처리하려고 할 때 어떤 일이 발생하는지 살펴보겠습니다. Python 인터프리터 (interpreter) 를 열어보겠습니다. 이는 Python 코드를 대화식으로 테스트할 수 있는 놀이터와 같습니다. Python 인터프리터를 열려면 터미널에서 다음 명령을 사용합니다.

python3

Python 인터프리터가 열리면 missing.csv 파일을 읽어보겠습니다. 이 파일에는 누락되거나 잘못된 데이터가 포함되어 있습니다. reader.py 파일의 read_csv_as_dicts() 함수를 사용하여 데이터를 읽습니다.

from reader import read_csv_as_dicts
port = read_csv_as_dicts('missing.csv', types=[str, int, float])

이 코드를 실행하면 다음과 같은 오류 메시지가 표시됩니다.

Traceback (most recent call last):
  ...
ValueError: invalid literal for int() with base 10: ''

이 오류는 코드가 빈 문자열을 정수로 변환하려고 하기 때문에 발생합니다. 빈 문자열은 유효한 정수를 나타내지 않으므로 Python 은 변환할 수 없습니다. 함수는 처음 만나는 오류에서 충돌하며 파일의 나머지 유효한 데이터 처리를 중단합니다.

Python 인터프리터를 종료하려면 다음 명령을 입력합니다.

exit()

오류 흐름 이해

오류는 convert_csv() 함수, 특히 다음 줄에서 발생합니다.

return list(map(lambda row: converter(headers, row), rows))

map() 함수는 converter 함수를 rows 목록의 각 행에 적용합니다. converter 함수는 유형 (str, int, float) 을 각 행에 적용하려고 시도합니다. 그러나 누락된 데이터가 있는 행을 만나면 실패합니다. map() 함수에는 예외를 처리하는 내장된 방법이 없습니다. 따라서 예외가 발생하면 전체 프로세스가 충돌합니다.

다음 단계에서는 이러한 예외를 적절하게 처리하도록 코드를 수정합니다. 즉, 충돌하는 대신 프로그램이 오류를 처리하고 나머지 데이터를 계속 처리할 수 있습니다.

예외 처리 구현

이 단계에서는 코드를 더욱 강력하게 만드는 데 집중하겠습니다. 프로그램이 잘못된 데이터를 만나면 종종 충돌합니다. 그러나 예외 처리 (exception handling) 라는 기술을 사용하여 이러한 문제를 적절하게 처리할 수 있습니다. 이를 구현하기 위해 reader.py 파일을 수정합니다. 예외 처리를 통해 프로그램은 예기치 않은 데이터에 직면하더라도 갑자기 중단되는 대신 계속 실행될 수 있습니다.

Try-Except 블록 이해

Python 은 try-except 블록을 사용하여 예외를 처리하는 강력한 방법을 제공합니다. 작동 방식을 자세히 살펴보겠습니다.

try:
    ## 예외를 발생시킬 수 있는 코드
    result = risky_operation()
except SomeExceptionType as e:
    ## 예외가 발생할 경우 실행되는 코드
    handle_exception(e)

try 블록에는 예외를 발생시킬 수 있는 코드를 넣습니다. 예외는 프로그램 실행 중에 발생하는 오류입니다. 예를 들어, 숫자를 0 으로 나누려고 하면 Python 은 ZeroDivisionError 예외를 발생시킵니다. try 블록에서 예외가 발생하면 Python 은 try 블록의 코드 실행을 중단하고 일치하는 except 블록으로 이동합니다. except 블록에는 예외를 처리하는 코드가 포함되어 있습니다. SomeExceptionType은 잡으려는 예외의 유형입니다. 특정 유형의 예외를 잡거나 일반 Exception을 사용하여 모든 유형의 예외를 잡을 수 있습니다. as e 부분은 오류에 대한 정보가 포함된 예외 객체에 액세스할 수 있도록 합니다.

코드 수정

이제 try-except 블록에 대해 배운 내용을 convert_csv() 함수에 적용해 보겠습니다. 편집기에서 reader.py 파일을 엽니다.

  1. 현재 convert_csv() 함수를 다음 코드로 바꿉니다.
def convert_csv(rows, converter, header=True):
    """
    Convert a sequence of rows to an output sequence according to a conversion function.
    """
    if header:
        headers = next(rows)
    else:
        headers = []

    result = []
    for row_idx, row in enumerate(rows, start=1):
        try:
            ## Try to convert the row
            result.append(converter(headers, row))
        except Exception as e:
            ## Print a warning message for bad rows
            print(f"Row {row_idx}: Bad row: {row}")
            continue

    return result

이 새로운 구현에서:

  • 각 행을 처리하기 위해 map() 대신 for 루프를 사용합니다. 이렇게 하면 각 행의 처리를 더 잘 제어할 수 있습니다.
  • 변환 코드를 try-except 블록으로 묶습니다. 즉, 행 변환 중에 예외가 발생하더라도 프로그램이 충돌하지 않습니다. 대신 except 블록으로 이동합니다.
  • except 블록에서 잘못된 행에 대한 오류 메시지를 출력합니다. 이렇게 하면 문제가 있는 행을 식별하는 데 도움이 됩니다.
  • 오류 메시지를 출력한 후 continue 문을 사용하여 현재 행을 건너뛰고 나머지 행 처리를 계속합니다.

이러한 변경을 수행한 후 파일을 저장합니다.

변경 사항 테스트

missing.csv 파일로 수정된 코드를 테스트해 보겠습니다. 먼저 터미널에서 다음 명령을 실행하여 Python 인터프리터를 엽니다.

python3

Python 인터프리터에 들어가면 다음 코드를 실행합니다.

from reader import read_csv_as_dicts
port = read_csv_as_dicts('missing.csv', types=[str, int, float])
print(f"Number of valid rows processed: {len(port)}")

이 코드를 실행하면 문제가 있는 각 행에 대한 오류 메시지가 표시됩니다. 그러나 프로그램은 계속 처리하고 유효한 행을 반환합니다. 다음은 표시될 수 있는 예입니다.

Row 4: Bad row: ['C', '', '53.08']
Row 7: Bad row: ['DIS', '50', 'N/A']
Row 8: Bad row: ['GE', '', '37.23']
Row 13: Bad row: ['INTC', '', '21.84']
Row 17: Bad row: ['MCD', '', '51.11']
Row 19: Bad row: ['MO', '', '70.09']
Row 22: Bad row: ['PFE', '', '26.40']
Row 26: Bad row: ['VZ', '', '42.92']
Number of valid rows processed: 20

또한 프로그램이 유효한 데이터로 올바르게 작동하는지 확인해 보겠습니다. Python 인터프리터에서 다음 코드를 실행합니다.

valid_port = read_csv_as_dicts('valid.csv', types=[str, int, float])
print(f"Number of valid rows processed: {len(valid_port)}")

모든 행이 오류 없이 처리되는 것을 볼 수 있습니다. 다음은 출력의 예입니다.

Number of valid rows processed: 17

Python 인터프리터를 종료하려면 다음 명령을 실행합니다.

exit()

이제 코드가 더 강력해졌습니다. 충돌하는 대신 잘못된 행을 건너뛰어 잘못된 데이터를 적절하게 처리할 수 있습니다. 이렇게 하면 프로그램의 신뢰성과 사용자 편의성이 향상됩니다.

로깅 구현

이 단계에서는 코드를 개선할 것입니다. 간단한 print 메시지를 사용하는 대신, 적절한 로깅을 위해 Python 의 logging 모듈을 사용합니다. 로깅은 특히 오류를 처리하고 코드의 흐름을 이해하는 데 있어 프로그램이 수행하는 작업을 추적하는 훌륭한 방법입니다.

로깅 모듈 이해

Python 의 logging 모듈은 애플리케이션에서 로그 메시지를 보낼 수 있는 유연한 방법을 제공합니다. 간단한 print 문을 사용하는 것보다 훨씬 강력합니다. 다음과 같은 기능을 수행할 수 있습니다.

  1. 다양한 로그 레벨 (DEBUG, INFO, WARNING, ERROR, CRITICAL): 이러한 레벨은 메시지의 중요도를 분류하는 데 도움이 됩니다. 예를 들어, DEBUG 는 개발 중에 유용한 자세한 정보를 위한 것이고, CRITICAL 은 프로그램을 중지시킬 수 있는 심각한 오류를 위한 것입니다.
  2. 구성 가능한 출력 형식: 타임스탬프 또는 기타 유용한 정보를 추가하는 등 로그 메시지의 모양을 결정할 수 있습니다.
  3. 메시지를 다른 출력 (콘솔, 파일 등) 으로 보낼 수 있습니다. 콘솔에 로그 메시지를 표시하거나, 파일에 저장하거나, 원격 서버로 보낼 수도 있습니다.
  4. 심각도에 따른 로그 필터링: 로그 레벨에 따라 표시할 메시지를 제어할 수 있습니다.

reader.py 에 로깅 추가

이제 로깅 모듈을 사용하도록 코드를 변경해 보겠습니다. reader.py 파일을 엽니다.

먼저 logging 모듈을 가져오고 이 모듈에 대한 로거를 설정해야 합니다. 파일 맨 위에 다음 코드를 추가합니다.

import logging

## Set up a logger for this module
logger = logging.getLogger(__name__)

import logging 문은 logging 모듈을 가져와서 해당 함수를 사용할 수 있도록 합니다. logging.getLogger(__name__)은 이 특정 모듈에 대한 로거를 생성합니다. __name__을 사용하면 로거가 모듈과 관련된 고유한 이름을 갖게 됩니다.

다음으로, convert_csv() 함수를 수정하여 print 문 대신 로깅을 사용합니다. 업데이트된 코드는 다음과 같습니다.

def convert_csv(rows, converter, header=True):
    """
    Convert a sequence of rows to an output sequence according to a conversion function.
    """
    if header:
        headers = next(rows)
    else:
        headers = []

    result = []
    for row_idx, row in enumerate(rows, start=1):
        try:
            ## Try to convert the row
            result.append(converter(headers, row))
        except Exception as e:
            ## Log a warning message for bad rows
            logger.warning(f"Row {row_idx}: Bad row: {row}")
            ## Log the reason at debug level
            logger.debug(f"Row {row_idx}: Reason: {str(e)}")
            continue

    return result

여기서 주요 변경 사항은 다음과 같습니다.

  • 오류 메시지에 대해 print()logger.warning()으로 바꿨습니다. 이렇게 하면 메시지가 적절한 경고 레벨로 기록되고 나중에 표시 여부를 제어할 수 있습니다.
  • 예외에 대한 세부 정보를 포함하는 새로운 logger.debug() 메시지를 추가했습니다. 이렇게 하면 무엇이 잘못되었는지에 대한 자세한 정보를 얻을 수 있지만 로깅 레벨이 DEBUG 이하로 설정된 경우에만 표시됩니다.
  • str(e)는 예외를 문자열로 변환하므로 로그 메시지에 오류 이유를 표시할 수 있습니다.

이러한 변경을 수행한 후 파일을 저장합니다.

로깅 테스트

로깅이 활성화된 상태에서 코드를 테스트해 보겠습니다. 터미널에서 다음 명령을 실행하여 Python 인터프리터를 엽니다.

python3

Python 인터프리터에 들어가면 다음 코드를 실행합니다.

import logging
import reader

## Configure logging level to see all messages
logging.basicConfig(level=logging.DEBUG)

port = reader.read_csv_as_dicts('missing.csv', types=[str, int, float])
print(f"Number of valid rows processed: {len(port)}")

여기서는 먼저 logging 모듈과 reader 모듈을 가져옵니다. 그런 다음 logging.basicConfig(level=logging.DEBUG)를 사용하여 로깅 레벨을 DEBUG 로 설정합니다. 즉, DEBUG, INFO, WARNING, ERROR 및 CRITICAL 을 포함한 모든 로그 메시지를 볼 수 있습니다. 그런 다음 reader 모듈에서 read_csv_as_dicts 함수를 호출하고 처리된 유효한 행의 수를 출력합니다.

다음과 같은 출력이 표시됩니다.

WARNING:reader:Row 4: Bad row: ['C', '', '53.08']
DEBUG:reader:Row 4: Reason: invalid literal for int() with base 10: ''
WARNING:reader:Row 7: Bad row: ['DIS', '50', 'N/A']
DEBUG:reader:Row 7: Reason: could not convert string to float: 'N/A'
...
Number of valid rows processed: 20

로깅 모듈이 각 메시지에 접두사를 추가하여 로그 레벨 (WARNING/DEBUG) 과 모듈 이름을 표시하는 것을 확인합니다.

이제 로그 레벨을 변경하여 경고만 표시하는 경우 어떻게 되는지 살펴보겠습니다. Python 인터프리터에서 다음 코드를 실행합니다.

## Reset the logging configuration
import logging
logging.basicConfig(level=logging.WARNING)

port = reader.read_csv_as_dicts('missing.csv', types=[str, int, float])

이번에는 logging.basicConfig(level=logging.WARNING)를 사용하여 로깅 레벨을 WARNING 으로 설정했습니다. 이제 WARNING 메시지만 표시되고 DEBUG 메시지는 숨겨집니다.

WARNING:reader:Row 4: Bad row: ['C', '', '53.08']
WARNING:reader:Row 7: Bad row: ['DIS', '50', 'N/A']
...

이는 서로 다른 로깅 레벨을 사용하는 것의 장점을 보여줍니다. 코드를 변경하지 않고 로그에 표시되는 세부 정보를 제어할 수 있습니다.

Python 인터프리터를 종료하려면 다음 명령을 실행합니다.

exit()

축하합니다! 이제 Python 프로그램에서 적절한 예외 처리 및 로깅을 구현했습니다. 이렇게 하면 코드가 더 안정적이고 오류가 발생할 때 더 나은 정보를 얻을 수 있습니다.

요약

이 Lab 에서 Python 의 예외 처리 (exception handling) 및 로깅 (logging) 에 대한 몇 가지 핵심 개념을 배웠습니다. 먼저, 데이터 처리 중에 예외가 발생하는 방식을 이해하고 try-except 블록을 구현하여 이를 적절하게 처리했습니다. 또한 오류가 발생할 때 유효한 데이터를 계속 처리하도록 코드를 수정했습니다.

둘째, Python 의 로깅 모듈과 print 문에 비해 갖는 이점에 대해 배웠습니다. WARNING 및 DEBUG 와 같은 다양한 로그 레벨을 구현하고, 다양한 세부 수준에 맞게 로깅을 구성하는 방법을 확인했습니다. 이러한 기술은 특히 오류가 발생하기 쉬운 외부 데이터를 처리하거나, 무인 애플리케이션을 구축하거나, 진단 정보가 필요한 시스템을 개발할 때 강력한 Python 애플리케이션을 작성하는 데 매우 중요합니다. 이제 이러한 기술을 Python 프로젝트에 적용하여 안정성과 유지 관리성을 향상시킬 수 있습니다.