Skip to content
주제
Python
Python Try Except: 예외를 올바르게 처리하는 방법

Python Try Except: 예외를 올바르게 처리하는 방법

Updated on

여러분의 Python 스크립트는 파일을 읽고, 데이터를 파싱하고, API로 전송합니다. 여러분의 컴퓨터에서는 완벽하게 작동합니다. 그런데 파일 경로가 잘못되었거나, JSON이 잘못된 형식이거나, 네트워크가 다운된 서버에서 실행하면 프로그램이 트레이스백과 함께 충돌하고 전체 파이프라인이 멈춥니다. 이것이 바로 try/except가 해결하는 문제입니다. 오류가 프로그램을 종료시키는 대신, 오류를 잡아서 처리하고 계속 실행합니다.

📚

Python에서 예외란 무엇인가?

예외는 프로그램의 정상적인 흐름을 방해하는 이벤트입니다. Python이 수행할 수 없는 연산을 만나면 -- 0으로 나누기, 존재하지 않는 딕셔너리 키 접근, 존재하지 않는 파일 열기 -- 예외 객체를 생성하고 실행을 중단합니다. 아무것도 그 예외를 잡지 않으면 프로그램이 종료되고 트레이스백이 출력됩니다.

# This crashes the program
result = 10 / 0

출력:

Traceback (most recent call last):
  File "example.py", line 2, in <module>
    result = 10 / 0
ZeroDivisionError: division by zero

예외는 구문 오류와 다릅니다. 구문 오류는 Python이 코드를 전혀 파싱할 수 없다는 것을 의미합니다. 예외는 코드가 성공적으로 파싱된 후 실행 중에 발생합니다.

기본 Try/Except 문법

try/except 블록은 실패할 수 있는 코드를 시도하고 실패할 경우 어떻게 할지 정의할 수 있게 해줍니다.

try:
    result = 10 / 0
except ZeroDivisionError:
    print("Cannot divide by zero.")

출력:

Cannot divide by zero.

프로그램이 충돌하지 않습니다. Python은 try 안의 코드를 실행합니다. ZeroDivisionError가 발생하면 실행이 except 블록으로 점프합니다. try/except 이후의 모든 것이 정상적으로 계속됩니다.

예외 객체를 캡처하여 오류 메시지를 확인할 수도 있습니다:

try:
    result = 10 / 0
except ZeroDivisionError as e:
    print(f"Error: {e}")

출력:

Error: division by zero

특정 예외 잡기

Python에는 수십 가지의 내장 예외 타입이 있습니다. 올바른 것을 잡으면 코드가 정확해집니다. 가장 자주 처리하게 될 예외들입니다.

ValueError

함수가 올바른 타입이지만 부적절한 값의 인수를 받았을 때 발생합니다.

try:
    number = int("not_a_number")
except ValueError as e:
    print(f"Invalid value: {e}")

출력:

Invalid value: invalid literal for int() with base 10: 'not_a_number'

TypeError

잘못된 타입의 객체에 연산이 적용되었을 때 발생합니다.

try:
    result = "hello" + 42
except TypeError as e:
    print(f"Type error: {e}")

출력:

Type error: can only concatenate str (not "int") to str

FileNotFoundError

존재하지 않는 파일을 열려고 할 때 발생합니다.

try:
    with open("nonexistent_file.txt", "r") as f:
        content = f.read()
except FileNotFoundError:
    print("File not found. Check the file path.")

KeyError

존재하지 않는 딕셔너리 키에 접근할 때 발생합니다.

data = {"name": "Alice", "age": 30}
 
try:
    email = data["email"]
except KeyError as e:
    print(f"Missing key: {e}")

출력:

Missing key: 'email'

IndexError

범위를 벗어난 리스트 인덱스에 접근할 때 발생합니다.

items = [10, 20, 30]
 
try:
    value = items[5]
except IndexError:
    print("Index out of range.")

다중 Except 블록

서로 다른 예외 타입을 별도의 except 블록으로 처리할 수 있습니다. Python은 순서대로 확인하고 첫 번째로 일치하는 것을 실행합니다.

def parse_config(raw_value):
    try:
        parts = raw_value.split(":")
        key = parts[0]
        value = int(parts[1])
        return {key: value}
    except IndexError:
        print("Config format error: missing colon separator.")
    except ValueError:
        print("Config format error: value is not a number.")
    except AttributeError:
        print("Config format error: input is not a string.")
 
parse_config("timeout:30")    # Returns {'timeout': 30}
parse_config("timeout")       # Config format error: missing colon separator.
parse_config("timeout:abc")   # Config format error: value is not a number.
parse_config(12345)            # Config format error: input is not a string.

튜플을 사용하여 하나의 except 블록에서 여러 예외를 잡을 수도 있습니다:

try:
    value = int(input("Enter a number: "))
    result = 100 / value
except (ValueError, ZeroDivisionError) as e:
    print(f"Invalid input: {e}")

Else 블록

else 블록은 try 블록에서 예외가 발생하지 않았을 때만 실행됩니다. "정상 경로" 코드를 오류 처리 코드와 분리합니다.

try:
    number = int("42")
except ValueError:
    print("That is not a valid number.")
else:
    print(f"Successfully parsed: {number}")

출력:

Successfully parsed: 42

else 블록이 유용한 이유는 그 안에서 발생한 예외가 앞의 except 블록에 의해 잡히지 않기 때문입니다. 이는 성공 경로 코드에서 실수로 버그를 숨기는 것을 방지합니다.

filename = "data.txt"
 
try:
    f = open(filename, "r")
except FileNotFoundError:
    print(f"File '{filename}' does not exist.")
else:
    content = f.read()
    f.close()
    print(f"Read {len(content)} characters.")

open()이 실패하면 except 블록이 처리합니다. open()이 성공하면 else 블록이 파일을 읽습니다. f.read() 중에 발생하는 오류는 여기서 잡히지 않습니다 -- 상위로 전파되며, 이것이 올바른 동작입니다.

Finally 블록

finally 블록은 예외가 발생했든 안 했든 항상 실행됩니다. 정리 코드를 위한 올바른 장소입니다: 파일 닫기, 잠금 해제, 데이터베이스 연결 해제.

f = None
try:
    f = open("data.txt", "r")
    content = f.read()
except FileNotFoundError:
    print("File not found.")
finally:
    if f is not None:
        f.close()
        print("File handle closed.")

finally 블록은 try 블록이 값을 반환하거나 처리되지 않은 예외를 발생시켜도 실행됩니다.

def divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        return None
    finally:
        print("Division attempted.")
 
result = divide(10, 3)
# Output: Division attempted.
# result = 3.3333333333333335
 
result = divide(10, 0)
# Output: Division attempted.
# result = None

완전한 Try/Except/Else/Finally 패턴

네 가지 블록이 함께 작동하는 방법입니다:

import json
 
def load_config(filepath):
    """Load and parse a JSON config file."""
    f = None
    try:
        f = open(filepath, "r")
        data = json.load(f)
    except FileNotFoundError:
        print(f"Config file '{filepath}' not found.")
        return {}
    except json.JSONDecodeError as e:
        print(f"Invalid JSON in '{filepath}': {e}")
        return {}
    else:
        print(f"Config loaded successfully with {len(data)} keys.")
        return data
    finally:
        if f is not None:
            f.close()
            print("File handle released.")
 
config = load_config("settings.json")
블록실행 시점목적
try항상 (처음)예외를 발생시킬 수 있는 코드를 포함
except일치하는 예외가 발생했을 때만오류를 처리
elsetry에서 예외가 발생하지 않았을 때만성공 경로 로직 실행
finally항상 (마지막)반드시 실행되어야 하는 정리 코드

일반적인 내장 예외 참조

Python은 내장 예외의 계층 구조를 포함합니다. 가장 자주 마주치게 될 것들입니다.

예외발생 시점예시
Exception대부분의 예외의 기본 클래스모든 비시스템 종료 예외의 부모
ValueError올바른 타입에 잘못된 값int("abc")
TypeError연산에 잘못된 타입"text" + 5
KeyError딕셔너리 키 누락d["missing"]
IndexError리스트 인덱스 범위 초과[1,2,3][10]
FileNotFoundError파일이 존재하지 않음open("no.txt")
ZeroDivisionError0으로 나누기 또는 모듈로1 / 0
AttributeError객체에 해당 속성이 없음None.append(1)
ImportError모듈 가져오기 실패import nonexistent
OSErrorOS 수준 작업 실패디스크 가득 참, 권한 거부
StopIteration이터레이터에 더 이상 항목 없음소진된 이터레이터에서 next()
RuntimeError일반적인 런타임 오류기타 오류에 대한 포괄적 처리

이 모든 것은 BaseException에서 상속됩니다. 실제로는 Exception 또는 그 하위 클래스를 잡아야 하며, BaseException을 직접 잡으면 안 됩니다 (SystemExitKeyboardInterrupt를 포함하기 때문).

raise로 예외 발생시키기

코드가 유효하지 않은 조건을 감지했을 때 명시적으로 예외를 발생시킬 수 있습니다. 이것이 전제 조건을 강제하고 호출자에게 오류를 알리는 방법입니다.

def set_age(age):
    if not isinstance(age, int):
        raise TypeError(f"Age must be an integer, got {type(age).__name__}")
    if age < 0 or age > 150:
        raise ValueError(f"Age must be between 0 and 150, got {age}")
    return age
 
# Valid usage
print(set_age(25))  # 25
 
# Invalid usage
try:
    set_age(-5)
except ValueError as e:
    print(e)  # Age must be between 0 and 150, got -5
 
try:
    set_age("thirty")
except TypeError as e:
    print(e)  # Age must be an integer, got str

except 블록 안에서 인수 없이 raise를 사용하여 현재 예외를 다시 발생시킬 수도 있습니다. 오류를 로그에 기록한 후 전파시키고 싶을 때 유용합니다.

import logging
 
try:
    result = 10 / 0
except ZeroDivisionError:
    logging.error("Division by zero encountered")
    raise  # re-raises the original ZeroDivisionError

사용자 정의 예외 클래스

더 큰 프로젝트에서는 고유한 예외 클래스를 정의합니다. 사용자 정의 예외는 오류 처리를 더 읽기 쉽게 만들고 호출자가 특정 실패 모드를 잡을 수 있게 합니다.

class ValidationError(Exception):
    """Raised when input data fails validation."""
    def __init__(self, field, message):
        self.field = field
        self.message = message
        super().__init__(f"Validation failed on '{field}': {message}")
 
class DatabaseConnectionError(Exception):
    """Raised when the database connection fails."""
    pass
 
# Usage
def validate_email(email):
    if "@" not in email:
        raise ValidationError("email", "Must contain @ symbol")
    if "." not in email.split("@")[1]:
        raise ValidationError("email", "Domain must contain a dot")
    return email
 
try:
    validate_email("userexample.com")
except ValidationError as e:
    print(e)          # Validation failed on 'email': Must contain @ symbol
    print(e.field)    # email
    print(e.message)  # Must contain @ symbol

사용자 정의 예외는 BaseException이 아닌 Exception에서 상속해야 합니다. 관련 예외를 공통 기본 클래스 아래에 그룹화하여 호출자가 넓거나 좁은 범주를 잡을 수 있게 합니다:

class AppError(Exception):
    """Base exception for this application."""
    pass
 
class ConfigError(AppError):
    pass
 
class NetworkError(AppError):
    pass
 
# Caller can catch all app errors or specific ones
try:
    raise NetworkError("Connection timed out")
except AppError as e:
    print(f"Application error: {e}")

Python 예외 처리 모범 사례

1. 빈 예외를 절대 잡지 마세요

except:KeyboardInterruptSystemExit을 포함한 모든 것을 잡습니다. 이는 버그를 숨기고 디버깅을 악몽으로 만듭니다.

# BAD - catches everything, hides real bugs
try:
    do_something()
except:
    pass
 
# GOOD - catches specific exceptions
try:
    do_something()
except ValueError as e:
    logging.warning(f"Invalid value: {e}")

넓은 범위를 잡아야 한다면 대신 except Exception을 사용하세요. 이렇게 하면 KeyboardInterruptSystemExit은 여전히 전파됩니다.

2. Try 블록을 작게 유지하세요

예외를 발생시킬 수 있는 코드만 try 블록 안에 넣으세요. 큰 try 블록은 어떤 줄이 오류를 일으켰는지 불분명하게 만듭니다.

# BAD - too much code in try
try:
    data = load_data()
    cleaned = clean_data(data)
    result = analyze(cleaned)
    save_results(result)
except Exception as e:
    print(f"Something failed: {e}")
 
# GOOD - narrow try blocks
data = load_data()
cleaned = clean_data(data)
 
try:
    result = analyze(cleaned)
except ValueError as e:
    print(f"Analysis failed: {e}")
    result = default_result()
 
save_results(result)

3. 예외를 로그에 기록하고, 숨기지 마세요

pass로 예외를 삼키면 보이지 않는 버그가 생깁니다. 항상 오류를 로그에 기록하거나 보고하세요.

import logging
 
try:
    process_record(record)
except ValueError as e:
    logging.error(f"Failed to process record {record['id']}: {e}")

4. 구체적인 예외 타입을 사용하세요

가능한 한 가장 구체적인 예외 타입을 잡으세요. 이는 예상하지 못한 오류를 실수로 처리하는 것을 방지합니다.

접근 방식잡는 대상위험 수준
except:SystemExit을 포함한 모든 것매우 높음
except Exception:모든 표준 예외높음
except ValueError:ValueError만낮음
except (ValueError, TypeError):두 가지 특정 타입낮음

5. Finally에서 리소스를 정리하거나 컨텍스트 관리자를 사용하세요

파일 핸들, 데이터베이스 연결, 잠금에는 항상 finally 또는 (더 좋은 방법은) with 문을 사용하세요.

# Prefer context managers for resource cleanup
with open("data.txt", "r") as f:
    content = f.read()
# File is automatically closed, even if an exception occurs

실전 예제

JSON 파일 읽기 및 파싱

import json
 
def read_json_config(filepath):
    """Read a JSON configuration file with proper error handling."""
    try:
        with open(filepath, "r") as f:
            config = json.load(f)
    except FileNotFoundError:
        print(f"Config file not found: {filepath}")
        return None
    except PermissionError:
        print(f"No permission to read: {filepath}")
        return None
    except json.JSONDecodeError as e:
        print(f"Invalid JSON at line {e.lineno}, column {e.colno}: {e.msg}")
        return None
    else:
        print(f"Loaded config with keys: {list(config.keys())}")
        return config
 
config = read_json_config("app_config.json")
if config:
    db_host = config.get("database_host", "localhost")

HTTP API 호출하기

import urllib.request
import urllib.error
import json
 
def fetch_user(user_id):
    """Fetch user data from an API with retry logic."""
    url = f"https://jsonplaceholder.typicode.com/users/{user_id}"
    max_retries = 3
 
    for attempt in range(1, max_retries + 1):
        try:
            with urllib.request.urlopen(url, timeout=5) as response:
                data = json.loads(response.read().decode())
            return data
        except urllib.error.HTTPError as e:
            if e.code == 404:
                print(f"User {user_id} not found.")
                return None
            print(f"HTTP error {e.code} on attempt {attempt}")
        except urllib.error.URLError as e:
            print(f"Network error on attempt {attempt}: {e.reason}")
        except json.JSONDecodeError:
            print("API returned invalid JSON.")
            return None
 
    print(f"All {max_retries} attempts failed.")
    return None
 
user = fetch_user(1)
if user:
    print(f"Found user: {user['name']}")

CSV 데이터 처리

import csv
 
def process_sales_data(filepath):
    """Process a CSV file with robust error handling."""
    results = []
 
    try:
        with open(filepath, "r", newline="") as f:
            reader = csv.DictReader(f)
            for row_num, row in enumerate(reader, start=2):
                try:
                    amount = float(row["amount"])
                    quantity = int(row["quantity"])
                    results.append({
                        "product": row["product"],
                        "total": amount * quantity,
                    })
                except KeyError as e:
                    print(f"Row {row_num}: Missing column {e}")
                except ValueError as e:
                    print(f"Row {row_num}: Invalid number - {e}")
    except FileNotFoundError:
        print(f"File not found: {filepath}")
    except PermissionError:
        print(f"Cannot read file: {filepath}")
 
    return results

Try/Except vs If/Else: 각각 언제 사용할까

Python은 EAFP 원칙을 따릅니다: "Easier to Ask Forgiveness than Permission" (허락보다 용서를 구하는 것이 쉽다). 이는 LBYL 접근 방식: "Look Before You Leap" (뛰기 전에 살펴보라)과 대비됩니다.

# LBYL (Look Before You Leap) - using if/else
if "email" in user_data:
    email = user_data["email"]
else:
    email = "unknown"
 
# EAFP (Easier to Ask Forgiveness) - using try/except
try:
    email = user_data["email"]
except KeyError:
    email = "unknown"
기준if/else (LBYL)try/except (EAFP)
최적 상황검사가 저렴하고 오류가 흔할 때오류가 드물거나 검사가 비쌀 때
성능 (오류 없음)약간 느림 (매번 추가 검사)약간 빠름 (검사 오버헤드 없음)
성능 (오류 발생)동일느림 (예외 생성에 오버헤드)
경쟁 조건가능 (검사와 사용 사이에 상태 변경 가능)없음 (원자적 연산)
가독성단순한 조건에 명확여러 방식으로 실패할 수 있는 연산에 더 좋음
파일 연산if os.path.exists(path) -- 검사와 열기 사이에 파일이 삭제될 수 있음try: open(path) -- 실제 실패를 처리
딕셔너리 접근if key in dict -- 간단하고 빠름try: dict[key] -- 또는 dict.get(key, default) 사용

try/except를 사용할 때:

  • 실패 케이스가 드문 경우 (예외는 "오류 없음" 경로에 최적화되어 있음).
  • 검사 자체가 연산만큼 비싼 경우 (예: 파일 존재 여부 확인 후 열기).
  • 연산 시퀀스에서 여러 가지가 잘못될 수 있는 경우.
  • 경쟁 조건이 우려되는 경우 (특히 파일과 네트워크 리소스).

if/else를 사용할 때:

  • 조건 검사가 저렴하고 실패가 흔한 경우.
  • 처리 전에 사용자 입력을 검증하는 경우.
  • 로직이 조건문으로 더 명확하게 읽히는 경우.

RunCell로 예외를 더 빠르게 디버그하기

Jupyter 노트북에서 작업할 때 예외가 분석 흐름을 끊을 수 있습니다. DataFrame의 50,000번째 행에서 KeyError를 만나거나, 파이프라인 3셀 깊이에서 TypeError가 나타납니다. 근본 원인을 추적하려면 트레이스백을 스크롤하고 수동으로 변수를 검사해야 합니다.

RunCell (opens in a new tab)은 Jupyter 안에서 직접 실행되는 AI 에이전트입니다. 전체 트레이스백을 읽고, 현재 범위의 변수를 검사하고, 맥락에 맞는 수정을 제안합니다. 예외 처리에 어떻게 도움이 되는지 소개합니다:

  • 트레이스백 분석. RunCell은 예외 체인을 분석하고 중첩된 함수 호출에서도 어떤 변수나 연산이 실패를 일으켰는지 정확히 파악합니다.
  • 수정 제안. Stack Overflow를 검색하는 대신, RunCell은 즉시 실행할 수 있는 수정된 코드 셀을 생성합니다. try/except를 추가해야 하는지, 타입 변환을 수정해야 하는지, 누락된 키를 처리해야 하는지 알고 있습니다.
  • 예방 검사. RunCell은 코드를 스캔하고 예외를 발생시킬 가능성이 있는 연산 -- .get() 없이 딕셔너리 키 접근, 0 확인 없이 나누기 -- 을 셀 실행 전에 경고합니다.

RunCell은 기존 Jupyter 환경 안에서 작동하므로 실제 데이터와 변수에 접근할 수 있습니다. 제공하는 제안은 일반적인 조언이 아니라 여러분의 상황에 맞는 것입니다.

FAQ

Python에서 try/except와 try/catch의 차이점은 무엇인가요?

Python은 try/except를 사용하며 try/catch는 사용하지 않습니다. try/catch 문법은 Java, JavaScript, C++ 같은 언어에 속합니다. Python에서 키워드는 except입니다. 기능은 동일합니다: 실패할 수 있는 코드를 시도하고 실패 경우에 대한 핸들러를 정의합니다.

Python에서 여러 except 블록을 사용할 수 있나요?

네. 필요한 만큼 except 블록을 연결할 수 있으며, 각각 다른 예외 타입을 잡습니다. Python은 순서대로 평가하고 첫 번째로 일치하는 블록을 실행합니다. 튜플을 사용하여 하나의 블록에서 여러 예외를 잡을 수도 있습니다: except (ValueError, TypeError) as e:.

try/except에서 else를 언제 사용해야 하나요?

try 블록이 성공했을 때만 실행해야 하는 코드가 있을 때 else 블록을 사용합니다. 주요 이점은 else 블록에서 발생한 예외가 앞의 except 블록에 의해 잡히지 않아 관련 없는 오류를 실수로 숨기는 것을 방지한다는 것입니다.

Python에서 finally는 항상 실행되나요?

네. finally 블록은 try 블록이 정상적으로 완료되었든, 처리된 예외를 발생시켰든, 처리되지 않은 예외를 발생시켰든 실행됩니다. try 또는 except 블록에 return 문이 있어도 실행됩니다. 유일한 예외는 Python 프로세스가 외부에서 종료되거나 os._exit()가 호출된 경우입니다.

Python에서 사용자 정의 예외를 만드는 방법은?

Exception을 상속하는 새 클래스를 만듭니다. 사용자 정의 속성을 추가하고 __init__ 메서드를 오버라이드할 수 있습니다. 예: class MyError(Exception): pass. 더 복잡한 경우에는 오류 코드나 컨텍스트 데이터 같은 필드를 추가합니다. 항상 BaseException이 아닌 Exception에서 상속하세요.

결론

Python의 try/except는 런타임 오류를 처리하는 표준 메커니즘입니다. 특정 예외를 잡고, 정리 코드를 실행하고, 문제가 발생해도 프로그램을 안정적으로 유지할 수 있게 해줍니다. 완전한 패턴 -- try/except/else/finally -- 은 모든 시나리오를 커버합니다: 연산 시도, 실패 처리, 성공 로직 실행, 리소스 정리.

핵심 원칙은 간단합니다. 넓은 예외가 아닌 구체적인 예외를 잡으세요. try 블록을 작게 유지하세요. 오류를 숨기지 말고 항상 로그에 기록하거나 보고하세요. 정리에는 finally 또는 컨텍스트 관리자를 사용하세요. 자신의 코드에서 명확한 오류 메시지와 함께 의미 있는 예외를 발생시키세요.

파일 읽기, API 호출, 사용자 입력 처리 등 무엇을 하든 적절한 예외 처리는 새벽 2시에 충돌하는 스크립트와 오류를 로그에 기록하고 계속 실행하는 스크립트의 차이입니다. 기본부터 시작하세요 -- 실패할 수 있는 코드를 감싸는 간단한 try/except -- 그리고 프로젝트가 성장함에 따라 사용자 정의 예외 계층을 구축해 나가세요.

📚