[Python 100일 챌린지] Day 46 - 예외 처리 고급
class PaymentError(Exception)→raise PaymentError("잔액 부족")→ 명확한 에러! 😊“FileNotFoundError” 대신 “회원정보없음Error”, “결제실패Error” 같은 나만의 에러 만들기! 대형 프로젝트처럼 전문적인 예외 처리를 배웁니다!
(35-45분 완독 ⭐⭐⭐)
🎯 오늘의 학습 목표
📚 사전 지식
- Day 45: 예외 처리 기초 (try-except, finally)
- Phase 4의 객체지향 프로그래밍 개념 (클래스, 상속)
🎯 학습 목표 1: 사용자 정의 예외 만들기
1.1 기본 커스텀 예외
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ValidationError(Exception):
"""데이터 검증 오류"""
pass
def validate_age(age):
if age < 0:
raise ValidationError("나이는 음수일 수 없습니다")
if age > 150:
raise ValidationError("나이가 너무 큽니다")
return True
# 사용
try:
validate_age(-5)
except ValidationError as e:
print(f"❌ 검증 오류: {e}")
1.2 속성을 가진 예외
예외 클래스에 추가 정보를 저장할 수 있습니다.
💡
super().__init__(message)설명
super()는 부모 클래스를 의미합니다. 여기서APIError의 부모는Exception이죠.super().__init__(message)는 부모 클래스의 생성자를 호출해서 에러 메시지를 전달합니다. 이렇게 해야str(e)나print(e)로 에러 메시지를 출력할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class APIError(Exception):
"""API 오류"""
def __init__(self, message, status_code, response=None):
# 부모 클래스(Exception)에 메시지 전달
super().__init__(message)
# 추가 정보를 속성으로 저장
self.status_code = status_code
self.response = response
def __str__(self):
return f"[{self.status_code}] {super().__str__()}"
# 사용
try:
raise APIError("서버 오류", status_code=500, response={"error": "..."})
except APIError as e:
print(e) # [500] 서버 오류
print(f"상태 코드: {e.status_code}")
1.3 예외 계층 구조
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class AppError(Exception):
"""앱 기본 예외"""
pass
class DatabaseError(AppError):
"""데이터베이스 오류"""
pass
class NetworkError(AppError):
"""네트워크 오류"""
pass
class AuthenticationError(AppError):
"""인증 오류"""
pass
# 사용
try:
raise DatabaseError("연결 실패")
except AppError as e: # 모든 앱 예외 처리
print(f"앱 오류: {e}")
🎯 학습 목표 2: 예외 발생시키기와 체이닝
2.1 raise 기본 사용법
raise 키워드로 의도적으로 예외를 발생시킬 수 있습니다:
1
2
3
4
5
6
7
8
9
10
def validate_age(age):
if age < 0:
raise ValueError("나이는 음수일 수 없습니다")
return age
# 사용
try:
validate_age(-5)
except ValueError as e:
print(f"❌ 오류: {e}")
2.2 예외 체이닝 (from 키워드)
다른 예외를 처리하다가 새 예외를 발생시킬 때, from을 사용해 원인을 연결합니다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def process_data(data):
try:
result = int(data) * 2
return result
except ValueError as e:
# 원인 예외를 포함하여 새 예외 발생
raise TypeError("데이터 처리 실패") from e
# 사용
try:
process_data("abc")
except TypeError as e:
print(f"오류: {e}")
print(f"원인: {e.__cause__}") # 원래 ValueError 확인 가능
출력 결과:
1
2
오류: 데이터 처리 실패
원인: invalid literal for int() with base 10: 'abc'
2.3 암시적 예외 컨텍스트
from을 사용하지 않아도, except 블록에서 새 예외를 발생시키면 자동으로 컨텍스트가 연결됩니다:
1
2
3
4
5
6
7
8
9
try:
try:
result = 10 / 0
except ZeroDivisionError:
raise ValueError("잘못된 계산")
except ValueError as e:
print(f"예외: {e}")
print(f"컨텍스트: {e.__context__}") # 원래 ZeroDivisionError
💡
__cause__vs__context__
__cause__:raise ... from e로 명시적으로 연결된 원인__context__: except 블록에서 자동으로 연결된 이전 예외
🎯 학습 목표 3: 컨텍스트 매니저와 예외
💡 컨텍스트 매니저란?
with문과 함께 사용되는 객체입니다. 파일, 데이터베이스 연결 등 자원을 안전하게 관리합니다.__enter__(시작할 때)와__exit__(끝날 때) 메서드를 구현합니다.
1 2 3 4 # 가장 흔한 예: 파일 처리 with open('file.txt') as f: # __enter__ 호출 content = f.read() # __exit__ 호출 → 자동으로 파일 닫힘
3.1 __exit__에서 예외 처리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class DatabaseConnection:
def __enter__(self):
print("DB 연결")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""예외 정보를 받음"""
print("DB 연결 해제")
if exc_type is not None:
print(f"예외 발생: {exc_type.__name__}: {exc_val}")
# False 반환: 예외를 다시 발생
# True 반환: 예외를 억제
return False
# 사용
try:
with DatabaseConnection() as db:
raise ValueError("데이터 오류")
except ValueError:
print("외부에서 처리")
3.2 예외 억제
1
2
3
4
5
6
7
8
from contextlib import suppress
# 예외 무시하고 계속 진행
with suppress(FileNotFoundError):
with open('없는파일.txt') as f:
content = f.read()
print("계속 실행")
🎯 학습 목표 4: 예외 처리 베스트 프랙티스
4.1 구체적인 예외 처리
1
2
3
4
5
6
7
8
9
10
11
12
13
# ❌ 나쁜 예
try:
# ...
except:
pass
# ✅ 좋은 예
try:
# ...
except ValueError as e:
logging.error(f"값 오류: {e}")
except TypeError as e:
logging.error(f"타입 오류: {e}")
4.2 예외를 무시하지 말기
1
2
3
4
5
6
7
8
9
10
11
12
# ❌ 나쁜 예
try:
# ...
except Exception:
pass # 완전히 무시!
# ✅ 좋은 예
try:
# ...
except Exception as e:
logging.exception("예외 발생")
# 또는 기본값 반환
4.3 EAFP vs LBYL
Python에서 조건 검사를 하는 두 가지 스타일이 있습니다:
💡 LBYL: “Look Before You Leap” - “뛰기 전에 살펴보라”
- 먼저 조건을 확인하고 작업 실행
- 다른 언어에서 흔한 방식
EAFP: “Easier to Ask for Forgiveness than Permission” - “허락보다 용서가 쉽다”
- 일단 실행하고, 문제가 생기면 예외 처리
- Python에서 권장하는 방식!
1
2
3
4
5
6
7
8
9
10
11
# LBYL - 먼저 확인하고 실행 (조건문 사용)
if key in dictionary:
value = dictionary[key]
else:
value = default
# EAFP - 일단 실행하고 예외 처리 (Python스러움! ✅)
try:
value = dictionary[key]
except KeyError:
value = default
왜 EAFP가 더 좋을까요?
- 코드가 더 깔끔함
- 경쟁 조건(race condition) 방지
- 파일 존재 확인 후 열기 vs 바로 열기 (파일이 그 사이 삭제될 수 있음!)
4.4 예외 로깅
1
2
3
4
5
6
7
8
import logging
logging.basicConfig(level=logging.INFO)
try:
result = 10 / 0
except ZeroDivisionError:
logging.exception("0으로 나누기 시도") # 스택 트레이스 포함
4.5 실전 패턴
패턴 1: Retry (재시도)
💡 데코레이터(Decorator)란?
함수를 꾸며주는 함수입니다.
@데코레이터형태로 함수 위에 붙입니다. 함수 실행 전/후에 추가 기능을 넣을 수 있습니다.
1 2 3 4 5 6 @retry # ← 이게 데코레이터! def my_function(): pass # 위 코드는 아래와 같습니다: # my_function = retry(my_function)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import time
from functools import wraps
def retry(max_attempts=3, delay=1, exceptions=(Exception,)):
"""
재시도 데코레이터
- max_attempts: 최대 시도 횟수
- delay: 재시도 간격 (초)
- exceptions: 재시도할 예외 종류
"""
def decorator(func):
@wraps(func) # 원래 함수의 이름, 문서 등을 유지
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except exceptions as e:
if attempt == max_attempts - 1:
raise # 마지막 시도에서 실패하면 예외 발생
print(f"재시도 {attempt + 1}/{max_attempts}...")
time.sleep(delay)
return wrapper
return decorator
# 사용 - @retry를 붙이면 자동으로 재시도 기능이 추가됨!
@retry(max_attempts=3, delay=0.5, exceptions=(ConnectionError,))
def fetch_data(url):
# 네트워크 요청 (실패하면 자동 재시도)
pass
패턴 2: Fallback 체인
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def get_config_value(key):
"""설정 값 가져오기 (여러 소스 시도)"""
# 1. 환경 변수
try:
import os
return os.environ[key]
except KeyError:
pass
# 2. 설정 파일
try:
import json
with open('config.json') as f:
config = json.load(f)
return config[key]
except (FileNotFoundError, KeyError):
pass
# 3. 기본값
defaults = {'timeout': 30, 'debug': False}
try:
return defaults[key]
except KeyError:
raise ValueError(f"설정 없음: {key}")
패턴 3: 예외 변환
내부 예외를 외부용 예외로 변환하는 패턴입니다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 예외 클래스 정의
class ServiceError(Exception):
"""서비스 오류 (외부용)"""
pass
class DatabaseError(Exception):
"""데이터베이스 오류 (내부용)"""
pass
# 예외 변환 예제
def get_user_data(user_id):
"""사용자 데이터 가져오기"""
try:
# 파일에서 데이터 읽기 (DB 대신 간단한 예제)
with open(f"users/{user_id}.json") as f:
return f.read()
except FileNotFoundError as e:
# 내부 예외 → 외부용 예외로 변환
raise ServiceError(f"사용자 {user_id}를 찾을 수 없습니다") from e
except PermissionError as e:
raise ServiceError("데이터 접근 권한이 없습니다") from e
# 사용
try:
data = get_user_data(123)
except ServiceError as e:
print(f"❌ 서비스 오류: {e}")
print(f"원인: {e.__cause__}") # 원본 예외 확인 가능
4.6 종합 예제
예제 1: 견고한 파일 처리기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import logging
class FileProcessor:
"""예외 처리가 강화된 파일 처리기"""
def __init__(self, filename, mode='r', encoding='utf-8'):
self.filename = filename
self.mode = mode
self.encoding = encoding
self.file = None
def __enter__(self):
try:
self.file = open(self.filename, self.mode, encoding=self.encoding)
return self.file
except FileNotFoundError:
raise FileProcessingError(
f"파일 없음: {self.filename}"
)
except PermissionError:
raise FileProcessingError(
f"권한 없음: {self.filename}"
)
def __exit__(self, exc_type, exc_val, exc_tb):
if self.file:
try:
self.file.close()
except Exception as e:
logging.warning(f"파일 닫기 실패: {e}")
# 예외를 다시 발생시킴
return False
class FileProcessingError(Exception):
"""파일 처리 오류"""
pass
# 사용
try:
with FileProcessor('data.txt') as f:
content = f.read()
print(content)
except FileProcessingError as e:
print(f"❌ {e}")
예제 2: 재시도 로직이 있는 API 클라이언트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import time
import random
# 예외 클래스 정의
class APIError(Exception):
"""API 오류"""
pass
class NetworkError(Exception):
"""네트워크 오류"""
pass
class APIClient:
"""예외 처리가 강화된 API 클라이언트"""
def request(self, url, max_retries=3):
"""API 요청 (재시도 로직 포함)"""
for attempt in range(max_retries):
try:
response = self._do_request(url)
return response
except NetworkError as e:
# 마지막 시도면 예외 발생
if attempt == max_retries - 1:
raise APIError(f"네트워크 오류: {e}") from e
# 재시도 (지수 백오프: 1초, 2초, 4초...)
wait_time = 2 ** attempt
print(f"⚠️ 재시도 {attempt + 1}/{max_retries} ({wait_time}초 후)")
time.sleep(wait_time)
def _do_request(self, url):
"""실제 요청 (시뮬레이션)"""
# 30% 확률로 실패
if random.random() < 0.3:
raise NetworkError("연결 실패")
return {"status": "ok", "data": "..."}
# 사용 예제
client = APIClient()
try:
result = client.request("https://api.example.com/data")
print(f"✅ 성공: {result}")
except APIError as e:
print(f"❌ API 오류: {e}")
💡 실전 팁 & 주의사항
✅ 커스텀 예외 설계 팁
- 명확한 이름
1 2 3 4 5 6 7
# ✅ 좋은 예 class PaymentProcessingError(Exception): pass # ❌ 나쁜 예 class Error1(Exception): pass
- 예외 계층 구조
1 2 3 4 5 6 7 8 9 10 11
class AppError(Exception): """기본 예외""" pass class DatabaseError(AppError): """DB 예외""" pass class NetworkError(AppError): """네트워크 예외""" pass
- 유용한 정보 포함
1 2 3 4
class ValidationError(Exception): def __init__(self, field, message): self.field = field super().__init__(f"{field}: {message}")
⚠️ 주의사항
- 예외 클래스는
Exception을 상속받으세요 - 너무 많은 커스텀 예외는 복잡도를 높입니다
- 예외 체이닝으로 원인을 명확히 하세요
🧪 연습 문제
문제 1: 커스텀 예외 클래스
목표: 은행 계좌 시스템을 위한 커스텀 예외를 작성하세요.
요구사항:
InsufficientFundsError: 잔액 부족 (부족한 금액 정보 포함)InvalidAmountError: 잘못된 금액 (음수 등)AccountLockedError: 계좌 잠김
해답 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class InsufficientFundsError(Exception):
"""잔액 부족 예외"""
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
shortage = amount - balance
super().__init__(
f"잔액 부족: {balance}원 (부족액: {shortage}원)"
)
class InvalidAmountError(Exception):
"""잘못된 금액 예외"""
pass
class AccountLockedError(Exception):
"""계좌 잠김 예외"""
pass
class BankAccount:
def __init__(self, balance=0):
self.balance = balance
self.locked = False
def withdraw(self, amount):
if self.locked:
raise AccountLockedError("계좌가 잠겨있습니다")
if amount <= 0:
raise InvalidAmountError("금액은 양수여야 합니다")
if amount > self.balance:
raise InsufficientFundsError(self.balance, amount)
self.balance -= amount
return self.balance
# 테스트
account = BankAccount(10000)
try:
account.withdraw(15000)
except InsufficientFundsError as e:
print(f"❌ {e}")
print(f"현재 잔액: {e.balance}원")
문제 2: Retry 데코레이터
목표: 실패 시 자동으로 재시도하는 데코레이터를 작성하세요.
요구사항:
- 최대 3회 재시도
- 재시도 간격 1초
- 특정 예외만 재시도
해답 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import time
from functools import wraps
def retry(max_attempts=3, delay=1, exceptions=(Exception,)):
"""재시도 데코레이터"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except exceptions as e:
if attempt == max_attempts - 1:
raise
print(f"재시도 {attempt + 1}/{max_attempts}... ({e})")
time.sleep(delay)
return wrapper
return decorator
# 사용 예
@retry(max_attempts=3, delay=0.5)
def unstable_operation():
import random
if random.random() < 0.7:
raise ConnectionError("연결 실패")
return "성공!"
# 테스트
try:
result = unstable_operation()
print(f"✅ {result}")
except ConnectionError:
print("❌ 최종 실패")
📝 오늘 배운 내용 정리
| 개념 | 설명 | 예시 |
|---|---|---|
| 커스텀 예외 | 사용자 정의 예외 클래스 | class MyError(Exception) |
| super().__init__() | 부모 클래스 생성자 호출 | 에러 메시지 전달용 |
| 예외 체이닝 | 원인 예외 연결 | raise NewError() from e |
| 컨텍스트 매니저 | with문과 함께 자원 관리 | __enter__, __exit__ 메서드 |
| EAFP | 일단 실행, 문제시 예외 처리 | Python 권장 스타일 |
| LBYL | 먼저 확인 후 실행 | 다른 언어에서 흔함 |
| 데코레이터 | 함수를 꾸며주는 함수 | @retry 형태로 사용 |
핵심 코드 패턴
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 커스텀 예외
class ValidationError(Exception):
def __init__(self, field, message):
self.field = field
super().__init__(f"{field}: {message}")
# 예외 체이닝
try:
result = process()
except ValueError as e:
raise ProcessingError("처리 실패") from e
# Retry 패턴
for attempt in range(max_retries):
try:
return operation()
except TempError:
if attempt == max_retries - 1:
raise
time.sleep(delay)
🔗 관련 자료
📚 이전 학습
Day 45: 예외 처리 기초 ⭐⭐⭐
어제는 try-except 기본 구조, 예외 타입, finally 절 등 예외 처리의 기초를 배웠습니다!
📚 다음 학습
Day 47: 로깅 시스템 ⭐⭐⭐
내일은 logging 모듈로 프로그램의 실행 기록을 파일로 남기고 관리하는 방법을 배웁니다!
“늦었다고 생각할 때가 가장 빠른 시기입니다!” 🚀
Day 46/100 Phase 5: 파일 처리와 예외 처리 #100DaysOfPython
이제와서 시작하는 Python 마스터하기 - Day 46 완료! 🎉
