Python은 오류를 반환값이 아니라 예외(exception)로 알립니다. 이 장에서는 예외를 잡고 다루는 try 문의 네 부분, 직접 예외를 일으키고 만드는 법, 자원을 안전하게 정리하는 컨텍스트 관리자, 그리고 pytest와 unittest로 코드를 검증하는 방법을 살펴봅니다.
예외와 try 문
예외가 일어나면 실행이 그 자리에서 멈추고, 이를 처리하는 except를 찾을 때까지 호출한 쪽으로 거슬러 올라갑니다. 끝까지 처리되지 않으면 프로그램이 트레이스백(traceback)을 출력하고 종료합니다. try 문은 네 부분으로 이루어집니다.
def read_number(path: str) -> int:
try:
f = open(path, encoding="utf-8")
except FileNotFoundError:
print(f"{path} 파일이 없습니다")
return 0
else:
try:
return int(f.read().strip())
except ValueError as e:
print(f"숫자가 아닙니다: {e}")
return 0
finally:
f.close() # 성공하든 실패하든 파일을 닫습니다
except에는 처리할 수 있는 구체적인 예외만 적습니다. 아무 예외나 잡는 맨 except:나 except Exception:을 남발하면 진짜 버그까지 숨겨 버립니다. 여러 예외를 함께 처리할 때는 except (KeyError, IndexError):처럼 튜플로 묶습니다.
예외 일으키기와 사용자 정의 예외
잘못된 입력이나 상태를 만나면 raise로 예외를 일으킵니다. 도메인에 맞는 예외가 필요하면 Exception을 상속해 직접 만듭니다. 기반 예외 클래스를 하나 두고 세부 예외를 그 아래에 두면, 호출하는 쪽이 원하는 수준에서 골라 잡을 수 있습니다.
class PaymentError(Exception):
"""결제 관련 오류의 기반 클래스"""
class InsufficientFunds(PaymentError):
def __init__(self, needed: int, available: int) -> None:
super().__init__(f"{needed}원이 필요하지만 {available}원뿐입니다")
self.needed = needed
self.available = available
def pay(balance: int, amount: int) -> int:
if amount <= 0:
raise ValueError("amount must be positive")
if amount > balance:
raise InsufficientFunds(amount, balance)
return balance - amount
try:
pay(1000, 5000)
except PaymentError as e:
print("결제 실패:", e)
다른 예외를 잡은 뒤 더 의미 있는 예외로 바꿔 던질 때는 raise NewError(...) from e를 씁니다. 원래 원인이 트레이스백에 함께 남아 디버깅이 쉬워집니다.
컨텍스트 관리자와 with 문
파일, 네트워크 연결, 잠금처럼 쓰고 나면 반드시 정리해야 하는 자원은 with 문으로 다룹니다. 블록을 벗어날 때 예외가 났더라도 자원이 자동으로 정리되므로 try/finally를 직접 쓰는 것보다 짧고 안전합니다. 직접 만들 때는 contextlib.contextmanager 데코레이터가 간편합니다.
import time
from contextlib import contextmanager
with open("notes.txt", "w", encoding="utf-8") as f:
f.write("hello\n")
# 여기서 파일은 이미 닫혀 있습니다
@contextmanager
def timer(label: str):
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
print(f"{label}: {elapsed:.3f}초")
with timer("합계 계산"):
total = sum(range(1_000_000))
pytest로 테스트하기
pytest는 Python에서 가장 널리 쓰이는 테스트 도구입니다. test_로 시작하는 파일과 함수를 자동으로 찾아 실행하며, 특별한 단언 메서드 없이 평범한 assert만 쓰면 실패했을 때 값을 자세히 보여 줍니다.
# tests/test_pay.py
import pytest
from payments import InsufficientFunds, pay
def test_pay_reduces_balance():
assert pay(1000, 300) == 700
def test_pay_rejects_overdraft():
with pytest.raises(InsufficientFunds):
pay(100, 500)
@pytest.mark.parametrize("amount", [0, -10])
def test_pay_rejects_non_positive(amount):
with pytest.raises(ValueError, match="positive"):
pay(1000, amount)
def test_writes_file(tmp_path):
target = tmp_path / "out.txt" # 테스트마다 새로 만드는 임시 폴더
target.write_text("ok", encoding="utf-8")
assert target.read_text(encoding="utf-8") == "ok"
python -m pip install pytest
pytest # 모든 테스트 실행
pytest -k overdraft -v # 이름에 overdraft 가 들어간 테스트만 자세히
tmp_path처럼 테스트 함수의 매개변수 이름으로 준비물을 받는 기능을 픽스처(fixture)라고 합니다. @pytest.fixture로 직접 만들 수도 있습니다.
unittest
표준 라이브러리에는 unittest도 들어 있어 추가 설치 없이 쓸 수 있습니다. TestCase를 상속한 클래스에 test_로 시작하는 메서드를 두고 assertEqual 같은 메서드로 검사합니다. pytest는 unittest 형식의 테스트도 그대로 실행합니다.
import unittest
from payments import pay
class PayTest(unittest.TestCase):
def test_pay(self):
self.assertEqual(pay(1000, 300), 700)
def test_negative(self):
with self.assertRaises(ValueError):
pay(1000, -1)
if __name__ == "__main__":
unittest.main()
정리
try/except/else/finally로 예외를 다루고, 처리할 수 있는 구체적인 예외만 잡습니다.raise로 예외를 일으키고, Exception을 상속해 도메인 예외를 만들며, 원인은 from으로 잇습니다.- 정리가 필요한 자원은
with 문과 컨텍스트 관리자로 다룹니다. - 테스트는 pytest의
assert, pytest.raises, 픽스처로 간결하게 쓰고, 표준 unittest도 함께 쓸 수 있습니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.