10주차 · 예외 처리와 방어적 코딩
10장. 예외 처리와 방어적 코딩
Section titled “10장. 예외 처리와 방어적 코딩”이 장을 마치면 다음을 할 수 있습니다.
- 실행 오류(runtime error)와 문법/논리 오류를 구분한다.
try/except/else/finally문법과 들여쓰기 구조를 역할에 맞게 배치한다.- 파일 모드
r/w/a를 구분하고 안전한 로그 기록 패턴을 적용한다. - CSV/JSON 파일을 예외 처리와 함께 읽어 구조화된 데이터로 변환한다.
- 핵심 계산 전에 입력 검증 계층(존재/타입/범위)을 설계한다.
raise와raise ... from err로 원인을 설명하는 예외를 발생시킨다.- 재시도(retry) / 대체값(fallback) / 건너뛰기(skip) / 즉시 중단(stop) 전략을 상황에 맞게 선택한다.
enumerate()로 인덱스와 값을 동시에 꺼낸다.
이번 주 이야기: “왜 잘 돌아가던 코드가 갑자기 멈췄을까?”
Section titled “이번 주 이야기: “왜 잘 돌아가던 코드가 갑자기 멈췄을까?””처음 프로그래밍을 배우면 “코드가 맞으면 항상 동작한다”고 느끼기 쉽습니다. 하지만 실제 데이터는 늘 불완전합니다.
- 숫자여야 하는 칸에
N/A가 들어오고 - 파일이 없거나 이름이 틀리고
- 인코딩이 달라 한글이 깨지기도 합니다.
W9에서 배운 assert는 “이 함수가 올바른가”를 검증하는 도구였습니다. 이번 주는 한 발 더 나아가 “예상 밖의 입력이 들어와도 프로그램이 멈추지 않게” 만드는 방법을 배웁니다.
flowchart TB A["Part 1: try/except/else/finally<br/>기본 문법"] --> B["Part 2: 파일 I/O<br/>CSV · JSON 읽기"] B --> C["Part 3: 검증 계층 + raise<br/>입력을 미리 막는다"] C --> D["Part 4: 실패 정책<br/>retry / fallback / skip / stop"]
Part 1: try/except/else/finally 기본 문법
Section titled “Part 1: try/except/else/finally 기본 문법”1.1 traceback 읽기: 공포 대신 절차
Section titled “1.1 traceback 읽기: 공포 대신 절차”traceback이 길어 보여도 읽는 순서는 항상 같습니다.
- 마지막 줄에서 예외 타입 + 메시지 확인
- 그 위에서 내 파일 경로와 줄 번호 확인
- 그 줄에서 사용한 값(변수/입력) 출력해 재현
예:
ValueError: could not convert string to float: 'N/A'해석: float()로 바꿀 수 없는 문자열이 들어왔다는 뜻입니다.
1.2 기본 문법 구조
Section titled “1.2 기본 문법 구조”예외 처리도 결국 콜론(:) + 들여쓰기 블록입니다.
try: value = float(token)except ValueError as err: print("변환 실패:", str(err))else: print("성공:", value)finally: print("정리 작업")처음 쓸 때 체크할 4가지
Section titled “처음 쓸 때 체크할 4가지”try:와except ...:줄 끝에:를 붙인다.except는 가능하면ValueError,FileNotFoundError처럼 구체적으로 적는다.- 성공했을 때만 할 일은
else:에 두면 읽기 쉽다. - 항상 해야 하는 마무리는
finally:에 둔다.
1.3 각 블록의 역할
Section titled “1.3 각 블록의 역할”try: # 실패 가능성이 있는 최소 코드except ValueError as err: # 예상 가능한 실패 기록/복구else: # 성공했을 때만 실행finally: # 성공/실패와 무관하게 항상 실행| 블록 | 언제 실행 | 용도 |
|---|---|---|
try | 항상(진입) | 실패 가능 코드를 최소로 묶기 |
except | 예외 발생 시 | 실패 기록 · 복구 |
else | 예외 없이 성공 시 | 정상 결과 처리 (가독성 향상) |
finally | 항상(종료) | 파일 닫기 · 마무리 로그 |
flowchart TB
A["try 블록 실행"] --> B{"예외 발생?"}
B -- "Yes" --> C["except 블록 실행"]
B -- "No" --> D["else 블록 실행"]
C --> E["finally 블록 실행<br/>(항상 실행)"]
D --> E
E --> F["다음 코드로 진행"] 1.4 C와 비교
Section titled “1.4 C와 비교”| 항목 | C | Python | 기억할 점 |
|---|---|---|---|
| 실패 확인 | 반환값/포인터를 직접 검사 | 예외가 발생하면 except로 이동 | Python은 “실패 신호”가 예외로 전달된다 |
| 파일 열기 | FILE *fp = fopen(...) | with open(...) as f: | with가 자동 정리를 도와준다 |
| 자원 정리 | fclose(fp) 직접 호출 | with 종료 시 자동 close | 초보자는 with를 기본형으로 사용 |
| 오류 정보 | errno, 반환 코드 | err 객체, traceback | 예외 타입과 메시지를 함께 기록 가능 |
FILE *fp = fopen("data.txt", "r");if (fp == NULL) { printf("open failed\n");} else { fclose(fp);}try: with open("data.txt", "r", encoding="utf-8") as file: text = file.read()except FileNotFoundError as err: print("open failed:", str(err))else: print(text)1.5 enumerate() — 인덱스와 값을 함께 꺼내기
Section titled “1.5 enumerate() — 인덱스와 값을 함께 꺼내기”예제 1: try/except/else로 성공·실패 분리하기
Section titled “예제 1: try/except/else로 성공·실패 분리하기” 예제 1 단계별 추적
Section titled “예제 1 단계별 추적”| line_no | token | float() 결과 | 이동 |
|---|---|---|---|
| 1 | "3.14" | 성공 3.14 | ok 리스트 |
| 2 | "N/A" | ValueError | errors 리스트 |
| 3 | "2.71" | 성공 2.71 | ok 리스트 |
| 4 | "bad" | ValueError | errors 리스트 |
| 5 | "5.00" | 성공 5.0 | ok 리스트 |
| 6 | "7" | 성공 7.0 | ok 리스트 |
핵심: 검증 합계 출력은 처리 누락이 없었음을 자동으로 증명합니다. 초보자 필수 습관입니다.
Part 2: 파일 I/O와 예외 처리
Section titled “Part 2: 파일 I/O와 예외 처리”2.1 파일 모드
Section titled “2.1 파일 모드”| 모드 | 의미 | 위험 포인트 | 권장 사용 |
|---|---|---|---|
r | 읽기 | 파일 없으면 즉시 실패 | 분석/검증 단계 |
w | 새로 쓰기 | 기존 내용 전부 삭제 | 새 리포트 시작 1회 |
a | 이어 쓰기 | 누적되므로 중복 주의 | 이벤트 로그 기록 |
x | 새 파일만 생성 | 파일 있으면 실패 | 덮어쓰기 방지 |
안전 기본형:
with open("events.log", "a", encoding="utf-8") as f: f.write("...\n")2.2 자주 나오는 실패 유형
Section titled “2.2 자주 나오는 실패 유형”| 실패 패턴 | 관찰되는 증상 | 교정 방향 |
|---|---|---|
except:만 사용 | 원인 추적 불가 | except ValueError as err로 바꾸고 str(err) 출력 |
try 범위 과대 | 어디서 실패했는지 불명확 | 실패 가능 1~2줄만 try로 축소 |
w를 루프에서 사용 | 로그가 마지막 줄만 남음 | 헤더만 w, 본문은 a로 분리 |
| 인코딩 생략 | 한글 깨짐/환경별 오동작 | 모든 open에 encoding='utf-8' 고정 |
| 검증 없음 | 누락 데이터 발견 못함 | total == ok + errors를 항상 출력 |
예제 2: 안전한 로그 파일 작성
Section titled “예제 2: 안전한 로그 파일 작성” 예제 2 해설
Section titled “예제 2 해설”- 로그 헤더는
w로 초기화합니다. 루프 밖에서 한 번만 실행하므로 기존 내용이 덮어씌워지지 않습니다. - 각 이벤트는
try로 변환을 시도합니다. - 실패해도
finally에서 append를 보장합니다. 기록이 누락되지 않습니다. - 마지막에 파일을 다시 읽어 실제 기록을 눈으로 검증합니다.
2.3 CSV / JSON 파일 읽기
Section titled “2.3 CSV / JSON 파일 읽기”실제 업무에서 가장 많이 만나는 파일 형식입니다.
CSV 읽기 패턴:
import csv
try: with open("data.csv", "r", encoding="utf-8") as f: reader = csv.DictReader(f) rows = list(reader)except FileNotFoundError as err: print("파일 없음:", str(err)) rows = []
print("읽은 행 수:", len(rows))JSON 읽기 패턴:
import json
try: with open("config.json", "r", encoding="utf-8") as f: config = json.load(f)except FileNotFoundError as err: print("파일 없음:", str(err)) config = {}except json.JSONDecodeError as err: print("JSON 형식 오류:", str(err)) config = {}
print("config:", config)Part 3: 검증 계층과 raise
Section titled “Part 3: 검증 계층과 raise”3.1 오류 3종류를 정확히 구분하기
Section titled “3.1 오류 3종류를 정확히 구분하기”문법 오류 (Syntax Error): 코드가 시작조차 안 되는 오류입니다.
# SyntaxError: 콜론 누락if score > 90 print("A")실행 오류 (Runtime Error): 코드는 시작되지만 실행 중에 중단됩니다.
x = int("hello") # 실행 중 ValueError논리 오류 (Logic Error): 에러 없이 실행되지만 결과가 틀립니다. 초보자에게 가장 위험한 오류입니다.
# 평균 계산 의도였지만 분모가 잘못됨scores = [80, 90, 100]avg = sum(scores) / 2 # 잘못된 로직3.2 입력 검증 계층
Section titled “3.2 입력 검증 계층”핵심 계산 전에 아래 순서대로 확인하면 실패를 크게 줄일 수 있습니다.
- 존재 검사: 값이 비어 있지 않은가?
- 타입 검사: 숫자/문자열/리스트 타입이 맞는가?
- 범위 검사: 허용 가능한 최소/최대인가?
- 형식 검사: 날짜/ID/코드 형식이 맞는가?
- 도메인 규칙 검사: 과목/실험 규칙을 만족하는가?
3.3 raise와 좋은 에러 메시지
Section titled “3.3 raise와 좋은 에러 메시지”방어적 코딩의 출발점은 **“계산 전에 막는다”**입니다.
def parse_temperature(raw): if raw is None: raise ValueError("temperature is required")
value = float(raw)
if not (-80 <= value <= 80): raise ValueError(f"temperature out of range: {value}")
return value좋은 메시지는 세 가지를 포함합니다:
- 무엇이 실패했는가
- 무엇을 기대했는가 (형식/범위)
- 실제로 무엇이 들어왔는가
raise ValueError("sample_rate must be integer in 1..10000, got 'abc'")3.4 raise … from err — 예외 체이닝
Section titled “3.4 raise … from err — 예외 체이닝”except ValueError as err: raise ValueError(f"temperature must be numeric: {raw!r}") from errfrom err를 붙이면 “이 새 예외는 원래의 err 때문에 발생했다”는 연결 정보가 저장됩니다. 나중에 오류 트레이스를 볼 때 원인 예외와 새 예외가 함께 표시되어 추적이 쉬워집니다.
!r — f-string에서 repr 출력:
f"got {raw!r}"!r은 값을 repr() 형식으로 출력하라는 지시자입니다. 문자열이면 따옴표가 붙어 타입 정보가 함께 보입니다. 예를 들어 raw = "abc"이면 !r 없이는 got abc, !r 있으면 got 'abc'로 출력됩니다.
3.5 Bad vs Good 예시
Section titled “3.5 Bad vs Good 예시”Bad: 원인을 숨김
def parse_temperature(raw): try: return float(raw) except: return 0Good: 검증 + 설명 가능한 메시지
def parse_temperature(raw): if raw is None: raise ValueError("temperature is required") try: value = float(raw) except ValueError as err: raise ValueError(f"temperature must be numeric: {raw!r}") from err
if not (-80 <= value <= 80): raise ValueError(f"temperature out of range (-80~80): {value}") return value예제 3: 검증 계층이 있는 파싱 함수
Section titled “예제 3: 검증 계층이 있는 파싱 함수” 예제 3 해설
Section titled “예제 3 해설”[OK]는 유효한 입력이 통과했음을 뜻합니다.[ERR]는 사용자가 어떻게 고치면 되는지 알려줘야 합니다.- 메시지가 모호하면 코드보다 먼저 메시지를 개선하세요.
Part 4: 실패 정책
Section titled “Part 4: 실패 정책”4.1 4가지 전략
Section titled “4.1 4가지 전략”| 전략 | 사용할 상황 | 예시 |
|---|---|---|
| Retry | 일시적 실패 | 센서 타임아웃, 네트워크 지연 |
| Fallback | 대체값 허용 가능 | 이전 보정값 사용 |
| Skip | 배치 중 일부 레코드만 문제 | 이상 행만 건너뛰고 계속 계산 |
| Stop Fast | 안전상 즉시 중단 필요 | 위험한 제어 명령 감지 |
4.2 skip 전략 패턴
Section titled “4.2 skip 전략 패턴”valid = []errors = []
for rec in records: try: valid.append(parse_record(rec)) except (KeyError, ValueError) as e: errors.append(str(e))
# 검증 합계print(len(records) == len(valid) + len(errors))4.3 fallback 전략 패턴
Section titled “4.3 fallback 전략 패턴”last_valid = Noneresults = []
for raw in stream: try: value = float(raw) last_valid = value results.append(("OK", value)) except ValueError: if last_valid is not None: results.append(("FB", last_valid)) # fallback 사용 else: results.append(("NONE", None)) # 대체값도 없음dict.get(key, default) 패턴도 fallback의 한 형태입니다.
# 딕셔너리에 키가 없을 때 KeyError 대신 기본값을 반환합니다.name = record.get("id", "unknown")예제 4: skip 정책과 요약 출력
Section titled “예제 4: skip 정책과 요약 출력” 예제 4 해설
Section titled “예제 4 해설”valid_count와error_count는 데이터 품질의 빠른 지표입니다.- 오류 로그는 데이터 정비 작업 목록이 됩니다.
- 이 예제는 skip 전략의 기본 형태입니다.
초보자가 자주 하는 실수
Section titled “초보자가 자주 하는 실수”| 실수 | 오류 유형 | 왜 위험한가 | 개선 패턴 |
|---|---|---|---|
except:만 사용 | 실행 오류 | 시스템 예외까지 숨김 | 예외 타입을 구체적으로 명시 |
모든 오류에 0 반환 | 논리 오류 | 뒤 계산을 오염시킴 | 에러를 발생시키거나 구조화된 반환 |
| 범위 검사 생략 | 논리/실행 오류 | 비현실 값이 계산에 유입 | min/max 검사 후 계산 |
print만 하고 종료 | 논리 오류 | 호출자가 실패를 감지 못함 | 예외 발생 또는 상태값 반환 |
| 큰 try 블록 한 덩어리 | 실행 오류 | 실패 지점 추적 어려움 | try 범위를 작게 유지 |
w를 루프에서 사용 | 실행 오류 | 로그가 마지막 줄만 남음 | 헤더만 w, 본문은 a로 분리 |
실습 문제 · 직접 코딩
Section titled “실습 문제 · 직접 코딩”문제: 숫자 문자열 리스트를 정수로 변환하고, 실패 항목은 따로 저장하세요.
문제: try/except/else/finally를 모두 사용해 처리 흐름을 구분하세요.
문제: 입력값 존재/타입/범위를 순서대로 검사하는 검증 함수를 작성하세요.
문제: 여러 레코드를 처리하면서 유효한 항목만 계산하고, 건너뛴 수를 요약하세요.
문제: 값 파싱 실패 시 마지막 유효값을 대체값으로 사용하는 fallback 정책을 구현하세요.