Launguage

Python 예외 처리와 컨텍스트 매니저 완벽 가이드

Somaz 2025. 12. 19. 00:00
728x90
반응형

Overview

예외 처리(Exception Handling)와 컨텍스트 매니저(Context Manager)는 견고하고 안전한 Python 코드를 작성하는 핵심 기능이다. 오류를 우아하게 처리하고, 리소스를 안전하게 관리하는 방법을 알아보자.

 

 

 

 

 

 

 


 

 

1. 예외 처리 기본

 

 

try-except 기본 구조

try:
    # 예외가 발생할 수 있는 코드
    result = 10 / 0
except ZeroDivisionError:
    # 예외 처리
    print("0으로 나눌 수 없습니다!")

 

 

여러 예외 처리하기

def safe_divide(a, b):
    try:
        result = a / b
        return result
    except ZeroDivisionError:
        print("0으로 나눌 수 없습니다!")
        return None
    except TypeError:
        print("숫자만 입력 가능합니다!")
        return None

print(safe_divide(10, 2))     # 5.0
print(safe_divide(10, 0))     # 0으로 나눌 수 없습니다! None
print(safe_divide(10, "a"))   # 숫자만 입력 가능합니다! None

 

 

여러 예외를 한 번에 처리

try:
    value = int(input("숫자 입력: "))
    result = 100 / value
except (ValueError, ZeroDivisionError) as e:
    print(f"오류 발생: {e}")

 

 

 

2. 예외 객체 활용

 

 

예외 정보 얻기

try:
    with open("없는파일.txt", "r") as f:
        content = f.read()
except FileNotFoundError as e:
    print(f"파일을 찾을 수 없습니다: {e}")
    print(f"예외 타입: {type(e)}")
    print(f"예외 메시지: {str(e)}")

 

 

모든 예외 잡기 (비권장)

try:
    # 위험한 코드
    risky_operation()
except Exception as e:
    print(f"예상치 못한 오류: {e}")
    # 하지만 가능하면 구체적인 예외를 잡는 게 좋음!

 

 

 

3. else와 finally

 

 

else: 예외가 발생하지 않았을 때

def read_file(filename):
    try:
        f = open(filename, 'r')
    except FileNotFoundError:
        print("파일이 없습니다.")
    else:
        # try 블록이 성공했을 때만 실행
        content = f.read()
        f.close()
        return content

print(read_file("data.txt"))

 

 

finally: 항상 실행

def process_file(filename):
    f = None
    try:
        f = open(filename, 'r')
        data = f.read()
        return data
    except FileNotFoundError:
        print("파일을 찾을 수 없습니다.")
        return None
    finally:
        # 예외 발생 여부와 관계없이 항상 실행
        if f:
            f.close()
            print("파일을 닫았습니다.")

process_file("test.txt")

 

 

완전한 구조

try:
    # 예외가 발생할 수 있는 코드
    result = dangerous_operation()
except SomeException as e:
    # 예외 처리
    handle_error(e)
else:
    # 예외가 발생하지 않았을 때
    print("성공!")
finally:
    # 항상 실행 (정리 작업)
    cleanup()

 

 

 

4. 예외 발생시키기 (raise)

 

 

기본 예외 발생

def validate_age(age):
    if age < 0:
        raise ValueError("나이는 음수일 수 없습니다!")
    if age > 150:
        raise ValueError("나이가 너무 많습니다!")
    return True

try:
    validate_age(-5)
except ValueError as e:
    print(f"유효성 검사 실패: {e}")

 

 

예외 다시 발생시키기

def process_data(data):
    try:
        # 데이터 처리
        result = complex_operation(data)
    except Exception as e:
        print(f"로그: 오류 발생 - {e}")
        raise  # 예외를 다시 발생시켜 상위로 전파

try:
    process_data(None)
except Exception:
    print("상위에서 예외 처리")

 

 

 

5. 커스텀 예외 만들기

 

 

사용자 정의 예외

class InsufficientFundsError(Exception):
    """잔액 부족 예외"""
    def __init__(self, balance, amount):
        self.balance = balance
        self.amount = amount
        message = f"잔액 부족: {balance}원 있음, {amount}원 필요"
        super().__init__(message)

class BankAccount:
    def __init__(self, balance):
        self.balance = balance
    
    def withdraw(self, amount):
        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}원")
    print(f"필요 금액: {e.amount}원")

 

 

예외 계층 구조

class ValidationError(Exception):
    """기본 검증 오류"""
    pass

class EmailValidationError(ValidationError):
    """이메일 검증 오류"""
    pass

class PasswordValidationError(ValidationError):
    """비밀번호 검증 오류"""
    pass

def validate_user(email, password):
    if "@" not in email:
        raise EmailValidationError("유효하지 않은 이메일 형식")
    if len(password) < 8:
        raise PasswordValidationError("비밀번호는 8자 이상이어야 합니다")

try:
    validate_user("test", "123")
except EmailValidationError as e:
    print(f"이메일 오류: {e}")
except PasswordValidationError as e:
    print(f"비밀번호 오류: {e}")
except ValidationError as e:
    # 모든 검증 오류를 잡음
    print(f"검증 오류: {e}")

 

 

 

6. 컨텍스트 매니저 (Context Manager)

컨텍스트 매니저는 with 문과 함께 사용하여 리소스를 안전하게 관리한다.

 

 

 

with 문 기본

# 나쁜 예: 파일을 닫지 않을 수 있음
f = open("data.txt", "r")
data = f.read()
f.close()  # 예외 발생 시 실행 안 됨!

# 좋은 예: 자동으로 파일 닫힘
with open("data.txt", "r") as f:
    data = f.read()
# 여기서 자동으로 f.close() 호출됨

 

 

여러 리소스 관리

with open("input.txt", "r") as infile, open("output.txt", "w") as outfile:
    content = infile.read()
    outfile.write(content.upper())
# 두 파일 모두 자동으로 닫힘

 

 

 

7. 커스텀 컨텍스트 매니저 만들기

 

 

클래스 기반 컨텍스트 매니저

class DatabaseConnection:
    def __init__(self, db_name):
        self.db_name = db_name
        self.connection = None
    
    def __enter__(self):
        print(f"{self.db_name} 연결 중...")
        self.connection = f"Connection to {self.db_name}"
        return self.connection
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        print(f"{self.db_name} 연결 종료")
        if exc_type is not None:
            print(f"예외 발생: {exc_type.__name__}: {exc_val}")
        # False 반환 시 예외가 전파됨
        # True 반환 시 예외를 억제
        return False

# 사용
with DatabaseConnection("mydb") as conn:
    print(f"연결됨: {conn}")
    # 작업 수행
# 자동으로 연결 종료

 

 

예외 처리가 있는 컨텍스트 매니저

class FileManager:
    def __init__(self, filename, mode):
        self.filename = filename
        self.mode = mode
        self.file = None
    
    def __enter__(self):
        try:
            self.file = open(self.filename, self.mode)
            return self.file
        except FileNotFoundError:
            print(f"파일을 찾을 수 없습니다: {self.filename}")
            raise
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.file:
            self.file.close()
            print("파일이 안전하게 닫혔습니다.")
        
        if exc_type is IOError:
            print("IO 오류가 발생했지만 처리했습니다.")
            return True  # 예외를 억제
        return False

# 사용
try:
    with FileManager("test.txt", "r") as f:
        content = f.read()
except FileNotFoundError:
    print("파일 처리 실패")

 

 

8. @contextmanager 데코레이터

더 간단하게 컨텍스트 매니저를 만들 수 있다.

from contextlib import contextmanager

@contextmanager
def timer(name):
    import time
    print(f"{name} 시작")
    start = time.time()
    try:
        yield  # with 블록이 실행되는 지점
    finally:
        end = time.time()
        print(f"{name} 완료: {end - start:.4f}초")

# 사용
with timer("데이터 처리"):
    # 시간을 측정할 작업
    total = sum(range(1000000))
    print(f"합계: {total}")

 

 

값을 반환하는 컨텍스트 매니저

from contextlib import contextmanager

@contextmanager
def temporary_setting(setting_name, new_value):
    import os
    old_value = os.environ.get(setting_name)
    
    # 설정 변경
    os.environ[setting_name] = new_value
    print(f"{setting_name} 변경: {old_value} → {new_value}")
    
    try:
        yield new_value  # with 문에 값 전달
    finally:
        # 원래 설정으로 복구
        if old_value is None:
            os.environ.pop(setting_name, None)
        else:
            os.environ[setting_name] = old_value
        print(f"{setting_name} 복구: {new_value} → {old_value}")

# 사용
with temporary_setting("DEBUG", "true") as debug:
    print(f"디버그 모드: {debug}")
    # 디버그 환경에서 작업
# 자동으로 원래 설정으로 복구

 

 

 

9. 실전 예제: 파일 처리 시스템

from contextlib import contextmanager
import json
import logging

# 로깅 설정
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class FileProcessingError(Exception):
    """파일 처리 오류"""
    pass

@contextmanager
def safe_file_operation(filename, mode='r'):
    """안전한 파일 작업을 위한 컨텍스트 매니저"""
    file_obj = None
    try:
        logger.info(f"파일 열기: {filename} (모드: {mode})")
        file_obj = open(filename, mode, encoding='utf-8')
        yield file_obj
    except FileNotFoundError:
        logger.error(f"파일을 찾을 수 없습니다: {filename}")
        raise FileProcessingError(f"파일 없음: {filename}")
    except PermissionError:
        logger.error(f"파일 접근 권한이 없습니다: {filename}")
        raise FileProcessingError(f"권한 없음: {filename}")
    except Exception as e:
        logger.error(f"예상치 못한 오류: {e}")
        raise
    finally:
        if file_obj:
            file_obj.close()
            logger.info(f"파일 닫기: {filename}")

class DataProcessor:
    def __init__(self, input_file, output_file):
        self.input_file = input_file
        self.output_file = output_file
    
    def process(self):
        """데이터 처리 메인 로직"""
        try:
            data = self._read_data()
            processed_data = self._transform_data(data)
            self._write_data(processed_data)
            logger.info("데이터 처리 완료")
            return True
        except FileProcessingError as e:
            logger.error(f"파일 처리 실패: {e}")
            return False
        except json.JSONDecodeError as e:
            logger.error(f"JSON 파싱 실패: {e}")
            return False
        except Exception as e:
            logger.error(f"처리 중 오류 발생: {e}")
            return False
    
    def _read_data(self):
        """파일에서 데이터 읽기"""
        with safe_file_operation(self.input_file, 'r') as f:
            return json.load(f)
    
    def _transform_data(self, data):
        """데이터 변환"""
        if not isinstance(data, list):
            raise ValueError("데이터는 리스트 형식이어야 합니다")
        
        processed = []
        for item in data:
            if 'name' not in item or 'value' not in item:
                logger.warning(f"잘못된 데이터 형식: {item}")
                continue
            
            processed.append({
                'name': item['name'].upper(),
                'value': item['value'] * 2,
                'processed': True
            })
        
        return processed
    
    def _write_data(self, data):
        """파일에 데이터 쓰기"""
        with safe_file_operation(self.output_file, 'w') as f:
            json.dump(data, f, indent=2, ensure_ascii=False)

# 사용 예시
if __name__ == "__main__":
    processor = DataProcessor("input.json", "output.json")
    
    # 입력 파일 생성 (테스트용)
    with safe_file_operation("input.json", "w") as f:
        json.dump([
            {"name": "apple", "value": 10},
            {"name": "banana", "value": 20},
            {"name": "cherry", "value": 15}
        ], f)
    
    # 데이터 처리
    success = processor.process()
    if success:
        print("✅ 처리 성공!")
    else:
        print("❌ 처리 실패!")

 

 

 

10. 주요 내장 예외

예외 발생 상황
ValueError 잘못된 값
TypeError 잘못된 타입
KeyError 딕셔너리 키 없음
IndexError 인덱스 범위 초과
FileNotFoundError 파일 없음
ZeroDivisionError 0으로 나눔
AttributeError 속성 없음
ImportError 임포트 실패
RuntimeError 일반적 실행 오류
StopIteration 반복자 종료

 

 

 

핵심 요약

 

 

예외 처리 베스트 프랙티스

  1. 구체적인 예외를 잡아라: except Exception보다는 except ValueError
  2. 예외를 무시하지 마라: 최소한 로그라도 남겨라
  3. finally로 정리하라: 리소스는 항상 정리
  4. 커스텀 예외를 만들어라: 도메인에 맞는 예외 정의
  5. 컨텍스트 매니저를 사용하라: with로 안전하게

 

 

컨텍스트 매니저 베스트 프랙티스

  1. 리소스 관리에 사용: 파일, 데이터베이스, 네트워크 연결
  2. 임시 상태 관리: 설정 변경, 트랜잭션
  3. 시간 측정, 로깅: 성능 모니터링
  4. @contextmanager 활용: 간단한 경우 데코레이터 사용

 

 

 

 


 

 

 

 

 

결론

예외 처리와 컨텍스트 매니저를 잘 활용하면

  • 예상치 못한 오류에 대응 가능
  • 리소스 누수 방지
  • 코드의 안정성과 신뢰성 향상
  • 유지보수하기 쉬운 코드 작성

 

 

견고한 프로그램은 예외를 잘 처리하는 프로그램이다!

 

 

 

 

 

 

 

 

 

 

 


Reference

728x90
반응형