본문 바로가기
자기 개발/Python

파이썬(Python) 공부 9편 — 예외 처리 완전 정복 | try·except·else·finally·raise·사용자 정의 예외까지

by conrad 2026. 4. 1.
09 / 15 Python 공부 시리즈 — 예외 처리(try/except)
← 8편: 파일 입출력 완전 정복 보러 가기
Python 공부 시리즈 · 9편 | try · except · else · finally · raise

파이썬(Python) 공부 9편
예외 처리 try/except 완전 정복

코드를 작성하다 보면 예상치 못한 오류가 반드시 생깁니다. 없는 파일을 열거나, 0으로 나누거나, 잘못된 타입의 값이 들어올 때 — 이런 상황에서 프로그램이 갑자기 멈추는 대신 우아하게 대응하게 만드는 것이 예외 처리입니다. try/except 기본부터 else·finally·raise·사용자 정의 예외까지 한 번에 정리합니다.

try / except else / finally 예외 계층 구조 raise 사용자 정의 예외 실전 패턴

예외(Exception)란 무엇인가

파이썬에서 프로그램 실행 중에 발생하는 오류를 예외(Exception)라고 합니다. 예외가 발생하면 파이썬은 즉시 실행을 멈추고 오류 메시지를 출력합니다. 예외 처리를 하지 않으면 프로그램이 강제 종료되지만, try/except 블록으로 감싸면 예외를 잡아서 처리하고 프로그램을 계속 실행할 수 있습니다.

 
exception_demo.py — 예외 처리 없을 때 vs 있을 때
# 예외 처리 없음 → 프로그램 강제 종료
result = 10 / 0
print(result)
# ZeroDivisionError: division by zero ← 여기서 프로그램 종료!
# 아래 코드는 절대 실행 안 됨

# 예외 처리 있음 → 오류 잡고 계속 실행
try:
    result = 10 / 0
except ZeroDivisionError:
    print("0으로 나눌 수 없습니다")
print("프로그램이 계속 실행됩니다")
# 0으로 나눌 수 없습니다
# 프로그램이 계속 실행됩니다
파이썬 예외 처리 에러 오류 코드 ▲ 예외 처리는 프로그램이 예상치 못한 상황에 부딪혔을 때 우아하게 대응하도록 만드는 핵심 기술이다. 잘 짜인 예외 처리는 사용자 경험과 프로그램 안정성을 모두 높인다. (출처: Unsplash / 참고 이미지)

📋 try/except/else/finally — 전체 구조 한눈에

try: ← 예외가 발생할 수 있는 코드를 여기에 실행할 코드 ← 예외 발생 시 즉시 except로 이동
 
except 예외종류 as e: ← 특정 예외 잡기 / as e로 예외 객체 받기 예외 처리 코드 ← try에서 예외 발생 시 실행
 
else: ← 선택 사항 / try가 예외 없이 완료됐을 때만 실행 성공 시 실행 코드
 
finally: ← 선택 사항 / 예외 여부와 무관하게 항상 실행 정리 코드 (파일 닫기, 연결 해제 등)
 
try_full.py — 4개 블록 전체 예시
def divide(a, b):
    try:
        result = a / b           # 예외 가능 코드
    except ZeroDivisionError:
        print("0으로 나눌 수 없음")
        return None
    else:
        print(f"성공: {a} ÷ {b} = {result}")  # 예외 없을 때만
        return result
    finally:
        print("계산 시도 완료")           # 항상 실행

divide(10, 2)
# 성공: 10 ÷ 2 = 5.0
# 계산 시도 완료

divide(10, 0)
# 0으로 나눌 수 없음
# 계산 시도 완료   ← finally는 여기서도 실행됨

🔍 자주 만나는 내장 예외 종류

파이썬에는 수십 가지 내장 예외가 있습니다. 처음에는 자주 마주치는 것들만 알아도 충분합니다.

예외 클래스 발생 상황 예시 코드
ValueError 값의 타입은 맞지만 값 자체가 잘못됨 int("abc")
TypeError 잘못된 타입으로 연산 시도 "2" + 2
ZeroDivisionError 0으로 나누기 10 / 0
IndexError 리스트 인덱스 범위 초과 lst[100]
KeyError 딕셔너리에 없는 키 접근 d["없는키"]
AttributeError 없는 속성·메서드 접근 None.upper()
FileNotFoundError 존재하지 않는 파일 열기 open("없는파일")
NameError 정의되지 않은 변수 사용 print(미정의변수)
ImportError 모듈 import 실패 import 없는모듈
StopIteration 반복자가 더 이상 값 없음 next(빈이터레이터)
RecursionError 재귀 깊이 한도 초과 종료 조건 없는 재귀 함수
MemoryError 메모리 부족 매우 큰 데이터 생성 시
예외 계층 구조 — BaseException → Exception → 개별 예외
  • 모든 예외의 최상위 클래스는 BaseException
  • 일반적인 프로그램 오류는 Exception을 상속함
  • except Exception은 대부분의 일반 예외를 잡을 수 있음
  • except: (예외 없이) 또는 except BaseException은 KeyboardInterrupt·SystemExit까지 잡아버려 권장하지 않음

🔄 except 심화 — 여러 예외 처리하기

 
except_advanced.py
# ① 여러 except — 예외 종류별로 다르게 처리
def safe_convert(value, divisor):
    try:
        num = int(value)         # ValueError 가능
        return num / divisor    # ZeroDivisionError 가능
    except ValueError:
        print(f"'{value}'은 숫자로 변환 불가")
    except ZeroDivisionError:
        print("divisor가 0입니다")

safe_convert("abc", 2)   # 'abc'은 숫자로 변환 불가
safe_convert("10", 0)   # divisor가 0입니다

# ② 한 except에서 여러 예외 동시 처리 (튜플)
try:
    x = int("bad")
except (ValueError, TypeError) as e:
    print(f"변환 오류: {e}")

# ③ as e — 예외 객체를 변수로 받아 상세 정보 확인
try:
    lst = [1, 2, 3]
    print(lst[10])
except IndexError as e:
    print(f"예외 타입: {type(e).__name__}")  # IndexError
    print(f"메시지:   {e}")                   # list index out of range

# ④ 가장 넓은 예외 처리 — 마지막에 배치
try:
    result = risky_operation()
except FileNotFoundError:
    print("파일 없음")
except ValueError:
    print("값 오류")
except Exception as e:
    print(f"예상치 못한 오류: {e}")   # 위에서 못 잡은 나머지
안티패턴 주의 except: 만 쓰거나 except Exception: pass 는 지양
  • except: pass — 모든 예외를 조용히 무시. 버그가 숨어도 알 방법이 없어 실무에서 최악의 패턴으로 꼽힌다
  • except Exception: pass — 위와 동일한 문제. 예외를 처리하는 척하면서 실제론 아무것도 안 함
  • 올바른 방향: 최소한 logging.exception(e)print(e)라도 남겨서 어디서 무슨 오류가 났는지 추적 가능하게 할 것
구체적인 예외 클래스를 명시하는 것이 원칙이다. 범위가 넓은 except Exception은 정말 예상치 못한 오류를 마지막에 잡는 용도로만 사용하는 것이 좋다.

else와 finally — 언제 실행되나

else 블록 try가 예외 없이 완료됐을 때만 실행

elsetry 블록에서 예외가 발생하지 않았을 때만 실행됩니다. "성공했을 때 추가 작업"을 try 안에 몰아넣지 않고 else로 분리하면 코드 의도가 명확해집니다.

  • try에 몰아넣으면: 성공 시 작업에서 예외가 나도 except가 잡아버림 → 의도치 않은 동작
  • else로 분리하면: try는 "예외가 날 수 있는 부분"만 담당, else는 "성공 후 작업" 담당 → 역할 분리 명확
🔄
finally 블록 예외 여부, return 여부와 무관하게 무조건 실행

finally예외가 나든 안 나든, return을 만나든 안 만나든 반드시 실행됩니다. 파일 닫기, DB 연결 해제, 네트워크 소켓 닫기처럼 반드시 정리해야 하는 작업에 사용합니다.

  • try 안에서 return을 만나도 finally는 실행됨
  • except에서 예외를 다시 raise해도 finally는 실행됨
  • 파일·DB 작업에서 with문이 finally와 같은 역할을 자동으로 처리해줌
with문(컨텍스트 매니저)은 내부적으로 finally와 같은 원리로 동작한다. 파일 작업이라면 try/finally 대신 with문을 쓰는 것이 더 파이썬스럽다.
 
else_finally.py — 실행 흐름 비교
import json

def load_config(path: str):
    try:
        with open(path, encoding="utf-8") as f:
            data = json.load(f)        # FileNotFoundError / JSONDecodeError 가능
    except FileNotFoundError:
        print(f"설정 파일 없음: {path}")
        return {}
    except json.JSONDecodeError as e:
        print(f"JSON 파싱 오류: {e}")
        return {}
    else:
        print(f"설정 로드 성공: {len(data)}개 항목")   # 성공 시만
        return data
    finally:
        print("load_config 완료")               # 항상 실행

# 흐름 정리:
# 파일 있고 JSON 정상 → try 실행 → else 실행 → finally 실행
# 파일 없음          → try → FileNotFoundError → except 실행 → finally 실행
# JSON 파싱 오류     → try → JSONDecodeError   → except 실행 → finally 실행

🚀 raise — 예외를 직접 발생시키기

raise는 코드에서 직접 예외를 발생시킬 때 사용합니다. 함수에 잘못된 인수가 전달됐을 때, 비즈니스 규칙을 위반했을 때 등 호출자에게 "이 상황은 허용되지 않는다"라고 알릴 때 씁니다.

 
raise_example.py
# ① raise — 예외 직접 발생
def set_age(age: int):
    if not isinstance(age, int):
        raise TypeError(f"age는 int여야 합니다. 받은 타입: {type(age).__name__}")
    if age < 0 or age > 150:
        raise ValueError(f"나이는 0~150 사이여야 합니다: {age}")
    print(f"나이 설정: {age}")

try:
    set_age(-5)
except ValueError as e:
    print(f"입력 오류: {e}")
# 입력 오류: 나이는 0~150 사이여야 합니다: -5

# ② raise ... from — 예외 연쇄 (원인 예외 명시)
def load_user(user_id: int):
    try:
        data = fetch_from_db(user_id)
    except ConnectionError as e:
        raise RuntimeError("유저 로드 실패") from e  # 원인 예외 연결

# ③ raise (인수 없이) — 잡은 예외 다시 던지기
try:
    result = 10 / 0
except ZeroDivisionError:
    print("로그 기록 후 재발생")
    raise   # 같은 예외를 다시 발생시킴

🔧 사용자 정의 예외 — 나만의 예외 클래스 만들기

내장 예외만으로는 표현하기 어려운 비즈니스 로직 오류가 있을 때 사용자 정의 예외 클래스를 만들 수 있습니다. Exception을 상속하는 것이 기본 원칙이며, 예외 이름은 관례상 Error로 끝내는 것이 일반적입니다.

 
custom_exception.py
# ① 기본 사용자 정의 예외
class InsufficientBalanceError(Exception):
    """잔액 부족 예외"""
    pass

# ② 속성을 추가한 예외
class ValidationError(Exception):
    """유효성 검사 실패 예외"""
    def __init__(self, field: str, message: str):
        self.field   = field
        self.message = message
        super().__init__(f"[{field}] {message}")

# ③ 예외 계층 구성
class AppError(Exception):
    """앱 기본 예외"""
    pass

class DatabaseError(AppError):   # AppError 상속
    pass

class NetworkError(AppError):    # AppError 상속
    pass

# ④ 활용 예시
def withdraw(balance: int, amount: int):
    if amount <= 0:
        raise ValidationError("amount", "출금액은 0보다 커야 합니다")
    if amount > balance:
        raise InsufficientBalanceError(
            f"잔액 부족: 잔액 {balance}원, 출금 시도 {amount}원"
        )
    return balance - amount

try:
    new_balance = withdraw(10000, 15000)
except ValidationError as e:
    print(f"유효성 오류 — 필드: {e.field}, 사유: {e.message}")
except InsufficientBalanceError as e:
    print(f"출금 실패: {e}")
# 출금 실패: 잔액 부족: 잔액 10000원, 출금 시도 15000원

💻 실전 패턴 — 예외 처리를 잘 쓰는 방법

 
practical_patterns.py
import logging

# 패턴 1: 기본값 반환 (EAFP — 허락보다 용서를 구하라)
def get_value(d: dict, key: str, default=None):
    try:
        return d[key]
    except KeyError:
        return default

# 패턴 2: 재시도 (retry)
import time

def fetch_with_retry(url: str, retries: int = 3):
    for attempt in range(retries):
        try:
            return http_get(url)
        except ConnectionError as e:
            print(f"시도 {attempt+1}/{retries} 실패: {e}")
            if attempt == retries - 1:
                raise               # 마지막 시도면 예외 재발생
            time.sleep(2 ** attempt)  # 지수 백오프

# 패턴 3: 컨텍스트 매니저에서의 예외 처리
def process_file(path: str):
    try:
        with open(path, encoding="utf-8") as f:
            return f.read()
    except FileNotFoundError:
        logging.warning(f"파일 없음: {path}")
        return ""
    except PermissionError:
        logging.error(f"읽기 권한 없음: {path}")
        return ""

# 패턴 4: 예외 무시 (suppress) — contextlib 활용
from contextlib import suppress

with suppress(FileNotFoundError):
    open("없어도_괜찮은_파일.txt").close()
# FileNotFoundError 발생해도 조용히 무시 (suppress 사용이 pass보다 의도 명확)

📝 9편 실습 문제

실습 1 — 안전한 숫자 입력 처리
  • 사용자 입력 문자열을 받아 정수로 변환하는 함수 safe_int(value, default=0) 작성
  • 변환 불가 시 default 반환, 성공 시 변환된 정수 반환
  • 음수 입력 시 ValueError를 raise로 발생시키기
실습 2 — 사용자 정의 예외 활용
  • AgeError 사용자 정의 예외 클래스 만들기 (min_age, max_age 속성 포함)
  • 나이 범위 검증 함수에 적용하고, 예외를 잡아서 사용자 친화적 메시지 출력
 
practice_09.py — 예시 답안
# 실습 1 — safe_int
def safe_int(value: str, default: int = 0) -> int:
    try:
        result = int(value)
    except (ValueError, TypeError):
        return default
    else:
        if result < 0:
            raise ValueError(f"음수는 허용되지 않습니다: {result}")
        return result

print(safe_int("42"))       # 42
print(safe_int("abc"))      # 0 (default)
print(safe_int("3.14"))     # 0 (float 문자열은 int 변환 불가)
try:
    safe_int("-5")
except ValueError as e:
    print(e)                  # 음수는 허용되지 않습니다: -5

# 실습 2 — AgeError 사용자 정의 예외
class AgeError(ValueError):
    def __init__(self, age: int, min_age: int, max_age: int):
        self.age     = age
        self.min_age = min_age
        self.max_age = max_age
        super().__init__(f"나이 {age}는 허용 범위 {min_age}~{max_age}를 벗어남")

def check_age(age: int, min_age: int = 0, max_age: int = 120):
    if not (min_age <= age <= max_age):
        raise AgeError(age, min_age, max_age)
    return age

try:
    check_age(200)
except AgeError as e:
    print(f"오류: {e}")
    print(f"입력값 {e.age}은 최대 {e.max_age}세를 초과합니다")
# 오류: 나이 200는 허용 범위 0~120를 벗어남
# 입력값 200은 최대 120세를 초과합니다
9편 핵심 요약
  • try/except 기본: try에 예외 가능 코드, except에 처리 코드 — 예외 발생 시 프로그램 종료 대신 우아하게 대응
  • 여러 except: 예외 종류별로 다른 블록 / 튜플로 묶어서 동시 처리 / as e로 예외 객체 받기
  • else 블록: try가 예외 없이 완료됐을 때만 실행 — 성공 시 작업과 예외 처리 명확히 분리
  • finally 블록: 예외·return 여부 무관하게 항상 실행 — 파일·DB·네트워크 연결 정리에 사용
  • 안티패턴: except: pass는 버그를 숨김 — 최소한 로그나 print라도 남길 것
  • raise: 직접 예외 발생 / raise ... from e로 원인 연결 / 인수 없는 raise로 재발생
  • 사용자 정의 예외: Exception 상속 / __init__에서 속성 추가 / 계층 구조로 구성 가능
  • contextlib.suppress: 특정 예외를 조용히 무시할 때 pass보다 의도가 명확
  • EAFP 원칙: 파이썬은 "미리 검사(LBYL)"보다 "시도 후 예외 처리(EAFP)"를 더 파이썬스럽게 봄
다음 편 예고 10편 — 클래스(Class)와 객체지향 기초

class 정의 / __init__ / 인스턴스·클래스 변수 / 상속 / 캡슐화 / 매직 메서드

🐍

※ 본 포스팅은 Python 3 공식 문서(docs.python.org)의 예외 처리(Errors and Exceptions) 레퍼런스를 기반으로 작성된 학습용 콘텐츠입니다. 코드 예시는 Python 3.10 이상 환경에서 테스트되었습니다.