파이썬에서 예외 처리는 프로그램 실행 중 발생하는 오류를 우아하게 처리하는 메커니즘입니다. 예외가 발생했을 때 프로그램이 갑작스럽게 중단되는 것을 방지하고, 적절한 대응 방법을 제공합니다. 이는 사용자 경험을 향상시키고 시스템의 안정성을 보장하는 핵심 기능입니다.
try-except 기본 구조
try-except 구문은 파이썬에서 예상 가능한 오류를 미리 대비하여 처리하는 가장 기본적인 방법이다. 예를 들어 사용자로부터 나이를 입력받는 상황에서 숫자 대신 문자열을 입력하거나, 음수나 200살 같은 비현실적인 값을 입력하는 경우가 발생할 수 있다.
이런 상황에서 try 블록에는 오류가 발생할 가능성이 있는 코드를 작성한다. 사용자 입력을 정수로 변환하는 int() 함수 호출이 대표적인 예시다. 만약 사용자가 “abc”나 “스무살” 같은 문자열을 입력하면 int() 함수는 변환할 수 없어서 ValueError를 발생시킨다.
except 블록에서는 발생할 수 있는 특정 예외를 지정하여 처리한다. except ValueError는 숫자 변환 과정에서 발생하는 오류만을 잡아내어 사용자에게 친근한 메시지를 보여주거나 다시 입력받을 기회를 제공할 수 있다. 이를 통해 프로그램이 갑작스럽게 중단되지 않고 사용자와 계속 상호작용할 수 있게 된다.
try-except 구조의 핵심은 예외가 발생할 수 있는 부분을 미리 예상하고, 각각의 오류 상황에 맞는 적절한 대응책을 마련하는 것이다. 이는 사용자 경험을 향상시키고 프로그램의 안정성을 크게 높이는 중요한 프로그래밍 기법이다.
def get_user_age():
"""사용자로부터 나이를 입력받아 유효성을 검사하는 함수"""
try:
age_str = input("나이를 입력하세요: ")
age = int(age_str)
if not (0 < age < 150):
print("오류: 유효한 나이 범위(1-149)를 벗어났습니다.")
return None
return age
except ValueError:
print("오류: 숫자로 변환할 수 없는 값을 입력했습니다.")
return None
# 함수 사용
user_age = get_user_age()
if user_age:
print(f"확인된 나이: {user_age}세")

여러 예외 타입 처리
여러 예외 타입을 개별적으로 처리하는 것은 각기 다른 오류 상황에 맞는 최적의 해결책을 제공하기 위한 중요한 기법이다. 설정 파일을 읽어와서 JSON으로 파싱하는 작업에서는 여러 단계에서 서로 다른 종류의 오류가 발생할 수 있다.
except FileNotFoundError는 지정한 경로에 파일이 존재하지 않을 때 발생하는 예외를 처리한다. 이 경우에는 기본 설정값을 사용하거나 새로운 설정 파일을 생성하는 등의 대응이 적절하다. 사용자에게 “설정 파일이 없어서 기본 설정으로 시작합니다”라는 메시지를 보여주고 프로그램을 계속 실행할 수 있다.
except json.JSONDecodeError는 파일은 존재하지만 그 내용이 올바른 JSON 형식이 아닐 때 발생한다. 예를 들어 파일이 손상되었거나, 사용자가 수동으로 편집하다가 문법 오류를 만든 경우다. 이때는 파일 내용을 백업하고 기본 설정으로 복구하거나, 사용자에게 파일을 다시 작성하도록 안내하는 것이 좋다.
각 예외 타입별로 다른 처리를 하는 이유는 문제의 원인과 해결 방법이 완전히 다르기 때문이다. 파일이 없는 것은 쉽게 해결할 수 있지만, JSON 형식 오류는 더 신중한 접근이 필요하다. 이런 세밀한 예외 처리를 통해 사용자는 문제 상황을 정확히 이해하고 적절한 조치를 취할 수 있게 된다.
import json
def read_config_file(filename):
"""설정 파일을 읽고 파싱하는 함수"""
try:
with open(filename, 'r', encoding='utf-8') as file:
return json.load(file)
except FileNotFoundError:
print(f"경고: '{filename}' 파일을 찾을 수 없어 기본 설정을 사용합니다.")
return {"api_key": "default-key", "timeout": 30}
except json.JSONDecodeError:
print(f"오류: '{filename}' 파일의 JSON 형식이 올바르지 않습니다.")
return None
# 사용 예시
config = read_config_file("config.json")
if config:
print(f"설정 로드 성공! API 키: {config.get('api_key')}")

예외 정보 활용하기
as 키워드를 사용한 예외 정보 활용은 오류의 정확한 원인을 파악하고 적절한 대응을 하기 위한 핵심 기법이다. 외부 API와 통신할 때는 네트워크 상태, 서버 상황, 인증 문제 등 다양한 요인으로 인해 여러 종류의 오류가 발생할 수 있다.
except Exception as e 구문에서 as e 부분은 발생한 예외 객체를 변수 e에 저장한다는 의미다. 이렇게 저장된 예외 객체를 통해 오류에 대한 상세한 정보를 얻을 수 있다. str(e)로 사람이 읽기 쉬운 오류 메시지를 확인하고, type(e).__name__으로 예외의 정확한 타입을 알 수 있다.
예를 들어 API 요청에서 타임아웃이 발생하면 “Request timeout after 5 seconds”와 같은 구체적인 메시지를 얻을 수 있고, 연결 실패 시에는 “Connection refused”나 특정 IP 주소 정보를 포함한 오류 내용을 확인할 수 있다. 이런 정보는 문제 해결을 위한 중요한 단서가 된다.
특히 운영 환경에서는 이런 상세한 오류 정보를 로그 파일에 기록하여 나중에 분석할 수 있도록 하는 것이 중요하다. 단순히 “오류가 발생했습니다”라고 기록하는 것보다 “ConnectionTimeout: API 서버 응답 시간 초과 (5초)”처럼 구체적인 정보를 남기면 시스템 관리자나 개발자가 문제의 패턴을 파악하고 근본적인 해결책을 찾을 수 있다.
또한 예외 객체에는 오류 메시지 외에도 스택 트레이스, HTTP 상태 코드(웹 요청의 경우), 발생 시간 등의 추가 정보가 포함되어 있어 디버깅과 모니터링에 매우 유용하다.
import requests
import time
def fetch_url(url, max_retries=2):
"""URL에서 데이터를 가져오되, 실패 시 재시도하는 함수"""
for attempt in range(max_retries):
try:
response = requests.get(url, timeout=5)
response.raise_for_status() # 200번대 상태 코드가 아니면 HTTPError 발생
return response.json()
except requests.exceptions.Timeout as e:
print(f"시도 {attempt + 1}: 타임아웃 발생. {e}")
except requests.exceptions.RequestException as e:
print(f"시도 {attempt + 1}: 요청 오류 발생. {e}")
if attempt < max_retries - 1:
time.sleep(1) # 잠시 후 재시도
print("모든 재시도 실패.")
return None
# 사용 예시
data = fetch_url("https://api.github.com/invalid-url")
if data:
print("데이터 요청 성공!")

finally 블록 활용
finally 블록은 파이썬 예외 처리에서 리소스 관리의 핵심 요소로, 예외 발생 여부와 상관없이 반드시 실행되어야 하는 정리 작업을 담당한다. 데이터베이스 연결처럼 시스템 리소스를 사용하는 작업에서는 작업이 성공적으로 완료되든 중간에 오류가 발생하든 관계없이 연결을 안전하게 종료해야 한다.
데이터베이스 작업에서 연결을 제대로 닫지 않으면 연결 풀이 고갈되어 다른 작업들이 대기하게 되거나, 메모리 누수가 발생할 수 있다. 이는 시스템 성능 저하나 심각한 경우 서버 다운으로 이어질 수 있는 중대한 문제다. finally 블록은 이런 상황을 방지하는 안전장치 역할을 한다.
finally 블록의 가장 큰 특징은 실행 보장성이다. try 블록에서 정상적으로 작업이 완료되거나, except 블록에서 예외를 처리하거나, 심지어 처리되지 않은 예외가 발생하여 프로그램이 종료되는 상황에서도 finally 블록은 반드시 실행된다. 이는 return 문이나 break 문을 만나더라도 마찬가지다.
실제 개발에서는 데이터베이스 연결 외에도 파일 핸들 닫기, 네트워크 소켓 종료, 임시 파일 삭제, 잠금(lock) 해제 등 다양한 정리 작업에 finally를 활용한다. 이를 통해 예외 상황에서도 시스템이 안정적으로 동작하고 리소스 낭비를 방지할 수 있다.
다만 finally 블록에서도 예외가 발생할 수 있으므로, 정리 작업 자체도 try-except로 감싸거나 None 체크 등의 방어 코드를 작성하는 것이 좋다. 현대 파이썬에서는 with 문과 컨텍스트 매니저를 사용하여 이런 패턴을 더 안전하고 간결하게 구현할 수 있지만, finally의 동작 원리를 이해하는 것은 여전히 중요하다.
import sqlite3
def execute_db_query(db_path, query):
"""데이터베이스에 연결하여 쿼리를 실행하고 항상 연결을 닫는 함수"""
connection = None
try:
connection = sqlite3.connect(db_path)
cursor = connection.cursor()
cursor.execute(query)
print("쿼리 실행 성공.")
return cursor.fetchall()
except sqlite3.Error as e:
print(f"데이터베이스 오류 발생: {e}")
return None
finally:
if connection:
connection.close()
print("데이터베이스 연결이 안전하게 종료되었습니다.")
# 사용 예시
execute_db_query("test.db", "SELECT * FROM users")

사용자 정의 예외
사용자 정의 예외는 일반적인 시스템 오류와 구별되는 애플리케이션 고유의 비즈니스 로직 위반이나 특별한 상황을 명확하게 표현하기 위한 도구다. 은행 애플리케이션에서 출금 시 잔액 부족은 기술적인 오류가 아니라 명확한 비즈니스 규칙 위반으로, 이를 ValueError나 RuntimeError 같은 일반적인 예외로 처리하면 오류의 성격을 정확히 파악하기 어렵다.
InsufficientFundsError 같은 사용자 정의 예외를 만들면 코드를 읽는 사람이 즉시 “아, 이건 잔액 부족 문제구나”라고 이해할 수 있다. 이는 코드의 가독성과 유지보수성을 크게 향상시킨다. 또한 예외 처리 코드에서도 구체적인 상황에 맞는 적절한 대응을 할 수 있게 된다.
class InsufficientFundsError(Exception): pass처럼 Exception 클래스를 상속받아 새로운 예외를 정의하는 것은 매우 간단하다. 여기서 pass는 추가적인 기능 구현 없이 기본 예외 기능만 사용한다는 의미다. 필요에 따라 생성자에서 현재 잔액과 요청 금액 같은 추가 정보를 저장하거나, 특별한 메시지 포맷을 정의할 수도 있다.
사용자 정의 예외의 가장 큰 장점은 예외 계층 구조를 만들 수 있다는 것이다. 예를 들어 BankingError라는 상위 예외를 만들고, 그 하위에 InsufficientFundsError, InvalidAccountError, TransactionLimitError 등을 정의하면 은행 업무 관련 모든 오류를 포괄적으로 처리하거나 개별적으로 처리할 수 있다.
이런 명확한 예외 정의는 팀 개발에서 특히 중요하다. 다른 개발자가 코드를 보았을 때 어떤 상황에서 어떤 예외가 발생하는지 직관적으로 알 수 있고, API 문서화나 오류 로깅에서도 훨씬 의미 있는 정보를 제공할 수 있다.
# 우리 앱 전용 예외 정의
class InsufficientFundsError(Exception):
"""잔액 부족 예외"""
pass
class BankAccount:
def __init__(self, balance=100_000):
self.balance = balance
def withdraw(self, amount):
"""출금 함수"""
if amount > self.balance:
# 비즈니스 규칙에 맞지 않으면 사용자 정의 예외 발생
raise InsufficientFundsError(
f"잔액 부족! (현재 잔액: {self.balance}, 요청액: {amount})"
)
self.balance -= amount
print(f"출금 완료. 남은 잔액: {self.balance}")
# 사용 예시
account = BankAccount()
try:
account.withdraw(50_000) # 정상 처리
account.withdraw(200_000) # InsufficientFundsError 발생
except InsufficientFundsError as e:
print(f"출금 실패: {e}")

내장 예외
파이썬에는 다양한 상황에서 발생할 수 있는 내장 예외들이 있으며, 이들을 적절히 이해하고 처리하는 것이 안정적인 프로그램을 만드는 핵심이다. 가장 자주 마주치는 예외 중 하나인 ValueError는 타입은 올바르지만 값이 부적절할 때 발생한다. 예를 들어 “스무살”이라는 문자열을 int() 함수로 숫자로 변환하려 하거나, 리스트에서 존재하지 않는 값을 제거하려 할 때 나타난다. 이와 유사하게 TypeError는 완전히 잘못된 타입의 객체에 연산을 수행하려 할 때 발생하는데, 문자열과 숫자를 더하거나 숫자에 len() 함수를 적용하려 할 때 볼 수 있다.
데이터 구조와 관련된 예외들로는 IndexError와 KeyError가 있다. IndexError는 리스트나 튜플에서 유효하지 않은 인덱스에 접근할 때 발생하며, 특히 빈 리스트에 인덱스로 접근하거나 배열 크기를 벗어난 인덱스를 사용할 때 나타난다. KeyError는 딕셔너리에서 존재하지 않는 키에 접근할 때 발생하는 예외로, JSON 데이터 처리나 설정 파일을 다룰 때 자주 마주치게 된다. 이런 상황에서는 get() 메서드를 사용하거나 in 연산자로 키의 존재를 먼저 확인하는 것이 좋다.
객체와 관련된 AttributeError는 존재하지 않는 속성이나 메서드에 접근할 때 발생한다. 특히 None 객체에 메서드를 호출하거나, 문자열 객체에 리스트 전용 메서드인 append()를 사용하려 할 때 이 예외를 볼 수 있다. 파일 처리에서는 FileNotFoundError가 가장 기본적인 예외로, 지정한 경로에 파일이 없거나 경로 설정이 잘못되었을 때 발생한다. 수학 연산에서는 ZeroDivisionError가 0으로 나누기를 시도할 때 발생하며, 이는 계산 프로그램에서 반드시 처리해야 하는 예외다.
코딩 실수로 인한 예외들도 있는데, NameError는 정의되지 않은 변수나 함수를 사용하려 할 때 발생한다. 이와 연관된 ImportError와 ModuleNotFoundError는 모듈을 찾을 수 없거나 import에 실패했을 때 나타나며, 특히 외부 라이브러리를 사용할 때 해당 패키지가 설치되지 않았거나 모듈 경로가 잘못되었을 때 발생한다.
def comprehensive_exception_demo():
"""다양한 내장 예외들을 보여주는 종합 예제"""
# ValueError와 TypeError
try:
age = int("스무살") # ValueError
result = "hello" + 5 # TypeError
except (ValueError, TypeError) as e:
print(f"값/타입 오류: {e}")
# IndexError와 KeyError
try:
fruits = ["apple", "banana"]
print(fruits[5]) # IndexError
student = {"name": "김철수"}
print(student["grade"]) # KeyError
except (IndexError, KeyError) as e:
print(f"인덱스/키 오류: {e}")
# AttributeError와 NameError
try:
text = "hello"
text.append("world") # AttributeError
print(undefined_var) # NameError
except (AttributeError, NameError) as e:
print(f"속성/이름 오류: {e}")
# FileNotFoundError와 ZeroDivisionError
try:
with open("없는파일.txt") as f: # FileNotFoundError
pass
result = 10 / 0 # ZeroDivisionError
except (FileNotFoundError, ZeroDivisionError) as e:
print(f"파일/수학 오류: {e}")
# ImportError
try:
import nonexistent_module # ModuleNotFoundError
except ImportError as e:
print(f"모듈 오류: {e}")
comprehensive_exception_demo()

이러한 내장 예외들을 제대로 이해하고 적절히 처리하면 사용자에게 친근한 오류 메시지를 제공하고, 프로그램이 예상치 못한 상황에서도 안정적으로 동작하도록 만들 수 있다. 각 예외의 특성을 파악하여 상황에 맞는 처리 방법을 선택하는 것이 좋은 파이썬 개발자가 되는 중요한 능력이다.
컨텍스트 매니저(with 문)를 활용한 고급 리소스 관리
finally 블록을 사용한 수동 리소스 관리는 확실하지만 코드가 길어지고 반복적인 패턴이 나타나는 문제가 있다. 특히 여러 리소스를 동시에 관리하거나 중첩된 리소스 처리가 필요할 때는 코드의 복잡성이 기하급수적으로 증가한다. with 문과 컨텍스트 매니저는 이런 문제를 해결하는 파이썬의 우아한 해결책으로, 리소스 획득과 해제를 자동화하여 개발자가 비즈니스 로직에 집중할 수 있게 해준다.
with 문은 enter__와 __exit 메서드를 가진 객체와 함께 작동한다. with 블록이 시작될 때 enter 메서드가 호출되어 리소스를 준비하고, 블록을 벗어나는 순간 성공 여부나 예외 발생 여부와 관계없이 exit 메서드가 자동으로 실행되어 정리 작업을 수행한다. 이는 finally 블록과 동일한 안전성을 제공하면서도 훨씬 간결하고 읽기 쉬운 코드를 만들 수 있게 해준다.
가장 큰 장점은 리소스 관리 로직이 해당 리소스와 함께 캡슐화된다는 점이다. 파일 처리, 데이터베이스 연결, 네트워크 소켓, 스레드 락 등 모든 종류의 리소스에 대해 일관된 패턴으로 안전한 관리가 가능하며, 개발자가 실수로 리소스 해제를 빠뜨릴 위험을 원천적으로 차단한다.
# finally를 사용하는 대신 with 문을 사용한 파일 처리
try:
with open("my_file.txt", "w") as file:
file.write("Hello, with statement!")
# with 블록을 벗어나는 순간, file.close()가 자동으로 호출됩니다.
print("파일 쓰기 및 자동 닫기 완료.")
except IOError as e:
print(f"파일 처리 중 오류가 발생했습니다: {e}")
예외 체인과 중첩 예외 처리
저수준 예외를 고수준 예외로 감싸는 것은 소프트웨어의 추상화 계층을 명확히 하고 사용자에게 이해하기 쉬운 오류 메시지를 제공하는 중요한 기법이다. 예를 들어 데이터 처리 함수에서 KeyError나 TypeError 같은 파이썬 내부 예외가 발생했을 때, 이를 그대로 노출하면 사용자는 문제의 실제 맥락을 파악하기 어렵다. 하지만 원본 예외 정보를 완전히 잃어버리면 개발자가 디버깅할 때 중요한 단서를 놓치게 된다.
예외 체인은 이런 딜레마를 해결하는 파이썬의 우아한 해결책이다. raise NewException from original_exception 구문을 사용하면 원래 발생했던 예외를 원인으로 하는 새로운 예외를 발생시킬 수 있다. 이때 새로운 예외는 사용자 친화적인 메시지를 담고, 원본 예외는 cause 속성을 통해 접근할 수 있어 디버깅 정보를 보존한다. 이는 소프트웨어의 각 계층이 자신에게 적합한 추상화 수준으로 예외를 처리하면서도 전체적인 오류 맥락을 유지할 수 있게 해준다.
특히 복잡한 시스템에서는 여러 계층을 거쳐 예외가 전파되면서 각 계층마다 적절한 컨텍스트 정보를 추가할 수 있다. 예를 들어 데이터베이스 연결 오류가 발생하면, 데이터 액세스 계층에서는 “데이터베이스 연결 실패”로, 비즈니스 로직 계층에서는 “사용자 정보 조회 실패”로, 프레젠테이션 계층에서는 “일시적인 서비스 장애”로 각각 의미 있는 메시지를 제공하면서도 근본 원인을 추적할 수 있다.
class DataProcessingError(Exception):
"""데이터 처리 과정에서 발생하는 상위 예외"""
pass
def process_user_data(user_data):
"""사용자 데이터를 처리하고 이름을 반환하는 함수"""
try:
# 'name' 키가 없을 경우 KeyError 발생
name = user_data['name']
# 이름의 첫 글자를 대문자로 변경
return name.upper()
except KeyError as e:
# KeyError를 더 의미 있는 예외로 감싸서 다시 발생시킴
raise DataProcessingError("필수 'name' 키가 데이터에 없습니다.") from e
except TypeError as e:
# 이름이 문자열이 아닐 경우 TypeError 발생
raise DataProcessingError("'name' 값은 문자열이어야 합니다.") from e
# 사용 예시
user1 = {"name": "alice"}
user2 = {"age": 30} # 'name' 키가 없음
try:
print(process_user_data(user1)) # 정상 실행
print(process_user_data(user2)) # 오류 발생
except DataProcessingError as e:
print(f"데이터 처리 실패: {e}")
# __cause__를 통해 원래 예외를 확인할 수 있음
if e.__cause__:
print(f"근본 원인: {type(e.__cause__).__name__}: {e.__cause__}")
파이썬의 예외 처리는 단순히 오류를 잡아내는 기술을 넘어서 사용자 친화적이고 안정적인 소프트웨어를 만드는 핵심 설계 철학이다. try-except의 기본 구조부터 시작하여 다양한 예외 타입을 세밀하게 처리하고, as 키워드로 오류 정보를 활용하며, finally 블록으로 리소스를 안전하게 관리하는 모든 과정은 예측 가능하고 신뢰할 수 있는 프로그램을 구축하기 위한 필수 요소들이다.
특히 사용자 정의 예외를 통해 비즈니스 로직의 의도를 명확히 표현하고, 파이썬의 다양한 내장 예외들의 특성을 이해하여 적절히 대응하는 능력은 개발자의 성숙도를 보여주는 중요한 지표다. 예외 처리는 코드의 가독성을 높이고 디버깅을 용이하게 만들며, 무엇보다 사용자가 예상치 못한 상황에서도 당황하지 않고 적절한 조치를 취할 수 있도록 안내하는 역할을 한다.
현대의 소프트웨어 개발에서는 단순히 기능이 동작하는 것을 넘어서 예외 상황에서도 우아하게 대응하는 복원력 있는 시스템이 요구된다. 파이썬의 예외 처리 메커니즘을 제대로 활용하면 이런 요구사항을 충족하는 견고한 애플리케이션을 개발할 수 있다. 예외 처리는 선택사항이 아닌 필수 기술이며, 이를 체계적으로 적용하는 습관이 파이썬 개발자로 성장하는 지름길이라 할 수 있다.