Skip to content

22. 예외 처리(Exception Handling)


1. 키워드

  • 예외 처리(Exception Handling)
  • try, thorw, catch
  • 스택 풀기(Stack Unwinding)
  • exception 클래스
  • stdexcept 헤더 파일
  • logic_errorruntime_error 클래스
  • terminate()set_terminate() 함수


2. try, throw, catch

1) 예외란?

  • 예외란 컴퓨터 시스템이 동작하는 도중에 예상하지 못한 오류가 발생하여, 실행되고 있던 프로그램이 중지되는 것을 의미한다.
  • 예외 처리는 이러한 예외 상황을 처리할 수 있도록 코드의 흐름을 바꾸는 행위를 의미한다.
  • C++은 언어 차원에서 예외 처리 문법을 제공하여, 예외 처리하는 방식을 확장하고 관리하기 쉽도록 해준다.


2) try, throw, catch

  • C++에서는 예외 처리의 구현을 위해서 try, throw, catch 문을 제공한다.


1] try

  • 예외가 발생할 가능성이 있는 코드 블록

2] throw

  • try 문에서 발생한 오류에 대한 정보를 전달

3] catch

  • 발생한 예외에 대해 예외 핸들러가 처리할 내용을 담은 코드 블록


  • try 문으로 예외를 처리하기 위해서는 try 문 다음에 반드시 하나 이상의 catch 절을 구현해야 한다.
  • 또한, 각 catch 절은 자신이 처리할 수 있는 예외 타입을 지정할 수 있다.
  • 이때 특정 예외 타입 대신에 ...(줄임표)를 사용하면, 해당 catch 절은 모든 타입의 예외를 처리하게 된다.
  • 하지만 이러한 ...를 사용한 catch 절의 위치는 언제나 catch 절 중 맨 마지막에 위치해야 한다.


  • 다음의 코드는 사용자가 정확히 양의 정수를 입력했는가를 throw 문으로 검사하는 것이다.


#include <iostream>
using namespace std;

int IncreaseNumber(int n) {
    if (n < 0) {
        throw n;
    } else if (n == 0) {
        throw "0은 입력할 수 없습니다.";
    }

    return ++n;
}

int main(void) {
    int num;
    int inc_num;

    cout << "양의 정수를 하나 입력해 주세요: ";

    while (cin >> num) {
        try {
            inc_num = IncreaseNumber(num);
            cout << "입력한 정수에서 1을 증가시킨 값: " << inc_num << endl;
        } catch (int input) {
            cout << input << "은 양의 정수가 아닙니다." << endl;
            cout << "양의 정수를 다시 입력해 주세요: ";
            continue;
        } catch (const char *st) {
            cout << st << endl;
            cout << "양의 정수를 다시 입력해 주세요: ";
            continue;
        }

        break;
    }

    return 0;
}

// 양의 정수를 하나 입력해 주세요: -3
// -3은 양의 정수가 아닙니다.
// 양의 정수를 다시 입력해 주세요: 7
// 입력한 정수에서 1을 증가시킨 값: 8


  • 위의 코드처럼 throw 문의 피연산자는 변수와 문자열을 포함한 모든 타입의 예외를 사용할 수 있다.
  • 하지만 C++ 표준에서는 throw 문의 피연산자로 std::exception에서 파생되는 예외 타입을 사용하도록 권장하고 있다.


3) 예외 메커니즘

  • C++에서 예외 처리는 다음과 같은 순서로 진행된다.


1] try 문에 도달한 프로그램의 제어는 try 문 내의 코드를 실행한다.

2] 이때 예외가 발생(throw)하지 않으면 프로그램의 제어는 맨 마지막 catch 절 바로 다음으로 이동한다.

3] 만약 예외가 발생하면 catch 핸들러는 다음과 같은 순서로 적절한 catch 절을 찾게 된다.

3-1] 스택에서 try 문과 가장 가까운 catch 절부터 차례대로 검사한다.

3-2] 만약 적절한 catch 절을 찾지 못하면, 바로 다음 바깥쪽 try 문 다음에 위치한 catch 절을 차례대로 검사한다.

3-3] 이러한 과정을 가장 바깥쪽 try 문까지 계속 검사하게 된다.

3-4] 그래도 적절한 catch 절을 찾지 못하면, 미리 정의된 terminate() 함수가 호출된다.

4] 만약 적절한 catch 절을 찾게 되면, throw 문의 피연산자는 예외 객체의 형식 매개변수로 전달된다.

5] 모든 예외 처리가 끝나면 프로그램의 제어는 맨 마지막 catch 절 바로 다음으로 이동한다.


4) 스택 풀기

  • 앞서 살펴본 예외 메커니즘의 (3-1)부터 (3-4)까지의 과정을 스택 풀기라고 한다.
  • 스택 풀기란 예외를 처리하는 영역을 찾지 못해서 해당 예외가 호출된 영역을 상위 함수로 예외가 계속해서 전달되는 현상을 가리킨다.


#include <iostream>
using namespace std;

void Func03() { throw 0; }
void Func02() { Func03(); }
void Func01() { Func02(); }

int main(void) {
    try {
        Func01();
    } catch (int ex) {
        cout << "예외 처리(main): " << ex << endl;
    }

    return 0;
}

// 예외 처리(main): 0


  • 위의 코드에서는 연속된 함수 호출을 통해 호출된 Func03() 함수에서 예외가 발생한다.
  • 하지만 Func03() 함수에는 예외를 처리할 catch 절이 없으므로, 프로그램은 Func02() 함수로 예외를 전달한다.
  • 이때 스택에 저장되어 있던 Func03() 함수에 관한 스택 프레임을 모두 인출(Pop)하고 Func02() 함수로 이동한다.
  • 하지만 Func02() 함수에서도 예외를 처리할 수 없으므로, 또다시 Func01() 함수로 예외를 전달한다.
  • Func01() 함수에도 예외를 처리할 catch 절이 없기 때문에 마지막으로 Func01() 함수를 호출한 main() 함수로 전달된다.
  • 따라서 예외의 실제 처리는 main() 함수 내의 catch 절에서 수행되게 된다.


3. exception 클래스

1) 예외 클래스

  • C++은 여러 예외를 처리하기 위해 exception 헤더 파일을 통한 다양한 예외 클래스를 제공하고 있다.
  • 이러한 예외 클래스는 오류 코드값을 가지는 멤버 변수 및 오류 코드를 검사하거나 오류 메시지를 출력하는 멤버 함수 등 오류에 대한 모든 처리가 가능하도록 다양한 멤버를 포함하고 있다.
  • 이와 같은 예외 클래스도 클래스이므로, 상속할 수도 있으며 다형성도 성립한다.
  • 또한, C++에서는 생성자와 연산자에서도 예외 처리 기능을 사용할 수 있다.


2) exception 클래스

  • C++은 여러 예외 클래스의 기초 클래스로 사용할 수 있는 exception 클래스를 제공한다.
  • exception 클래스는 시스템에 따라 하나의 문자열 포인터를 반환하는 what()이라는 가상 멤버 함수를 제공한다.
  • 이 멤버 함수는 가상 함수이므로, exception 클래스로부터 파생된 클래스 내에서 재정의할 수 있다.
  • exception 클래스의 what() 멤버 함수는 별다른 일을 하지는 않지만, 파생 클래스에서는 원하는 문자열을 출력할 수 있도록 재정의할 수 있다.


3) 표준 예외 클래스

  • 표준 C++ 라이브러리는 exception 클래스로부터 파생된 다양한 표준 예외 클래스를 정의하고 있다.
  • 이러한 표준 예외 클래스는 stdexcept 헤더 파일을 통해 제공되며, 논리 오류와 런타임 오류로 나눌 수 있다.


001


  • stdexcept 헤더 파일은 우선 다른 표준 예외 클래스의 기초 클래스가 되는 두 개의 클래스를 정의한다.


1] logic_error

  • 일반적인 논리에 관한 오류들을 처리할 수 있음

2] runtime_error

  • 프로그램이 실행되는 동안 발생할 수 있는 다양한 오류들을 처리할 수 있음


4) 처리되지 않은 예외

  • C++ 프로그램은 발생한 예외를 처리할 catch 절을 찾을 수 없을 때, 미리 정의된 terminate() 함수를 호출한다.
  • terminate() 함수는 기본적으로 abort() 함수를 호출하여 프로그램을 강제로 종료시킨다.
  • 하지만 set_terminate() 함수를 사용하면 이러한 terminate() 함수의 기본 동작을 변경할 수 있다.


  • 다음의 코드는 set_terminate() 함수를 이용해 처리되지 않은 예외를 위한 함수를 재정의하는 것이다.


#include <exception>
#include <iostream>
using namespace std;

void MyErrorHandler() {
    cout << "처리되지 않은 예외가 발생했습니다." << endl;
    exit(-1);
}

int main(void) {
    set_terminate(MyErrorHandler);
    try {
        throw 1;
    } catch (char* const ex) {
        // 이 catch 절은 정수형 예외를 처리할 수 없음
    }

    return 0;
}

// 처리되지 않은 예외가 발생했습니다.


  • try 문에서는 정수형 예외가 발생했지만, 아래의 catch 절은 해당 에외를 처리하지 못한다.
  • 따라서 발생한 예외는 처리되지 않으며, 따라서 set_terminate() 함수로 미리 정의한 내용이 출력되게 된다.


5) 예외 처리 시 주의 사항

  • 예외가 발생하여 실행되고 있던 프로그램이 중지되는 현상은 매우 심각한 상황이다.
  • 따라서 발생한 예외를 처리하는 노력은 매우 중요하다.
  • 하지만 이러한 예외 처리에는 개발자의 많은 노력이 필요하게 된다.
  • 또한, C++의 예외 처리 기능을 사용하면 프로그램의 크기가 커지고 실행 속도가 크게 떨어지는 단점이 있다.
  • 따라서 무조건 발생할 수 있는 모든 예외를 다 처리하는 것이 아니라, 적당한 타협점을 찾는 게 중요하다.

References