try / except / else / finally — 예외 흐름 완전 이해
프로그램을 작성하다 보면 반드시 오류 상황을 만납니다. 파일이 없거나, 네트워크가 끊기거나, 숫자가 아닌 값을 변환하려 할 때 Python은 예외(Exception) 를 발생시킵니다. try-except 구문은 이런 예외를 안전하게 잡아 처리하는 핵심 도구입니다.
예외 흐름 다이어그램
try 블록 실행
│
├─ 예외 없음 ──▶ else 블록 실행 ──▶ finally 블록 실행 ──▶ 정상 종료
│
└─ 예외 발생 ──▶ 매칭 except 탐색
│
├─ 매칭 성공 ──▶ except 블록 실행 ──▶ finally 블록 실행 ──▶ 계속
│
└─ 매칭 실패 ──▶ 상위 호출 스택으로 예외 전파
핵심 규칙
else는 예외가 없을 때만 실행됩니다.finally는 예외 발생 여부와 무관하게 항상 실행됩니다.except절은 위에서 아래로 순서대로 검사합니다.
기본 구조
try:
# 예외가 발생할 수 있는 코드
result = 10 / 0
except ZeroDivisionError:
# 특정 예외 처리
print("0으로 나눌 수 없습니다.")
여러 except 절 — 순서 중요성
여러 예외를 각각 처리하려면 except 절을 여러 개 작성합니다. 구체적인 예외를 먼저, 범용 예외를 나중에 작성해야 합니다.
def parse_number(value: str) -> float:
try:
return float(value)
except ValueError:
print(f"'{value}'는 유효한 숫자가 아닙니다.")
return 0.0
except TypeError:
print(f"문자열이 필요합니다. 받은 타입: {type(value).__name__}")
return 0.0
except Exception as e:
# 예상치 못한 예외를 모두 잡는 안전망
print(f"예기치 않은 오류: {e}")
return 0.0
print(parse_number("3.14")) # 3.14
print(parse_number("abc")) # 0.0
print(parse_number(None)) # 0.0
잘못된 순서 예시 — 절대 피해야 할 패턴
# 잘못된 예: Exception이 ValueError보다 먼저 오면
# ValueError는 절대 실행되지 않는다 (dead code)
try:
float("abc")
except Exception: # 이미 모든 예외를 잡아버림
print("일반 오류")
except ValueError: # 절대 도달 불가
print("값 오류") # 이 줄은 실행되지 않음
as 키워드 — 예외 객체 참조
as 키워드로 예외 객체를 변수에 바인딩하면 상세 정보를 활용할 수 있습니다.
try:
numbers = [1, 2, 3]
print(numbers[10])
except IndexError as e:
print(f"예외 타입: {type(e).__name__}")
print(f"예외 메시지: {e}")
print(f"예외 인수: {e.args}")
예외 타입: IndexError
예외 메시지: list index out of range
예외 인수: ('list index out of range',)
else 절 — 예외 없을 때 실행
else 블록은 try 블록이 예외 없이 성공했을 때만 실행됩니다. 성공 후 처리 로직과 예외 처리 로직을 명확히 분리할 수 있습니다.
def divide(a: float, b: float) -> float | None:
try:
result = a / b
except ZeroDivisionError:
print("0으로 나눌 수 없습니다.")
return None
else:
# 예외 없이 성공한 경우에만 실행
print(f"계산 성공: {a} / {b} = {result}")
return result
divide(10, 2) # 계산 성공: 10 / 2 = 5.0
divide(10, 0) # 0으로 나눌 수 없습니다.
else를 사용하는 이유: try 블록 안에 너무 많은 코드를 넣으면 의도하지 않은 예외도 잡힐 수 있습니다. else로 성공 시 처리를 분리하면 예외 처리 범위를 최소화할 수 있습니다.
finally 절 — 항상 실행 (리소스 해제)
finally는 예외 발생 여부와 무관하게 반드시 실행됩니다. 파일, 네트워크 연결, 데이터베이스 연결 같은 리소스 해제에 사용합니다.
def read_file(path: str) -> str:
file = None
try:
file = open(path, "r", encoding="utf-8")
return file.read()
except FileNotFoundError:
print(f"파일을 찾을 수 없습니다: {path}")
return ""
except PermissionError:
print(f"파일 읽기 권한이 없습니다: {path}")
return ""
finally:
# 예외 발생 여부와 무관하게 파일을 닫는다
if file is not None:
file.close()
print("파일 핸들 해제 완료")
주의: finally 안에서 return을 사용하면 try/except의 return 값이나 예외가 무시됩니다.
def risky():
try:
raise ValueError("오류")
finally:
return "finally에서 반환" # 예외가 억제되어 버림!
print(risky()) # "finally에서 반환" — 예외가 사라짐
완전한 구조 — try / except / else / finally 조합
import logging
logger = logging.getLogger(__name__)
def load_config(path: str) -> dict:
"""설정 파일을 읽어 딕셔너리로 반환합니다."""
file = None
try:
file = open(path, "r", encoding="utf-8")
import json
data = json.load(file)
except FileNotFoundError:
logger.error("설정 파일 없음: %s", path)
return {}
except json.JSONDecodeError as e:
logger.error("JSON 파싱 실패: %s (line %d)", e.msg, e.lineno)
return {}
else:
logger.info("설정 파일 로드 성공: %d개 키", len(data))
return data
finally:
if file is not None:
file.close()
실전 예제 1 — 파일 처리 패턴
from pathlib import Path
def safe_read_lines(path: str | Path) -> list[str]:
"""
파일을 읽어 줄 목록을 반환합니다.
오류 발생 시 빈 리스트를 반환합니다.
"""
try:
with open(path, "r", encoding="utf-8") as f:
lines = f.readlines()
except FileNotFoundError:
print(f"[경고] 파일 없음: {path}")
return []
except UnicodeDecodeError:
# UTF-8 실패 시 CP949(Windows 한글) 재시도
try:
with open(path, "r", encoding="cp949") as f:
lines = f.readlines()
except Exception as e:
print(f"[오류] 인코딩 실패: {e}")
return []
else:
return [line.rstrip("\n") for line in lines]
return [line.rstrip("\n") for line in lines]
실전 예제 2 — DB 연결 패턴
import sqlite3
from typing import Any
def execute_query(
db_path: str,
query: str,
params: tuple = ()
) -> list[dict[str, Any]]:
"""
SQLite 쿼리를 실행하고 결과를 반환합니다.
"""
conn = None
try:
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute(query, params)
rows = cursor.fetchall()
except sqlite3.OperationalError as e:
print(f"SQL 오류: {e}")
return []
except sqlite3.DatabaseError as e:
print(f"DB 오류: {e}")
return []
else:
return [dict(row) for row in rows]
finally:
if conn:
conn.close()
# 사용 예시
results = execute_query(
"app.db",
"SELECT * FROM users WHERE active = ?",
(1,)
)
실전 예제 3 — 재시도 패턴
import time
from typing import Callable, TypeVar
T = TypeVar("T")
def retry(
func: Callable[[], T],
max_attempts: int = 3,
delay: float = 1.0,
exceptions: tuple[type[Exception], ...] = (Exception,)
) -> T:
"""
함수를 최대 max_attempts번 재시도합니다.
"""
last_exception: Exception | None = None
for attempt in range(1, max_attempts + 1):
try:
return func()
except exceptions as e:
last_exception = e
print(f"시도 {attempt}/{max_attempts} 실패: {e}")
if attempt < max_attempts:
time.sleep(delay)
else:
break # 성공 시 반복 종료
raise RuntimeError(
f"{max_attempts}번 모두 실패"
) from last_exception
# 사용 예시
import random
def flaky_operation() -> str:
if random.random() < 0.7: # 70% 확률로 실패
raise ConnectionError("네트워크 오류")
return "성공"
result = retry(flaky_operation, max_attempts=5, delay=0.1)
print(result)
고수 팁
팁 1 — try 블록을 최소화하라
# 나쁜 예: try 블록이 너무 넓다
try:
data = load_data()
processed = transform(data)
result = save(processed)
except Exception:
pass # 어디서 실패했는지 모름
# 좋은 예: 각 단계를 개별 처리
data = load_data() # 별도 예외 처리
processed = transform(data) # 별도 예외 처리
result = save(processed) # 별도 예외 처리
팁 2 — bare except는 절대 사용하지 말 것
# 절대 금지: KeyboardInterrupt, SystemExit까지 잡아버림
try:
do_something()
except: # bare except
pass
# 올바른 방법: Exception 명시
try:
do_something()
except Exception:
pass
팁 3 — except 절에서 예외를 삼키지 마라
# 나쁜 예: 예외를 무시하면 버그 추적이 불가능
try:
result = compute()
except ValueError:
pass # 조용히 무시
# 좋은 예: 최소한 로깅이라도
import logging
try:
result = compute()
except ValueError as e:
logging.warning("compute() 실패, 기본값 사용: %s", e)
result = default_value
팁 4 — with 문으로 finally 패턴을 대체하라
# finally로 리소스 해제하는 구식 패턴
file = None
try:
file = open("data.txt")
content = file.read()
finally:
if file:
file.close()
# with 문을 사용하면 훨씬 간결하고 안전
with open("data.txt") as file:
content = file.read()
팁 5 — Python 3.11+ ExceptionGroup
Python 3.11부터 여러 예외를 동시에 처리하는 ExceptionGroup과 except* 구문이 도입되었습니다.
# Python 3.11+
try:
raise ExceptionGroup("다중 오류", [
ValueError("값 오류"),
TypeError("타입 오류"),
])
except* ValueError as eg:
print(f"ValueError 그룹: {eg.exceptions}")
except* TypeError as eg:
print(f"TypeError 그룹: {eg.exceptions}")