예외 처리는 프로그램의 안정성과 복원력을 높이는 데 핵심적인 역할을 합니다. 하지만 try-except 구문을 잘못 사용하면 오히려 버그를 숨기거나 성능 저하를 유발할 수 있습니다.
Python 예외(Exception) 처리 : try-except-finally, with

목차2. 사용자 정의 예외(Custom Exception) 만들기
5. with 사용법과 예시 : Context Manager
1. 기본구조: try-except-finally
예외(Exception)**는 프로그램 실행 중 발생하는 예기치 못한 오류를 말합니다.
예: 파일이 존재하지 않음, 0으로 나누기, 타입 불일치 등
예외는 프로그램을 중단시키는 대신 에러를 감지하고 대응할 수 있는 구조를 제공합니다.
▶ 대표적인 예외 상황 예시
# 0으로 나누기
print(10 / 0) # ZeroDivisionError
# 존재하지 않는 파일 열기
with open('nonexistent.txt') as f: # FileNotFoundError
print(f.read())
▶ 기본구조
try:
# 예외 발생 가능 코드
except SomeException as e:
# 예외 발생 시 실행
else:
# 예외 없을 때 실행 (선택)
finally:
# 예외 여부와 관계없이 항상 실행 (선택)
as e를 통해 예외 객체에 접근하면 상세 메시지를 확인할 수 있습니다.
▶ 자주 사용되는 예외 클래스들
| 예외 타입 | 설명 |
| ZeroDivisionError | 0으로 나누기 시 발생 |
| ValueError | 타입은 맞지만 값이 부적절할 때 |
| TypeError | 타입이 맞지 않을 때 |
| FileNotFoundError | 파일이 존재하지 않을 때 |
| IndexError | 인덱스 범위 초과 |
| KeyError | 딕셔너리에 존재하지 않는 키 접근 |
2. 사용자 정의 예외(Custom Exception) 만들기
▶ 사용자 정의 예외 Best Practices 요약
| 원칙 | 설명 |
| 의미가 명확한 이름 | 예외 상황을 명확히 설명하는 이름 사용 (InvalidEmailError 등) |
| Exception 상속 | 대부분의 경우 Exception을 직접 상속받는다 |
| __init__에 추가 정보 | 상태값을 저장하고, 메시지를 유연하게 구성하자 |
| 예외 계층 구조 | 예외를 상위/하위로 분류해 처리 유연성을 높일 수 있다 |
▶ 사용자 정의 예외
사용자 정의 예외는 대부분 Exception 또는 그 하위 클래스를 상속받아 생성합니다.
class InvalidScoreError(Exception):
"""0~100 범위를 벗어난 점수에 대한 사용자 정의 예외"""
pass
▶ raise 키워드로 예외 발생시키기
정의한 예외는 raise 키워드로 발생시킬 수 있습니다.
def set_score(score):
if not (0 <= score <= 100):
raise InvalidScoreError(f"점수가 잘못되었습니다: {score}")
print("점수 저장 완료:", score)
# 사용 예
try:
set_score(150)
except InvalidScoreError as e:
print("예외 발생:", e)
예외 발생: 점수가 잘못되었습니다: 150
▶ 사용자 정의 예외에 상태값 추가하기
예외 클래스에 속성(attribute)를 추가해 더 많은 정보를 담을 수도 있습니다.
class LoginError(Exception):
def __init__(self, username, reason):
self.username = username
self.reason = reason
super().__init__(f"[{username}] 로그인 실패 – {reason}")
def login(username, password):
if password != "secret":
raise LoginError(username, "비밀번호가 틀렸습니다")
try:
login("admin", "wrongpass")
except LoginError as e:
print(e)
[admin] 로그인 실패 – 비밀번호가 틀렸습니다
예외 객체에 .username, .reason 속성을 통해 상세한 정보에 프로그래밍적으로 접근할 수 있습니다.
3. 잘못된 예외 처리 예시와 개선 방법
예외 처리는 프로그램의 오류를 우아하게 처리하기 위한 장치이지만,
잘못 사용하면 오히려 디버깅을 어렵게 만들고, 오류를 숨기며, 성능까지 저하시킬 수 있습니다.
▶ 모든 예외를 무조건 잡기 (except:)
- 어떤 예외가 발생했는지 알 수 없음
- KeyboardInterrupt, SystemExit 같은 시스템 종료 예외도 막아버림
- 로그가 없어 디버깅 불가
try:
do_something()
except:
print("문제가 발생했습니다.")
#어떤 예외가 발생했는지 알 수 없음
#KeyboardInterrupt, SystemExit 같은 시스템 종료 예외도 막아버림
#로그가 없어 디버깅 불가
try:
do_something()
except ValueError as e:
print("값이 잘못되었습니다:", e)
except Exception as e:
print("기타 예외 발생:", e)
#except Exception:은 일반 예외를 잡지만, KeyboardInterrupt는 잡지 않음
▶ 예외 객체를 무시하고 정보 유실
- 어떤 코드에서 예외가 발생했는지, 어떤 상황이었는지 기록되지 않음
- 운영 중 문제가 발생했을 때 원인을 추적하기 어려움
try:
result = 10 / 0
except ZeroDivisionError:
print("0으로 나눌 수 없습니다.")
import logging
try:
result = 10 / 0
except ZeroDivisionError as e:
logging.exception("0으로 나누기 시도 중 예외 발생") # 스택 트레이스 포함 기록
▶ 예외를 흐름 제어용으로 사용하는 경우
- 의도하지 않은 예외도 잡을 수 있음
- 성능 저하: 예외 발생은 일반 조건문보다 훨씬 무거움
- 가독성 저하
def is_digit_slow(s):
try:
int(s)
return True
except ValueError:
return False
def is_digit_fast(s):
return s.isdigit()
▶ try 블록이 너무 큼
- 예외가 어느 부분에서 발생했는지 알기 어려움
- do_a, do_b, do_c 중 어디가 문제인지 파악 불가능
try:
do_a()
do_b()
do_c()
except Exception as e:
print("에러 발생:", e)
try:
do_a()
except Exception as e:
print("do_a 에러:", e)
try:
do_b()
except Exception as e:
print("do_b 에러:", e)
do_c() # 예외 없으면 굳이 try 필요 없음
try 블록은 오류가 발생할 가능성이 있는 최소한의 코드 범위만 감싸는 것이 좋습니다.
▶ 예외를 삼키고 무시함
- 오류가 발생했는데, 전혀 알 수 없음
- 나중에 더 큰 문제로 이어질 수 있음 (예: 설정이 비어 있는 상태로 앱 실행됨)
try:
open_config_file()
except FileNotFoundError:
pass # 그냥 무시
try:
open_config_file()
except FileNotFoundError as e:
logging.warning(f"설정 파일을 찾을 수 없습니다: {e}")
load_default_config()
▶ 예외 처리 위치가 부적절한 경우
- 예외가 발생해도 호출자는 성공한 줄로 착각할 수 있음
- 빈 문자열 반환은 성공을 의미할 수도 있어, 논리 오류 발생 위험
def load_data():
try:
with open("data.txt") as f:
return f.read()
except:
return "" # 무조건 빈 문자열 반환
def load_data():
with open("data.txt") as f:
return f.read()
# 호출하는 쪽에서 적절히 처리
try:
data = load_data()
except FileNotFoundError:
print("파일이 없습니다.")
▶ 전체 반복을 감싸는 예외 처리(너무 크게 쓰지 말것)
- 예외가 발생하면 반복이 중단됨
- 이후 항목은 전혀 처리되지 않음
# ❌ try가 반복 전체를 감싸면 문제 위치 파악 어려움
try:
for item in data:
process(item)
except Exception as e:
print("에러 발생:", e)
for item in data:
try:
process(item)
except Exception as e:
print(f"{item} 처리 중 에러:", e)
예외는 반복 단위로 처리할수록 디버깅과 복원력 측면에서 유리합니다.
▶ 반복문에서 예외로 제어 흐름 만들기
- 마지막에서 IndexError가 발생하여 루프 종료 → 느림
lst = [1, 2, 3, 4]
while True:
try:
item = lst.pop(0)
print(item)
except IndexError:
break
while lst:
item = lst.pop(0)
print(item)
4. finally 사용법과 예시
Python의 try-except 구조에서 finally 블록은 예외 발생 여부와 관계없이 반드시 실행되는 코드 블록입니다.
주로 자원 정리, 종료 로그 기록, 락 해제 등 중요한 정리 작업에 사용됩니다.
▶ 파일 열기/닫기
- 자원 정리: 파일, 소켓, DB 연결 등을 반드시 닫아야 하는 경우
try:
f = open('data.txt', 'r')
contents = f.read()
except FileNotFoundError:
print("파일이 없습니다.")
finally:
f.close() # 항상 실행됨
▶ 함수에서 return이 있어도 finally는 실행됨
- finally는 return보다 먼저 실행됩니다!
def process():
try:
return "처리 완료"
finally:
print("자원 정리 수행")
print(process())
#
자원 정리 수행
처리 완료
▶ 예외가 발생해도 finally는 실행
- 에러가 나든 안 나든 로그, DB 커넥션 해제 등은 안전하게 실행됨
try:
1 / 0
except ZeroDivisionError:
print("0으로 나눌 수 없습니다.")
finally:
print("로그 남기기 완료")
▶ finally는 예외 재발생 전에도 실행됨
- finally는 예외가 호출자에게 전달되기 전에 실행됩니다.
def critical():
try:
raise RuntimeError("에러 발생")
finally:
print("클린업 수행 중...")
try:
critical()
except RuntimeError as e:
print("예외 처리됨:", e)
#
클린업 수행 중...
예외 처리됨: 에러 발생
▶ loop + break/continue에서도 실행됨
- break, continue가 있어도 finally는 반드시 실행됩니다.
for i in range(3):
try:
if i == 1:
break
finally:
print(f"finally in loop {i}")
#
finally in loop 0
finally in loop 1
▶ 주의사항
- finally에서 예외를 발생시키면, 원래 예외는 덮여짐
- 자원 해제를 finally에서 하려면, 그 자원이 정의되었는지 확인 필요
try:
f = open('data.txt', 'r')
except FileNotFoundError:
print("파일 없음")
finally:
try:
f.close()
except:
pass # f가 존재하지 않을 수 있음
5. with 사용법과 예시 : Context Manager
Python의 with 구문은 context manager(컨텍스트 관리자)를 사용하는 문법입니다.
복잡한 예외 처리와 자원 정리를 간결하고 안전하게 처리할 수 있는 파이썬다운 방식으로,
파일, 소켓, 데이터베이스, 락 등 “열고 닫는” 자원을 다룰 때 거의 필수로 사용됩니다.
▶ with 구문의 기본 구조
- expression은 context manager 객체 (예: open(), threading.Lock() 등)
- as variable: context 객체를 받을 변수 (선택 사항)
with expression as variable:
# 작업 블록
▶ 예외가 발생해도 자원 정리 보장
- 이 경우에도 f.close()는 자동으로 호출됩니다.
- with 구문은 finally 블록처럼 작동합니다.
with open('data.txt', 'r') as f:
raise RuntimeError("작업 중 문제 발생")
▶ 여러 자원을 동시에 다루기
- 두 개의 파일을 열고 하나의 with 구문으로 처리 가능
- 각각의 자원에 대해 __exit__() 호출됨 → 둘 다 안전하게 닫힘
with open('input.txt') as fin, open('output.txt', 'w') as fout:
for line in fin:
fout.write(line.upper())
▶ with를 사용하는 대표적인 라이브러리들
| 도구 | 목적 | 예시 |
| open() | 파일 열기 | with open(...) |
| threading.Lock() | 멀티스레드 락 | with lock: |
| sqlite3.connect() | DB 연결 | with sqlite3.connect(...) |
| tempfile.NamedTemporaryFile() | 임시 파일 | with tempfile.NamedTemporaryFile() as tmp: |
| contextlib | 사용자 정의 context manager | with contextlib.suppress(...) |
6. 사용자 정의 context manager 만들기: __enter__, __exit__
class CustomContext:
def __enter__(self):
print("자원 열기")
return "작업 리소스"
def __exit__(self, exc_type, exc_val, exc_tb):
print("자원 정리")
return False # 예외를 다시 던짐 (처리하지 않음)
with CustomContext() as resource:
print("리소스 사용:", resource)
▶ __enter__() 메서드
- with 블록이 시작되면 가장 먼저 이 메서드가 호출됩니다.
- 주로 여기서 파일 열기, 연결 설정, 리소스 초기화 등을 합니다.
- 이 함수의 반환값은 as 뒤 변수에 할당됩니다.
▶ __exit__() 메서드
- with 블록이 끝났을 때 호출됩니다.
- 이 메서드는 3개의 인자를 자동으로 받습니다:
▶ contextlib 사용 – 함수 기반으로 간결하게
- yield 앞은 __enter__, 뒤는 __exit__처럼 동작합니다.
from contextlib import contextmanager
@contextmanager
def custom_resource():
print("리소스 준비")
yield "데이터"
print("리소스 해제")
with custom_resource() as res:
print("작업:", res)
7. with vs try-finally 비교
with 문과 try-finally는 자원을 안전하게 다루기 위한 공통된 목적을 갖고 있지만, 사용성, 가독성, 예외 처리의 명확성 측면에서 차이가 존재합니다.
▶ with
with open('example.txt', 'r') as f:
contents = f.read()
print(contents)
▶ try-finally
f = open('example.txt', 'r')
try:
contents = f.read()
finally:
f.close()
▶ try-finally 비교
| 항목 | with 문 | try-finally |
| 자원 정리 자동화 | 자동 호출 (__exit__) | 수동 호출 (close() 직접 호출) |
| 예외 발생 시 안전성 | 예외 발생해도 정리 보장 | 동일 (finally는 무조건 실행됨) |
| 코드 길이 / 가독성 | 짧고 명확 | 구조가 복잡, 중첩 시 불편 |
| 여러 자원 처리 | 한 줄에 여러 개 가능 | try 블록 중첩 필요 |
| 예외 정보 처리 | __exit__가 예외 인자 받아 처리 가능 | 예외 자체를 처리하려면 except 추가 필요 |
| 오류 방지 | 실수 방지 (닫기 빼먹지 않음) | close() 호출 누락 위험 |
관련 글 링크
4. Python 함수구조, 함수인자, 람다함수, 클로저, 고차함수, 데코레이터
4. Python 함수구조, 함수인자, 람다함수, 클로저, 고차함수, 데코레이터
Python 함수구조, 함수인자, 람다함수, 클로저, 고차함수, 데코레이터 목차 1. 함수 구조 2. 함수의 인자(Arguments) 3. 전역변수와 지역변수 4. 람다함수(Lambda function) 5. 클로저(Closure) 6. 고차 함수(High-Or
quadcube.tistory.com
2. Python 변수 정리: 지역변수, 전역변수, global, 클래스 변수
2. Python 변수 정리: 지역변수, 전역변수, global, 클래스 변수
Python 변수 정리: 지역변수, 전역변수, global, 클래스 변수 목차 1. 지역변수 vs 전역변수 2. global 키워드의 역할과 주의사항 3. nonlocal 키워드 4. 변수처럼 다루는 함수-일급객체로서의 함수 5. 클래스
quadcube.tistory.com
3. Python 클래스 정리: 클래스, 상속, 메서드, 접근제어
3. Python 클래스 정리: 클래스, 상속, 메서드, 접근제어
Python 클래스의 기본 구조부터 생성자, 메서드, 상속, 소멸자까지 핵심 개념을 정리했습니다.실무에 바로 적용 가능한 예제와 함께 __init__, self, __del__, __enter__, __exit__까지 전체 흐름을 이해할 수
quadcube.tistory.com
'3.SW개발 > Python' 카테고리의 다른 글
| [Python요약]7. Python 자료형 정리 : List, Tuple, Dictionary, Set, Sequence, Range (0) | 2025.11.05 |
|---|---|
| [Python요약]6. Python 병렬 처리 : GIL, threading, asyncio, multiprocessing (0) | 2025.11.05 |
| [Python요약]4. Python 함수구조, 함수인자, 람다함수, 클로저, 고차함수, 데코레이터 (0) | 2025.11.05 |
| [Python요약]3. Python 클래스 정리: 클래스, 상속, 메서드, 접근제어 (0) | 2025.11.05 |
| [Python요약]2. Python 변수 정리: 지역변수, 전역변수, global, 클래스 변수 (0) | 2025.11.05 |