포스트

[Python 100일 챌린지] Day 45 - 예외 처리 기초

[Python 100일 챌린지] Day 45 - 예외 처리 기초

try: open('없는파일.txt')except: print('파일 없어요!') → 프로그램 계속 실행! 😊

에러 나도 튕기지 않는 프로그램 만들기! 파일 없음, 0으로 나누기, 네트워크 끊김… 모든 에러를 우아하게 처리합니다!

(30-40분 완독 ⭐⭐⭐)

🎯 오늘의 학습 목표

📚 사전 지식


🎯 학습 목표 1: 예외와 에러의 개념 이해하기

1.1 예외의 개념

예외(Exception): 프로그램 실행 중 발생하는 오류

1
2
# ❌ 예외 발생 예제
result = 10 / 0  # ZeroDivisionError!

예외가 발생하면:

  • 프로그램이 즉시 중단됨
  • 오류 메시지 출력

1.2 예외 vs 구문 오류

1
2
3
4
5
6
7
# 구문 오류 (Syntax Error) - 실행 전에 발견
# if True
#     print("Hello")  # SyntaxError: invalid syntax

# 예외 (Exception) - 실행 중에 발생
numbers = [1, 2, 3]
print(numbers[10])  # IndexError: list index out of range

🎯 학습 목표 2: try-except로 예외 처리하기 기본

2.1 기본 구조

1
2
3
4
5
6
7
8
try:
    # 예외가 발생할 수 있는 코드
    result = 10 / 0
except:
    # 예외 발생 시 실행
    print("오류가 발생했습니다!")

print("프로그램 계속 실행")

2.2 특정 예외 처리

1
2
3
4
5
6
7
8
9
10
11
12
try:
    num = int(input("숫자 입력: "))
    result = 10 / num
    print(f"결과: {result}")

except ValueError:
    print("❌ 숫자가 아닙니다!")

except ZeroDivisionError:
    print("❌ 0으로 나눌 수 없습니다!")

print("프로그램 종료")

2.3 여러 예외 한 번에

1
2
3
4
try:
    # ...
except (ValueError, TypeError, ZeroDivisionError):
    print("오류 발생!")

🎯 학습 목표 3: 다양한 예외 타입 다루기

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
# ZeroDivisionError - 0으로 나누기
result = 10 / 0

# ValueError - 부적절한 값
num = int("abc")

# TypeError - 타입 불일치
result = "5" + 3

# IndexError - 인덱스 범위 초과
numbers = [1, 2, 3]
print(numbers[10])

# KeyError - 존재하지 않는 키
data = {"name": "Alice"}
print(data["age"])

# FileNotFoundError - 파일 없음
with open("없는파일.txt") as f:
    content = f.read()

# AttributeError - 속성 없음
text = "Hello"
text.append("!")  # 문자열엔 append 없음

3.2 예외 계층 구조

graph TD
    A[BaseException] --> B[Exception]
    B --> C[ValueError]
    B --> D[TypeError]
    B --> E[KeyError]
    B --> F[FileNotFoundError]
    B --> G[ZeroDivisionError]

Exception 계층 이해하기:

  • Exception을 잡으면 모든 하위 예외가 잡힙니다
  • 구체적인 예외를 먼저, 일반적인 예외를 나중에 처리하세요
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ✅ 좋은 예 (구체적 → 일반적)
try:
    result = int(data)
except ValueError:
    print("숫자 변환 실패")
except Exception as e:
    print(f"예상치 못한 오류: {e}")

# ❌ 나쁜 예 (일반적 예외가 먼저)
try:
    result = int(data)
except Exception:  # ValueError까지 여기서 잡힘!
    print("오류 발생")
except ValueError:  # 이 코드는 절대 실행되지 않음
    print("숫자 변환 실패")

🎯 학습 목표 4: 예외 정보 다루기

4.1 예외 객체 활용

1
2
3
4
5
6
7
8
9
10
11
try:
    num = int("abc")
except ValueError as e:
    print(f"오류 타입: {type(e).__name__}")
    print(f"오류 메시지: {e}")
    print(f"오류 인자: {e.args}")

# 출력:
# 오류 타입: ValueError
# 오류 메시지: invalid literal for int() with base 10: 'abc'
# 오류 인자: ("invalid literal for int() with base 10: 'abc'",)

4.2 상세한 오류 정보

1
2
3
4
5
6
7
8
9
10
import traceback

try:
    result = 10 / 0
except Exception as e:
    print("오류 발생!")
    print(f"타입: {type(e).__name__}")
    print(f"메시지: {e}")
    print("\n전체 스택 트레이스:")
    traceback.print_exc()

🎯 학습 목표 5: finally와 else 절 사용하기

5.1 else절

else절은 언제 실행되나요?

  • try 블록이 예외 없이 정상 완료되었을 때만 실행됩니다
  • 예외가 발생하면 else절은 건너뛰고 except절로 갑니다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
try:
    num = int(input("숫자 입력: "))
    result = 10 / num

except ValueError:
    print("❌ 숫자가 아닙니다")

except ZeroDivisionError:
    print("❌ 0으로 나눌 수 없습니다")

else:
    # 예외가 발생하지 않았을 때만 실행
    print(f"✅ 결과: {result}")

print("프로그램 종료")

5.2 finally절

finally절의 특징:

  • 예외 발생 여부와 관계없이 항상 실행됩니다
  • 주로 파일 닫기, 연결 해제 등 리소스 정리에 사용합니다

💡 더 나은 패턴 (권장):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
f = None  # 파일 변수를 먼저 초기화
try:
    f = open("data.txt", "r")
    content = f.read()
    print(content)

except FileNotFoundError:
    print("❌ 파일을 찾을 수 없습니다")

finally:
    # f가 열렸을 때만 닫기
    if f:
        f.close()
        print("파일 닫힘")

🔍 왜 이렇게 하나요?

  • open()에서 예외가 발생하면 f가 정의되지 않을 수 있습니다
  • if f: 조건으로 파일이 실제로 열렸는지 확인합니다

5.3 전체 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
try:
    # 예외가 발생할 수 있는 코드
    pass

except SomeException:
    # 예외 처리
    pass

else:
    # 예외가 없을 때
    pass

finally:
    # 항상 실행
    pass

5.4 실행 흐름 이해하기

graph TD
    A[try 블록 시작] --> B{예외 발생?}
    B -->|예외 없음| C[else 블록 실행]
    B -->|예외 발생| D[except 블록 실행]
    C --> E[finally 블록 실행]
    D --> E
    E --> F[프로그램 계속]

흐름 정리:

  1. try 실행 → 예외 없으면 else → finally → 계속
  2. try 실행 → 예외 발생 → except → finally → 계속
  3. finally는 항상 실행됩니다

🎯 학습 목표 6: 실전 예외 처리 패턴

패턴 1: 재시도

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def get_number_with_retry(max_attempts=3):
    """사용자 입력 재시도"""
    for attempt in range(max_attempts):
        try:
            num = int(input("숫자 입력: "))
            return num

        except ValueError:
            remaining = max_attempts - attempt - 1
            if remaining > 0:
                print(f"❌ 숫자가 아닙니다. ({remaining}번 남음)")
            else:
                print("❌ 시도 횟수 초과")
                raise

# 사용
# num = get_number_with_retry()

패턴 2: 안전한 파일 처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def safe_read_file(filename):
    """안전한 파일 읽기"""
    try:
        with open(filename, 'r', encoding='utf-8') as f:
            return f.read()

    except FileNotFoundError:
        print(f"⚠️ 파일 없음: {filename}")
        return None

    except PermissionError:
        print(f"⚠️ 권한 없음: {filename}")
        return None

    except Exception as e:
        print(f"⚠️ 예상치 못한 오류: {e}")
        return None

# 사용
content = safe_read_file("data.txt")
if content:
    print(content)

패턴 3: 기본값 제공

1
2
3
4
5
6
7
8
9
10
def get_config_value(config, key, default=None):
    """설정 값 안전하게 가져오기"""
    try:
        return config[key]
    except KeyError:
        return default

# 사용
config = {"debug": True}
timeout = get_config_value(config, "timeout", default=30)

패턴 4: 타입 변환

1
2
3
4
5
6
7
8
9
10
11
def safe_int(value, default=0):
    """안전한 정수 변환"""
    try:
        return int(value)
    except (ValueError, TypeError):
        return default

# 사용
age = safe_int("25")     # 25
age = safe_int("abc")    # 0
age = safe_int(None)     # 0

패턴 5: raise - 예외 발생시키기

언제 사용하나요?

  • 함수에서 잘못된 입력을 받았을 때
  • 비즈니스 로직 규칙 위반 시
  • 예외를 기록한 후 다시 던질 때
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def divide(a, b):
    """나눗셈 (음수 불가)"""
    if b == 0:
        raise ZeroDivisionError("0으로 나눌 수 없습니다")

    if a < 0 or b < 0:
        raise ValueError("음수는 사용할 수 없습니다")

    return a / b

# 사용
try:
    result = divide(10, -2)
except ValueError as e:
    print(f"{e}")

예외 재발생 (raise)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def process_data(data):
    try:
        # 데이터 처리
        result = int(data) * 2

    except ValueError as e:
        print("⚠️ 로그: 데이터 처리 실패")
        raise  # 예외를 다시 발생시킴

# 사용
try:
    process_data("abc")
except ValueError:
    print("❌ 외부에서 처리")

패턴 6: 종합 실전 예제

예제 1: 안전한 JSON 파일 로드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import json

def load_json_safe(filename, default=None):
    """안전한 JSON 로드"""
    try:
        with open(filename, 'r', encoding='utf-8') as f:
            return json.load(f)

    except FileNotFoundError:
        print(f"⚠️ 파일 없음: {filename}")
        return default or {}

    except json.JSONDecodeError as e:
        print(f"⚠️ JSON 파싱 오류: {e}")
        return default or {}

    except Exception as e:
        print(f"⚠️ 예상치 못한 오류: {e}")
        return default or {}

# 사용
config = load_json_safe('config.json', default={'debug': False})
print(config)

예제 2: 계산기

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
def calculator():
    """간단한 계산기 (예외 처리)"""
    while True:
        try:
            expr = input("계산식 입력 (종료: q): ")

            if expr.lower() == 'q':
                break

            # ⚠️ 주의: eval()은 보안상 위험할 수 있습니다
            # 실제 프로덕션에서는 ast.literal_eval() 사용 권장
            result = eval(expr)
            print(f"결과: {result}")

        except ZeroDivisionError:
            print("❌ 0으로 나눌 수 없습니다")

        except (ValueError, SyntaxError, NameError):
            print("❌ 잘못된 수식입니다")

        except KeyboardInterrupt:
            print("\n프로그램 중단")
            break

        except Exception as e:
            print(f"❌ 오류: {e}")

# calculator()

예제 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def validate_user_data(data):
    """사용자 데이터 검증"""
    errors = []

    # 이름 검증
    try:
        name = data['name']
        if not isinstance(name, str) or len(name) < 2:
            errors.append("이름은 2자 이상이어야 합니다")
    except KeyError:
        errors.append("이름은 필수입니다")

    # 나이 검증
    try:
        age = int(data['age'])
        if age < 0 or age > 150:
            errors.append("나이는 0-150 사이여야 합니다")
    except KeyError:
        errors.append("나이는 필수입니다")
    except ValueError:
        errors.append("나이는 숫자여야 합니다")

    # 이메일 검증
    try:
        email = data['email']
        if '@' not in email:
            errors.append("올바른 이메일 형식이 아닙니다")
    except KeyError:
        pass  # 이메일은 선택사항

    if errors:
        raise ValueError(f"검증 실패:\n" + "\n".join(f"{e}" for e in errors))

# 사용
user1 = {"name": "Alice", "age": "25", "email": "alice@example.com"}
user2 = {"name": "B", "age": "-5"}

try:
    validate_user_data(user1)
    print("✅ user1 검증 성공")
except ValueError as e:
    print(e)

try:
    validate_user_data(user2)
except ValueError as e:
    print(f"❌ user2 검증 실패:\n{e}")

💡 실전 팁 & 주의사항

✅ 좋은 예외 처리 습관

  1. 구체적인 예외 처리
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    # ❌ 나쁜 예
    try:
        result = int(data)
    except:  # KeyboardInterrupt, SystemExit까지 잡아버림!
        pass  # 심지어 무시까지!
    
    # ✅ 좋은 예
    try:
        result = int(data)
    except ValueError:  # 특정 예외만
        result = 0
    

    왜 나쁜가요?

    • except:는 모든 예외를 잡아버립니다 (Ctrl+C도 잡힘!)
    • 디버깅이 매우 어려워집니다
    • 예상치 못한 오류를 숨길 수 있습니다
  2. 예외 무시하지 않기
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    # ❌ 나쁜 예
    try:
        process_data()
    except Exception:
        pass  # 완전히 무시
    
    # ✅ 좋은 예
    try:
        process_data()
    except Exception as e:
        logging.error(f"처리 실패: {e}")
    
  3. 적절한 로깅
    1
    2
    3
    4
    5
    6
    7
    
    import logging
    
    try:
        result = risky_operation()
    except ValueError as e:
        logging.exception("작업 실패")  # 스택 트레이스 포함
        raise
    
  4. 디버깅 시 전체 스택 트레이스 출력
    1
    2
    3
    4
    5
    6
    7
    
    import traceback
    
    try:
        risky_operation()
    except Exception as e:
        traceback.print_exc()  # 전체 에러 추적 정보
        raise  # 예외 재발생
    

⚠️ 주의사항

  • 너무 넓은 범위의 except는 디버깅을 어렵게 만듭니다
  • 예외를 잡았으면 반드시 처리하거나 로깅하세요
  • finally는 리소스 정리에만 사용하세요
  • Exception을 잡으면 모든 하위 예외가 잡힙니다

🧪 연습 문제

문제 1: 안전한 리스트 접근

목표: 리스트에서 인덱스로 값을 가져오되, 범위를 벗어나면 기본값을 반환하는 함수를 작성하세요.

요구사항:

  • 함수명: safe_get(lst, index, default=None)
  • IndexErrorTypeError 처리
  • 기본값 반환 기능
해답 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def safe_get(lst, index, default=None):
    """안전한 리스트 접근"""
    try:
        return lst[index]
    except IndexError:
        return default
    except TypeError:
        return default

# 테스트
numbers = [1, 2, 3, 4, 5]
print(safe_get(numbers, 2))      # 3
print(safe_get(numbers, 10, 0))  # 0
print(safe_get(None, 0, -1))     # -1

문제 2: 파일 복사 (예외 처리)

목표: 파일을 복사하는 함수를 작성하되, 발생할 수 있는 모든 오류를 안전하게 처리하세요.

요구사항:

  • 함수명: safe_copy_file(source, destination)
  • FileNotFoundError, PermissionError 처리
  • 성공/실패 여부 반환
해답 보기
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 safe_copy_file(source, destination):
    """안전한 파일 복사"""
    try:
        with open(source, 'rb') as src:
            with open(destination, 'wb') as dst:
                dst.write(src.read())

        print(f"✅ 복사 완료: {source}{destination}")
        return True

    except FileNotFoundError:
        print(f"❌ 원본 파일 없음: {source}")
        return False

    except PermissionError:
        print(f"❌ 권한 없음")
        return False

    except Exception as e:
        print(f"❌ 복사 실패: {e}")
        return False

# 테스트
safe_copy_file('source.txt', 'backup.txt')

📝 오늘 배운 내용 정리

개념 설명 예시
try-except 예외 처리 기본 구조 try: ... except ValueError: ...
예외 타입 다양한 내장 예외 ValueError, TypeError, KeyError
else 절 예외가 없을 때 실행 try: ... except: ... else: ...
finally 절 항상 실행 (리소스 정리) try: ... finally: f.close()
raise 예외 발생시키기 raise ValueError("오류")
예외 객체 예외 정보 접근 except ValueError as e:

핵심 코드 패턴

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
# 기본 예외 처리
try:
    result = risky_operation()
except ValueError as e:
    print(f"값 오류: {e}")
except TypeError:
    print("타입 오류")
else:
    print("성공!")
finally:
    cleanup()

# 여러 예외 한 번에
try:
    result = operation()
except (ValueError, TypeError) as e:
    print(f"오류: {e}")

# 안전한 함수 패턴
def safe_operation(data, default=None):
    try:
        return process(data)
    except Exception as e:
        logging.error(f"실패: {e}")
        return default

🔗 관련 자료


📚 이전 학습

Day 44: CSV 파일 처리 ⭐⭐⭐

어제는 CSV 모듈과 Pandas로 표 형식 데이터를 읽고 쓰고 분석하는 방법을 배웠습니다!

📚 다음 학습

Day 46: 예외 처리 고급 ⭐⭐⭐

내일은 사용자 정의 예외, 예외 체이닝, 컨텍스트 매니저 등 고급 예외 처리 기법을 배웁니다!


“늦었다고 생각할 때가 가장 빠른 시기입니다!” 🚀

Day 45/100 Phase 5: 파일 처리와 예외 처리 #100DaysOfPython

이제와서 시작하는 Python 마스터하기 - Day 45 완료! 🎉

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.