프로그래밍/C++

C++ 예외 처리와 예외 안전성: 안정적인 프로그램을 위한 핵심 기법

shimdh 2025. 2. 3. 13:29
728x90

1. 예외 처리란?

예외 처리는 프로그램 실행 중 발생할 수 있는 오류나 예외적인 상황을 관리하는 메커니즘입니다. C++에서는 try, catch, throw 키워드를 사용하여 예외 처리를 구현합니다. 예외 처리를 통해 프로그램이 예상치 못한 상황에서도 안정적으로 동작하도록 할 수 있습니다.

1.1 기본 구조

  • try 블록: 예외가 발생할 가능성이 있는 코드를 포함합니다.
  • throw 문: 특정 조건이 만족되지 않을 때 예외를 발생시킵니다.
  • catch 블록: throw된 예외를 처리하는 코드가 위치합니다.

1.2 예제: 파일 열기 실패 시 예외 처리

#include <iostream>
#include <fstream>
#include <stdexcept>

void openFile(const std::string& filename) {
    std::ifstream file(filename);
    if (!file.is_open()) {
        throw std::runtime_error("파일을 열 수 없습니다.");
    }
}

int main() {
    try {
        openFile("없는파일.txt");
    } catch (const std::runtime_error& e) {
        std::cerr << "오류: " << e.what() << std::endl;
    }
    return 0;
}

위 코드에서 openFile 함수는 파일을 열려고 시도하고, 실패하면 std::runtime_error 타입의 예외를 던집니다. main 함수에서는 이 예외를 잡아 적절한 오류 메시지를 출력합니다. 이렇게 하면 프로그램이 비정상적으로 종료되지 않고, 사용자에게 오류 메시지를 제공할 수 있습니다.


2. 사용자 정의 예외 클래스

C++에서는 사용자 정의 예외 클래스를 만들어 보다 구체적이고 의미 있는 오류 정보를 제공할 수 있습니다. 기본적으로 모든 사용자 정의 예외 클래스는 std::exception 클래스를 상속받아야 합니다.

2.1 사용자 정의 예외 클래스 예제

#include <iostream>
#include <exception>

class MyException : public std::exception {
public:
    const char* what() const noexcept override {
        return "내가 만든 사용자 정의 오류입니다.";
    }
};

void mightGoWrong() {
    bool errorOccurred = true; // 일부 조건에 따라 변경될 수 있음
    if (errorOccurred) {
        throw MyException();
    }
}

int main() {
    try {
        mightGoWrong();
    } catch (const MyException& e) {
        std::cerr << "오류: " << e.what() << std::endl;
    }

   return 0;
}

위 코드에서 MyException 클래스는 what() 함수를 오버라이드하여 자신만의 오류 메시지를 제공합니다. 이렇게 사용자 정의 예외 클래스를 사용하면 더 명확한 정보 전달이 가능합니다. 예를 들어, 특정 도메인에서 발생할 수 있는 오류를 더 세밀하게 표현할 수 있습니다.

2.2 사용자 정의 예외 클래스의 장점

  • 오류의 의미를 명확히 전달: 표준 예외 클래스로는 표현하기 어려운 도메인 특화 오류를 정의할 수 있습니다.
  • 코드의 가독성 향상: 사용자 정의 예외 클래스를 사용하면 코드의 의도를 더 명확히 표현할 수 있습니다.
  • 오류 처리의 유연성: 특정 예외에 대한 처리를 더 세밀하게 제어할 수 있습니다.

3. 예외 안전성

예외 안전성은 프로그램이 예상치 못한 상황에서도 일관된 상태로 유지되는 것을 보장하는 개념입니다. 예외 안전성에는 세 가지 주요 수준이 있습니다.

3.1 예외 안전성의 수준

  1. 기본 안전성(Basic Guarantee):

    • 함수 호출 후 객체가 유효하며 이전 상태와 관련된 정보가 손실되지 않습니다.
    • 하지만 객체들의 내용은 변할 수 있습니다.
  2. 강력한 안전성(Strong Guarantee):

    • 함수 호출 전후에 객체들이 동일하게 유지되며 모든 변경사항이 롤백됩니다.
    • 이는 트랜잭션처럼 작동하여 실패 시 이전 상태로 복구 됩니다.
  3. 무조건적 안전성(No-Throw Guarantee):

    • 해당 함수 내에서 절대로 예외가 발생하지 않는다는 보장을 제공합니다.
    • 주로 생성자나 소멸자와 같은 특별한 경우에 적용됩니다.

3.2 강력한 안전성을 제공하는 코드 예제

#include <iostream>
#include <vector>
#include <stdexcept>

class SafeArray {
public:
    SafeArray(size_t size) : data(size), size(size) {}

    void set(size_t index, int value) {
        if (index >= size)
            throw std::out_of_range("Index out of range");

        data[index] = value; // 여기서 문제가 생길 경우 전체 배열은 영향을 받지 않아야 함
    }

private:
    std::vector<int> data;
    size_t size;
};

int main() {
    try {
        SafeArray arr(5);
        arr.set(6, 10); // 이 줄에서 out_of_range 에러가 발생함
    } catch (const std::exception& e) {
        std::cerr << "예외 발생: " << e.what() << '\n';
        // 배열은 여전히 유효하며 다른 동작을 계속 진행할 수 있음
    }

    return 0;
}

위 코드에서는 SafeArray 클래스를 정의하고 있으며, 배열 크기가 초과될 때마다 적절하게 오류를 처리하도록 설계되었습니다. 이 클래스는 기본적인 안전성을 제공하면서도 데이터 무결성을 유지합니다.


4. 예외 처리의 추가적인 고려 사항

4.1 예외 전파

예외는 호출 스택을 따라 전파될 수 있습니다. 즉, 예외가 발생한 함수에서 처리되지 않으면 상위 호출자로 전파됩니다. 이는 예외를 적절한 위치에서 처리할 수 있도록 해줍니다.

void functionA() {
    throw std::runtime_error("예외 발생!");
}

void functionB() {
    functionA();
}

int main() {
    try {
        functionB();
    } catch (const std::runtime_error& e) {
        std::cerr << "예외 처리: " << e.what() << std::endl;
    }
    return 0;
}

위 코드에서 functionA에서 발생한 예외는 functionB를 거쳐 main 함수에서 처리됩니다.

4.2 예외 처리와 리소스 관리

예외가 발생하면 리소스가 누수되지 않도록 주의해야 합니다. 이를 위해 RAII(Resource Acquisition Is Initialization) 패턴을 사용할 수 있습니다. RAII는 리소스의 수명을 객체의 수명에 바인딩하여 리소스 누수를 방지합니다.

#include <iostream>
#include <memory>

class Resource {
public:
    Resource() { std::cout << "리소스 획득\n"; }
    ~Resource() { std::cout << "리소스 해제\n"; }
};

void useResource() {
    std::unique_ptr<Resource> res = std::make_unique<Resource>();
    throw std::runtime_error("예외 발생!");
}

int main() {
    try {
        useResource();
    } catch (const std::runtime_error& e) {
        std::cerr << "예외 처리: " << e.what() << std::endl;
    }
    return 0;
}

위 코드에서 std::unique_ptr를 사용하여 리소스를 관리하면 예외가 발생해도 리소스가 자동으로 해제됩니다.


5. 예외 처리의 추가적인 고려 사항

5.1 예외 처리와 성능

예외 처리는 프로그램의 안정성을 높이지만, 성능에 영향을 미칠 수 있습니다. 예외가 발생하면 호출 스택을 거슬러 올라가며 예외를 처리해야 하기 때문에 추가적인 오버헤드가 발생할 수 있습니다. 따라서 예외 처리는 예외적인 상황에만 사용하고, 일반적인 오류 처리는 다른 방법을 고려하는 것이 좋습니다.

5.2 예외 처리와 다중 스레드

다중 스레드 환경에서 예외 처리는 더 복잡해질 수 있습니다. 각 스레드에서 발생한 예외는 해당 스레드 내에서 처리되어야 하며, 다른 스레드로 전파되지 않습니다. 따라서 다중 스레드 프로그램에서는 각 스레드의 예외를 적절히 처리하고, 필요한 경우 스레드 간 통신을 통해 예외 정보를 공유해야 합니다.

#include <iostream>
#include <thread>
#include <stdexcept>

void threadFunction() {
    throw std::runtime_error("스레드에서 예외 발생!");
}

int main() {
    std::thread t(threadFunction);
    try {
        t.join();
    } catch (const std::exception& e) {
        std::cerr << "예외 처리: " << e.what() << std::endl;
    }
    return 0;
}

위 코드에서는 스레드에서 발생한 예외를 메인 스레드에서 처리합니다. 이를 통해 다중 스레드 환경에서도 예외를 안전하게 처리할 수 있습니다.


6. 결론

C++에서의 예외 처리 및 예외 안전성은 프로그래밍 품질을 높이는 중요한 요소입니다. 잘 설계된 에러 핸들링 시스템은 사용자에게 보다 나은 경험을 제공하고 버그 수정 및 유지보수를 용이하게 합니다. 다양한 수준의 안정성을 이해하고 활용함으로써 더욱 견고한 애플리케이션 개발이 가능합니다.

예외 처리를 통해 프로그램의 안정성을 높이고, 사용자 정의 예외 클래스를 통해 더 명확한 오류 정보를 제공할 수 있습니다. 또한, 예외 안전성을 고려하여 프로그램이 예상치 못한 상황에서도 일관된 상태를 유지할 수 있도록 설계하는 것이 중요합니다. 이러한 기법들을 활용하면 더 안정적이고 유지보수하기 쉬운 코드를 작성할 수 있습니다.

728x90