PythonPython 기초 · 6입문

예외 처리 — 오류가 나도 멈추지 않는 코드 만들기

Python예외처리tryexcept오류에러

오류는 반드시 발생한다

완벽한 코드를 써도 예상 못한 오류는 발생합니다. 파일이 없을 수도, 네트워크가 끊길 수도, 사용자가 이상한 값을 입력할 수도 있습니다.

# ❌ 예외 처리 없음 → 프로그램 전체 종료
number = int(input("숫자 입력: "))  # "abc" 입력하면 ValueError!
print(f"입력값: {number}")
# 이후 코드가 전혀 실행되지 않음

# ✅ 예외 처리 → 오류 후에도 계속 실행
try:
    number = int(input("숫자 입력: "))
    print(f"입력값: {number}")
except ValueError:
    print("숫자를 입력해주세요.")
print("프로그램 계속 실행 중")  # 항상 실행됨

try / except 기본 구조

flowchart TB
    TRY["try 블록\n오류가 날 수 있는 코드"]
    TRY -->|"성공"| ELSE["else 블록\n오류 없을 때 실행"]
    TRY -->|"오류 발생"| EXCEPT["except 블록\n오류 처리"]
    ELSE & EXCEPT --> FINALLY["finally 블록\n항상 실행"]
try:
    result = 10 / 0
except ZeroDivisionError:
    print("0으로 나눌 수 없습니다.")
else:
    print(f"결과: {result}")  # 오류 없을 때만
finally:
    print("항상 실행")        # 오류 유무 관계없이

주요 내장 예외

# ValueError: 잘못된 값
int("hello")    # ValueError: invalid literal

# TypeError: 잘못된 타입
"문자" + 123    # TypeError: can only concatenate str (not "int")

# KeyError: 딕셔너리에 없는 키
d = {"a": 1}
d["b"]          # KeyError: 'b'

# IndexError: 리스트 범위 초과
lst = [1, 2, 3]
lst[10]         # IndexError: list index out of range

# FileNotFoundError: 파일 없음
open("없는파일.txt")  # FileNotFoundError

# ZeroDivisionError: 0으로 나눔
10 / 0          # ZeroDivisionError

# AttributeError: 없는 속성/메서드
None.upper()    # AttributeError: 'NoneType' has no attribute 'upper'

여러 예외 처리

def safe_divide(a, b):
    try:
        result = a / b
        return result
    except ZeroDivisionError:
        print("0으로 나눌 수 없습니다.")
        return None
    except TypeError:
        print("숫자만 입력 가능합니다.")
        return None

# 여러 예외를 한 번에 처리
try:
    value = int(input("숫자: "))
    result = 100 / value
except (ValueError, ZeroDivisionError) as e:
    print(f"오류 발생: {e}")

# 모든 예외 잡기 (마지막 수단)
try:
    risky_operation()
except Exception as e:
    print(f"예상치 못한 오류: {type(e).__name__}: {e}")

예외 정보 활용

try:
    with open("data.json", "r") as f:
        import json
        data = json.load(f)
except FileNotFoundError as e:
    print(f"파일을 찾을 수 없습니다: {e.filename}")
except json.JSONDecodeError as e:
    print(f"JSON 파싱 오류 (줄 {e.lineno}): {e.msg}")

커스텀 예외 만들기

도메인에 맞는 의미 있는 예외를 정의합니다.

class ValidationError(Exception):
    """입력값 검증 실패"""
    pass

class APIError(Exception):
    """API 호출 오류"""
    def __init__(self, status_code: int, message: str):
        self.status_code = status_code
        super().__init__(f"[{status_code}] {message}")

# 사용
def validate_age(age: int):
    if age < 0:
        raise ValidationError("나이는 0 이상이어야 합니다.")
    if age > 150:
        raise ValidationError("유효하지 않은 나이입니다.")
    return age

try:
    validate_age(-5)
except ValidationError as e:
    print(f"검증 오류: {e}")

실전 패턴: LLM API 오류 처리

import time
from openai import OpenAI, RateLimitError, APIConnectionError

client = OpenAI()

def call_llm_safely(prompt: str, max_retries: int = 3) -> str:
    for attempt in range(max_retries):
        try:
            response = client.chat.completions.create(
                model="gpt-4o-mini",
                messages=[{"role": "user", "content": prompt}]
            )
            return response.choices[0].message.content
            
        except RateLimitError:
            wait = 2 ** attempt  # 1초, 2초, 4초 ...
            print(f"요청 한도 초과. {wait}초 후 재시도 ({attempt + 1}/{max_retries})")
            time.sleep(wait)
            
        except APIConnectionError:
            print("네트워크 오류. 재시도 중...")
            time.sleep(1)
            
        except Exception as e:
            print(f"예상치 못한 오류: {e}")
            raise  # 알 수 없는 오류는 다시 올림
    
    raise Exception("최대 재시도 횟수 초과")

로깅: 오류를 파일에 기록

print로 오류를 출력하면 나중에 추적이 어렵습니다.

import logging

# 로거 설정
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler("app.log", encoding="utf-8"),
        logging.StreamHandler()  # 콘솔에도 출력
    ]
)

logger = logging.getLogger(__name__)

# 사용
def process_data(data):
    logger.info(f"데이터 처리 시작: {len(data)}건")
    
    try:
        result = expensive_operation(data)
        logger.info("처리 완료")
        return result
    except Exception as e:
        logger.error(f"처리 실패: {e}", exc_info=True)  # 스택 트레이스 포함
        raise

# 로그 레벨
logger.debug("디버그 정보")     # 개발 시 상세 정보
logger.info("일반 정보")        # 정상 동작 기록
logger.warning("경고")          # 문제 가능성
logger.error("오류")            # 오류 발생
logger.critical("심각한 오류")  # 프로그램 중단 수준

assert: 개발 중 조건 검증

def calculate_discount(price: float, rate: float) -> float:
    assert 0 <= rate <= 1, f"할인율은 0~1 사이여야 합니다. 입력값: {rate}"
    assert price >= 0, "가격은 0 이상이어야 합니다."
    return price * (1 - rate)

try:
    calculate_discount(10000, 1.5)  # AssertionError
except AssertionError as e:
    print(f"검증 실패: {e}")

개발·테스트 단계에서 잘못된 입력을 빠르게 발견하는 데 유용합니다.


정리

구문역할
try오류 가능성 있는 코드
except ExceptionType특정 오류 처리
except Exception as e오류 객체 변수에 저장
else오류 없을 때만 실행
finally항상 실행 (정리 작업)
raise예외 직접 발생
logging오류를 파일에 체계적 기록

다음 편에서는 클래스와 객체 — 데이터와 기능을 하나로 묶는 객체지향 프로그래밍을 배웁니다.

궁금한 점이 있으신가요?

협업·의뢰는 아래로, 가벼운 소통은 인스타그램 @bluefox._.hi도 환영이에요.