# 파이썬 예외 처리: try, except, else, finally, raise와 사용자 정의 예외

URL: https://skillaria.top/ko/guides/python/python-exceptions

파이썬 예외는 실행을 멈추게 하는 오류 객체입니다. try/except로 잡고, else는 오류가 없을 때, finally는 항상 실행됩니다. raise 문으로 직접 발생시킬 수 있으며(예: raise ValueError("금액이 음수입니다")), 사용자 정의 예외는 Exception을 상속한 클래스로 만듭니다.

파이썬 예외는 오류가 났다는 신호를 나타내는 객체이며, 평소의 실행 흐름을 멈춥니다. 아무도 잡지 않으면 파이썬은 트레이스백을 출력하고 프로그램을 종료합니다. 예외는 `try/except` 구문으로 잡고, `raise` 문으로 직접 발생시킵니다. 이 글에서는 동작 방식, 자주 나오는 오류 종류, 그리고 예외 처리가 오류를 숨기는 일이 되지 않도록 하는 방법을 정리합니다.

## 파이썬 예외란

연산을 끝낼 수 없을 때 — 0으로 나누기, 없는 키 조회, 글자를 숫자로 바꾸기 — 파이썬은 예외 객체를 만들어 '던집니다'. 현재 함수의 실행은 멈추고, 예외는 호출 체인을 따라 올라가며 적절한 핸들러를 찾습니다. 찾지 못하면 프로그램이 종료됩니다.

```
>>> 10 / 0
Traceback (most recent call last):
  File "", line 1, in
ZeroDivisionError: division by zero
```

자주 묻는 질문에 답하면, 파이썬에서 0으로 나누면 `ZeroDivisionError`가 발생합니다. `/`, `//`, `%` 모두 해당합니다.

## 트레이스백 읽는 법

트레이스백은 프로그램이 오류까지 어떻게 도달했는지 보여 주는 보고서입니다. 아래에서 위로 읽는 것이 편합니다. 마지막 줄이 가장 중요합니다. 예외의 종류와 메시지가 나옵니다. 그 위에는 오류가 난 코드 줄이 있고, 더 위에는 그 함수를 호출한 곳이 있습니다. 시작점까지 이런 식으로 이어집니다.

```
Traceback (most recent call last):
  File "tracker.py", line 9, in
    total = parse_amount(row)
  File "tracker.py", line 4, in parse_amount
    return float(raw.strip())
           ~~~~~^^^^^^^^^^^^^
ValueError: could not convert string to float: '백'
```

Python 3.11부터는 오류가 난 코드 줄 아래에 `~~~^^^` 표시가 붙습니다(PEP 657). 어느 부분 식이 깨졌는지 정확히 보여 줍니다. 한 줄에 함수 호출이 여러 번 있을 때 특히 도움이 됩니다.

### 'Did you mean' 힌트

Python 3.10부터는 이름을 잘못 쓰면 비슷한 이름을 제안합니다. Python 3.15에서는 힌트가 더 똑똑해졌습니다. 다른 언어의 습관도 알아봅니다. `list.push`를 호출하면 `append`를, `"text".toUpperCase()`를 호출하면 `upper`를 제안합니다. 힌트는 중첩된 필드까지 살핍니다. 딕셔너리가 객체의 `totals` 속성에 있는데 객체에서 바로 `items()`를 호출하면 `.totals.items`를 제안합니다. 가까운 이름은 `delattr()`에서도 제안됩니다.

## 파이썬 예외의 종류

내장 예외는 수십 가지지만, 처음에는 다음 몇 가지를 자주 보게 됩니다.

| 예외 | 발생하는 경우 | 예시 |
| --- | --- | --- |

| `NameError` | 정의되지 않은 이름, 대부분 오타 | `print(totl)` |

| `TypeError` | 해당 타입에 맞지 않는 연산 | `"5" + 3` |

| `ValueError` | 타입은 맞지만 값이 잘못됨 | `int("백")` |

| `KeyError` | 딕셔너리에 없는 키 | `d["missing"]` |

| `IndexError` | 리스트 범위를 벗어난 인덱스 | `[1, 2][5]` |

| `AttributeError` | 객체에 없는 메서드나 필드 | `[].push(1)` |

| `ZeroDivisionError` | 0으로 나누기 | `1 / 0` |

| `FileNotFoundError` | 파일이 없음 | `open("nope.txt")` |

`SyntaxError`(와 그 하위인 `IndentationError`)는 코드를 읽는 단계에서 발생하며, 코드가 실행되기 전입니다. 그래서 같은 파일의 `try`로는 잡을 수 없습니다.

### 예외의 계층 구조

예외는 클래스이며, 계층 구조를 이룹니다. 최상위는 `BaseException`입니다. 그 아래에 일반적인 오류를 담는 `Exception`과 몇 가지 시스템 예외(`KeyboardInterrupt`, `SystemExit`, `GeneratorExit`)가 있습니다.

```
BaseException
 ├── KeyboardInterrupt
 ├── SystemExit
 └── Exception
      ├── ArithmeticError
      │    └── ZeroDivisionError
      ├── LookupError
      │    ├── KeyError
      │    └── IndexError
      ├── OSError
      │    └── FileNotFoundError
      ├── TypeError
      └── ValueError
```

계층 구조는 예외를 잡을 때 중요합니다. `except LookupError`는 `KeyError`와 `IndexError`를 모두 잡습니다. `KeyboardInterrupt`는 코드의 오류가 아닙니다. 사용자가 Ctrl+C를 눌렀다는 뜻입니다. `Exception`을 상속하지 않도록 설계된 이유는, `except Exception`이 프로그램 중단을 막지 않게 하기 위해서입니다.

## 예외 처리: try/except/else/finally

오류가 날 수 있는 코드는 `try` 블록에 넣습니다. `except` 블록에는 어떤 예외를 잡을지와 그때 할 일을 적습니다. 파이썬에서 예외 처리에 쓰는 블록이 바로 `except`입니다.

```
def parse_amount(raw):
    try:
        value = float(raw)
    except ValueError as e:
        print(f"줄을 건너뜁니다: {e}")
        return None
    else:
        return value          # 오류가 없었을 때만 실행
    finally:
        print("파싱 완료")       # 항상 실행

print(parse_amount("250"))
```

출력 순서는 먼저 '파싱 완료', 그다음 `250.0`입니다. `else` 블록의 `return`은 값을 기억해 두고, 파이썬은 `finally`를 반드시 실행한 뒤 결과를 돌려줍니다. 그래서 `finally`는 파일 닫기, 연결 해제 같은 정리 작업에 믿고 쓸 수 있습니다.

`finally` 안에서 `return`을 쓰면 앞의 반환값을 덮어쓰고, 예외마저 삼켜 버릴 수 있습니다. Python 3.14부터는 이런 코드에 `SyntaxWarning`(PEP 765)이 나옵니다. `finally`에서는 `return`을 쓰지 마세요.

### 여러 예외를 한꺼번에 처리하기

예외 종류별로 여러 `except` 블록을 쓰거나, 괄호 튜플에 여러 타입을 넣을 수 있습니다.

```
try:
    amount = int(data["amount"])
except KeyError:
    amount = 0
except (ValueError, TypeError) as e:
    print("잘못된 금액:", e)
    amount = 0
```

블록은 위에서 아래로 검사되며, 처음 맞는 블록이 실행됩니다. 따라서 구체적인 타입을 일반적인 타입보다 위에 둡니다. Python 3.14(PEP 758)부터는 괄호 없이 `except ValueError, TypeError:`처럼 쓸 수 있습니다. 다만 `as`를 함께 쓸 때는 여전히 괄호가 필요합니다.

### 예외 메시지와 타입 확인하기

`as e`로 예외 객체를 받을 수 있습니다. `str(e)`는 메시지를, `type(e).__name__`은 타입 이름을 줍니다.

```
try:
    int("백")
except ValueError as e:
    print(type(e).__name__)   # ValueError
    print(e)                  # invalid literal for int() with base 10: '백'
```

## traceback 모듈: 전체 오류 텍스트

오류를 잡되 전체 트레이스백은 로그나 보고서에 남기고 싶을 때가 있습니다. 이때는 `traceback` 모듈을 씁니다.

```
import traceback

try:
    1 / 0
except ZeroDivisionError:
    traceback.print_exc()             # 트레이스백을 stderr에 출력
    text = traceback.format_exc()     # 같은 내용을 문자열로
```

`logging` 모듈로 로그를 남긴다면, `except` 안에서 `logging.exception("계산에 실패했습니다")`를 호출하세요. 트레이스백이 메시지에 자동으로 붙습니다.

## 파이썬 raise: 예외 직접 발생시키기

`raise` 문은 예외를 직접 던집니다. 함수가 주어진 데이터로는 작업을 수행할 수 없다고 알릴 때 씁니다.

```
def add_expense(amount):
    if amount < 0:
        raise ValueError(f"금액은 음수가 될 수 없습니다: {amount}")
    ...

add_expense(-5)
# ValueError: 금액은 음수가 될 수 없습니다: -5
```

`raise`는 함수가 아니라 문장이므로 뒤에 괄호를 붙이지 않습니다. 괄호 안에는 예외 생성자에 넘길 인자를 씁니다. 종류는 의미에 맞게 고르세요. 잘못된 값이면 `ValueError`, 잘못된 타입이면 `TypeError`입니다.

### 인자 없는 raise와 raise from

`except` 안에서 인자 없이 `raise`만 쓰면 현재 예외를 그대로 다시 던집니다. 로그만 남기고 처리는 호출한 쪽에 맡길 때 씁니다.

한 예외를 다른 예외로 바꿔 던질 때는 `raise ... from e`를 씁니다. 원래 오류가 체인에 남아서 트레이스백에 두 오류가 모두 나옵니다.

```
def load_config(path):
    try:
        with open(path) as f:
            return f.read()
    except FileNotFoundError as e:
        raise RuntimeError(f"설정 파일이 없습니다: {path}") from e
```

### add_note: 오류에 설명 덧붙이기

Python 3.11(PEP 678)부터 예외에 `add_note()`로 메모를 붙인 뒤 다시 던질 수 있습니다. 예외 타입은 그대로이고, 메모는 메시지 아래에 출력됩니다.

```
def parse(raw, line_no):
    try:
        return float(raw)
    except ValueError as e:
        e.add_note(f"파일 expenses.csv, {line_no}번째 줄")
        raise

parse("백", 17)
# ValueError: could not convert string to float: '백'
# 파일 expenses.csv, 17번째 줄
```

## 파이썬 사용자 정의 예외 만들기

사용자 정의 예외는 업무 도메인의 오류를 표현할 때 씁니다. 예산 초과, 이미 결제된 주문, 차단된 사용자 같은 경우입니다. `Exception`을 상속한 클래스를 만들면 됩니다.

```
class BudgetExceeded(Exception):
    def __init__(self, category, limit, spent):
        super().__init__(f"{category}: 한도 {limit}, 지출 {spent}")
        self.category = category
        self.limit = limit
        self.spent = spent

def check_budget(category, limit, spent):
    if spent > limit:
        raise BudgetExceeded(category, limit, spent)

try:
    check_budget("식비", 10000, 12500)
except BudgetExceeded as e:
    print("경고:", e)   # 경고: 식비: 한도 10000, 지출 12500
```

이제 호출하는 쪽은 예산 초과만 골라 잡을 수 있고, 오타나 다른 오류와 혼동하지 않습니다. 큰 프로젝트에서는 공통 기반 클래스(`class TrackerError(Exception)`)를 만들고 모든 사용자 정의 예외가 이를 상속하게 하면, `except TrackerError` 하나로 모두 잡을 수 있습니다. `BaseException`이 아니라 `Exception`을 상속하세요.

## 예외 그룹과 except*

여러 오류가 동시에 생기기도 합니다. 예를 들어 파일 전체를 검사하거나, `asyncio.TaskGroup`으로 병렬 작업을 돌릴 때입니다. Python 3.11부터는 이런 오류를 `ExceptionGroup`으로 묶고, `except*`로 타입별로 처리할 수 있습니다.

```
def check_all(rows):
    errors = []
    for r in rows:
        try:
            float(r)
        except ValueError as e:
            errors.append(e)
    if errors:
        raise ExceptionGroup("파일 오류", errors)

try:
    check_all(["1", "a", "b"])
except* ValueError as eg:
    print(f"잘못된 줄: {len(eg.exceptions)}개")   # 잘못된 줄: 2개
```

## 모든 예외를 잡는 try except: 왜 하면 안 되나

가장 해로운 습관은 모든 것을 잡고 무시하는 `except: pass`입니다. 프로그램은 '죽지 않게' 되지만, 실제로는 문제를 알려 주지 않게 됩니다. 아무 타입도 없는 `except`는 `KeyboardInterrupt`와 `SystemExit`까지 잡으므로 Ctrl+C로 프로그램을 멈출 수도 없게 됩니다. `except Exception: pass`가 조금 낫긴 해도, 이름 오타나 논리 오류를 조용히 삼켜 버립니다.

예외를 올바르게 처리하는 방법은 다음과 같습니다.

- 처리할 수 있는 타입만 잡으세요. `except ValueError`는 좋고, 모든 것을 잡는 것은 나쁩니다.
- `try` 블록은 짧게 유지하세요. 그 오류가 날 수 있는 줄만 넣습니다.
- 프로그램 최상위에서 넓게 잡는다면(`except Exception`) 반드시 트레이스백을 기록하세요.
- 단순한 분기에 예외를 쓰지 마세요. 간단한 경우는 `try/except KeyError` 대신 `dict.get()`이 낫습니다.
- 자원 관리에는 `with`를 쓰세요. 오류가 나도 파일을 닫아 줍니다.

## 정리

파이썬 예외는 오류를 알리는 정상적인 방법이지, 숨길 이유가 아닙니다. 트레이스백은 아래에서 위로 읽고, `try/except`에서는 구체적인 타입을 잡으며, `else`는 '성공했을 때', `finally`는 '정리'를 위해 씁니다. `raise`로 직접 오류를 던지고, `raise from`과 `add_note`로 맥락을 남기고, 사용자 정의 예외는 `Exception`을 상속해 만드세요. 이 주제는 과정 강의에서 실습합니다. 지출 관리기가 CSV의 잘못된 한 줄 때문에 멈추지 않고, 예산 초과를 자체 예외로 표현하도록 만듭니다.

## FAQ

**파이썬에서 오류와 예외는 무엇이 다른가요?**

일상적으로는 같은 말입니다. 엄밀히는 SyntaxError는 코드를 읽는 단계에서 발생하고, 나머지 오류는 실행 중에 예외 객체로 발생하여 잡을 수 있습니다.

**예외가 난 뒤 같은 자리에서 실행을 이어 갈 수 있나요?**

아닙니다. 예외를 잡은 뒤에는 오류가 난 줄이 아니라 try/except 블록 뒤에서 실행이 이어집니다. 파일의 각 줄을 개별적으로 처리하고 싶다면 try를 반복문 안에 넣으세요.

**except 블록 안에서 예외가 나면 어떻게 되나요?**

새 예외가 위로 전파되며, 원래 예외는 새 예외의 __context__ 속성에 남습니다. 트레이스백에는 'During handling of the above exception, another exception occurred'라는 문장이 나타납니다.

**테스트에서 함수가 예외를 던지는지 어떻게 확인하나요?**

pytest에서는 with pytest.raises(ValueError):, unittest에서는 self.assertRaises(ValueError)를 씁니다. 블록 안에서 원하는 타입의 예외가 나야 테스트가 통과합니다.

**assert와 raise는 무엇이 다른가요?**

assert 조건, "메시지"는 조건이 거짓일 때 AssertionError를 발생시키며, 개발 중의 검사 용도입니다. -O 옵션으로 실행하면 assert가 꺼지므로, 사용자 입력은 if와 raise로 검사해야 합니다.


---
Source: Skillaria.Top (https://skillaria.top)
