Skip to content

10주차 · 예외 처리와 방어적 코딩

이 장을 마치면 다음을 할 수 있습니다.

  • 실행 오류(runtime error)와 문법/논리 오류를 구분한다.
  • try/except/else/finally 문법과 들여쓰기 구조를 역할에 맞게 배치한다.
  • 파일 모드 r/w/a를 구분하고 안전한 로그 기록 패턴을 적용한다.
  • CSV/JSON 파일을 예외 처리와 함께 읽어 구조화된 데이터로 변환한다.
  • 핵심 계산 전에 입력 검증 계층(존재/타입/범위)을 설계한다.
  • raiseraise ... 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이 길어 보여도 읽는 순서는 항상 같습니다.

  1. 마지막 줄에서 예외 타입 + 메시지 확인
  2. 그 위에서 내 파일 경로와 줄 번호 확인
  3. 그 줄에서 사용한 값(변수/입력) 출력해 재현

예:

ValueError: could not convert string to float: 'N/A'

해석: float()로 바꿀 수 없는 문자열이 들어왔다는 뜻입니다.

예외 처리도 결국 콜론(:) + 들여쓰기 블록입니다.

try:
value = float(token)
except ValueError as err:
print("변환 실패:", str(err))
else:
print("성공:", value)
finally:
print("정리 작업")
  1. try:except ...: 줄 끝에 :를 붙인다.
  2. except는 가능하면 ValueError, FileNotFoundError처럼 구체적으로 적는다.
  3. 성공했을 때만 할 일은 else:에 두면 읽기 쉽다.
  4. 항상 해야 하는 마무리는 finally:에 둔다.
try:
# 실패 가능성이 있는 최소 코드
except ValueError as err:
# 예상 가능한 실패 기록/복구
else:
# 성공했을 때만 실행
finally:
# 성공/실패와 무관하게 항상 실행
블록언제 실행용도
try항상(진입)실패 가능 코드를 최소로 묶기
except예외 발생 시실패 기록 · 복구
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["다음 코드로 진행"]
항목CPython기억할 점
실패 확인반환값/포인터를 직접 검사예외가 발생하면 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);
}

1.5 enumerate() — 인덱스와 값을 함께 꺼내기

Section titled “1.5 enumerate() — 인덱스와 값을 함께 꺼내기”

예제 1: try/except/else로 성공·실패 분리하기

Section titled “예제 1: try/except/else로 성공·실패 분리하기”
예제 1 · 파싱, 분류, 해석 Runs in-browser with Pyodide
Ready
line_notokenfloat() 결과이동
1"3.14"성공 3.14ok 리스트
2"N/A"ValueErrorerrors 리스트
3"2.71"성공 2.71ok 리스트
4"bad"ValueErrorerrors 리스트
5"5.00"성공 5.0ok 리스트
6"7"성공 7.0ok 리스트

핵심: 검증 합계 출력은 처리 누락이 없었음을 자동으로 증명합니다. 초보자 필수 습관입니다.


모드의미위험 포인트권장 사용
r읽기파일 없으면 즉시 실패분석/검증 단계
w새로 쓰기기존 내용 전부 삭제새 리포트 시작 1회
a이어 쓰기누적되므로 중복 주의이벤트 로그 기록
x새 파일만 생성파일 있으면 실패덮어쓰기 방지

안전 기본형:

with open("events.log", "a", encoding="utf-8") as f:
f.write("...\n")
실패 패턴관찰되는 증상교정 방향
except:만 사용원인 추적 불가except ValueError as err로 바꾸고 str(err) 출력
try 범위 과대어디서 실패했는지 불명확실패 가능 1~2줄만 try로 축소
w를 루프에서 사용로그가 마지막 줄만 남음헤더만 w, 본문은 a로 분리
인코딩 생략한글 깨짐/환경별 오동작모든 openencoding='utf-8' 고정
검증 없음누락 데이터 발견 못함total == ok + errors를 항상 출력

예제 2 · 안전한 로그 파일 작성 Runs in-browser with Pyodide
Ready
  1. 로그 헤더는 w로 초기화합니다. 루프 밖에서 한 번만 실행하므로 기존 내용이 덮어씌워지지 않습니다.
  2. 각 이벤트는 try로 변환을 시도합니다.
  3. 실패해도 finally에서 append를 보장합니다. 기록이 누락되지 않습니다.
  4. 마지막에 파일을 다시 읽어 실제 기록을 눈으로 검증합니다.

실제 업무에서 가장 많이 만나는 파일 형식입니다.

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)

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 # 잘못된 로직

핵심 계산 전에 아래 순서대로 확인하면 실패를 크게 줄일 수 있습니다.

  1. 존재 검사: 값이 비어 있지 않은가?
  2. 타입 검사: 숫자/문자열/리스트 타입이 맞는가?
  3. 범위 검사: 허용 가능한 최소/최대인가?
  4. 형식 검사: 날짜/ID/코드 형식이 맞는가?
  5. 도메인 규칙 검사: 과목/실험 규칙을 만족하는가?

방어적 코딩의 출발점은 **“계산 전에 막는다”**입니다.

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

좋은 메시지는 세 가지를 포함합니다:

  1. 무엇이 실패했는가
  2. 무엇을 기대했는가 (형식/범위)
  3. 실제로 무엇이 들어왔는가
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 err

from err를 붙이면 “이 새 예외는 원래의 err 때문에 발생했다”는 연결 정보가 저장됩니다. 나중에 오류 트레이스를 볼 때 원인 예외와 새 예외가 함께 표시되어 추적이 쉬워집니다.

!r — f-string에서 repr 출력:

f"got {raw!r}"

!r은 값을 repr() 형식으로 출력하라는 지시자입니다. 문자열이면 따옴표가 붙어 타입 정보가 함께 보입니다. 예를 들어 raw = "abc"이면 !r 없이는 got abc, !r 있으면 got 'abc'로 출력됩니다.

Bad: 원인을 숨김

def parse_temperature(raw):
try:
return float(raw)
except:
return 0

Good: 검증 + 설명 가능한 메시지

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 · 파싱 + 검증 계층 Runs in-browser with Pyodide
Ready
  • [OK]는 유효한 입력이 통과했음을 뜻합니다.
  • [ERR]는 사용자가 어떻게 고치면 되는지 알려줘야 합니다.
  • 메시지가 모호하면 코드보다 먼저 메시지를 개선하세요.

전략사용할 상황예시
Retry일시적 실패센서 타임아웃, 네트워크 지연
Fallback대체값 허용 가능이전 보정값 사용
Skip배치 중 일부 레코드만 문제이상 행만 건너뛰고 계속 계산
Stop Fast안전상 즉시 중단 필요위험한 제어 명령 감지
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))
last_valid = None
results = []
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 · 건너뛰기 정책과 요약 출력 Runs in-browser with Pyodide
Ready
  • valid_counterror_count는 데이터 품질의 빠른 지표입니다.
  • 오류 로그는 데이터 정비 작업 목록이 됩니다.
  • 이 예제는 skip 전략의 기본 형태입니다.

실수오류 유형왜 위험한가개선 패턴
except:만 사용실행 오류시스템 예외까지 숨김예외 타입을 구체적으로 명시
모든 오류에 0 반환논리 오류뒤 계산을 오염시킴에러를 발생시키거나 구조화된 반환
범위 검사 생략논리/실행 오류비현실 값이 계산에 유입min/max 검사 후 계산
print만 하고 종료논리 오류호출자가 실패를 감지 못함예외 발생 또는 상태값 반환
큰 try 블록 한 덩어리실행 오류실패 지점 추적 어려움try 범위를 작게 유지
w를 루프에서 사용실행 오류로그가 마지막 줄만 남음헤더만 w, 본문은 a로 분리

문제: 숫자 문자열 리스트를 정수로 변환하고, 실패 항목은 따로 저장하세요.

실습 문제 1 · 성공과 오류 분리 Runs in-browser with Pyodide
Ready