파이썬의 오류 관리 메커니즘과 코드 재구축 전략

예외 처리의 기초와 적용

파이썬 프로그램이 실행되는 과정에서 발생 가능한 오류를 효과적으로 제어하기 위해 '예외 (Exception)' 객체가 사용됩니다. 코드가 의도하지 않은 상황을 마주했을 때 파이썬 인터프리터는 해당 상태에 대한 정보를 담은 예외 객체를 생성합니다. 이 예외가 개발자에 의해 적절히 포착되고 처리되면 프로그램은 중단되지 않고 정상 흐름을 유지하며 계속 실행됩니다. 반대로 예외 처리 로직이 부재할 경우, 프로그램은 즉시 종료되며 구체적인 에러 내역이 포함된 추적 보고서 (Traceback) 가 표시됩니다.

비정적 오류 대응: try-except 블록

예상치 못한 상황이 발생할 수 있는 코드 영역에는 try-except 문을 사용합니다. 이는 특정 코드 블록을 실행 시도하되, 지정된 유형의 예외가 발생 시 어떻게 행동해야 할지를 정의하는 역할을 합니다.

제로 디비전 에러 처리

0 으로 나누려는 시도가 감지될 때 발생하는 ZeroDivisionError 는 대표적인 수치 연산 예외입니다. 이를 무시하면 프로세스가 강제로 종료되지만, 예외 핸들링을 통해 우아하게 처리할 수 있습니다.

def safe_division(numerator, denominator):
    """나눗셈 연산을 수행하고 제로 디비션 오류를 방지합니다."""
    try:
        result = numerator / denominator
        print(f"결과: {result}")
    except ZeroDivisionError:
        print("주의: 분모는 0 일 수 없습니다.")

safe_division(10, 0)

사용자 입력 기반의 안정화

사용자와 상호작용하는 애플리케이션에서는 잘못된 타입의 데이터가 입력될 가능성이 높습니다. 계산기 기능과 같이 두 숫자의 나눗셈 결과를 구하는 시나리오를 예로 들면, 사용자가 숫자가 아닌 문자를 입력하거나 0 을 분모로 사용할 경우 프로그램이 크래시되지 않도록 보호 장치를 마련해야 합니다.

다음 코드는 반복 구조 내에서 입력을 검증하고 다양한 예외 유형을 구분하여 처리하는 방식을 보여줍니다.

while True:
    first_val = input("첫 번째 값을 입력하세요 (종료: q): ")
    if first_val.lower() == 'q':
        break
    
    second_val = input("두 번째 값을 입력하세요 (종료: q): ")
    if second_val.lower() == 'q':
        break

    try:
        num1 = int(first_val)
        num2 = int(second_val)
        quotient = num1 / num2
        print(f"{num1} / {num2} = {quotient}")
    except ZeroDivisionError:
        print("오류: 0 으로 나눌 수 없습니다.")
    except ValueError:
        print("오류: 정수로 변환할 수 없는 값을 입력했습니다.")
    else:
        # 예외가 발생하지 않았을 때만 실행되거나 추가 로직 처리 가능
        pass

코드 품질 향상을 위한 리팩토링

기능적으로는 작동하지만 복잡성이 높거나 가독성이 떨어진 코드를 개선하는 과정을 '리팩토링'이라고 합니다. 외부 동작 변화 없이 내부 구조를 다듬어 유지보수성을 높이는 것이 핵심 목표입니다. 특히 하나의 함수가 너무 많은 책임을 지고 있다면, 이를 작은 단위의 독립적인 함수들로 분리하여 명확한 역할을 부여해야 합니다.

파일 입출력을 활용한 사용자 관리 시스템 예제

사용자 이름을 JSON 파일에 저장하고 불러오는 기능을 구현해 보겠습니다. 초기 단계에서는 모든 로직이 하나의 함수에 집중되어 있지만, 단계별로 추출 과정을 거칩니다.

단계 1: 전체 로직 통합 (Before)

초기 설계에서는 사용자를 환영하고, 파일 존재 여부를 확인하며, 새로운 이름을 등록까지 모두 단일 함수에서 처리하는 형태입니다. 이는 확장성 측면에서 비효율적입니다.

import json

def welcome_system():
    data_file = "user_profile.json"
    try:
        with open(data_file, 'r', encoding='utf-8') as file:
            stored_user = json.load(file)
            print(f"환영합니다, {stored_user}님!")
    except FileNotFoundError:
        new_user = input("계정이 없습니다. 아이디를 입력해주세요: ")
        with open(data_file, 'w', encoding='utf-8') as file:
            json.dump(new_user, file)
        print(f"{new_user}님, 가입을 환영합니다.")

welcome_system()

단계 2: 데이터 조회 기능 분리

파일에서 기존 데이터를 읽어오는 작업만을 담당하는 유틸리티 함수를 생성합니다. 파일이 없을 경우 None 을 반환하도록 하여 호출 측에서 결정을 내려게 합니다.

import json

def retrieve_saved_id(json_path):
    """저장된 사용자 ID 를 로드합니다. 없으면 None 반환."""
    try:
        with open(json_path, 'r', encoding='utf-8') as file:
            return json.load(file)
    except FileNotFoundError:
        return None

단계 3: 신규 데이터 입력 기능 분리

새로운 사용자를 받아들이고 기록하는 과정 또한 별도의 함수로 격리합니다. 각 함수는 하나의 명확한 목적을 수행하므로 테스트 및 수정이 용이해집니다.

import json

def register_new_id(json_path):
    """새로운 사용자 ID 를 입력받아 저장합니다."""
    new_id = input("신규 계정으로 아이디를 입력하세요: ")
    with open(json_path, 'w', encoding='utf-8') as file:
        json.dump(new_id, file)
    return new_id

def run_welcome_app():
    """메인 진입점: 사용자 조회 또는 등록 후 인사 메시지 출력."""
    db_file = "user_profile.json"
    
    current_user = retrieve_saved_id(db_file)
    
    if current_user:
        print(f"다시 돌아오셨군요! {current_user}님.")
    else:
        current_user = register_new_id(db_file)
        print(f"가입을 축하드립니다. {current_user}님.")

run_welcome_app()

이처럼 복잡한 논리를 기능별로 세분화함으로써, 향후 파일 포맷 변경이나 저장 경로 조정 등 요구사항 변更に 유연하게 대응할 수 있는 구조가 완성됩니다.

태그: python Exception-Handling Refactoring JSON file-io

7월 27일 21:45에 게시됨