跳至正文

예외 처리

프로그램 실행 중 발생하는 에러를 우아하게 처리하는 방법让我们学习. 예외 처리는 안정적인 프로그램을 만드는 핵심입니다.

예외란? 🤔

예외(Exception)는 프로그램 실행 중 발생하는 오류입니다.

# 예외가 발생하는 코드
result = 10 / 0 # ZeroDivisionError
ZeroDivisionError: division by zero

기본 예외 처리: try-except

단순 예외 처리

try:
result = 10 / 0
except:
print('에러가 발생했습니다')

특정 예외 처리

try:
number = int(input('숫자를 입력하세요: '))
result = 10 / number
except ZeroDivisionError:
print('0으로 나눌 수 없습니다')
except ValueError:
print('올바른 숫자를 입력하세요')

예외 메시지 받기

try:
file = open('없는파일.txt', 'r')
except FileNotFoundError as e:
print(f'파일을 찾을 수 없습니다: {e}')

try-except-else-finally

전체 구조

try:
# 실행할 코드
result = 10 / 2
except ZeroDivisionError:
# 예외 발생 시 실행
print('0으로 나눌 수 없습니다')
else:
# 예외가 없을 때만 실행
print(f'결과: {result}')
finally:
# 항상 실행 (예외 발생 여부와 무관)
print('계산 완료')

实践示例: 파일 처리

def read_file(filename):
file = None
try:
file = open(filename, 'r', encoding='utf-8')
content = file.read()
return content
except FileNotFoundError:
print(f'파일이 없습니다: {filename}')
return None
except PermissionError:
print(f'读取文件 권한이 없습니다: {filename}')
return None
finally:
if file:
file.close()
print('파일을 닫았습니다')

# 사용 예제
content = read_file('data.txt')

주요 예외 종류 📋

일반적인 예외

# ZeroDivisionError: 0으로 나누기
try:
10 / 0
except ZeroDivisionError:
print('0으로 나눌 수 없습니다')

# ValueError: 잘못된 값
try:
int('abc')
except ValueError:
print('숫자로 변환할 수 없습니다')

# TypeError: 잘못된 타입
try:
'문자열' + 123
except TypeError:
print('타입이 맞지 않습니다')

# IndexError: 잘못된 인덱스
try:
my_list = [1, 2, 3]
print(my_list[10])
except IndexError:
print('인덱스가 범위를 벗어났습니다')

# KeyError: 없는 키
try:
my_dict = {'a': 1}
print(my_dict['b'])
except KeyError:
print('키가 존재하지 않습니다')

# AttributeError: 없는 속성
try:
my_str = 'hello'
my_str.append('!')
except AttributeError:
print('해당 속성이 없습니다')

파일 관련 예외

# FileNotFoundError: 파일 없음
try:
open('없는파일.txt', 'r')
except FileNotFoundError:
print('파일을 찾을 수 없습니다')

# PermissionError: 권한 없음
try:
open('/root/file.txt', 'r')
except PermissionError:
print('파일 접근 권한이 없습니다')

# IsADirectoryError: 디렉토리를 파일로 열기
try:
open('some_directory', 'r')
except IsADirectoryError:
print('디렉토리입니다')

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

튜플로 묶기

try:
# 어떤 작업
result = risky_operation()
except (ValueError, TypeError, KeyError):
print('값, 타입 또는 키 에러가 발생했습니다')

각각 다르게 처리하기

try:
data = fetch_data()
value = int(data)
except ValueError as e:
print(f'값 변환 에러: {e}')
except ConnectionError as e:
print(f'연결 에러: {e}')
except Exception as e: # 모든 예외 처리
print(f'알 수 없는 에러: {e}')

예외 발생시키기 (raise) 🚀

기본 raise

def divide(a, b):
if b == 0:
raise ValueError('b는 0이 될 수 없습니다')
return a / b

try:
result = divide(10, 0)
except ValueError as e:
print(e) # 'b는 0이 될 수 없습니다'

예외 재발생

def process_data(data):
try:
result = complex_operation(data)
except ValueError as e:
print(f'데이터 처리 중 에러 발생: {e}')
raise # 예외를 다시 발생시킴

try:
process_data(invalid_data)
except ValueError:
print('상위 레벨에서 처리')

예외 체인

def load_config():
try:
with open('config.txt', 'r') as f:
return f.read()
except FileNotFoundError as e:
raise RuntimeError('설정 파일을 로드할 수 없습니다') from e

try:
config = load_config()
except RuntimeError as e:
print(e)
print(f'원인: {e.__cause__}')

커스텀 예외 만들기 🎨

기본 커스텀 예외

class InvalidAgeError(Exception):
"""나이가 유효하지 않을 때 발생하는 예외"""
pass

def set_age(age):
if age < 0 or age > 150:
raise InvalidAgeError(f'유효하지 않은 나이: {age}')
return age

try:
set_age(-5)
except InvalidAgeError as e:
print(e)

메시지와 데이터를 포함하는 예외

class ValidationError(Exception):
"""데이터 검증 실패 예외"""

def __init__(self, field, value, message):
self.field = field
self.value = value
self.message = message
super().__init__(f'{field}: {message} (값: {value})')

def validate_user(user):
if not user.get('email'):
raise ValidationError('email', user.get('email'), '이메일은 필수입니다')
if len(user.get('name', '')) < 2:
raise ValidationError('name', user.get('name'), '이름은 2자 이상이어야 합니다')

try:
validate_user({'name': 'A'})
except ValidationError as e:
print(f'필드: {e.field}')
print(f'값: {e.value}')
print(f'메시지: {e.message}')

예외 계층 구조

class AppError(Exception):
"""애플리케이션 기본 예외"""
pass

class DatabaseError(AppError):
"""데이터베이스 관련 예외"""
pass

class NetworkError(AppError):
"""네트워크 관련 예외"""
pass

class ConnectionError(NetworkError):
"""연결 실패 예외"""
pass

# 사용 예제
try:
# 어떤 작업
raise ConnectionError('서버에 연결할 수 없습니다')
except DatabaseError:
print('데이터베이스 에러')
except NetworkError: # ConnectionError도 포함
print('네트워크 에러')
except AppError: # 모든 앱 에러 포함
print('애플리케이션 에러')

assert 문 ✅

디버깅과 개발 중 조건 검증에 사용합니다.

def calculate_average(numbers):
assert len(numbers) > 0, '리스트가 비어있습니다'
return sum(numbers) / len(numbers)

# 정상 작동
result = calculate_average([1, 2, 3])
print(result) # 2.0

# AssertionError 발생
result = calculate_average([]) # AssertionError: 리스트가 비어있습니다

assert vs 예외 처리

# assert: 개발 중 버그 확인용 (프로덕션에서는 비활성화 가능)
def internal_function(value):
assert value > 0, 'value는 양수여야 합니다'
return value * 2

# raise: 실제 에러 처리용 (항상 활성화)
def public_api(value):
if value <= 0:
raise ValueError('value는 양수여야 합니다')
return value * 2

实践示例 💡

예제 1: 안전한 读取文件

from pathlib import Path

def safe_read_file(file_path, default=None):
"""안전하게 파일을 읽고 에러 시 기본값 반환"""
try:
path = Path(file_path)
return path.read_text(encoding='utf-8')
except FileNotFoundError:
print(f'파일이 없습니다: {file_path}')
return default
except PermissionError:
print(f'파일 접근 권한이 없습니다: {file_path}')
return default
except UnicodeDecodeError:
print(f'파일 인코딩 오류: {file_path}')
return default
except Exception as e:
print(f'알 수 없는 오류: {e}')
return default

# 사용 예제
content = safe_read_file('config.txt', default='# 기본 설정')
print(content)

예제 2: 사용자 입력 검증

def get_positive_integer(prompt):
"""양의 정수를 입력받을 때까지 반복"""
while True:
try:
value = input(prompt)
number = int(value)

if number <= 0:
raise ValueError('양수를 입력해야 합니다')

return number

except ValueError as e:
if 'invalid literal' in str(e):
print('숫자를 입력해주세요')
else:
print(e)
except KeyboardInterrupt:
print('\n입력이 취소되었습니다')
return None

# 사용 예제
# age = get_positive_integer('나이를 입력하세요: ')

예제 3: API 호출 재시도

import time

class APIError(Exception):
"""API 호출 실패"""
pass

def call_api_with_retry(url, max_retries=3, delay=1):
"""API 호출을 재시도 로직과 함께 실행"""
last_error = None

for attempt in range(max_retries):
try:
# 실제로는 requests 등을 사용
response = fetch_data(url)

if response.status_code == 200:
return response.data
else:
raise APIError(f'상태 코드: {response.status_code}')

except (APIError, ConnectionError) as e:
last_error = e
print(f'시도 {attempt + 1}/{max_retries} 실패: {e}')

if attempt < max_retries - 1:
print(f'{delay}초 후 재시도...')
time.sleep(delay)
else:
print('모든 재시도 실패')

raise APIError(f'API 호출 실패: {last_error}')

# 사용 예제
try:
data = call_api_with_retry('https://api.example.com/data')
except APIError as e:
print(f'최종 실패: {e}')

예제 4: 데이터 검증 시스템

class ValidationError(Exception):
"""검증 실패 예외"""
pass

class Validator:
"""데이터 검증 클래스"""

@staticmethod
def validate_email(email):
"""이메일 검증"""
if not email or '@' not in email:
raise ValidationError(f'유효하지 않은 이메일: {email}')
return True

@staticmethod
def validate_age(age):
"""나이 검증"""
try:
age = int(age)
except (ValueError, TypeError):
raise ValidationError(f'나이는 숫자여야 합니다: {age}')

if age < 0 or age > 150:
raise ValidationError(f'유효하지 않은 나이: {age}')
return True

@staticmethod
def validate_phone(phone):
"""전화번호 검증 (간단한 예제)"""
phone = str(phone).replace('-', '').replace(' ', '')
if not phone.isdigit() or len(phone) < 10:
raise ValidationError(f'유효하지 않은 전화번호: {phone}')
return True

def register_user(user_data):
"""사용자 등록 (검증 포함)"""
errors = []

# 각 필드 검증
try:
Validator.validate_email(user_data.get('email'))
except ValidationError as e:
errors.append(str(e))

try:
Validator.validate_age(user_data.get('age'))
except ValidationError as e:
errors.append(str(e))

try:
Validator.validate_phone(user_data.get('phone'))
except ValidationError as e:
errors.append(str(e))

# 에러가 있으면 모두 보고
if errors:
error_msg = '\n'.join(errors)
raise ValidationError(f'검증 실패:\n{error_msg}')

return True

# 사용 예제
try:
user = {
'email': 'invalid-email',
'age': -5,
'phone': '123'
}
register_user(user)
except ValidationError as e:
print(e)

예제 5: 컨텍스트 매니저와 예외 처리

class DatabaseConnection:
"""데이터베이스 연결 관리 (예제)"""

def __init__(self, db_name):
self.db_name = db_name
self.connection = None

def __enter__(self):
print(f'{self.db_name}에 연결 중...')
try:
self.connection = self.connect(self.db_name)
return self.connection
except Exception as e:
raise ConnectionError(f'DB 연결 실패: {e}')

def __exit__(self, exc_type, exc_val, exc_tb):
if self.connection:
print(f'{self.db_name} 연결 종료')
self.connection.close()

# 예외를 억제하지 않음
return False

def connect(self, db_name):
# 실제 연결 로직
return {'connected': True, 'db': db_name}

# 사용 예제
try:
with DatabaseConnection('mydb') as db:
# 데이터베이스 작업
print(f'작업 중: {db}')
# 에러 발생 시뮬레이션
# raise ValueError('쿼리 실패')
except ConnectionError as e:
print(f'연결 에러: {e}')
except ValueError as e:
print(f'작업 에러: {e}')

예제 6: 로깅과 예외 처리

import logging
from datetime import datetime

# 로거 설정
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

def process_transaction(transaction_id, amount):
"""트랜잭션 처리 (로깅 포함)"""
try:
logger.info(f'트랜잭션 시작: {transaction_id}')

# 검증
if amount <= 0:
raise ValueError(f'유효하지 않은 금액: {amount}')

# 처리
result = perform_transaction(transaction_id, amount)

logger.info(f'트랜잭션 성공: {transaction_id}')
return result

except ValueError as e:
logger.error(f'검증 실패 [{transaction_id}]: {e}')
raise
except ConnectionError as e:
logger.critical(f'연결 실패 [{transaction_id}]: {e}')
raise
except Exception as e:
logger.exception(f'예상치 못한 오류 [{transaction_id}]')
raise
finally:
logger.debug(f'트랜잭션 종료: {transaction_id}')

def perform_transaction(tid, amount):
# 실제 트랜잭션 로직
return {'id': tid, 'amount': amount, 'status': 'completed'}

# 사용 예제
try:
result = process_transaction('TXN001', 1000)
except ValueError:
print('유효하지 않은 트랜잭션')
except Exception:
print('트랜잭션 처리 실패')

예외 처리 베스트 프랙티스 🌟

1. 구체적인 예외를 먼저 처리

# 좋음
try:
operation()
except FileNotFoundError:
# 구체적 처리
pass
except IOError:
# 더 일반적인 처리
pass
except Exception:
# 가장 일반적인 처리
pass

# 나쁨 - 절대 도달하지 않음
try:
operation()
except Exception:
pass
except FileNotFoundError: # 도달하지 않음!
pass

2. 빈 except 피하기

# 나쁨
try:
risky_operation()
except: # 모든 예외를 잡음 (KeyboardInterrupt도!)
pass

# 좋음
try:
risky_operation()
except Exception as e: # 시스템 예외는 제외
logger.error(f'에러 발생: {e}')

3. 예외 메시지에 컨텍스트 포함

# 나쁨
raise ValueError('유효하지 않음')

# 좋음
raise ValueError(f'유효하지 않은 나이: {age} (0-150 사이여야 함)')

4. 조용히 실패하지 않기

# 나쁨 - 에러를 숨김
try:
important_operation()
except:
pass # 에러가 발생해도 모름

# 좋음 - 최소한 로깅
try:
important_operation()
except Exception as e:
logger.error(f'중요한 작업 실패: {e}')
raise # 또는 적절한 처리

常见问题 ❓

Q1: except Exception과 except만 쓰는 것의 차이는?

# except: 모든 것을 잡음 (BaseException)
try:
operation()
except:
# KeyboardInterrupt, SystemExit도 잡음 (위험!)
pass

# except Exception: 일반 예외만 잡음
try:
operation()
except Exception:
# 시스템 종료 시그널은 통과시킴 (안전)
pass

Q2: 예외를 무시해도 되는 경우는?

# 선택적 기능이고 실패해도 괜찮을 때
try:
import optional_module
use_advanced_feature()
except ImportError:
# 모듈이 없어도 계속 실행
use_basic_feature()

# 파일이 없는 것이 정상일 때
try:
config = load_config()
except FileNotFoundError:
config = default_config()

Q3: raise와 raise e의 차이는?

try:
operation()
except ValueError as e:
# raise: 원래 스택 트레이스 유지 (권장)
raise

# raise e: 새로운 스택 트레이스 생성
# raise e

Q4: finally 없이 리소스를 정리하는 방법은?

# with 문 사용 (권장)
with open('file.txt', 'r') as f:
content = f.read()
# 자동으로 파일이 닫힘

Q5: 여러 예외를 하나로 묶는 방법은?

# Python 3.11+ : ExceptionGroup
try:
operations()
except* ValueError as eg:
# 여러 ValueError를 한 번에 처리
for e in eg.exceptions:
print(e)

# 이전 버전: 수동으로 수집
errors = []
for item in items:
try:
process(item)
except ValueError as e:
errors.append(e)

if errors:
print(f'{len(errors)}개의 에러 발생')

下一步

  • 파일 입출력: 파일 작업에서 예외 처리 활용하기
  • 날짜와 시간: 시간 관련 예외 처리
  • logging 모듈: 예외를 효과적으로 기록하는 방법
  • 디버깅 기법: pdb를 사용한 예외 분석