// education

예외 처리와 로깅

들어가며

예외 처리와 로깅은 안정적이고 디버깅하기 쉬운 프로그램을 만드는 필수 요소입니다. 올바른 에러 처리로 프로그램의 견고성을 높이고, 로깅으로 문제를 추적하세요.

예외 처리의 기본

예제 1: try-except 기본

# 예외가 없는 경우
try:
    result = 10 / 2
    print(f"결과: {result}")
except ZeroDivisionError:
    print("0으로 나눌 수 없습니다")

# 예외가 있는 경우
try:
    result = 10 / 0
    print(f"결과: {result}")
except ZeroDivisionError:
    print("0으로 나눌 수 없습니다")

# 다양한 예외 처리
try:
    value = int("abc")
except ValueError:
    print("정수로 변환할 수 없습니다")

출력:

결과: 5.0
0으로 나눌 수 없습니다
정수로 변환할 수 없습니다

예제 2: 여러 예외 처리

def process_data(data):
    try:
        number = int(data)
        result = 100 / number
        return result
    except ValueError:
        print("ValueError: 정수가 아닙니다")
    except ZeroDivisionError:
        print("ZeroDivisionError: 0으로 나눌 수 없습니다")
    except Exception as e:
        print(f"예상치 못한 오류: {e}")

# 테스트
process_data("abc")      # ValueError
process_data("0")        # ZeroDivisionError
process_data("10")       # 성공

출력:

ValueError: 정수가 아닙니다
ZeroDivisionError: 0으로 나눌 수 없습니다

예외 처리 심화

예제 3: else와 finally

def divide_numbers(a, b):
    try:
        result = a / b
    except ZeroDivisionError:
        print("오류: 0으로 나눌 수 없습니다")
    else:
        # 예외가 없을 때만 실행
        print(f"성공: {a} / {b} = {result}")
    finally:
        # 항상 실행
        print("처리 완료\n")

divide_numbers(10, 2)
divide_numbers(10, 0)
divide_numbers(20, 5)

출력:

성공: 10 / 2 = 5.0
처리 완료

오류: 0으로 나눌 수 없습니다
처리 완료

성공: 20 / 5 = 4.0
처리 완료

예제 4: 사용자 정의 예외

class CustomError(Exception):
    """사용자 정의 예외"""
    pass

class AgeError(Exception):
    """나이 관련 예외"""
    def __init__(self, age, message="유효하지 않은 나이입니다"):
        self.age = age
        self.message = message
        super().__init__(self.message)

def validate_age(age):
    if age < 0:
        raise AgeError(age, "나이는 음수일 수 없습니다")
    if age > 150:
        raise AgeError(age, "나이가 너무 큽니다")
    return age

try:
    validate_age(25)
    print("나이 검증 성공")
except AgeError as e:
    print(f"오류: {e}")

try:
    validate_age(-5)
except AgeError as e:
    print(f"오류: {e}")

출력:

나이 검증 성공
오류: 나이는 음수일 수 없습니다

예제 5: 파일 처리와 with 문

# 수동 관리 (좋지 않음)
file = open("example.txt", "w")
try:
    file.write("Hello, World!")
finally:
    file.close()

# with를 사용한 관리 (권장)
with open("example.txt", "w") as file:
    file.write("Hello, World!")
    # 자동으로 파일이 닫힙니다

# 파일 읽기 예제
try:
    with open("example.txt", "r") as file:
        content = file.read()
        print(f"파일 내용: {content}")
except FileNotFoundError:
    print("파일을 찾을 수 없습니다")

출력:

파일 내용: Hello, World!

로깅

예제 6: 기본 로깅

import logging

# 로깅 설정
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

logger = logging.getLogger(__name__)

# 다양한 로그 레벨
logger.debug("디버그 메시지")
logger.info("정보 메시지")
logger.warning("경고 메시지")
logger.error("에러 메시지")
logger.critical("심각한 에러")

출력:

2024-01-15 14:30:45,123 - __main__ - DEBUG - 디버그 메시지
2024-01-15 14:30:45,124 - __main__ - INFO - 정보 메시지
2024-01-15 14:30:45,124 - __main__ - WARNING - 경고 메시지
2024-01-15 14:30:45,125 - __main__ - ERROR - 에러 메시지
2024-01-15 14:30:45,125 - __main__ - CRITICAL - 심각한 에러

예제 7: 고급 로깅 설정

import logging

# 로거 생성
logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)

# 콘솔 핸들러
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)

# 파일 핸들러
file_handler = logging.FileHandler("app.log")
file_handler.setLevel(logging.DEBUG)

# 포맷터
formatter = logging.Formatter(
    '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)

# 핸들러 추가
logger.addHandler(console_handler)
logger.addHandler(file_handler)

# 사용
logger.info("애플리케이션 시작")
logger.debug("디버그 정보")
logger.error("오류 발생")

출력:

2024-01-15 14:30:45,123 - myapp - INFO - 애플리케이션 시작
2024-01-15 14:30:45,124 - myapp - ERROR - 오류 발생

예제 8: 예외와 로깅 함께 사용

import logging

logger = logging.getLogger(__name__)

def risky_operation(value):
    try:
        result = 100 / value
        logger.info(f"연산 성공: 100 / {value} = {result}")
        return result
    except ZeroDivisionError as e:
        logger.error(f"0으로 나눔 오류: {e}", exc_info=True)
    except Exception as e:
        logger.critical(f"예상치 못한 오류: {e}", exc_info=True)

# 테스트
risky_operation(5)
risky_operation(0)

출력:

ERROR:__main__:0으로 나눔 오류: division by zero
Traceback (most recent call last):
  File "...", line 7, in risky_operation
    result = 100 / value
ZeroDivisionError: division by zero

흔한 실수와 해결책

실수 1: 너무 넓은 예외 처리

틀린 예제:

try:
    value = int(input("숫자: "))
    result = 100 / value
except:  # 모든 예외를 처리 - 위험!
    print("오류가 발생했습니다")

올바른 예제:

try:
    value = int(input("숫자: "))
    result = 100 / value
except ValueError:
    print("정수를 입력하세요")
except ZeroDivisionError:
    print("0이 아닌 수를 입력하세요")

실수 2: 예외 정보 손실

틀린 예제:

try:
    # 코드
    pass
except Exception:
    print("오류 발생")  # 원인을 알 수 없음

올바른 예제:

import logging
import traceback

logger = logging.getLogger(__name__)

try:
    # 코드
    pass
except Exception as e:
    logger.error(f"오류 발생: {e}", exc_info=True)
    # 또는
    traceback.print_exc()

실수 3: 리소스 누수

틀린 예제:

file = open("data.txt")
lines = file.readlines()
# 예외 발생 시 파일이 닫혀지지 않음

올바른 예제:

with open("data.txt") as file:
    lines = file.readlines()
# 자동으로 파일이 닫힘

연습 문제

문제 1: 안전한 계산기

사용자 입력을 받아 나누기를 수행하되, 모든 예외를 처리하세요.

문제 2: 로그 분석

로그 파일을 읽어 각 레벨별 로그 수를 세는 프로그램을 작성하세요.

문제 3: 재시도 로직

실패할 수 있는 작업을 여러 번 재시도하는 함수를 작성하세요.

풀이

문제 1 풀이

def safe_divide():
    try:
        a = int(input("첫 번째 숫자: "))
        b = int(input("두 번째 숫자: "))
        result = a / b
        print(f"{a} / {b} = {result}")
    except ValueError:
        print("정수를 입력하세요")
    except ZeroDivisionError:
        print("0으로 나눌 수 없습니다")
    except Exception as e:
        print(f"오류: {e}")

safe_divide()

문제 2 풀이

import logging
from collections import Counter

logging.basicConfig(filename='app.log', level=logging.DEBUG)
logger = logging.getLogger(__name__)

# 로그 생성
for i in range(10):
    if i % 3 == 0:
        logger.info(f"정보 메시지 {i}")
    elif i % 2 == 0:
        logger.warning(f"경고 메시지 {i}")
    else:
        logger.error(f"에러 메시지 {i}")

# 로그 분석
with open('app.log') as f:
    lines = f.readlines()
    levels = [line.split('-')[2].strip() for line in lines]
    counter = Counter(levels)
    print(f"로그 통계: {dict(counter)}")

문제 3 풀이

import logging
import time

logger = logging.getLogger(__name__)

def retry_operation(func, max_retries=3, delay=1):
    """작업을 재시도하는 데코레이터"""
    for attempt in range(max_retries):
        try:
            return func()
        except Exception as e:
            logger.warning(f"시도 {attempt + 1}/{max_retries} 실패: {e}")
            if attempt < max_retries - 1:
                time.sleep(delay)
            else:
                raise

def unreliable_task():
    import random
    if random.random() < 0.7:
        raise Exception("작업 실패")
    return "성공"

try:
    result = retry_operation(unreliable_task)
    print(result)
except Exception as e:
    print(f"최종 실패: {e}")

마무리

예외 처리와 로깅은 프로덕션 코드의 안정성과 유지보수성을 크게 향상시킵니다. 항상 적절한 예외를 처리하고 충분한 로그를 남기세요.

← 이전고급 OOP 개념 다음 →파일 I/O와 데이터 처리