💻 프로그래밍

Exception

예외

프로그램 실행 중 발생하는 오류. try-catch로 처리. 예외 처리는 안정성의 핵심.

📖 상세 설명

Exception(예외)은 프로그램 실행 중 발생하는 예기치 않은 상황이나 오류를 나타내는 객체입니다. 일반적인 프로그램 흐름을 벗어나는 비정상적인 조건이 발생했을 때 예외가 throw(던져)되며, 이를 적절히 처리하지 않으면 프로그램이 비정상 종료됩니다.

예외 처리는 try-catch-finally 구문을 통해 이루어집니다. try 블록에서 예외가 발생할 수 있는 코드를 실행하고, catch 블록에서 발생한 예외를 잡아 처리하며, finally 블록에서는 예외 발생 여부와 관계없이 항상 실행되어야 하는 정리 코드를 작성합니다.

대부분의 프로그래밍 언어는 계층적 예외 클래스 구조를 제공합니다. Java에서는 Throwable을 최상위로 Error와 Exception이 있고, Python에서는 BaseException 아래 Exception이 있습니다. 개발자는 이러한 기본 클래스를 상속받아 커스텀 예외를 정의하여 도메인 특화 오류를 표현할 수 있습니다.

실무에서 예외 처리는 시스템 안정성과 사용자 경험의 핵심입니다. 적절한 예외 처리를 통해 오류 상황에서도 프로그램이 graceful하게 동작하고, 의미 있는 에러 메시지를 사용자에게 전달하며, 디버깅에 필요한 정보를 로깅할 수 있습니다.

💻 코드 예제

# Python 예외 처리 예제

# 1. 기본 try-catch-finally 구문
def divide(a, b):
    try:
        result = a / b
        return result
    except ZeroDivisionError as e:
        print(f"오류: 0으로 나눌 수 없습니다 - {e}")
        return None
    except TypeError as e:
        print(f"오류: 잘못된 타입입니다 - {e}")
        return None
    finally:
        print("연산 시도 완료")

print(divide(10, 2))   # 출력: 연산 시도 완료 → 5.0
print(divide(10, 0))   # 출력: 오류: 0으로 나눌 수 없습니다 → None

# 2. 커스텀 예외 클래스 정의
class ValidationError(Exception):
    """유효성 검사 실패 시 발생하는 커스텀 예외"""
    def __init__(self, field, message):
        self.field = field
        self.message = message
        super().__init__(f"{field}: {message}")

class User:
    def __init__(self, email, age):
        self.email = self.validate_email(email)
        self.age = self.validate_age(age)

    def validate_email(self, email):
        if '@' not in email:
            raise ValidationError('email', '유효한 이메일 형식이 아닙니다')
        return email

    def validate_age(self, age):
        if not isinstance(age, int) or age < 0 or age > 150:
            raise ValidationError('age', '나이는 0-150 사이의 정수여야 합니다')
        return age

# 사용 예시
try:
    user = User('invalid-email', 25)
except ValidationError as e:
    print(f"유효성 검사 실패: {e.field} - {e.message}")

# 3. 예외 체이닝과 컨텍스트 관리자
class DatabaseConnection:
    def __enter__(self):
        print("DB 연결 열기")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("DB 연결 닫기")
        return False  # 예외를 다시 발생시킴

    def query(self, sql):
        if "DROP" in sql.upper():
            raise PermissionError("DROP 명령어는 허용되지 않습니다")
        return f"쿼리 실행: {sql}"

# with 문으로 안전한 리소스 관리
try:
    with DatabaseConnection() as db:
        result = db.query("SELECT * FROM users")
        print(result)
        result = db.query("DROP TABLE users")  # 예외 발생
except PermissionError as e:
    print(f"권한 오류: {e}")
# 출력: DB 연결 열기 → 쿼리 실행... → 권한 오류... → DB 연결 닫기

🗣️ 실무에서 이렇게 말하세요

💬 회의에서
"외부 API 호출하는 부분에 예외 처리가 빠져있어서 타임아웃 시 서비스 전체가 멈췄습니다. try-catch로 감싸고 재시도 로직과 fallback 응답을 추가해야 합니다. 서킷 브레이커 패턴도 고려해볼까요?"
💬 면접에서
"Checked Exception과 Unchecked Exception의 차이는 컴파일 타임 검사 여부입니다. Checked Exception은 반드시 처리해야 하고, Unchecked는 RuntimeException을 상속받아 선택적으로 처리합니다. 일반적으로 복구 가능한 상황은 Checked, 프로그래밍 오류는 Unchecked를 사용합니다."
💬 코드 리뷰에서
"catch 블록에서 Exception을 너무 넓게 잡고 있고, 로깅도 없네요. 구체적인 예외 타입별로 분기하고, 최소한 에러 로그는 남겨주세요. 예외를 삼켜버리면 디버깅이 불가능합니다."

⚠️ 흔한 실수 & 주의사항

빈 catch 블록 (예외 삼키기)

catch 블록을 비워두면 예외가 발생해도 알 수 없습니다. 최소한 로깅을 하거나, 적절한 대체 동작을 수행하세요. 무시해야 하는 경우라도 주석으로 이유를 명시하세요.

너무 넓은 예외 타입으로 잡기

`catch (Exception e)`처럼 모든 예외를 한꺼번에 잡으면 예상치 못한 버그가 숨겨집니다. 처리할 수 있는 구체적인 예외 타입만 catch하고, 나머지는 상위로 전파하세요.

예외를 제어 흐름으로 사용

예외는 예외적인 상황을 위한 것입니다. 정상적인 분기 처리에 try-catch를 사용하면 성능 저하와 가독성 문제가 발생합니다. 조건문으로 미리 검사하세요.

finally에서 리소스 정리하기

파일, DB 연결, 네트워크 소켓 등은 finally 블록이나 try-with-resources(Java), with문(Python), defer(Go)를 사용해 반드시 정리하세요. 리소스 누수는 메모리 문제로 이어집니다.

🔗 관련 용어

📚 더 배우기