Step 46. 예외처리와 디버깅 — 넘어져도 다시 일어나는 프로그램
Level 1 — 프로그래밍과 컴퓨터 내부 | 난이도 ★★☆☆☆ | 예상 소요 시간 3시간
전제: Step 41~45를 마쳤다. 함수, 파일 입출력, 자료구조를 쓸 수 있다.
- 준비물: 파이썬이 설치된 컴퓨터, 텍스트 에디터. 새로 설치하는 것은 없습니다.
- 주의: 오늘 실습은 100% 안전합니다. 일부러 오류를 내는 실험이 많지만, 전부 우리가 만든 작은 스크립트 안에서만 일어나는 일입니다.
지금까지 우리가 만든 프로그램에는 공통점이 있습니다. 착한 사용자만 상대한다는 것입니다. 숫자를 달라면 숫자를 주고, 파일이 있을 때만 엽니다. 그런데 현실의 사용자는 나이를 물었는데 "스무"라고 쓰고, 파일 이름을 틀리고, 빈 칸으로 엔터를 칩니다. 그때마다 프로그램이 빨간 글자를 뿜으며 죽는다면, 그것은 도구가 아니라 유리 조각상입니다. 오늘은 프로그램의 낙법을 배웁니다 — 넘어지지 않는 프로그램이 아니라, 넘어져도 다시 일어나는 프로그램을 만드는 날입니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- 오류 메시지(트레이스백)를 아래부터 읽어 원인과 현장을 찾는다
- 대표 예외(ZeroDivisionError, ValueError, FileNotFoundError)를 이름으로 구분한다
try/except로 예외 종류별 대처를 만들어 프로그램이 죽지 않게 한다while + try조합으로 "틀리면 다시 묻는" 입력 루프를 만든다raise로 함수가 잘못된 값을 스스로 거부하게 한다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | 파이썬 3 (스크립트 파일 실행) |
| 오늘의 문법 | try / except, else, finally, raise, except ... as e |
| 오늘 만나는 예외 | ZeroDivisionError, ValueError, FileNotFoundError |
| 필요한 개념 | 예외(Exception), 트레이스백(traceback), 방어적 프로그래밍 |
2-1. 예외 — "더는 못 가겠습니다" 신호
파이썬은 실행 중 감당할 수 없는 상황을 만나면 예외(Exception)를 던지며 멈춥니다. 0으로 나누기, 글자를 숫자로 바꾸기, 없는 파일 열기가 대표적입니다.
예외는 파이썬이 화내는 것이 아니라 정확한 상황 보고입니다. 어디서, 무엇이, 왜 안 됐는지가 다 적혀 있습니다. 그래서 고수는 오류 메시지를 "실패"가 아니라 "가장 정직한 문서"로 읽습니다.
2-2. 트레이스백 — 사건 경위서
예외가 나면 이런 출력이 나옵니다 (2026-09-09 실측, 파일 경로는 줄임):
Traceback (most recent call last):
File "e1_zero.py", line 1, in <module>
print(1 / 0)
~~^~~
ZeroDivisionError: division by zero
이것을 트레이스백(traceback, 발자취)이라고 합니다. 읽는 요령은 아래에서 위로입니다.
- 맨 아래 줄:
ZeroDivisionError: division by zero— 무슨 사고인가 (0으로 나눔) - 그 위 줄:
File "e1_zero.py", line 1— 어디서 났는가 (1번째 줄) ~~^~~같은 꺽쇠 표시: 파이썬 3.11부터 문제가 된 식의 위치를 밑줄로 표시해 줍니다
길이가 길어져도 원칙은 같습니다. 아래부터 읽기. 이 순서 하나가 디버깅 속도를 크게 올려 줍니다.
2-3. try/except — 안전망 치기
예외를 잡는 문법이 try/except입니다.
try:
x = int(input("숫자: "))
print(10 / x)
except ZeroDivisionError:
print("0으로는 나눌 수 없습니다.")
try블록: 위험할 수 있는 일을 넣는 곳except블록: 그 사고가 나면 대신 실행할 대처법
예외가 나도 프로그램이 죽지 않고 except의 대처법을 실행한 뒤 계속 갑니다. 넘어져도 낙법으로 구르고 일어나는 것입니다.
2-4. 방어적 프로그래밍 — 사용자를 요정으로 믿지 않기
예외 처리의 철학에는 이름이 있습니다. 방어적 프로그래밍(defensive programming). "입력은 항상 이상할 수 있다"를 기본 가정으로 삼는 태도입니다.
숫자를 묻되 글자가 올 것을 대비하고, 파일을 열 되 없을 것을 대비합니다. 이 태도가 있으면 프로그램이 단단해지고, 이 태도 그 자체가 보안 사고방식의 뿌리이기도 합니다.
3. 따라 하기
3-1. 일부러 죽여 보기 — 예외 삼형제 만나기
입력 (e1_zero.py)
print(1 / 0)
실행하면 아까 본 트레이스백이 뜹니다. 다음으로 파일을 바꿉니다.
입력 (e2_value.py)
print(int("abc"))
Traceback (most recent call last):
File "e2_value.py", line 1, in <module>
print(int("abc"))
^^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'abc'
(2026-09-09 실측, 경로는 줄임.)
다음.
입력 (e3_file.py)
open("없는파일.txt")
Traceback (most recent call last):
File "e3_file.py", line 1, in <module>
open("없는파일.txt")
FileNotFoundError: [Errno 2] No such file or directory: '없는파일.txt'
(2026-09-09 실측, 경로는 줄임.)
읽는 법: 세 예외의 이름을 노트에 적으세요. 이름이 곧 진단명입니다 — ZeroDivisionError(0 나누기), ValueError(값이 부적합), FileNotFoundError(파일 없음). 앞으로 수백 번 더 만날 친구들입니다. 그리고 주목할 것: 예외가 나면 그 아래 코드는 실행되지 않습니다. 프로그램은 그 자리에서 멈춥니다.
예측:
int("3.14")는 어떤 예외가 날까요? "3.14"는 소수인데 int는 정수를 원합니다. 예측하고 확인하세요. (2026-09-09 실측 결과:ValueError: invalid literal for int() with base 10: '3.14'— 소수점 문자열도 정수 변환은 거부당합니다.)
3-2. 첫 안전망 — 죽지 않는 나눗셈
입력 (safe1.py)
try:
x = int(input("숫자를 입력하세요: "))
print(f"10 나누기 {x}는 {10 / x}")
except ValueError:
print("숫자가 아닙니다! 숫자로 입력해 주세요.")
except ZeroDivisionError:
print("0으로는 나눌 수 없어요.")
print("프로그램이 정상 종료되었습니다.")
세 가지 입력으로 시험합니다. 0을 넣은 경우 (2026-09-09 실측):
숫자를 입력하세요: 0으로는 나눌 수 없어요.
프로그램이 정상 종료되었습니다.
글자를 넣은 경우:
숫자를 입력하세요: 숫자가 아닙니다! 숫자로 입력해 주세요.
프로그램이 정상 종료되었습니다.
정상 입력(5)의 경우:
숫자를 입력하세요: 10 나누기 5는 2.0
프로그램이 정상 종료되었습니다.
(2026-09-09 실측. 프롬프트 뒤에 입력값이 이어서 출력됩니다.)
읽는 법: except를 두 개 달면 사고 종류별로 다른 대처가 됩니다. ValueError(글자 입력)와 ZeroDivisionError(0 입력)가 각자의 대사를 가집니다. 그리고 어떤 경우든 마지막 줄은 실행됩니다 — 프로그램이 죽지 않았다는 증거입니다.
왜: 사고별 대처가 달라야 사용자가 다음 행동을 압니다. "다시 쓰세요"와 "0은 안 돼요"는 다른 안내이니까요.
3-3. 트레이스백 정밀 독해
일부러 조금 긴 사건을 만듭니다.
입력 (trace.py)
def divide(a, b):
return a / b
def start():
x = 10
y = 0
print(divide(x, y))
start()
출력 (2026-09-09 실측, 경로와 꺽쇠 표시는 줄임):
Traceback (most recent call last):
File "trace.py", line 10, in <module>
start()
File "trace.py", line 8, in start
print(divide(x, y))
File "trace.py", line 3, in divide
return a / b
ZeroDivisionError: division by zero
읽는 법: 아래에서 위로 읽습니다. 사고 진단은 0 나누기, 사고 현장은 3번 줄(divide 안), 그곳에 이르기까지의 경로가 8번 줄 → 10번 줄 순으로 쌓여 있습니다. 함수가 깊어질수록 발자취도 길어지지만, 읽는 법은 똑같습니다. 마지막 줄로 진단, 맨 아래 File 줄로 진짜 현장 확인.
왜: 함수를 배운 지금부터 오류는 대부분 이렇게 "여러 층의 발자취"로 나옵니다. 겁먹지 않는 법을 오늘 익히는 것입니다.
3-4. 다시 묻기 — 이의신청 루프
except와 while을 합치면 "틀리면 다시 묻는" 인내심 있는 프로그램이 됩니다.
입력 (patient.py)
while True:
try:
age = int(input("나이를 숫자로 입력하세요: "))
break
except ValueError:
print("숫자가 아니네요. 다시 부탁합니다.")
print(f"{age}살이시군요. 감사합니다!")
"abc", "열다섯", 빈 칸(그냥 엔터)을 차례로 넣은 뒤 마지막에 15를 넣은 실측 (2026-09-09):
나이를 숫자로 입력하세요: 숫자가 아니네요. 다시 부탁합니다.
나이를 숫자로 입력하세요: 숫자가 아니네요. 다시 부탁합니다.
나이를 숫자로 입력하세요: 숫자가 아니네요. 다시 부탁합니다.
나이를 숫자로 입력하세요: 15살이시군요. 감사합니다!
읽는 법: 틀릴 때마다 except가 대사를 하고 while이 처음으로 돌아갑니다. 빈 칸도 int("")가 ValueError를 내기 때문에 같은 대사가 나옵니다. 올바른 입력이 들어오면 try가 성공하고 break로 탈출합니다. "성공할 때까지 다시 묻기"는 사용자 입력 처리의 국민 패턴입니다.
3-5. 파일 안전망 — 메모장에 낙법 입히기
Step 45의 메모장에 낙법을 입힙니다.
입력 (safememo.py)
try:
with open("diary.txt", "r", encoding="utf-8") as f:
print(f.read())
except FileNotFoundError:
print("일기장이 아직 없습니다. 첫 메모부터 시작하세요!")
diary.txt가 없는 상태에서 실행하면 (2026-09-09 실측):
일기장이 아직 없습니다. 첫 메모부터 시작하세요!
읽는 법: Step 45에서는 os.path.exists로 미리 물었습니다. 오늘의 방식은 "일단 해 보고, 사고나면 대처"입니다. 두 방식 모두 널리 쓰이며, 파이썬 세계에서는 try 방식을 더 좋아하는 경향이 있습니다. 둘 다 알아두세요.
3-6. finally와 else — 사고와 무관하게 할 일
입력 (elsefinally.py)
try:
x = int(input("숫자: "))
except ValueError:
print("숫자가 아닙니다.")
else:
print("입력 성공! 계산을 시작합니다.")
finally:
print("--- 입력 처리 끝 ---")
7을 넣은 경우와 abc를 넣은 경우 (2026-09-09 실측):
입력 성공! 계산을 시작합니다.
--- 입력 처리 끝 ---
숫자가 아닙니다.
--- 입력 처리 끝 ---
읽는 법: else는 "사고가 없었을 때만", finally는 "사고가 있든 없든 무조건" 실행됩니다. 두 실행 모두 마지막에 --- 입력 처리 끝 ---이 나온 것이 finally의 증거입니다. finally의 대표 용도는 "파일 닫기, 정리 작업"처럼 어떤 경우에도 해야 하는 마무리입니다.
3-7. raise — 우리도 예외를 던질 수 있다
예외는 파이썬만 던지는 것이 아닙니다. 우리 코드도 "이건 아니다" 싶은 상황에서 예외를 던질 수 있습니다.
입력 (raisetest.py)
def set_age(age):
if age < 0 or age > 150:
raise ValueError("나이는 0~150 사이여야 합니다.")
return age
try:
set_age(-5)
except ValueError as e:
print("거부됨:", e)
출력 (2026-09-09 실측):
거부됨: 나이는 0~150 사이여야 합니다.
읽는 법: raise 예외이름("메시지")가 "여기서 사고 선언!"입니다. except ... as e의 e에는 그 메시지가 담깁니다. "함수가 받아서는 안 되는 값을 거부하는" 이 패턴은, 함수를 만드는 사람이 쓰는 사람을 지키는 장치입니다.
왜: 나중에 도구를 만들면, 잘못된 사용을 조용히 넘기는 것보다 큰 소리로 거부하는 것이 훨씬 안전합니다. 조용한 오진이 가장 위험하니까요.
4. 미션과 연습문제
미션 — 무적의 계산기
Step 44의 메뉴형 계산기를 기반으로 무적의 계산기를 만드세요. 요구사항:
- 숫자를 묻는 모든 곳에서 글자, 빈 칸, 이상한 기호가 들어와도 죽지 않고 다시 묻는다
- 나누기에서 0을 입력하면 죽지 않고 "0으로 나눌 수 없습니다"를 출력한다
- 메뉴 번호에 범위 밖 숫자나 글자가 와도 "1~4 또는 0을 입력하세요"로 안내한다
- 사용자가 0(종료)을 고를 때까지 절대 죽지 않는다
시험 방법: 스스로 나쁜 입력을 열 가지 이상 넣어 보세요. 가능하면 가족이나 친구에게 "망가뜨려 봐"라고 부탁해 보세요. 방어는 공격적으로 시험할 때 완성됩니다.
연습문제
문제 1. 트레이스백이 길게 출력됐습니다. 어느 줄부터 읽어야 하며, 각 줄은 무엇을 알려 주나요?
문제 2. int("3.14")를 실행하면 어떤 예외가 나고, 그 이유는 무엇인가요?
문제 3. except에 예외 이름을 적지 않고 except:라고만 쓰면 어떤 문제가 생기나요?
문제 4. try 블록에 코드를 수십 줄 넣는 것이 나쁜 습관인 이유를 설명해 보세요.
5. 모범 답안과 완료 기준
미션 모범 답안
핵심 구조의 예 (전체 코드는 Step 44의 계산기에 아래 패턴을 입힌 것입니다):
def ask_number(prompt):
while True:
try:
return int(input(prompt))
except ValueError:
print("숫자로 입력해 주세요.")
while True:
print("1.더하기 2.빼기 3.곱하기 4.나누기 0.종료")
menu = input("선택: ").strip()
if menu == "0":
break
if menu not in ["1", "2", "3", "4"]:
print("1~4 또는 0을 입력하세요.")
continue
a = ask_number("첫 번째 숫자: ")
b = ask_number("두 번째 숫자: ")
if menu == "1":
print("결과:", a + b)
elif menu == "2":
print("결과:", a - b)
elif menu == "3":
print("결과:", a * b)
else:
try:
print("결과:", a / b)
except ZeroDivisionError:
print("0으로 나눌 수 없습니다.")
검증하는 법: ① 숫자 입력 자리에 "abc", 빈 칸, "3.14"를 넣어도 다시 묻는가. ② 나누기에서 0을 넣으면 안내 문구가 뜨고 메뉴로 돌아오는가. ③ 메뉴에 "9"나 "메뉴"를 넣으면 안내가 뜨는가. ④ 이 모든 것을 지나도 0을 누르기 전에는 종료되지 않는가. 네 가지가 전부 ‘예’이면 합격입니다.
설계 포인트: "숫자 묻기"를 함수(ask_number)로 분리하면 while + try 루프를 한 번만 쓰고 재사용할 수 있습니다. 나쁜 입력 시험 열 가지의 예: 글자, 빈 칸, 소수, 음수, 아주 큰 수, 공백만, 특수문자, 한글 숫자("삼"), 탭 문자, 메뉴에 0.5.
연습문제 해답
문제 1 해답. 맨 아래 줄부터 읽습니다. 맨 아래 줄은 진단(예외 이름과 사유), 그 위의 File 줄은 사고 현장(파일명과 줄 번호), 더 위의 File 줄들은 그곳에 이르기까지의 호출 경로입니다. "진단 → 현장 → 경로" 순서입니다.
문제 2 해답. ValueError가 납니다 (2026-09-09 실측: ValueError: invalid literal for int() with base 10: '3.14'). "3.14"는 소수를 나타내는 문자열이라 정수 변환 규칙에 맞지 않기 때문입니다. int()가 받아들이는 것은 "10", "-3"처럼 정수 모양의 문자열뿐입니다.
문제 3 해답. 맨 except는 "모든 사고를 삼킨다"는 뜻이라, 예상한 사고뿐 아니라 오타 같은 진짜 버그까지 조용히 숨깁니다. 실험으로 확인할 수 있습니다 — 일부러 pritn이라는 오타를 내고 맨 except로 감쌌더니, 오류 보고 없이 조용히 넘어갔습니다 (2026-09-09 실측). 조용한 버그는 가장 찾기 어려운 버그입니다. 원칙은 except ValueError:처럼 구체적 이름을 적는 것입니다.
문제 4 해답. try가 넓으면 예외를 잡기는 해도 어느 줄에서 난 사고인지 특정할 수 없습니다. 안전망은 위험한 한두 줄 위에만 치는 것이 철칙이고, 위험한 줄이 여러 곳이면 try도 여러 개로 나눕니다.
완료 기준 체크리스트
- [ ] 트레이스백을 아래부터 읽어 진단과 현장을 찾을 수 있다
- [ ] ZeroDivisionError, ValueError, FileNotFoundError를 구분한다
- [ ] try/except로 예외 종류별 대처를 만들 수 있다
- [ ] while + try로 "틀리면 다시 묻기"를 만들 수 있다
- [ ] else와 finally의 실행 조건을 설명할 수 있다
- [ ] raise로 함수가 잘못된 값을 거부하게 할 수 있다
- [ ] 미션: 무적의 계산기를 완성하고 나쁜 입력 열 가지로 시험했다
6. 흔한 실수와 해결
벽 1. except를 달았는데도 죽는다
증상: try/except를 썼는데 빨간 글자가 뜹니다.
원인: 두 가지 대표 경우. (가) 예외가 난 줄이 try 블록 밖에 있다. (나) 잡은 예외 종류가 다르다 — ValueError를 잡는 except인데 FileNotFoundError가 났다.
해결: 트레이스백 맨 아래의 예외 이름과 except에 적은 이름이 같은지, 오류가 난 줄이 try 안에 있는지 확인합니다. 안전망은 공연 구역 위에만 쳐야 합니다.
벽 2. try 안이 너무 넓다
증상: 예외를 잡기는 하는데, 어디서 난 건지 알 수 없습니다.
원인: try 블록에 코드를 수십 줄 넣은 것입니다. 안전망이 넓으면 사고 현장을 특정할 수 없습니다.
해결: try에는 "진짜 위험한 한두 줄"만 넣습니다. 위험한 줄이 여러 곳이면 try도 여러 개로 나눕니다.
벽 3. 아무 except나 달아 버린다 (except:)
증상: 예외 종류를 안 적고 except:만 썼더니, 오타 버그까지 삼켜져서 원인을 한참 찾았습니다.
원인: 맨 except는 모든 예외를 삼킵니다 — 심지어 문법 실수에서 나는 NameError까지. 2026-09-09 실험에서 pritn("오타")를 맨 except로 감싸자 아무 보고 없이 조용히 넘어갔습니다.
해결: except ValueError:처럼 예외 이름을 구체적으로 적습니다. "무슨 사고를 예상하는가"를 적는 행위 자체가 설계입니다.
벽 4. 오류 메시지를 위에서부터 읽는다
증상: 트레이스백이 길게 나오면 첫 줄부터 읽다가 지칩니다.
원인: 순서가 반대입니다. 첫 줄은 "사건 발생을 알림"일 뿐, 진단은 맨 아래 있습니다.
해결: 무조건 맨 아래 줄부터. "진단(맨 아래) → 현장(그 위 File 줄) → 경로(더 위)" 순서를 손에 익히세요.
벽 5. 빈 칸 입력이 예상과 다르게 동작한다
증상: 그냥 엔터를 쳤는데 "숫자가 아닙니다"가 아니라 죽거나, 이상한 값이 들어갑니다.
원인: input()은 빈 입력에 빈 문자열 ""을 돌려주고, int("")는 ValueError를 냅니다. except가 있으면 다시 묻는 것이 정상이고, 죽었다면 try 밖에서 변환한 것입니다.
해결: 3-4절의 루프 구조를 그대로 쓰세요. 빈 칸도 나쁜 입력의 하나로 자연스럽게 걸러집니다 (2026-09-09 실측 확인).
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| 예외(Exception) | 파이썬의 정확한 상황 보고 — 나면 그 자리에서 멈춤 |
| 트레이스백 | 사건 경위서. 아래부터: 진단 → 현장 → 경로 |
| 방어적 프로그래밍 | "입력은 항상 이상할 수 있다"를 기본 가정으로 |
| ZeroDivisionError | 0으로 나눔 |
| ValueError | 값이 규칙에 안 맞음 (글자→숫자 변환 실패 등) |
| FileNotFoundError | 없는 파일을 열려 함 |
오늘의 문법
| 문법 | 하는 일 |
|---|---|
try: |
위험한 일을 넣는 블록 시작 |
except 예외이름: |
그 사고가 나면 실행할 대처 |
except ... as e: |
예외 메시지를 e로 받기 |
else: |
사고가 없었을 때만 실행 |
finally: |
사고와 무관하게 무조건 실행 |
raise 예외이름("메시지") |
우리 코드가 사고를 선언 |
while True + try + break |
"틀리면 다시 묻기" 국민 패턴 |
명령어보다 중요한 감각
오류가 뜨면 초보는 창을 닫고, 중수는 메시지를 읽고, 고수는 재현합니다. "어떤 입력에서 죽었지? 같은 입력을 다시 넣어 보자." 재현 → 진단(맨 아래 줄) → 가설 → print 확인 → 수정. 이 다섯 박자를 오늘부터 습관으로 만드세요. 그리고 고쳤다면 반드시 같은 입력으로 다시 시험합니다 — "고침"을 증명하는 것은 재시험뿐입니다.
보안 관점을 하나 더 덧붙입니다. 트레이스백은 개발자에게는 친절한 문서지만, 사용자에게 그대로 보여 주면 파일 경로와 함수 이름 같은 내부 구조를 알려 주는 셈입니다. 그래서 실제 서비스는 "내부에는 자세히 기록하되, 사용자에게는 ‘일시적 오류입니다’만 보여 주는" 이중 처리를 합니다. 지금은 내 랩의 연습작이니 상관없지만, 이 원칙은 기억해 두세요.
오늘로 여러분의 프로그램은 낙법을 배웠고, 여러분 자신도 빨간 글자를 두려움이 아니라 문서로 읽는 법을 배웠습니다. 다음에 빨간 글자를 만나거든, 아래부터 읽고 차분히 조수의 보고를 듣듯이 읽어 보세요. 답은 반드시 보입니다.
전부 체크되면 Step 46 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.