이미지 로딩 중...
AI Generated
2025. 11. 21. · 13 Views
Python 파일 입출력 및 예외 처리 완벽 가이드
파일을 안전하게 읽고 쓰는 방법부터 예상치 못한 오류를 우아하게 처리하는 방법까지, 실무에서 바로 사용할 수 있는 파일 입출력과 예외 처리 기법을 쉽고 친근하게 알려드립니다. 초급 개발자도 따라하면서 배울 수 있도록 실제 작동하는 코드와 함께 설명합니다.
목차
- 기본_파일_읽기_쓰기
- try_except_기본_예외_처리
- else와_finally로_완성하는_예외_처리
- raise로_의도적인_예외_발생시키기
- 커스텀_예외_클래스_만들기
- 파일_한_줄씩_읽기와_메모리_효율성
- JSON_파일_다루기와_데이터_직렬화
- with_문과_컨텍스트_매니저
- 여러_예외를_한번에_처리하기
- 예외_체이닝과_원본_오류_보존
1. 기본_파일_읽기_쓰기
시작하며
여러분이 데이터 분석 프로젝트를 시작했는데, CSV 파일을 읽어야 하는 상황을 생각해보세요. 파일을 열었는데 프로그램이 갑자기 멈춰버리거나, 파일을 닫는 것을 깜빡해서 나중에 다시 열 수 없게 되는 경우가 있습니다.
이런 문제는 실제 개발 현장에서 정말 자주 발생합니다. 파일을 제대로 열고 닫지 않으면 메모리 누수가 생기거나, 데이터가 손실될 수도 있습니다.
특히 대용량 파일을 다룰 때는 이런 문제가 더욱 심각해집니다. 바로 이럴 때 필요한 것이 올바른 파일 입출력 방법입니다.
Python에서는 with 문을 사용하여 파일을 자동으로 안전하게 열고 닫을 수 있습니다.
개요
간단히 말해서, 파일 입출력은 컴퓨터에 저장된 파일을 읽거나 새로운 내용을 파일에 쓰는 작업입니다. 실무에서 데이터를 저장하거나 불러올 때 반드시 필요한 기능입니다.
예를 들어, 사용자 정보를 저장하거나 로그 파일을 기록하거나 설정 파일을 읽어오는 모든 작업에서 파일 입출력을 사용합니다. 웹 애플리케이션에서 업로드된 이미지를 저장하거나, 머신러닝 모델을 학습시킬 데이터를 불러오는 것도 모두 파일 입출력입니다.
기존에는 open()으로 파일을 열고 close()로 직접 닫아야 했다면, 이제는 with 문을 사용하여 자동으로 파일을 닫을 수 있습니다. 파일을 열 때는 모드를 지정할 수 있습니다.
읽기 모드('r'), 쓰기 모드('w'), 추가 모드('a') 등이 있으며, 각각의 모드는 파일을 어떻게 다룰지를 결정합니다. 이러한 특징들이 파일 데이터를 안전하게 관리하는 데 핵심적인 역할을 합니다.
코드 예제
# 파일 쓰기: 새로운 파일을 만들거나 기존 파일을 덮어씁니다
with open('data.txt', 'w', encoding='utf-8') as file:
file.write('안녕하세요, 파일 입출력 세계에 오신 것을 환영합니다!\n')
file.write('이 내용은 data.txt 파일에 저장됩니다.\n')
# 파일 읽기: 저장된 내용을 불러옵니다
with open('data.txt', 'r', encoding='utf-8') as file:
content = file.read()
print(content)
# 파일 추가: 기존 내용 뒤에 새로운 내용을 추가합니다
with open('data.txt', 'a', encoding='utf-8') as file:
file.write('추가된 내용입니다.\n')
설명
이것이 하는 일: 파일을 안전하게 열어서 데이터를 읽거나 쓴 후, 자동으로 파일을 닫아줍니다. 첫 번째로, with open('data.txt', 'w', encoding='utf-8') as file 부분은 'data.txt'라는 이름의 파일을 쓰기 모드로 여는 역할을 합니다.
마치 노트를 펼쳐놓는 것과 같습니다. 'w' 모드는 파일이 이미 있으면 내용을 모두 지우고 새로 쓰기 시작합니다.
encoding='utf-8'은 한글을 포함한 다양한 문자를 제대로 저장하기 위해 꼭 필요합니다. 그 다음으로, file.write()가 실행되면서 괄호 안의 문자열을 파일에 기록합니다.
마치 노트에 연필로 글을 쓰는 것처럼, 프로그램이 파일에 데이터를 저장합니다. 여러 번 write()를 호출하면 그만큼 내용이 계속 추가됩니다.
세 번째로, with 블록이 끝나면 Python이 자동으로 파일을 닫아줍니다. 이것이 with 문의 가장 큰 장점입니다.
직접 close()를 호출할 필요가 없어서, 파일을 닫는 것을 깜빡하는 실수를 방지할 수 있습니다. 마지막으로, 'a' 모드를 사용하면 기존 파일 내용을 유지한 채로 새로운 내용을 뒤에 추가할 수 있습니다.
마치 일기장의 다음 페이지에 계속 글을 쓰는 것과 같습니다. 여러분이 이 코드를 사용하면 파일을 안전하게 관리할 수 있고, 메모리 누수 걱정 없이 대용량 파일도 다룰 수 있습니다.
또한 코드가 깔끔해지고 읽기 쉬워지며, 예상치 못한 오류가 발생해도 파일이 제대로 닫히는 것을 보장받을 수 있습니다.
실전 팁
💡 항상 encoding='utf-8'을 명시하세요. 한글이나 특수문자가 깨지는 문제를 미리 방지할 수 있습니다.
💡 'w' 모드는 기존 파일을 완전히 덮어쓰므로 주의하세요. 기존 내용을 보존하려면 'a' 모드를 사용하세요.
💡 대용량 파일은 read() 대신 readline()이나 for 문으로 한 줄씩 읽으면 메모리를 절약할 수 있습니다.
💡 파일 경로에는 절대 경로보다는 상대 경로를 사용하거나, os.path.join()을 활용하면 운영체제에 상관없이 동작합니다.
💡 바이너리 파일(이미지, 동영상 등)을 다룰 때는 'rb', 'wb' 모드를 사용하세요.
2. try_except_기본_예외_처리
시작하며
여러분이 사용자로부터 파일 이름을 입력받아 파일을 열려고 하는데, 사용자가 존재하지 않는 파일 이름을 입력했다면 어떻게 될까요? 프로그램이 갑자기 멈추면서 빨간 에러 메시지가 화면에 가득 찰 것입니다.
이런 문제는 실제 개발 현장에서 매일 발생합니다. 사용자의 입력은 예측할 수 없고, 외부 파일이나 네트워크 상태도 항상 완벽할 수 없습니다.
프로그램이 예상치 못한 상황에서 갑자기 중단되면 사용자 경험이 나빠지고, 심각한 경우 데이터 손실까지 발생할 수 있습니다. 바로 이럴 때 필요한 것이 예외 처리입니다.
try-except 문을 사용하면 오류가 발생해도 프로그램이 멈추지 않고 적절하게 대응할 수 있습니다.
개요
간단히 말해서, 예외 처리는 프로그램 실행 중에 발생할 수 있는 오류를 미리 준비하고 대응하는 방법입니다. 실무에서는 절대적으로 필요한 기능입니다.
예를 들어, 데이터베이스 연결이 끊어지거나, 사용자가 숫자 대신 문자를 입력하거나, 디스크 공간이 부족한 상황 등 수많은 예외 상황이 발생할 수 있습니다. 이러한 상황들을 모두 예측하고 대비하는 것이 안정적인 프로그램을 만드는 핵심입니다.
기존에는 오류가 발생하면 프로그램이 그냥 중단되었다면, 이제는 try-except를 사용하여 오류를 잡아내고 사용자에게 친절한 메시지를 보여주거나 대안을 제시할 수 있습니다. try 블록에는 오류가 발생할 수 있는 코드를 작성하고, except 블록에는 오류가 발생했을 때 실행할 코드를 작성합니다.
또한 특정 종류의 오류만 잡아낼 수도 있어서, 각 상황에 맞는 정확한 대응이 가능합니다. 이러한 특징들이 견고한 프로그램을 만드는 데 필수적입니다.
코드 예제
# 파일을 열 때 발생할 수 있는 오류를 처리합니다
try:
# 오류가 발생할 수 있는 코드를 여기에 작성합니다
with open('존재하지않는파일.txt', 'r', encoding='utf-8') as file:
content = file.read()
print(content)
except FileNotFoundError:
# 파일이 없을 때 실행되는 코드입니다
print('죄송합니다. 파일을 찾을 수 없습니다.')
print('파일 이름을 다시 확인해주세요.')
except PermissionError:
# 파일 접근 권한이 없을 때 실행됩니다
print('파일에 접근할 권한이 없습니다.')
설명
이것이 하는 일: 오류가 발생할 수 있는 코드를 안전하게 실행하고, 오류가 발생하면 프로그램을 멈추지 않고 지정된 처리 방법을 수행합니다. 첫 번째로, try: 다음에 오는 코드 블록은 "이 코드를 실행하려고 시도해봐"라는 의미입니다.
마치 조심스럽게 문을 열어보는 것과 같습니다. 여기서는 파일을 열려고 시도하는데, 만약 파일이 없다면 오류가 발생할 것입니다.
그 다음으로, except FileNotFoundError: 부분이 실행되는데, 이것은 "만약 파일을 찾을 수 없다는 오류가 발생하면 이렇게 대응해"라는 의미입니다. Python은 오류의 종류를 정확히 구분할 수 있어서, FileNotFoundError(파일을 찾을 수 없음), PermissionError(권한 없음), ValueError(잘못된 값) 등 각각의 상황에 맞게 대응할 수 있습니다.
세 번째로, except 블록 안의 코드가 실행되면 사용자에게 친절한 메시지를 보여줍니다. 빨간 에러 메시지 대신 "파일을 찾을 수 없습니다"라는 이해하기 쉬운 메시지를 보여주는 것입니다.
이렇게 하면 사용자가 무슨 문제가 생겼는지 쉽게 이해할 수 있습니다. 마지막으로, 여러 개의 except 블록을 사용하면 다양한 종류의 오류에 각각 다르게 대응할 수 있습니다.
파일이 없는 것과 권한이 없는 것은 완전히 다른 문제이므로, 각각에 맞는 해결 방법을 제시할 수 있습니다. 여러분이 이 코드를 사용하면 프로그램이 예상치 못한 상황에서도 멈추지 않고 계속 동작합니다.
사용자에게 더 나은 경험을 제공할 수 있고, 디버깅도 훨씬 쉬워집니다. 또한 로그를 남기거나 대체 동작을 수행하는 등 유연한 오류 대응이 가능해집니다.
실전 팁
💡 except 뒤에 구체적인 오류 타입을 명시하세요. except만 쓰면 모든 오류를 잡아서 진짜 버그를 놓칠 수 있습니다.
💡 여러 종류의 오류를 한 번에 처리하려면 except (FileNotFoundError, PermissionError): 형식으로 괄호 안에 나열하세요.
💡 오류 메시지를 변수로 받아 로그에 기록하면 나중에 문제를 추적하기 쉽습니다. except Exception as e: 형식을 사용하세요.
💡 사용자에게 보여주는 메시지는 기술적인 용어보다는 쉬운 말로 설명하세요. "FileNotFoundError" 대신 "파일을 찾을 수 없습니다"가 훨씬 좋습니다.
💡 중요한 오류는 logging 모듈을 사용하여 파일에 기록해두면 나중에 분석할 때 유용합니다.
3. else와_finally로_완성하는_예외_처리
시작하며
여러분이 파일을 처리하는 프로그램을 만들었는데, 오류 없이 성공했을 때만 "작업 완료" 메시지를 보여주고 싶고, 성공하든 실패하든 항상 로그를 남기고 싶다면 어떻게 해야 할까요? try-except만으로는 이런 세밀한 제어가 어렵습니다.
이런 문제는 실제 개발 현장에서 자주 마주치는 상황입니다. 데이터베이스 트랜잭션을 처리하거나, 네트워크 연결을 관리하거나, 파일 처리 후 정리 작업을 해야 할 때 "성공했을 때만", "항상" 실행되어야 하는 코드가 필요합니다.
바로 이럴 때 필요한 것이 else와 finally 절입니다. else는 오류가 없을 때만 실행되고, finally는 무슨 일이 있어도 항상 실행됩니다.
개요
간단히 말해서, else는 try 블록이 성공적으로 완료되었을 때만 실행되는 코드이고, finally는 성공이든 실패든 무조건 실행되는 코드입니다. 실무에서 리소스 관리나 정리 작업을 할 때 매우 중요합니다.
예를 들어, 데이터베이스 연결을 열었다면 성공하든 실패하든 반드시 연결을 닫아야 합니다. 또는 파일 처리가 성공했을 때만 성공 로그를 남기고, 실패했을 때는 다른 로그를 남겨야 할 수도 있습니다.
임시 파일을 만들었다면 작업이 끝난 후 반드시 삭제해야 합니다. 기존에는 try-except만 사용해서 성공 케이스를 명확히 구분하기 어려웠다면, 이제는 else를 사용하여 "오류가 없을 때만 실행할 코드"를 분명히 구분할 수 있습니다.
else는 try 블록에서 어떤 오류도 발생하지 않았을 때만 실행됩니다. finally는 try, except, else 중 무엇이 실행되든 상관없이 마지막에 항상 실행됩니다.
이러한 특징들이 복잡한 오류 처리 로직을 명확하고 안전하게 만들어줍니다.
코드 예제
# 파일 처리의 전체 과정을 제어합니다
import os
try:
# 파일 읽기를 시도합니다
with open('data.txt', 'r', encoding='utf-8') as file:
data = file.read()
lines = len(data.split('\n'))
except FileNotFoundError:
# 파일이 없을 때의 처리
print('오류: 파일이 존재하지 않습니다.')
else:
# 오류 없이 성공했을 때만 실행됩니다
print(f'성공! 파일에서 {lines}줄을 읽었습니다.')
print('데이터 처리를 시작합니다.')
finally:
# 성공이든 실패든 항상 실행됩니다
print('파일 처리 작업이 종료되었습니다.')
print('로그를 기록합니다.')
설명
이것이 하는 일: try-except-else-finally를 조합하여 오류 발생 여부와 상관없이 정확하게 코드 흐름을 제어합니다. 첫 번째로, try 블록에서 파일을 읽는 작업을 시도합니다.
이 부분은 평소처럼 실행되며, 오류가 발생할 수도 있고 성공할 수도 있습니다. 마치 시험을 보는 것과 같습니다.
그 다음으로, 만약 파일이 없어서 FileNotFoundError가 발생하면 except 블록이 실행됩니다. 시험에 떨어진 경우라고 생각하면 됩니다.
여기서는 "파일이 없다"는 메시지를 출력하고 else와 finally로 넘어갑니다. 세 번째로, 만약 try 블록이 아무 오류 없이 성공적으로 완료되면 else 블록이 실행됩니다.
시험에 합격한 경우입니다. 여기서 중요한 점은 except가 실행되면 else는 건너뛴다는 것입니다.
즉, 정말로 성공했을 때만 "데이터 처리를 시작합니다"라는 메시지가 나옵니다. 마지막으로, finally 블록은 무슨 일이 있어도 항상 실행됩니다.
시험에 합격하든 떨어지든, 시험장을 정리하고 나가야 하는 것과 같습니다. 여기서는 로그를 기록하는데, 이것은 성공 여부와 관계없이 작업 내역을 남기기 위한 것입니다.
여러분이 이 코드를 사용하면 복잡한 오류 처리 로직을 명확하게 구조화할 수 있습니다. 성공 케이스와 실패 케이스를 확실히 구분할 수 있고, 반드시 실행되어야 하는 정리 코드를 보장할 수 있습니다.
데이터베이스 연결 닫기, 파일 잠금 해제, 임시 리소스 삭제 등의 작업을 안전하게 수행할 수 있습니다.
실전 팁
💡 finally는 return 문보다도 나중에 실행됩니다. 함수에서 return 하더라도 finally는 반드시 실행되므로 정리 작업에 완벽합니다.
💡 else 블록에는 try 블록이 성공했을 때의 후속 작업을 넣으세요. try 블록은 최대한 짧게 유지하는 것이 좋습니다.
💡 데이터베이스나 네트워크 연결은 finally에서 닫으면 누수를 방지할 수 있습니다.
💡 try 블록 안에서 return을 하더라도 finally는 실행되므로, 로그 기록이나 리소스 해제를 finally에 넣으면 안전합니다.
💡 else 블록은 선택사항이지만, 코드의 의도를 명확히 하려면 사용하는 것이 좋습니다.
4. raise로_의도적인_예외_발생시키기
시작하며
여러분이 사용자의 나이를 입력받는 함수를 만들었는데, 누군가 음수나 200살 같은 이상한 값을 입력했다면 어떻게 해야 할까요? 그냥 넘어가면 나중에 더 큰 문제가 생길 수 있습니다.
이런 문제는 실제 개발 현장에서 데이터 검증을 할 때 항상 마주치는 상황입니다. 잘못된 데이터가 시스템 깊숙이 들어가면 찾기 어려운 버그가 되거나, 보안 문제가 될 수도 있습니다.
입력 단계에서 명확히 거부하는 것이 나중에 디버깅하는 것보다 훨씬 효율적입니다. 바로 이럴 때 필요한 것이 raise 문입니다.
조건이 맞지 않을 때 의도적으로 예외를 발생시켜서, 문제를 즉시 알리고 처리할 수 있습니다.
개요
간단히 말해서, raise는 프로그래머가 직접 예외를 발생시키는 명령어입니다. 실무에서 데이터 검증, 비즈니스 로직 검사, 전제 조건 확인 등에 필수적입니다.
예를 들어, 사용자가 로그인하지 않은 상태에서 프로필 수정을 시도하거나, 잔액이 부족한데 결제를 시도하거나, 파일 크기가 제한을 초과하는 경우 등에 raise를 사용하여 명확히 오류를 발생시킵니다. API를 만들 때도 잘못된 요청에 대해 적절한 예외를 발생시켜야 합니다.
기존에는 if 문으로 검사하고 print로 메시지만 출력했다면, 이제는 raise를 사용하여 프로그램의 흐름을 명확히 제어하고 호출자에게 문제를 알릴 수 있습니다. raise 뒤에는 예외 타입과 메시지를 함께 작성할 수 있습니다.
ValueError, TypeError, 또는 직접 만든 커스텀 예외도 사용할 수 있습니다. 이러한 특징들이 견고하고 예측 가능한 프로그램을 만드는 데 핵심적입니다.
코드 예제
# 사용자 나이를 검증하는 함수입니다
def register_user(name, age):
# 나이가 유효한 범위인지 검사합니다
if age < 0:
# 음수면 즉시 예외를 발생시킵니다
raise ValueError('나이는 음수일 수 없습니다.')
if age > 150:
# 너무 크면 예외를 발생시킵니다
raise ValueError('나이가 150을 초과할 수 없습니다.')
# 모든 검증을 통과하면 정상 처리합니다
print(f'{name}님, 환영합니다! (나이: {age}세)')
return True
# 함수를 사용하는 코드
try:
register_user('철수', -5)
except ValueError as e:
print(f'입력 오류: {e}')
설명
이것이 하는 일: 특정 조건을 만족하지 않을 때 프로그램을 멈추고 명확한 오류 메시지를 전달합니다. 첫 번째로, 함수 안에서 if 문으로 나이가 0보다 작은지 검사합니다.
이것은 "문지기"처럼 잘못된 데이터를 걸러내는 역할입니다. 마치 놀이공원 입장 시 키를 재는 것과 비슷합니다.
그 다음으로, 조건이 맞지 않으면 raise ValueError('나이는 음수일 수 없습니다.')가 실행됩니다. ValueError는 "값이 잘못되었다"는 의미의 예외 타입이고, 괄호 안의 문자열은 무엇이 잘못되었는지 설명하는 메시지입니다.
이 순간 함수는 즉시 멈추고, 예외가 호출한 쪽으로 전달됩니다. 세 번째로, try-except 블록에서 이 예외를 잡습니다.
except ValueError as e: 부분에서 e는 예외 객체를 담고 있고, 이 안에 우리가 작성한 메시지가 들어 있습니다. 이 메시지를 출력하면 "입력 오류: 나이는 음수일 수 없습니다."처럼 정확한 오류 내용을 사용자에게 알려줄 수 있습니다.
마지막으로, 모든 검증을 통과한 정상적인 데이터만 실제 처리 로직으로 넘어갑니다. 이렇게 하면 함수 안에서는 "여기까지 왔다면 데이터는 안전하다"고 확신할 수 있어서, 코드가 훨씬 단순하고 명확해집니다.
여러분이 이 코드를 사용하면 잘못된 데이터가 시스템에 들어가는 것을 초기에 차단할 수 있습니다. 디버깅이 쉬워지고, 오류 메시지가 명확해지며, 코드의 의도가 분명해집니다.
API를 만들 때도 클라이언트에게 정확한 오류 정보를 전달할 수 있어서 협업이 훨씬 수월해집니다.
실전 팁
💡 ValueError는 값이 잘못되었을 때, TypeError는 타입이 잘못되었을 때 사용하세요. 상황에 맞는 예외 타입을 선택하면 의도가 명확해집니다.
💡 예외 메시지는 구체적으로 작성하세요. "오류"보다는 "나이는 0보다 커야 합니다"가 훨씬 도움이 됩니다.
💡 함수의 시작 부분에서 입력 검증을 하면, 나머지 코드는 안전한 데이터만 다루므로 간결해집니다.
💡 여러 조건을 검사할 때는 각각 별도의 if-raise로 나누면 어떤 조건이 문제인지 정확히 알 수 있습니다.
💡 중요한 비즈니스 로직에는 커스텀 예외(class InsufficientBalanceError(Exception))를 만들어 사용하면 더 명확합니다.
5. 커스텀_예외_클래스_만들기
시작하며
여러분이 은행 시스템을 만들고 있는데, 잔액 부족, 일일 한도 초과, 계좌 정지 등 다양한 오류 상황이 있다면 어떻게 구분해야 할까요? 모두 ValueError로 처리하면 나중에 어떤 오류인지 구분하기 어렵습니다.
이런 문제는 실제 개발 현장에서 복잡한 비즈니스 로직을 다룰 때 자주 발생합니다. 일반적인 예외 타입만으로는 도메인 특화된 오류를 명확히 표현하기 어렵고, 오류별로 다른 처리를 하기도 힘듭니다.
코드를 읽는 사람도 무슨 오류인지 파악하는 데 시간이 걸립니다. 바로 이럴 때 필요한 것이 커스텀 예외 클래스입니다.
여러분만의 예외 타입을 만들어서 도메인 로직을 더 명확하고 전문적으로 표현할 수 있습니다.
개요
간단히 말해서, 커스텀 예외는 여러분이 직접 만드는 예외 클래스로, 특정 상황을 더 정확하게 표현할 수 있습니다. 실무에서 복잡한 애플리케이션을 만들 때 거의 필수적입니다.
예를 들어, 결제 시스템에서 PaymentFailedError, InsufficientFundsError, InvalidCardError 등을 만들면 각 오류를 정확히 구분하고 적절히 대응할 수 있습니다. 또한 오류마다 추가 정보를 담을 수도 있어서, 로깅이나 알림을 보낼 때 유용합니다.
기존에는 ValueError나 Exception 같은 일반적인 예외만 사용했다면, 이제는 InsufficientBalanceError처럼 의미가 명확한 예외를 만들어서 코드의 가독성과 유지보수성을 크게 높일 수 있습니다. 커스텀 예외는 Exception 클래스를 상속받아 만듭니다.
추가 정보를 저장할 속성을 넣을 수도 있고, 메시지 포맷을 커스터마이징할 수도 있습니다. 이러한 특징들이 전문적이고 유지보수하기 쉬운 코드를 만드는 데 핵심적입니다.
코드 예제
# 잔액 부족 예외를 정의합니다
class InsufficientBalanceError(Exception):
def __init__(self, balance, required):
self.balance = balance
self.required = required
# 사용자 친화적인 메시지를 만듭니다
message = f'잔액이 부족합니다. 현재: {balance}원, 필요: {required}원'
super().__init__(message)
# 계좌 클래스에서 커스텀 예외를 사용합니다
def withdraw(balance, amount):
if amount > balance:
# 커스텀 예외를 발생시킵니다
raise InsufficientBalanceError(balance, amount)
return balance - amount
# 예외를 구체적으로 처리할 수 있습니다
try:
new_balance = withdraw(10000, 50000)
except InsufficientBalanceError as e:
print(f'오류: {e}')
print(f'부족 금액: {e.required - e.balance}원')
설명
이것이 하는 일: 프로젝트에 특화된 예외를 정의하여 오류 상황을 더 정확하고 전문적으로 표현합니다. 첫 번째로, class InsufficientBalanceError(Exception): 부분은 "잔액 부족"이라는 새로운 예외 타입을 정의합니다.
괄호 안의 Exception은 이 클래스가 예외 클래스를 상속받는다는 의미입니다. 마치 "자동차"를 상속받아 "전기차"를 만드는 것과 비슷합니다.
그 다음으로, init 메서드에서 balance(현재 잔액)와 required(필요 금액)를 매개변수로 받아 저장합니다. 이렇게 하면 나중에 이 예외를 잡았을 때 정확히 얼마가 부족한지 알 수 있습니다.
self.balance와 self.required는 이 정보를 객체에 저장하는 역할입니다. 세 번째로, super().init(message) 부분은 부모 클래스(Exception)의 초기화 메서드를 호출하여 메시지를 설정합니다.
이렇게 하면 print(e)를 했을 때 우리가 만든 친절한 메시지가 출력됩니다. f-string을 사용하여 잔액과 필요 금액을 포함한 구체적인 메시지를 만듭니다.
마지막으로, except InsufficientBalanceError as e: 부분에서 이 커스텀 예외만 특별히 처리할 수 있습니다. e.balance와 e.required 속성을 사용하여 부족한 금액을 계산하고, 사용자에게 정확한 정보를 제공할 수 있습니다.
다른 종류의 예외(네트워크 오류 등)와 명확히 구분되므로, 각각에 맞는 처리를 할 수 있습니다. 여러분이 이 코드를 사용하면 코드의 의도가 매우 명확해집니다.
"잔액 부족"이라는 것이 예외 이름만 봐도 바로 이해되고, 필요한 정보도 함께 담을 수 있어서 오류 처리가 훨씬 정교해집니다. 대규모 프로젝트에서 팀원들과 협업할 때도 도메인 용어를 코드에 직접 반영할 수 있어서 소통이 원활해집니다.
실전 팁
💡 커스텀 예외 이름은 항상 Error나 Exception으로 끝나도록 하세요. InsufficientBalance보다는 InsufficientBalanceError가 명확합니다.
💡 여러 관련 예외가 있다면 공통 부모 예외를 만들어 계층 구조를 만드세요. BankError를 만들고, 이를 상속받는 InsufficientBalanceError, DailyLimitExceededError 등을 만들 수 있습니다.
💡 예외에 타임스탬프, 사용자 ID, 트랜잭션 ID 같은 디버깅 정보를 추가하면 로그 분석이 쉬워집니다.
💡 str 메서드를 오버라이드하면 예외가 출력될 때의 형식을 완전히 커스터마이징할 수 있습니다.
💡 너무 많은 커스텀 예외를 만들지 마세요. 정말 구분할 필요가 있는 경우에만 만들어야 코드가 복잡해지지 않습니다.
6. 파일_한_줄씩_읽기와_메모리_효율성
시작하며
여러분이 몇 GB 크기의 로그 파일을 분석해야 하는데, read()로 전체를 읽으려고 하면 컴퓨터 메모리가 부족해서 프로그램이 멈춰버리는 상황을 생각해보세요. 이런 문제는 실제 개발 현장에서 대용량 데이터를 다룰 때 반드시 마주치는 상황입니다.
서버 로그, 데이터베이스 덤프, 대용량 CSV 파일 등을 처리할 때 전체를 메모리에 올리면 시스템이 다운될 수 있습니다. 메모리 효율적인 방법이 필수입니다.
바로 이럴 때 필요한 것이 파일을 한 줄씩 읽는 방법입니다. for 문이나 readline()을 사용하면 파일 크기와 관계없이 안정적으로 처리할 수 있습니다.
개요
간단히 말해서, 파일을 한 줄씩 읽으면 전체 파일을 메모리에 올리지 않고도 처리할 수 있어서 메모리를 절약할 수 있습니다. 실무에서 대용량 파일을 다룰 때 필수적인 기법입니다.
예를 들어, 웹 서버의 액세스 로그를 분석하거나, 수백만 개의 사용자 데이터를 처리하거나, 스트리밍 데이터를 실시간으로 처리할 때 사용합니다. 100MB 파일이든 10GB 파일이든 같은 양의 메모리만 사용하여 처리할 수 있습니다.
기존에는 file.read()로 전체를 읽어서 메모리 문제를 겪었다면, 이제는 for 문으로 한 줄씩 읽어서 필요한 만큼만 메모리를 사용할 수 있습니다. 파일 객체는 이터레이터이므로 for 문에서 직접 사용할 수 있고, readline() 메서드로 수동으로 한 줄씩 읽을 수도 있습니다.
각 줄은 문자열로 반환되며, 줄바꿈 문자(\n)도 포함됩니다. 이러한 특징들이 메모리 효율적인 파일 처리의 핵심입니다.
코드 예제
# 대용량 파일을 메모리 효율적으로 처리합니다
# 먼저 테스트용 파일을 만듭니다
with open('large_log.txt', 'w', encoding='utf-8') as file:
for i in range(100):
file.write(f'2025-01-{i%28+1:02d} ERROR: 오류 발생 #{i}\n')
# 파일을 한 줄씩 읽어서 처리합니다
error_count = 0
with open('large_log.txt', 'r', encoding='utf-8') as file:
for line in file: # 한 줄씩 자동으로 읽습니다
# 줄바꿈 문자를 제거합니다
line = line.strip()
# 특정 조건을 만족하는 줄만 처리합니다
if 'ERROR' in line:
error_count += 1
print(f'발견: {line}')
print(f'\n총 {error_count}개의 에러를 찾았습니다.')
설명
이것이 하는 일: 파일 전체를 메모리에 올리지 않고, 한 번에 한 줄씩만 읽어서 처리하므로 대용량 파일도 안정적으로 다룰 수 있습니다. 첫 번째로, with open() as file 부분은 파일을 여는데, 여기서 중요한 점은 파일 전체를 읽지 않는다는 것입니다.
파일을 여는 것과 읽는 것은 별개의 작업입니다. 마치 책을 펼쳐놓기만 하고 아직 읽지 않은 상태와 같습니다.
그 다음으로, for line in file: 부분이 핵심입니다. Python은 파일 객체를 한 줄씩 순회할 수 있도록 만들어져 있습니다.
매번 반복할 때마다 다음 한 줄만 메모리에 올리고, 처리가 끝나면 그 줄은 버려집니다. 100GB 파일이라도 한 번에 한 줄씩만 메모리에 있으므로 몇 KB 정도만 사용합니다.
세 번째로, line.strip()은 줄 끝의 공백과 줄바꿈 문자(\n)를 제거합니다. 파일에서 읽은 줄에는 항상 \n이 포함되어 있어서, 이것을 제거하지 않으면 출력이나 비교할 때 의도와 다르게 동작할 수 있습니다.
마지막으로, if 문으로 필요한 줄만 처리합니다. 모든 줄을 저장하지 않고 조건에 맞는 것만 카운트하거나 출력하므로, 메모리를 최소한으로 사용합니다.
만약 1억 줄 중에 1만 줄만 필요하다면, 1만 줄만 처리하고 나머지는 그냥 건너뛰는 것입니다. 여러분이 이 코드를 사용하면 파일 크기에 구애받지 않고 안정적으로 데이터를 처리할 수 있습니다.
서버에서 실행할 때도 메모리 부족으로 인한 다운타임이 없고, 여러 파일을 동시에 처리해도 안전합니다. 스트리밍 방식으로 처리하므로 실시간 로그 분석에도 적합합니다.
실전 팁
💡 readline()보다는 for 문을 사용하세요. 더 간결하고 Pythonic하며, 자동으로 파일 끝을 감지합니다.
💡 strip()을 사용하여 불필요한 공백과 줄바꿈을 제거하세요. 문자열 비교나 파싱할 때 문제를 방지할 수 있습니다.
💡 특정 개수만 읽으려면 itertools.islice(file, 10)처럼 사용하면 처음 10줄만 읽을 수 있습니다.
💡 파일을 역순으로 읽어야 한다면 collections.deque를 사용하거나, Unix의 경우 tac 명령어를 subprocess로 실행하는 것이 효율적입니다.
💡 CSV 파일은 csv 모듈을 사용하면 자동으로 한 줄씩 읽으면서 파싱해줍니다. 직접 split(',')을 쓰는 것보다 안전합니다.
7. JSON_파일_다루기와_데이터_직렬화
시작하며
여러분이 사용자 설정이나 애플리케이션 데이터를 파일에 저장하고 싶은데, 딕셔너리나 리스트 같은 복잡한 데이터 구조를 어떻게 저장해야 할지 고민해본 적 있나요? 문자열로 직접 변환하면 나중에 다시 읽을 때 복잡해집니다.
이런 문제는 실제 개발 현장에서 데이터를 영구 저장할 때 항상 마주치는 상황입니다. 설정 파일, API 응답 저장, 캐시 데이터, 로그 구조화 등 거의 모든 곳에서 필요합니다.
데이터 구조를 유지하면서 파일로 저장하고 다시 불러올 수 있는 방법이 필요합니다. 바로 이럴 때 필요한 것이 JSON(JavaScript Object Notation) 형식입니다.
json 모듈을 사용하면 Python 객체를 파일에 저장하고 다시 불러올 수 있습니다.
개요
간단히 말해서, JSON은 데이터를 텍스트 형식으로 표현하는 방법이고, json 모듈은 Python 객체를 JSON으로, JSON을 Python 객체로 변환해줍니다. 실무에서 거의 모든 데이터 저장과 전송에 사용되는 표준 형식입니다.
예를 들어, 웹 API는 대부분 JSON으로 데이터를 주고받고, 설정 파일도 JSON으로 저장하며, NoSQL 데이터베이스도 JSON과 유사한 형식을 사용합니다. Python의 딕셔너리, 리스트, 문자열, 숫자, 불리언을 모두 JSON으로 저장할 수 있습니다.
기존에는 복잡한 데이터를 파일에 저장하려면 직접 문자열로 변환하고 파싱해야 했다면, 이제는 json.dump()와 json.load() 두 줄이면 끝납니다. json.dump()는 Python 객체를 JSON 파일로 저장하고, json.load()는 JSON 파일을 Python 객체로 읽어옵니다.
들여쓰기(indent)를 지정하면 사람이 읽기 쉬운 형식으로 저장할 수 있습니다. 이러한 특징들이 데이터를 간편하고 안전하게 관리하는 핵심입니다.
코드 예제
import json
# Python 객체를 준비합니다
user_data = {
'name': '김철수',
'age': 25,
'email': 'chulsoo@example.com',
'interests': ['Python', 'Machine Learning', 'Data Science'],
'is_active': True,
'scores': {'math': 95, 'english': 88, 'science': 92}
}
# JSON 파일로 저장합니다
with open('user_data.json', 'w', encoding='utf-8') as file:
# ensure_ascii=False로 한글을 제대로 저장합니다
json.dump(user_data, file, indent=2, ensure_ascii=False)
# JSON 파일에서 읽어옵니다
with open('user_data.json', 'r', encoding='utf-8') as file:
loaded_data = json.load(file)
print(f"이름: {loaded_data['name']}")
print(f"관심사: {', '.join(loaded_data['interests'])}")
설명
이것이 하는 일: Python의 딕셔너리, 리스트 등의 데이터 구조를 JSON 형식의 텍스트 파일로 저장하고, 다시 원래 구조로 복원합니다. 첫 번째로, user_data라는 딕셔너리를 만듭니다.
이 딕셔너리는 중첩된 리스트와 딕셔너리를 포함하고 있어서 복잡한 구조입니다. 이런 복잡한 데이터를 직접 파일에 저장하려면 매우 번거롭지만, JSON을 사용하면 간단합니다.
그 다음으로, json.dump(user_data, file, indent=2, ensure_ascii=False) 부분이 실행되면 Python 객체를 JSON 형식으로 변환하여 파일에 씁니다. indent=2는 들여쓰기를 2칸으로 하여 사람이 읽기 쉽게 만들고, ensure_ascii=False는 한글을 유니코드 이스케이프(\uXXXX) 없이 그대로 저장합니다.
이렇게 하면 파일을 열었을 때 "김철수"가 그대로 보입니다. 세 번째로, json.load(file) 부분은 JSON 파일을 읽어서 Python 객체로 변환합니다.
마법처럼 원래의 딕셔너리가 그대로 복원됩니다. loaded_data['name']처럼 원래대로 사용할 수 있고, 타입도 완벽히 유지됩니다.
숫자는 int로, 불리언은 bool로, 리스트는 list로 정확히 복원됩니다. 마지막으로, JSON은 텍스트 형식이므로 다른 프로그래밍 언어(JavaScript, Java, Go 등)에서도 읽을 수 있습니다.
프로젝트 간 데이터 교환이나 API 응답 저장에 완벽합니다. 여러분이 이 코드를 사용하면 복잡한 데이터를 쉽게 저장하고 불러올 수 있습니다.
설정 파일, 캐시, 사용자 데이터 등을 안전하게 관리할 수 있고, 버전 관리 시스템(Git)에 저장하기도 좋습니다. 텍스트 형식이므로 diff를 확인할 수 있고, 필요하면 손으로 직접 수정할 수도 있습니다.
실전 팁
💡 한글을 저장할 때는 반드시 ensure_ascii=False를 사용하세요. 그렇지 않으면 "\uXXXX" 형식으로 저장되어 읽기 어렵습니다.
💡 indent 매개변수로 들여쓰기를 지정하면 파일이 보기 좋지만, 파일 크기가 커집니다. 프로덕션에서는 indent 없이 압축해서 저장하세요.
💡 JSON은 datetime 객체를 직접 저장할 수 없습니다. 문자열로 변환하거나 커스텀 인코더를 만들어야 합니다.
💡 json.dumps()와 json.loads()는 파일 대신 문자열로 직접 변환합니다. API 응답을 다룰 때 유용합니다.
💡 대용량 JSON은 ijson 같은 스트리밍 라이브러리를 사용하면 메모리 효율적으로 처리할 수 있습니다.
8. with_문과_컨텍스트_매니저
시작하며
여러분이 데이터베이스 연결을 열었는데 오류가 발생해서 연결을 닫지 못하고 프로그램이 종료되면, 연결이 계속 열려있어서 나중에 "연결 수 초과" 오류가 발생할 수 있습니다. 이런 문제는 실제 개발 현장에서 리소스를 다룰 때 자주 발생합니다.
파일, 데이터베이스 연결, 네트워크 소켓, 락(lock) 등은 사용 후 반드시 해제해야 하는데, 중간에 오류가 발생하면 해제 코드가 실행되지 않을 수 있습니다. 이렇게 리소스가 누수되면 시스템이 불안정해집니다.
바로 이럴 때 필요한 것이 with 문과 컨텍스트 매니저입니다. with 문을 사용하면 오류가 발생해도 자동으로 리소스를 정리해줍니다.
개요
간단히 말해서, 컨텍스트 매니저는 리소스를 자동으로 관리해주는 객체이고, with 문은 이것을 사용하는 문법입니다. 실무에서 안전한 리소스 관리를 위해 필수적입니다.
예를 들어, 파일을 다룰 때, 데이터베이스에 연결할 때, 스레드 락을 사용할 때, 임시 디렉토리를 만들 때 등 "열고-사용하고-닫는" 패턴이 필요한 모든 곳에서 사용합니다. with 문을 사용하면 절대로 리소스를 닫는 것을 잊어버릴 일이 없습니다.
기존에는 try-finally를 사용하여 수동으로 리소스를 관리해야 했다면, 이제는 with 문 하나로 자동으로 관리할 수 있습니다. with 문은 __enter__와 exit 메서드를 가진 객체와 함께 사용합니다.
__enter__는 with 블록에 들어갈 때, __exit__는 나올 때 자동으로 호출됩니다. 오류가 발생해도 __exit__는 반드시 실행됩니다.
이러한 특징들이 안전하고 깔끔한 코드를 만드는 핵심입니다.
코드 예제
# 전통적인 방법: 수동으로 파일을 닫아야 합니다
file = open('data.txt', 'w')
try:
file.write('데이터')
finally:
file.close() # 이것을 잊으면 문제 발생!
# with 문 사용: 자동으로 닫힙니다
with open('data.txt', 'w', encoding='utf-8') as file:
file.write('데이터')
# 여기서 자동으로 파일이 닫힙니다
# 여러 리소스를 동시에 관리할 수 있습니다
with open('input.txt', 'r', encoding='utf-8') as infile, \
open('output.txt', 'w', encoding='utf-8') as outfile:
for line in infile:
# 각 줄을 대문자로 변환하여 저장합니다
outfile.write(line.upper())
# 두 파일 모두 자동으로 닫힙니다
설명
이것이 하는 일: 리소스를 사용하는 블록의 시작과 끝에서 자동으로 초기화와 정리 작업을 수행하여, 리소스 누수를 방지합니다. 첫 번째로, with open('data.txt', 'w') as file: 부분은 파일을 열고 file 변수에 할당합니다.
이때 내부적으로 파일 객체의 enter 메서드가 호출됩니다. 마치 문을 열고 들어가는 것과 같습니다.
그 다음으로, with 블록 안의 코드가 실행됩니다. 여기서는 파일에 데이터를 쓰는데, 만약 오류가 발생하더라도 문제없습니다.
Python은 무슨 일이 있어도 다음 단계를 실행하기 때문입니다. 세 번째로, with 블록이 끝나면(정상 종료든 오류든 상관없이) 자동으로 exit 메서드가 호출됩니다.
파일의 경우 이 메서드에서 close()를 호출하여 파일을 닫습니다. 이것은 finally 블록처럼 반드시 실행되므로, 절대로 파일을 닫는 것을 잊어버릴 수 없습니다.
마지막으로, 여러 리소스를 쉼표로 연결하여 동시에 관리할 수 있습니다. with open(...) as f1, open(...) as f2: 형식으로 쓰면 두 파일 모두 자동으로 관리되며, 순서대로 열리고 역순으로 닫힙니다.
여러분이 이 코드를 사용하면 리소스 관리 코드가 훨씬 간결해지고 안전해집니다. 파일, 데이터베이스, 락 등 어떤 리소스든 with 문으로 감싸면 누수 걱정이 없습니다.
코드 리뷰에서도 "파일을 닫았나요?" 같은 질문을 받지 않게 되고, 프로덕션 환경에서 리소스 고갈로 인한 장애를 예방할 수 있습니다.
실전 팁
💡 항상 파일을 다룰 때는 with 문을 사용하세요. open()을 직접 쓰고 close()를 호출하는 것은 권장하지 않습니다.
💡 데이터베이스 연결(SQLite, PostgreSQL 등)도 with 문을 지원하므로 적극 활용하세요.
💡 contextlib.contextmanager 데코레이터를 사용하면 제너레이터 함수로 간단히 컨텍스트 매니저를 만들 수 있습니다.
💡 with 문은 중첩해서 사용할 수 있지만, 너무 깊어지면 가독성이 떨어지므로 2-3단계 이내로 유지하세요.
💡 커스텀 클래스에 __enter__와 __exit__를 구현하면 with 문으로 사용할 수 있습니다. 타이머, 락, 임시 환경 등에 유용합니다.
9. 여러_예외를_한번에_처리하기
시작하며
여러분이 네트워크 요청을 하는 코드를 작성했는데, 연결 실패, 타임아웃, DNS 오류 등 여러 종류의 네트워크 오류가 발생할 수 있고, 이들을 모두 같은 방식으로 처리하고 싶다면 어떻게 해야 할까요? 이런 문제는 실제 개발 현장에서 관련된 여러 오류를 동일하게 처리해야 할 때 자주 발생합니다.
각 오류마다 except 블록을 만들면 코드가 길어지고 중복이 생깁니다. 비슷한 오류들을 그룹으로 묶어서 처리하는 방법이 필요합니다.
바로 이럴 때 필요한 것이 여러 예외를 튜플로 묶어서 한 번에 처리하는 기법입니다.
개요
간단히 말해서, except 뒤에 여러 예외 타입을 튜플로 묶으면, 그 중 하나라도 발생했을 때 같은 처리를 할 수 있습니다. 실무에서 관련된 오류들을 그룹화하여 처리할 때 매우 유용합니다.
예를 들어, 파일 관련 오류(FileNotFoundError, PermissionError, IsADirectoryError)를 모두 "파일 접근 실패"로 처리하거나, 네트워크 관련 오류를 모두 "연결 실패"로 처리할 수 있습니다. 코드 중복을 줄이고 유지보수를 쉽게 만듭니다.
기존에는 각 예외마다 except 블록을 만들어야 했다면, 이제는 튜플로 묶어서 한 번에 처리할 수 있습니다. except (Exception1, Exception2, Exception3) as e: 형식으로 작성하며, 괄호 안의 예외 중 하나라도 발생하면 해당 블록이 실행됩니다.
as e를 사용하면 실제로 발생한 예외 객체를 받을 수 있어서, 구체적인 오류 정보도 확인할 수 있습니다. 이러한 특징들이 간결하고 유지보수하기 쉬운 오류 처리 코드를 만드는 핵심입니다.
코드 예제
import os
# 파일 작업에서 발생할 수 있는 여러 오류를 처리합니다
def safe_read_file(filename):
try:
with open(filename, 'r', encoding='utf-8') as file:
return file.read()
except (FileNotFoundError, PermissionError, IsADirectoryError) as e:
# 파일 관련 오류들을 한 번에 처리합니다
print(f'파일 접근 오류: {type(e).__name__}')
print(f'상세 내용: {e}')
return None
except (UnicodeDecodeError, UnicodeError) as e:
# 인코딩 관련 오류들을 별도로 처리합니다
print(f'파일 인코딩 오류: {e}')
print('다른 인코딩을 시도해보세요.')
return None
# 테스트
result = safe_read_file('존재하지않는파일.txt')
if result:
print(result)
else:
print('파일을 읽을 수 없습니다.')
설명
이것이 하는 일: 서로 관련된 여러 종류의 예외를 하나의 블록에서 동일한 방식으로 처리하여 코드를 간결하게 만듭니다. 첫 번째로, except (FileNotFoundError, PermissionError, IsADirectoryError) as e: 부분은 세 가지 파일 관련 오류를 하나로 묶습니다.
파일이 없거나, 권한이 없거나, 디렉토리를 파일로 열려고 하는 등의 오류가 발생하면 모두 같은 블록이 실행됩니다. 튜플로 묶어야 하므로 반드시 괄호를 사용해야 합니다.
그 다음으로, as e 부분을 통해 실제로 발생한 예외 객체를 e 변수로 받습니다. 여러 예외를 묶어서 처리하더라도, type(e).__name__을 사용하면 정확히 어떤 예외가 발생했는지 확인할 수 있습니다.
FileNotFoundError인지 PermissionError인지 구분할 수 있어서 로그를 남기거나 디버깅할 때 유용합니다. 세 번째로, 다른 종류의 오류는 별도의 except 블록으로 처리할 수 있습니다.
예를 들어 인코딩 오류는 파일 접근 오류와는 다른 문제이므로, 다른 메시지를 보여주거나 다른 대응을 할 수 있습니다. 이렇게 오류를 논리적으로 그룹화하면 코드의 의도가 명확해집니다.
마지막으로, 함수가 None을 반환하면 호출하는 쪽에서 if result: 같은 방식으로 성공 여부를 확인할 수 있습니다. 이것은 Pythonic한 패턴으로, 오류가 발생해도 프로그램이 멈추지 않고 계속 진행할 수 있게 합니다.
여러분이 이 코드를 사용하면 비슷한 예외들을 효율적으로 처리할 수 있습니다. 코드 중복이 줄어들고, 오류 처리 로직이 명확해지며, 유지보수가 쉬워집니다.
새로운 관련 예외가 생기면 튜플에 추가하기만 하면 되므로 확장성도 좋습니다.
실전 팁
💡 논리적으로 관련된 예외만 묶으세요. 전혀 다른 종류의 예외를 묶으면 오히려 코드가 이해하기 어려워집니다.
💡 type(e).__name__을 사용하면 정확히 어떤 예외가 발생했는지 문자열로 얻을 수 있습니다. 로그에 기록할 때 유용합니다.
💡 상위 예외 클래스를 사용하면 더 간단합니다. OSError는 FileNotFoundError, PermissionError 등을 모두 포함합니다.
💡 너무 많은 예외를 한 번에 묶지 마세요. 3-4개 정도가 적당하며, 그 이상이면 상위 클래스를 사용하는 것을 고려하세요.
💡 except Exception:은 거의 모든 예외를 잡으므로, 정말 필요한 경우가 아니면 사용하지 마세요. KeyboardInterrupt까지 잡아버릴 수 있습니다.
10. 예외_체이닝과_원본_오류_보존
시작하며
여러분이 사용자 데이터를 파일에서 읽어 처리하는 함수를 만들었는데, 파일 읽기 오류가 발생했을 때 "사용자 데이터 로드 실패"라는 더 의미 있는 오류를 발생시키고 싶지만, 원래의 파일 오류 정보도 잃고 싶지 않다면 어떻게 해야 할까요? 이런 문제는 실제 개발 현장에서 저수준 오류를 고수준 오류로 변환할 때 자주 발생합니다.
데이터베이스 오류를 비즈니스 오류로, 네트워크 오류를 애플리케이션 오류로 변환하면서도 원본 오류 정보는 유지해야 디버깅이 쉽습니다. 바로 이럴 때 필요한 것이 예외 체이닝(Exception Chaining)입니다.
raise ... from 구문을 사용하면 새로운 예외를 발생시키면서도 원본 예외를 보존할 수 있습니다.
개요
간단히 말해서, 예외 체이닝은 새로운 예외를 발생시킬 때 원본 예외를 연결하여, 전체 오류 맥락을 유지하는 기법입니다. 실무에서 추상화 계층을 넘나들 때 필수적입니다.
예를 들어, 데이터베이스 레이어에서 발생한 ConnectionError를 서비스 레이어에서 UserDataLoadError로 변환하면서도, 원본 오류 정보를 유지하면 디버깅할 때 전체 호출 스택과 원인을 파악할 수 있습니다. API 개발, 라이브러리 제작, 복잡한 애플리케이션에서 특히 중요합니다.
기존에는 새로운 예외를 발생시키면 원본 정보가 사라졌다면, 이제는 from 키워드로 원본 예외를 보존할 수 있습니다. raise NewError(...) from original_error 형식으로 작성하며, cause 속성에 원본 예외가 저장됩니다.
Python은 자동으로 예외 체인을 출력하여 전체 오류 맥락을 보여줍니다. 이러한 특징들이 복잡한 시스템에서 효과적인 디버깅을 가능하게 합니다.
코드 예제
# 커스텀 예외를 정의합니다
class UserDataError(Exception):
"""사용자 데이터 처리 오류"""
pass
def load_user_data(filename):
"""사용자 데이터를 파일에서 로드합니다"""
try:
with open(filename, 'r', encoding='utf-8') as file:
data = file.read()
# 여기서 추가 처리를 한다고 가정
return data
except FileNotFoundError as e:
# 원본 오류를 보존하면서 새로운 오류를 발생시킵니다
raise UserDataError(
f'사용자 데이터를 로드할 수 없습니다: {filename}'
) from e
# 사용 예제
try:
data = load_user_data('user_profile.json')
except UserDataError as e:
print(f'오류 발생: {e}')
# 원본 오류 확인
if e.__cause__:
print(f'원인: {type(e.__cause__).__name__} - {e.__cause__}')
설명
이것이 하는 일: 저수준 오류를 고수준 오류로 변환하면서도 원본 오류 정보를 연결하여, 오류의 전체 맥락과 원인을 추적할 수 있게 합니다. 첫 번째로, try 블록에서 파일을 열려고 시도합니다.
이것은 저수준 작업으로, FileNotFoundError 같은 저수준 예외가 발생할 수 있습니다. 하지만 함수의 사용자 입장에서는 "파일이 없다"보다 "사용자 데이터를 로드할 수 없다"가 더 의미 있는 정보입니다.
그 다음으로, except FileNotFoundError as e: 부분에서 저수준 오류를 잡습니다. 여기서 중요한 것은 이 오류를 그냥 버리지 않고 다음 단계로 전달한다는 것입니다.
세 번째로, raise UserDataError(...) from e 부분이 핵심입니다. UserDataError라는 고수준 예외를 새로 발생시키는데, from e를 추가하여 원본 예외(FileNotFoundError)를 연결합니다.
이렇게 하면 Python이 자동으로 두 예외를 연결하여 cause 속성에 저장합니다. 마지막으로, 예외를 잡았을 때 e.__cause__를 확인하면 원본 예외에 접근할 수 있습니다.
Python의 기본 예외 출력도 자동으로 체인을 보여주므로, 터미널에서 "The above exception was the direct cause of the following exception:" 같은 메시지와 함께 전체 맥락을 볼 수 있습니다. 여러분이 이 코드를 사용하면 추상화 계층이 있는 애플리케이션에서 오류를 효과적으로 관리할 수 있습니다.
사용자에게는 의미 있는 고수준 오류를 보여주면서도, 개발자는 원본 오류를 통해 정확한 원인을 파악할 수 있습니다. 로그 시스템과 결합하면 프로덕션 환경에서 발생한 오류를 완벽히 추적할 수 있습니다.
실전 팁
💡 raise ... from None을 사용하면 의도적으로 원본 예외를 숨길 수 있습니다. 사용자에게 내부 구현을 노출하고 싶지 않을 때 유용합니다.
💡 __cause__와 __context__는 다릅니다. from을 사용하면 __cause__에 저장되고, 사용하지 않으면 __context__에 자동으로 저장됩니다.
💡 예외 체이닝은 여러 단계로 이어질 수 있습니다. A에서 B가 발생하고, B에서 C가 발생하면 전체 체인이 유지됩니다.
💡 로깅 시스템을 사용할 때는 logging.exception()이 자동으로 전체 예외 체인을 기록합니다.
💡 API를 만들 때는 내부 예외를 그대로 노출하지 말고, 적절한 고수준 예외로 변환하여 from으로 연결하세요.
댓글 (0)
함께 보면 좋은 카드 뉴스
데이터 증강과 정규화 완벽 가이드
머신러닝 모델의 성능을 극대화하는 핵심 기법인 데이터 증강과 정규화에 대해 알아봅니다. 실무에서 바로 활용할 수 있는 다양한 기법과 실전 예제를 통해 과적합을 방지하고 모델 성능을 향상시키는 방법을 배웁니다.
ResNet과 Skip Connection 완벽 가이드
딥러닝 모델이 깊어질수록 성능이 떨어지는 문제를 해결한 혁신적인 기법, ResNet과 Skip Connection을 초급자도 이해할 수 있도록 쉽게 설명합니다. 실제 구현 코드와 함께 배워보세요.
CNN 아키텍처 완벽 가이드 LeNet AlexNet VGGNet
컴퓨터 비전의 기초가 되는 세 가지 핵심 CNN 아키텍처를 배웁니다. 손글씨 인식부터 이미지 분류까지, 딥러닝의 발전 과정을 따라가며 각 모델의 구조와 특징을 실습 코드와 함께 이해합니다.
CNN 기초 Convolution과 Pooling 완벽 가이드
CNN의 핵심인 Convolution과 Pooling을 초급자도 쉽게 이해할 수 있도록 설명합니다. 이미지 인식의 원리부터 실제 코드 구현까지, 실무에서 바로 활용 가능한 내용을 담았습니다.
TensorFlow와 Keras 완벽 입문 가이드
머신러닝과 딥러닝의 세계로 들어가는 첫걸음! TensorFlow와 Keras 프레임워크를 처음 접하는 분들을 위한 친절한 가이드입니다. 실무에서 바로 활용할 수 있는 핵심 개념과 예제를 통해 AI 모델 개발의 기초를 탄탄히 다져보세요.