[9장] 예외 처리: 안정적인 C++ 프로그래밍 구축하기

이 글에서는 C++에서 예외 처리의 중요성과 기법을 탐구합니다. 안정적이고 효율적인 코드 작성을 위한 실용적인 팁과 전략을 제공하며, 초보자부터 전문가까지 모든 수준의 개발자들이 유용하게 활용할 수 있는 내용을 담고 있습니다.

우리같이 예외처리하실래요?
윤성우의 열혈 C++ 프로그래밍, 오렌지미디어

1. 예외 처리란?

  • 정의: 프로그램 실행 중 발생할 수 있는 예상치 못한 문제(예외)에 대응하는 프로그래밍 기법
  • 중요성: 예외 처리를 통해 프로그램의 안정성과 신뢰성을 보장

예외 처리 기법

  • try-catch 블록
    • try 블록 내에서 코드 실행
    • 예외 발생 시 catch 블록으로 이동하여 예외 처리
  • throw 문
    • 예외 발생 시점에 throw를 사용하여 예외를 발생시킴
  • 사용자 정의 예외
    • 표준 예외 클래스를 상속받아 사용자 정의 예외 클래스 생성


“예외 처리: 안정적인 C++ 프로그래밍 구축하기”라는 주제로 자세한 내용을 작성하겠습니다. 이 글은 C++ 프로그래밍에서 예외 처리의 중요성과 그 구현 방법을 설명하며, 안정적인 프로그램을 만드는 데 필요한 핵심 코드를 포함합니다.

서론

C++20: 풍부한 예제로 익히는 핵심 기능, 인사이트
  • 목적: 안정적인 C++ 프로그램 개발의 중요성 강조
  • 배경: 예외 처리가 왜 중요한지, 어떻게 프로그램의 안정성과 유지보수성을 향상시키는지 설명

예외 처리란?

  • 정의: 프로그램 실행 중 발생할 수 있는 예상치 못한 문제(예외)에 대응하는 프로그래밍 기법
  • 중요성: 예외 처리를 통해 프로그램의 안정성과 신뢰성을 보장

예외 처리 기법

  • try-catch 블록
    • try 블록 내에서 코드 실행
    • 예외 발생 시 catch 블록으로 이동하여 예외 처리
  • throw 문
    • 예외 발생 시점에 throw를 사용하여 예외를 발생시킴
  • 사용자 정의 예외
    • 표준 예외 클래스를 상속받아 사용자 정의 예외 클래스 생성

핵심 코드 예시

  • 기본 예외 처리
try {
    // 예외 발생 가능 코드
} catch (std::exception& e) {
    // 예외 처리 코드
}
  • 사용자 정의 예외 클래스
class MyException : public std::exception {
public:
    const char* what() const throw () {
        return "MyException occurred";
    }
};

try {
    throw MyException();
} catch (MyException& e) {
    std::cout << e.what() << std::endl;
}

예외 처리 전략

초보자를 위한 C++ 200제:C++시작을위한최고의입문서! 설치부터문법배우고JSON응용까지레벨업!, 정보문화사
  • 리소스 관리: RAII(Resource Acquisition Is Initialization) 패턴을 활용하여 리소스 누수 방지
  • 예외 안전성: 함수가 예외를 발생시킬 때, 리소스 누수, 데이터 손상, 불일치 상태 방지
  • 테스트와 디버깅: 예외 발생 시나리오에 대한 철저한 테스트 및 디버깅 전략 수립

2. 예외 처리 심화

C++ 표준 라이브러리에서 제공하는 주요 예외 클래스

C++ 표준 라이브러리에서 제공하는 주요 예외 클래스들에 대한 자세한 설명과 함께 코드 예시를 제공하겠습니다.

1. std::exception

  • 설명: 모든 표준 예외 클래스의 기본 클래스입니다. 이 클래스는 가상 함수인 what()을 제공하여 예외에 대한 설명을 문자열로 반환합니다.
try {
    // 예외 발생 가능 코드
} catch (const std::exception& e) {
    std::cerr << "Standard exception: " << e.what() << std::endl;
}

2. std::logic_error

  • 설명: 프로그램 로직 오류를 나타내는 클래스입니다. 이 클래스는 std::exception을 상속받으며, 프로그래머의 실수로 인해 발생하는 오류들, 예를 들어 잘못된 인수 전달(std::invalid_argument)이나 정의되지 않은 도메인에서의 연산(std::domain_error) 등을 나타냅니다.
try {
    throw std::invalid_argument("Invalid argument provided");
} catch (const std::logic_error& e) {
    std::cerr << "Logic error: " << e.what() << std::endl;
}

3. std::runtime_error

  • 설명: 런타임 중 발생하는 오류를 나타내는 클래스입니다. std::exception을 상속받으며, 프로그램 실행 중에 발생할 수 있는 오류들을 포함합니다. 예를 들어, 범위를 벗어난 접근(std::out_of_range), 오버플로(std::overflow_error), 언더플로(std::underflow_error) 등이 있습니다.
try {
    throw std::out_of_range("Index out of range");
} catch (const std::runtime_error& e) {
    std::cerr << "Runtime error: " << e.what() << std::endl;
}
전문가를 위한 C++ : C++20 병렬 알고리즘 파일시스템 제네릭 람다 디자인 패턴 객체지향의 원리를 익히는 확실한 방법 개정판, 한빛미디어

이러한 예외 클래스들은 C++ 프로그래밍에서 안정적인 오류 처리를 위해 광범위하게 사용되며, 개발자들이 보다 견고하고 안정적인 소프트웨어를 개발하는 데 도움을 줍니다. 예외가 발생했을 때 이를 적절히 처리함으로써 프로그램의 안정성을 유지하고 예기치 않은 종료를 방지할 수 있습니다.

사용자 정의 예외

사용자 정의 예외는 C++의 표준 예외 처리 시스템을 확장하여 특정 애플리케이션 또는 프로젝트에 특화된 예외 상황을 처리하기 위해 사용됩니다. 이를 통해 표준 라이브러리의 예외 클래스에는 없는, 특정 상황에 대한 예외를 정의하고 처리할 수 있습니다.

사용자 정의 클래스

  • 설명: std::exception 또는 기타 표준 예외 클래스를 상속받아 사용자가 직접 정의한 예외 클래스입니다. 사용자는 what() 메서드를 오버라이드하여 예외 발생 시 반환할 메시지를 지정할 수 있습니다.
  • 목적: 특정 애플리케이션의 요구사항에 맞추어 더 상세하고 명확한 예외 처리를 가능하게 함.
#include <exception>
#include <iostream>

// 사용자 정의 예외 클래스
class MyException : public std::exception {
public:
    const char* what() const throw() override {
        return "My custom exception occurred";
    }
};

int main() {
    try {
        // 예외 발생 시키기
        throw MyException();
    } catch (const MyException& e) {
        std::cerr << "Caught MyException: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "Caught std::exception: " << e.what() << std::endl;
    }

    return 0;
}
이것이 C#이다 단계별 학습으로 탄탄한 기본기를 다져줄 C# 입문서 3판, 한빛미디어

이 코드에서는 MyException이라는 사용자 정의 예외 클래스를 만들고 있습니다. 이 클래스는 std::exception을 상속받고 what() 메서드를 오버라이드하여 예외 발생 시 반환할 메시지를 정의합니다. main 함수에서는 이 예외를 throw하고, catch 블록을 통해 잡아내어 처리하고 있습니다.

사용자 정의 예외를 사용함으로써, 개발자는 애플리케이션의 특정 요구사항에 맞추어 더욱 세밀하고 명확한 예외 처리를 구현할 수 있습니다. 이를 통해 코드의 가독성과 유지보수성을 향상시키고, 애플리케이션의 전반적인 안정성을 높일 수 있습니다.

예외 사양 (Exception Specifications)

throw()

  • 설명: throw()는 함수가 예외를 발생시키지 않음을 명시하는 예외 사양입니다. 이는 함수 선언의 끝에 사용되며, 해당 함수에서 예외가 발생하면 std::unexpected가 호출됩니다. C++11 이후로는 noexcept에 의해 대체되었으며 현재는 사용이 권장되지 않습니다.
void myFunction() throw() {
    // 예외를 발생시키지 않는 코드
}

noexcept

  • 설명: noexcept는 C++11에서 도입되어 함수가 예외를 발생시키지 않음을 명시하는 데 사용됩니다. throw()에 비해 성능상 이점이 있으며, 예외 안전성을 향상시키는데 유용합니다.
void myFunction() noexcept {
    // 예외를 발생시키지 않는 코드
}

예외 처리 메커니즘

try-catch 블록

  • 설명: try-catch 블록은 예외를 감지하고 처리하는 기본 구조입니다. try 블록 내의 코드에서 예외가 발생하면, 해당 예외 유형에 맞는 catch 블록으로 제어가 이동합니다.
try {
    // 예외 발생 가능 코드
} catch (const std::exception& e) {
    // 표준 예외 처리
} catch (...) {
    // 그 외 모든 예외 처리
}

throw 문

  • 설명: throw 문은 특정 조건에서 예외를 발생시키는 데 사용됩니다. 이를 통해 개발자는 예외 상황을 명시적으로 표현할 수 있습니다.
if (errorCondition) {
    throw std::runtime_error("Error occurred");
}

catch-all 핸들러

  • 설명: catch(...)는 모든 유형의 예외를 잡기 위한 핸들러입니다. 이를 사용하면 예외 유형에 상관없이 모든 예외를 잡아낼 수 있습니다.
try {
    // 예외 발생 가능 코드
} catch (...) {
    // 모든 유형의 예외 처리
    std::cerr << "An unknown error occurred" << std::endl;
}
C++프로그래밍, 한국방송통신대학교출판문화원

이러한 예외 사양과 예외 처리 메커니즘은 C++ 프로그래밍에서 예외를 효과적으로 관리하고, 예외 상황에서도 프로그램의 안정성을 유지하는 데 중요한 역할을 합니다. 개발자는 이러한 도구들을 적절하게 활용하여 예외 상황에 대응할 수 있습니다.

특수 상황 예외 처리

std::bad_alloc

  • 설명: std::bad_alloc 예외는 메모리 할당 실패 시 발생합니다. 주로 new 연산자를 사용하여 메모리 할당을 시도했을 때 충분한 메모리가 없는 경우에 발생합니다.
try {
    int* myArray = new int[1000000000]; // 매우 큰 메모리 할당 시도
} catch (const std::bad_alloc& e) {
    std::cerr << "Memory allocation failed: " << e.what() << std::endl;
}

std::bad_cast

  • 설명: std::bad_castdynamic_cast를 사용하여 잘못된 타입 변환을 시도했을 때 발생하는 예외입니다. 주로 다운캐스팅 시에 잘못된 타입으로 캐스팅하려 할 때 발생합니다.
class Base {};
class Derived : public Base {};

Base* base = new Base();
try {
    Derived& derived = dynamic_cast<Derived&>(*base); // 잘못된 캐스팅 시도
} catch (const std::bad_cast& e) {
    std::cerr << "Bad cast: " << e.what() << std::endl;
}

std::bad_typeid

  • 설명: std::bad_typeid 예외는 typeid 연산자를 null 포인터에 대해 호출했을 때 발생합니다.
class MyClass {};
MyClass* myObject = nullptr;

try {
    std::cout << typeid(*myObject).name() << std::endl; // null 포인터에 대한 typeid 호출
} catch (const std::bad_typeid& e) {
    std::cerr << "Bad typeid: " << e.what() << std::endl;
}

기타 예외 관련 기능

std::uncaught_exception()

  • 설명: std::uncaught_exception() 함수는 현재 예외 처리 중인지 여부를 확인합니다. 이 함수가 true를 반환하면 현재 예외 처리 중임을 나타냅니다.
try {
    throw std::runtime_error("Error");
} catch (...) {
    if (std::uncaught_exception()) {
        std::cerr << "Exception is being processed." << std::endl;
    }
}

std::set_unexpected()

  • 설명: std::set_unexpected() 함수는 예상치 못한 예외 발생 시 호출될 함수를 설정합니다. C++11 이후로는 사용이 권장되지 않습니다.
void unexpectedHandler() {
    std::cerr << "Unexpected exception." << std::endl;
    std::terminate();
}

std::set_unexpected(unexpectedHandler);

std::terminate()

  • 설명: std::terminate() 함수는 예외 처리 불가능 상황에서 프로그램을 종료시킵니다. 이 함수는 예외가 잡히지 않았거나 std::set_unexpected()에 의해 호출될 수 있습니다.
try {
    throw std::runtime_error("Error");
} // catch 블록이 없음
[영진닷컴]그림으로 배우는 C++ Programming - 2nd Edition, 영진닷컴

이러한 특수 상황 예외 처리와 기타 예외 관련 기능들은 C++에서 예외 상황을 더욱 상세하게 관리하고, 프로그램의 안정성을 유지하는 데 중요한 역할을 합니다. 개발자는 이러한 기능들을 적절히 활용하여 다양한 예외 상황에 대응할 수 있습니다.

3. 결론

C++의 예외 처리는 프로그램의 안정성과 신뢰성을 높이는 데 중요한 역할을 합니다. 표준 라이브러리 예외들은 일반적인 오류 상황을 다루며, 사용자 정의 예외를 통해 특정 애플리케이션에 맞춘 상세한 예외 처리가 가능합니다. 예외 사양 및 처리 메커니즘은 프로그램의 안전성을 향상시키고, 예외가 발생했을 때 예측 가능한 반응을 제공합니다. 특수 상황 예외 처리는 메모리 할당 실패, 타입 캐스팅 오류 등 특정 상황에서 발생하는 예외들을 처리하며, 기타 예외 관련 기능은 예외 처리 중인 상태를 확인하고, 예외 처리 불가능 상황에서의 프로그램 종료를 관리합니다. 이러한 도구들을 통해 개발자들은 보다 견고하고 오류에 강한 소프트웨어를 개발할 수 있습니다.