icon안동민 개발노트

예외 처리의 기본 개념


예외 처리의 필요성

 프로그램 실행 중에는 다양한 오류 상황이 발생할 수 있습니다.

 이러한 오류를 적절히 처리하지 않으면 프로그램이 비정상적으로 종료되거나 예기치 않은 동작을 할 수 있습니다.

 예외 처리는 이러한 오류 상황을 체계적으로 관리하고 대응할 수 있게 해주는 메커니즘입니다.

 예외 처리의 주요 장점

  1. 오류 처리 코드와 정상 실행 코드의 분리
  2. 오류의 효과적인 전파 및 집중화된 처리
  3. 오류 상황에 대한 체계적이고 일관된 대응 가능

C++의 예외 처리 기본 구문

 C++에서는 try, catch, throw 키워드를 사용하여 예외를 처리합니다.

try {
    // 예외가 발생할 수 있는 코드
    if (someErrorCondition) {
        throw SomeException("Error message");
    }
} catch (ExceptionType1& e1) {
    // ExceptionType1 타입의 예외 처리
} catch (ExceptionType2& e2) {
    // ExceptionType2 타입의 예외 처리
} catch (...) {
    // 모든 타입의 예외 처리
}

예외 발생시키기 (throw)

 throw 키워드를 사용하여 예외를 발생시킬 수 있습니다.

 어떤 타입의 객체도 예외로 던질 수 있지만, 일반적으로 예외 클래스의 객체를 사용합니다.

void validateAge(int age) {
    if (age < 0) {
        throw std::invalid_argument("Age cannot be negative");
    }
    if (age > 150) {
        throw std::out_of_range("Age is unrealistically high");
    }
}

표준 예외 클래스

 C++ 표준 라이브러리는 <stdexcept> 헤더에 여러 가지 기본 예외 클래스를 제공합니다.

 주요 표준 예외 클래스

  • std::exception : 모든 표준 예외의 기본 클래스
  • std::runtime_error : 실행 시간에 감지될 수 있는 오류
  • std::logic_error : 프로그램의 내부 논리적 오류
  • std::out_of_range : 범위를 벗어난 접근 시도
  • std::invalid_argument : 잘못된 인자 전달
  • std::bad_alloc : 메모리 할당 실패
예제
#include <iostream>
#include <stdexcept>
#include <vector>
 
int main() {
    std::vector<int> vec = {1, 2, 3};
    
    try {
        std::cout << vec.at(5) << std::endl;  // 범위를 벗어난 접근
    } catch (const std::out_of_range& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }
 
    return 0;
}

사용자 정의 예외 클래스

 특정 상황에 맞는 예외를 정의하기 위해 사용자 정의 예외 클래스를 만들 수 있습니다.

 일반적으로 std::exception을 상속받아 구현합니다.

class NetworkException : public std::exception {
private:
    std::string message;
public:
    NetworkException(const std::string& msg) : message(msg) {}
    const char* what() const noexcept override {
        return message.c_str();
    }
};
 
void connectToServer() {
    // 서버 연결 시도
    throw NetworkException("Failed to connect to server");
}

예외 재전파

 catch 블록에서 예외를 부분적으로 처리한 후, 다시 상위 호출자에게 전달해야 할 때 예외를 재전파할 수 있습니다.

void function1() {
    try {
        // 일부 코드
        throw std::runtime_error("Error in function1");
    } catch (const std::exception& e) {
        std::cerr << "Caught in function1: " << e.what() << std::endl;
        throw;  // 예외 재전파
    }
}
 
void function2() {
    try {
        function1();
    } catch (const std::exception& e) {
        std::cerr << "Caught in function2: " << e.what() << std::endl;
    }
}

예외와 소멸자

 소멸자에서 예외가 발생하면 프로그램이 즉시 종료됩니다.

 따라서 소멸자는 예외를 던지지 않아야 합니다.

class Resource {
public:
    ~Resource() noexcept {
        try {
            // 리소스 정리 작업
        } catch (...) {
            // 예외 처리 (로깅 등)
        }
    }
};

함수 try 블록

 생성자의 초기화 리스트에서 발생하는 예외를 잡기 위해 함수 try 블록을 사용할 수 있습니다.

class MyClass {
    int* ptr;
public:
    MyClass(int size) try : ptr(new int[size]) {
        // 생성자 본문
    } catch (const std::bad_alloc& e) {
        std::cerr << "Memory allocation failed: " << e.what() << std::endl;
        throw;  // 예외 재전파
    }
};

noexcept 지정자

 함수가 예외를 던지지 않음을 명시하기 위해 noexcept 지정자를 사용할 수 있습니다.

void safeFunction() noexcept {
    // 이 함수는 예외를 던지지 않음을 보장
}
 
void potentiallyUnsafeFunction() noexcept(false) {
    // 이 함수는 예외를 던질 수 있음
}

예외 처리의 성능 고려사항

 예외 처리 메커니즘은 오류가 발생하지 않을 때는 거의 성능 저하를 일으키지 않습니다.

 그러나 예외가 실제로 발생하면 스택 풀기(stack unwinding) 과정 때문에 상당한 오버헤드가 발생할 수 있습니다.

실습 : 간단한 계산기 프로그램

 다음 요구사항을 만족하는 간단한 계산기 프로그램을 구현해보세요.

  1. DivisionByZeroExceptionInvalidInputException 예외 클래스 정의
  2. 사칙연산 함수 구현 (덧셈, 뺄셈, 곱셈, 나눗셈)
  3. 0으로 나누기 시도 시 DivisionByZeroException 발생
  4. 잘못된 입력 시 InvalidInputException 발생
#include <iostream>
#include <stdexcept>
#include <string>
 
class DivisionByZeroException : public std::runtime_error {
public:
    DivisionByZeroException() : std::runtime_error("Division by zero") {}
};
 
class InvalidInputException : public std::runtime_error {
public:
    InvalidInputException(const std::string& msg) : std::runtime_error(msg) {}
};
 
class Calculator {
public:
    static double add(double a, double b) { return a + b; }
    static double subtract(double a, double b) { return a - b; }
    static double multiply(double a, double b) { return a * b; }
    static double divide(double a, double b) {
        if (b == 0) {
            throw DivisionByZeroException();
        }
        return a / b;
    }
};
 
int main() {
    double a, b;
    char operation;
 
    try {
        std::cout << "Enter first number: ";
        if (!(std::cin >> a)) {
            throw InvalidInputException("Invalid first number");
        }
 
        std::cout << "Enter operation (+, -, *, /): ";
        if (!(std::cin >> operation)) {
            throw InvalidInputException("Invalid operation");
        }
 
        std::cout << "Enter second number: ";
        if (!(std::cin >> b)) {
            throw InvalidInputException("Invalid second number");
        }
 
        double result;
        switch (operation) {
            case '+': result = Calculator::add(a, b); break;
            case '-': result = Calculator::subtract(a, b); break;
            case '*': result = Calculator::multiply(a, b); break;
            case '/': result = Calculator::divide(a, b); break;
            default: throw InvalidInputException("Unknown operation");
        }
 
        std::cout << "Result: " << result << std::endl;
    } catch (const DivisionByZeroException& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    } catch (const InvalidInputException& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "Unexpected error: " << e.what() << std::endl;
    }
 
    return 0;
}

연습 문제

  1. std::vector를 사용하여 간단한 스택 클래스를 구현하세요. 스택이 비어있을 때 pop 연산을 시도하면 사용자 정의 예외를 던지도록 하세요.
  2. 파일 입출력을 수행하는 함수를 작성하고, 파일을 열 수 없거나 읽기 / 쓰기 오류가 발생할 때 적절한 예외를 던지도록 구현하세요.

 참고자료