Skip to main content

예외 처리 (Exception Handling)

학습 목표

  • try/except/else/finally 구문의 구조와 실행 순서를 이해한다
  • Python 내장 예외의 계층 구조를 파악할 수 있다
  • 사용자 정의 예외를 만들고 활용할 수 있다
  • 예외 처리의 적절한 사용 범위를 판단할 수 있다

왜 중요한가

프로그램은 항상 예상치 못한 상황에 직면합니다. 파일이 없거나, 네트워크가 끊기거나, 잘못된 입력이 들어올 수 있습니다. 예외 처리는 이런 오류 상황에서 프로그램이 우아하게 대응하도록 합니다. ML/DL에서는 OOM(메모리 부족), NaN 손실, 체크포인트 저장 실패 등의 상황에서 학습을 안전하게 복구하는 데 필수적입니다.

try / except

# 기본 구조
try:
    result = 10 / 0
except ZeroDivisionError:
    print("0으로 나눌 수 없습니다")

# 여러 예외 처리
try:
    value = int(input("숫자를 입력하세요: "))
    result = 100 / value
except ValueError:
    print("유효한 숫자가 아닙니다")
except ZeroDivisionError:
    print("0으로 나눌 수 없습니다")

# 여러 예외를 한 번에
try:
    result = int("abc") / 0
except (ValueError, ZeroDivisionError) as e:
    print(f"오류 발생: {e}")

# 예외 객체 접근
try:
    open("없는파일.txt")
except FileNotFoundError as e:
    print(f"에러 타입: {type(e).__name__}")
    print(f"에러 메시지: {e}")

try / except / else / finally

try:
    file = open("data.txt", "r")
    content = file.read()
except FileNotFoundError:
    print("파일을 찾을 수 없습니다")
    content = ""
else:
    # 예외가 발생하지 않았을 때만 실행
    print(f"파일 읽기 성공: {len(content)}자")
finally:
    # 예외 발생 여부와 관계없이 항상 실행
    print("파일 처리 완료")

실행 순서

상황tryexceptelsefinally
예외 없음실행건너뜀실행실행
예외 발생 (처리됨)예외 시점까지실행건너뜀실행
예외 발생 (미처리)예외 시점까지건너뜀건너뜀실행 후 예외 전파

주요 내장 예외

# 예외 계층 구조 (일부)
# BaseException
#  +-- SystemExit
#  +-- KeyboardInterrupt
#  +-- Exception
#       +-- ValueError
#       +-- TypeError
#       +-- KeyError
#       +-- IndexError
#       +-- FileNotFoundError
#       +-- AttributeError
#       +-- RuntimeError
#       +-- StopIteration
#       +-- ZeroDivisionError
예외발생 상황예시
ValueError값이 부적절int("abc")
TypeError타입이 부적절"2" + 3
KeyError딕셔너리 키 없음d["없는키"]
IndexError인덱스 범위 초과[1,2][5]
FileNotFoundError파일 없음open("없는파일")
AttributeError속성/메서드 없음None.split()
ZeroDivisionError0으로 나눔1 / 0

raise - 예외 발생시키기

# 명시적 예외 발생
def set_age(age):
    if age < 0:
        raise ValueError(f"나이는 음수일 수 없습니다: {age}")
    if age > 150:
        raise ValueError(f"비현실적인 나이입니다: {age}")
    return age

try:
    set_age(-5)
except ValueError as e:
    print(e)  # "나이는 음수일 수 없습니다: -5"

# 예외 다시 발생 (re-raise)
try:
    result = risky_operation()
except Exception as e:
    print(f"오류 로깅: {e}")
    raise  # 예외를 상위로 전파

사용자 정의 예외

# 기본 사용자 정의 예외
class ValidationError(Exception):
    """데이터 검증 실패 예외"""
    pass

# 속성을 가진 예외
class TrainingError(Exception):
    """모델 학습 중 발생하는 예외"""
    def __init__(self, message, epoch=None, loss=None):
        super().__init__(message)
        self.epoch = epoch
        self.loss = loss

# 예외 계층 구조 설계
class AppError(Exception):
    """애플리케이션 기본 예외"""
    pass

class DataError(AppError):
    """데이터 관련 예외"""
    pass

class ModelError(AppError):
    """모델 관련 예외"""
    pass

# 사용
try:
    raise TrainingError("NaN 손실 감지", epoch=15, loss=float("nan"))
except TrainingError as e:
    print(f"학습 오류: {e}")
    print(f"에포크: {e.epoch}, 손실: {e.loss}")

컨텍스트 매니저 (with)

with 문은 리소스의 안전한 획득과 해제를 보장합니다.
# with 문 - 파일 자동 닫기
with open("data.txt", "r") as f:
    content = f.read()
# f는 자동으로 닫힘 (예외가 발생해도)

# 여러 리소스
with open("input.txt") as fin, open("output.txt", "w") as fout:
    fout.write(fin.read())

AI/ML에서의 활용

import math

# 학습 중 NaN 감지 및 복구
def train_epoch(model, data_loader, optimizer):
    for batch in data_loader:
        loss = compute_loss(model, batch)

        if math.isnan(loss) or math.isinf(loss):
            raise TrainingError(
                f"비정상 손실값: {loss}",
                loss=loss
            )

        optimizer.step()

# 체크포인트 저장 안전 패턴
def save_checkpoint(model, path):
    """체크포인트를 안전하게 저장합니다."""
    temp_path = f"{path}.tmp"
    try:
        # 임시 파일에 먼저 저장
        save_model(model, temp_path)
        # 성공하면 실제 경로로 이동
        os.rename(temp_path, path)
    except Exception as e:
        # 실패 시 임시 파일 정리
        if os.path.exists(temp_path):
            os.remove(temp_path)
        raise  # 예외 전파

# 학습 루프 전체 예외 처리
try:
    for epoch in range(num_epochs):
        train_epoch(model, train_loader, optimizer)
        val_loss = evaluate(model, val_loader)
        save_checkpoint(model, f"checkpoint_epoch{epoch}.pt")
except KeyboardInterrupt:
    print("사용자가 학습을 중단했습니다")
    save_checkpoint(model, "checkpoint_interrupted.pt")
except TrainingError as e:
    print(f"학습 오류: {e}")
    # 마지막 정상 체크포인트로 복구 가능
except Exception as e:
    print(f"예상치 못한 오류: {e}")
    raise
finally:
    print("학습 종료")
너무 넓은 예외 처리(except Exception)는 디버깅을 어렵게 합니다. 가능한 한 구체적인 예외를 잡으세요.
# 나쁜 예
try:
    result = process(data)
except:  # 모든 예외를 잡음 (KeyboardInterrupt 포함!)
    pass

# 좋은 예
try:
    result = process(data)
except ValueError as e:
    logger.warning(f"값 오류: {e}")
    result = default_value
LBYL(Look Before You Leap): 실행 전에 조건 확인 (if key in dict). EAFP(Easier to Ask Forgiveness than Permission): 일단 실행하고 예외 처리 (try/except). Python은 EAFP 방식을 권장합니다.
except BaseExceptionKeyboardInterrupt, SystemExit 등도 잡으므로 위험합니다. 일반적으로 except Exception까지만 사용하세요.

체크리스트

  • try/except/else/finally의 실행 순서를 설명할 수 있다
  • 적절한 내장 예외를 선택하여 처리할 수 있다
  • 사용자 정의 예외를 만들고 활용할 수 있다
  • with 문으로 리소스를 안전하게 관리할 수 있다
  • 예외 처리의 범위를 적절히 좁힐 수 있다

다음 문서