Python 예외 처리 — try, except, raise
이 토픽을 마치면
Python에서 에러가 발생하는 원리를 이해하고, try/except로 에러를 잡고, raise로 직접 에러를 발생시키며, finally로 정리 작업을 수행하는 패턴을 익힙니다.
에러는 프로그램을 죽인다
Python에서 에러가 발생하면, 그 즉시 프로그램이 멈춥니다.
numbers = [1, 2, 3]print(numbers[10]) # IndexError: list index out of rangeprint("This line never runs")에러가 나는 줄 이후의 모든 코드가 실행되지 않습니다. 웹 서버라면 서버 자체가 죽습니다. 데이터 분석 스크립트라면 1000건 중 999건을 처리하고 1건 때문에 전체가 날아갑니다.
예외 처리는 "에러가 나도 죽지 않고 대응하는 방법"입니다.
try/except — 에러를 잡기
try: result = 10 / 0except ZeroDivisionError: print("0으로 나눌 수 없습니다")
print("Program continues") # 이 줄이 실행됨!try 블록 안에서 에러가 나면, except 블록으로 점프합니다. 프로그램은 멈추지 않고 계속 실행됩니다.
에러 메시지 가져오기
try: value = int("hello")except ValueError as e: print(f"Error: {e}") # Error: invalid literal for int() with base 10: 'hello'as e로 에러 객체를 받으면, 무엇이 잘못됐는지 확인할 수 있습니다.
여러 에러 각각 처리
def safe_divide(a, b): try: result = a / b return round(result, 2) except ZeroDivisionError: print("Denominator cannot be zero") return None except TypeError: print("Both arguments must be numbers") return None
safe_divide(10, 0) # Denominator cannot be zerosafe_divide("10", 2) # Both arguments must be numberssafe_divide(10, 3) # 3.33에러 종류별로 다른 대응을 할 수 있습니다. except Exception으로 모든 에러를 한 번에 잡을 수도 있지만, 권장하지 않습니다 — 어떤 에러인지 모르면 디버깅이 어렵습니다.
자주 만나는 에러 종류
| 에러 | 원인 | 예시 |
|---|---|---|
ValueError | 값이 올바르지 않음 | int("abc") |
TypeError | 타입이 맞지 않음 | "3" + 5 |
IndexError | 인덱스 범위 초과 | [1,2,3][10] |
KeyError | 딕셔너리에 키 없음 | {"a": 1}["b"] |
FileNotFoundError | 파일이 없음 | open("none.txt") |
ZeroDivisionError | 0으로 나눔 | 10 / 0 |
AttributeError | 없는 속성/메서드 접근 | None.upper() |
이 7가지가 일상적으로 가장 자주 만나는 에러입니다.
else와 finally
try: f = open("data.txt", "r") content = f.read()except FileNotFoundError: print("File not found")else: # try가 성공했을 때만 실행 print(f"Read {len(content)} characters")finally: # 성공이든 실패든 무조건 실행 print("Cleanup done")| 블록 | 실행 시점 |
|---|---|
try | 항상 시도 |
except | 에러 발생 시 |
else | 에러 없이 성공 시 |
finally | 무조건 (성공/실패 모두) |
finally는 파일 닫기, 네트워크 연결 종료, 임시 파일 삭제 같은 정리 작업에 씁니다. 에러가 나든 안 나든 반드시 실행되어야 하는 코드입니다.
raise — 에러를 직접 발생시키기
def withdraw(balance, amount): if amount <= 0: raise ValueError("Withdrawal amount must be positive") if amount > balance: raise ValueError(f"Insufficient funds: balance={balance}, requested={amount}") return balance - amount
try: new_balance = withdraw(1000, 5000)except ValueError as e: print(f"Transaction failed: {e}") # Transaction failed: Insufficient funds: balance=1000, requested=5000raise는 "이 상황은 정상이 아니다"를 선언하는 것입니다. 호출하는 쪽에서 try/except로 처리하도록 책임을 넘깁니다.
커스텀 에러 클래스
class InsufficientFundsError(Exception): def __init__(self, balance, amount): self.balance = balance self.amount = amount super().__init__( f"Cannot withdraw {amount} from balance {balance}" )
def withdraw(balance, amount): if amount > balance: raise InsufficientFundsError(balance, amount) return balance - amount
try: withdraw(1000, 5000)except InsufficientFundsError as e: print(e) # Cannot withdraw 5000 from balance 1000 print(e.balance) # 1000 print(e.amount) # 5000Exception을 상속해서 자신만의 에러를 만들 수 있습니다. 에러에 추가 정보(잔액, 요청 금액)를 담을 수 있어서, 에러를 잡는 쪽에서 더 정교하게 대응할 수 있습니다.
실전 패턴 — 파일 처리
def read_config(filepath): try: with open(filepath, "r", encoding="utf-8") as f: lines = f.readlines() except FileNotFoundError: print(f"Config file not found: {filepath}") return {} except PermissionError: print(f"Permission denied: {filepath}") return {} config = {} for i, line in enumerate(lines, 1): line = line.strip() if not line or line.startswith("#"): continue if "=" not in line: print(f"Warning: invalid format at line {i}: {line}") continue key, value = line.split("=", 1) config[key.strip()] = value.strip() return config
# config.txt:# host = localhost# port = 3000# # this is a commentsettings = read_config("config.txt")print(settings) # {'host': 'localhost', 'port': '3000'}이 패턴은 실무에서 매우 자주 사용됩니다:
- 파일 열기 실패를
except로 처리 - 파싱 중 잘못된 행은 경고만 출력하고 건너뜀
- 빈 줄과 주석(
#)은 무시
프로그램이 "설정 파일 하나 못 열었다고 죽는" 대신, 기본값으로 동작하거나 경고를 보여줍니다.
안티패턴 — 하면 안 되는 것들
모든 에러를 무시
# Bad — 에러를 삼켜버림try: risky_operation()except Exception: pass # What happened? Nobody knows
# Good — 최소한 로그는 남김try: risky_operation()except Exception as e: print(f"Warning: {e}") # or logging.warning(...)except: pass는 "모든 에러를 무시"합니다. 프로그램은 죽지 않지만, 무엇이 잘못됐는지 전혀 알 수 없습니다. 나중에 디버깅할 때 원인을 찾기 매우 어렵습니다.
너무 넓은 except
# Bad — 타이핑 실수(NameError)도 잡아버림try: valeu = int(input("Enter number: ")) # typo: valeuexcept Exception: print("Invalid input") # NameError인데 "Invalid input"?
# Good — 예상하는 에러만 잡기try: value = int(input("Enter number: "))except ValueError: print("Invalid input — please enter a number")핵심 정리
| 구문 | 역할 | 사용 시점 |
|---|---|---|
try | 에러가 날 수 있는 코드 감싸기 | 외부 입력, 파일, 네트워크 등 |
except | 특정 에러 잡아서 대응 | 에러별 다른 처리 필요 시 |
else | 성공 시에만 실행 | 에러 없을 때 추가 작업 |
finally | 무조건 실행 | 정리 작업 (파일 닫기, 연결 종료) |
raise | 에러 직접 발생 | 잘못된 입력, 비즈니스 규칙 위반 |
예외 처리의 핵심 원칙: 예상하는 에러만, 가능한 좁게 잡고, 무시하지 말 것. "일단 try/except로 감싸면 안전하겠지"는 오해입니다. 잘못된 예외 처리는 에러를 숨겨서, 에러가 없을 때보다 디버깅을 더 어렵게 만듭니다.