본문 바로가기
GD's IT Lectures : 기초부터 시리즈/C, C++ 기초부터 ~

[C/C++ 프로그래밍 : 중급] 8. 예외 처리와 오류 처리

by GDNGY 2023. 6. 3.

Chapter 8. 예외 처리와 오류 처리

예외 처리는 프로그램이 실행 중에 발생하는 예외적인 상황들, 즉 에러를 대처하는 방법에 대해 알아봅니다. 또한, 사용자가 직접 예외를 정의하고 사용하는 방법에 대해서도 학습합니다. 이후, 예외의 전파와 예외 안전성에 대해 배웁니다. 마지막으로, 오류 처리에 대해 알아보고, 예외 처리와 오류 처리의 차이점을 비교합니다.

 

반응형

 


[Chapter 8. 예외 처리와 오류 처리]

 

8.1. 예외 처리 이해하기

8.1.1. 예외란 무엇인가

8.1.2. 예외 처리의 필요성

8.1.3. C++에서의 예외 처리 메커니즘

 

8.2. try, catch, throw 사용하기

8.2.1. try 블록과 throw 문의 사용

8.2.2. catch 블록에서의 예외 처리

8.2.3. 다중 catch 블록 사용하기

 

8.3. 사용자 정의 예외 클래스 생성하기

8.3.1. 예외 클래스의 정의와 구조

8.3.2. 예외 클래스를 활용한 예외 처리

 

8.4. 예외의 전파와 예외 명세

8.4.1. 예외의 전파 이해하기

8.4.2. 함수에서 예외 명세하기

8.4.3. 예외 명세의 유용성과 주의점

 

8.5. 예외 안전성과 예외 중립성
8.5.1. 예외 안전성의 레벨과 원칙
8.5.2. 예외 중립성의 필요성
8.5.3. 예외 안전성과 예외 중립성을 유지하는 전략

8.6. 오류 처리 기법
8.6.1. 오류 코드와 오류 반환
8.6.2. C++에서의 assert 사용하기
8.6.3. 예외 처리와 오류 처리의 차이점


8.1 예외 처리 이해하기

예외 처리는 프로그램 실행 중 예외적인 상황을 효과적으로 다루는 방법입니다. 예외는 일반적인 프로그램 흐름을 방해하는 이벤트를 말합니다. 이 섹션에서는 예외가 무엇인지, 그리고 왜 예외 처리가 필요한지에 대해 알아보겠습니다. 또한, C++의 예외 처리 메커니즘을 배우게 됩니다. 이해력을 돕기 위해, 실제 코드 예제를 통해 예외 처리를 실습해 보겠습니다.

8.1.1 예외란 무엇인가

프로그래밍에서 '예외'는 프로그램이 정상적으로 실행되는 도중에 발생하는 예기치 않은 사건을 의미합니다. 이러한 예외 사건은 보통 오류나 문제를 일으키며, 이를 적절히 처리하지 않으면 프로그램이 중단되거나 예상치 못한 행동을 하게 됩니다. 예를 들어, 파일을 열려고 하는데 해당 파일이 존재하지 않는 경우, 또는 배열의 인덱스가 유효 범위를 벗어난 경우 등이 예외 상황으로 볼 수 있습니다. 

C++에서 예외 처리는 다음과 같이 구현될 수 있습니다.

[예제]

#include <iostream>

int main() {
    try {
        throw "This is an exception";
    }
    catch(const char* exception) {
        std::cout << "Caught an exception: " << exception << std::endl;
    }

    return 0;
}

 

이 코드는 "This is an exception" 문자열을 예외로 던지고(throw) 그것을 바로 잡는(catch) 간단한 C++ 예외 처리의 예입니다. try 블록 안에는 예외가 발생할 수 있는 코드를 넣습니다. catch 블록은 특정한 타입의 예외를 잡아 처리하는 데 사용됩니다. 여기서는 문자열 예외가 발생하면, 그 예외를 잡아서 콘솔에 출력합니다. 

 

이와 같이 예외 처리를 사용하면, 예외적인 상황을 더 효과적으로 관리할 수 있습니다. 예외 처리를 이용하면 오류 상황을 예상하고 이에 대비할 수 있으며, 이를 통해 프로그램의 안정성과 신뢰성을 크게 향상할 수 있습니다.

 

C 언어에서는 예외 처리 메커니즘을 직접 구현해야 합니다. 다음은 오류 코드를 반환하는 방식을 사용한 예입니다.

 

[예제]

#include <stdio.h>
#include <stdlib.h>

int divide(int dividend, int divisor, int* result) {
    if (divisor == 0) {
        return -1;  // 오류 코드 반환
    }
    *result = dividend / divisor;
    return 0;  // 정상 종료
}

int main() {
    int result;
    if (divide(10, 0, &result) == -1) {
        printf("An error occurred\n");
        return 1;
    }
    printf("10 / 0 = %d\n", result);
    return 0;
}

 

이 C 코드에서 divide 함수는 나눗셈을 수행하지만, divisor가 0인 경우 오류 코드를 반환합니다. 이런 식으로 오류 상황을 미리 예측하고, 그에 대비하는 코드를 작성하는 것이 중요합니다. 

 

오류를 반환하는 방식을 사용하면 main함수에서 오류를 쉽게 감지하고 적절히 대응할 수 있습니다. 그러나 이 방식은 여러 가지 이유로 불편할 수 있습니다. 함수의 반환값을 오류 코드로 사용하면 원래 반환하려던 값이 손실되거나 별도로 처리해야 하는 문제가 발생할 수 있습니다. 또한, 오류를 반환하는 모든 함수에 대해 오류 검사를 수행해야 합니다. 

 

반면 C++의 예외 처리 메커니즘은 이러한 문제를 해결하며, 예외가 발생한 경우 프로그램의 제어 흐름을 더욱 명확하게 다룰 수 있게 해줍니다. try-catch 블록은 예외를 던지는 코드와 이를 처리하는 코드를 명확하게 분리합니다. 예외가 발생하면 즉시 가장 가까운 catch 블록으로 제어가 이동하므로, 복잡한 제어 흐름을 신경 쓸 필요가 없습니다. 

 

이러한 예외 처리 방법을 이해하고 활용하면, 프로그램에서 예외 상황을 예상하고 적절히 대응할 수 있게 됩니다. 이를 통해 오류를 줄이고 프로그램의 안정성을 높일 수 있습니다.  

 

8.1.2. 예외 처리의 필요성

"예외 처리의 필요성"에 대해 이야기하려면, 우리는 먼저 소프트웨어에서 예외가 무엇이며 왜 발생하는지를 이해해야 합니다. 예외는 기본적으로 프로그램에서 예상치 못한 조건이나 상황을 나타냅니다. 예외 처리는 이러한 예외 상황을 감지하고 적절히 대응하는 것을 말합니다.  

 

그렇다면 왜 우리는 예외 처리가 필요할까요? 다음은 예외 처리가 필요한 주요 이유들입니다.

 

프로그램의 안정성 보장: 예외 처리 없이 프로그램이 실행되면 예외 상황에서 프로그램이 예기치 않게 종료될 수 있습니다. 예외 처리를 통해 이런 상황을 피하고, 프로그램이 안정적으로 실행될 수 있게 합니다. 

 

디버깅 용이성: 예외 처리는 예외가 발생한 위치와 이유를 쉽게 알 수 있게 도와주므로 디버깅을 용이하게 합니다.

 

사용자 친화적인 인터페이스 제공: 예외 처리를 통해 프로그램이 예외 상황에 대해 사용자에게 알릴 수 있습니다. 이를 통해 사용자는 무슨 일이 일어났는지 이해하고 적절히 대응할 수 있습니다. 

 

이런 이유들로 인해, 예외 처리는 프로그래밍에서 중요한 요소입니다.

 

아래는 C++에서의 예외 처리 코드 예제입니다.

[예제]

#include <iostream>
#include <stdexcept>

int main() {
    try {
        throw std::runtime_error("A problem occurred");
    }
    catch(const std::exception& e) {
        std::cerr << "Caught exception: " << e.what() << '\n';
    }
    catch(...) {
        std::cerr << "Caught unknown exception" << '\n';
    }
    return 0;
}

 

이 예제 코드에서, try 블록 내부에서 예외가 발생합니다. throw 문을 통해 std::runtime_error 예외를 발생시킵니다. 그리고 catch 블록에서 이 예외를 잡아내어 처리합니다. 위 예제 코드에서 볼 수 있듯이, 프로그램이 예외를 던지면 해당 예외를 catch 블록이 잡습니다. 이렇게 예외를 처리하면 프로그램의 제어 흐름을 관리할 수 있으며, 프로그램이 예기치 않게 종료되는 것을 방지할 수 있습니다.  

 

다음은 C에서 예외 처리를 시뮬레이션하는 방법에 대한 예제 코드입니다. C에는 C++와 같은 내장 예외 처리 메커니즘은 없지만, 반환 값이나 오류 코드를 사용하여 예외 처리를 흉내 낼 수 있습니다. 

 

[예제]

#include <stdio.h>
#include <stdlib.h>

int divide(int numerator, int denominator, int *result) {
    if (denominator == 0) {
        return -1; // 오류 코드 반환
    }

    *result = numerator / denominator;
    return 0; // 성공 코드 반환
}

int main() {
    int result;
    if (divide(5, 0, &result) < 0) {
        fprintf(stderr, "Error: Division by zero\n");
        exit(EXIT_FAILURE);
    }
    printf("Result: %d\n", result);

    return EXIT_SUCCESS;
}

 

이 C 예제에서, divide 함수는 나눗셈을 수행하고 결과를 result 포인터를 통해 반환합니다. 만약 분모가 0이면 함수는 -1을 반환하여 오류를 나타냅니다. 이 방식으로 예외 상황을 표현하고 처리하는 것이 가능합니다. 

 

이렇게 보면, 예외 처리는 프로그램에서 필수적인 요소임을 알 수 있습니다. 프로그램의 안정성을 보장하고, 디버깅을 용이하게 하며, 사용자 친화적인 인터페이스를 제공하기 위해 반드시 필요한 것입니다. 

 

8.1.3. C++에서의 예외 처리 메커니즘

C++에서의 예외 처리 메커니즘은 특별한 세 가지 키워드, 'try', 'catch', 그리고 'throw'를 사용합니다. 이들 키워드는 함께 작동하여 예외를 던지고, 그 예외를 캐치하며, 예외가 발생할 수 있는 코드를 정의하는 역할을 합니다. 이 섹션에서는 각 키워드와 그들이 어떻게 함께 작동하는지에 대해 설명하겠습니다. 

 

먼저, 'throw' 키워드는 예외를 던지는 역할을 합니다. 예외는 프로그램에서 문제가 발생했음을 알리는 신호입니다. 예외를 던질 때는 어떠한 데이터 타입도 사용할 수 있으며, 보통은 문자열이나 객체, 또는 특수한 예외 타입을 사용합니다. 

 

[예제]

throw "Division by zero exception";
throw std::runtime_error("Failed to open file");
throw 404;

 

위의 예시에서 보는 것처럼, 'throw' 키워드 다음에 오는 표현식이 예외입니다. 이것은 프로그램의 제어 흐름을 바꾸고, 이 예외를 처리할 수 있는 가장 가까운 'catch' 블록으로 이동시킵니다.

 

다음으로, 'try' 블록은 예외가 발생할 수 있는 코드를 감싸는 역할을 합니다. 만약 'try' 블록 내에서 예외가 발생하면, 그 블록의 실행은 즉시 중단되고 제어는 'catch' 블록으로 이동합니다.

 

[예제]

try {
    // 예외가 발생할 수 있는 코드
    int a = 5, b = 0;
    if (b == 0)
        throw "Division by zero exception";
    else
        std::cout << a/b;
}


마지막으로, 'catch' 블록은 특정한 타입의 예외를 잡아내어 처리하는 역할을 합니다. 'catch' 블록은 'try' 블록 바로 다음에 위치해야 하며, 여러 개의 'catch' 블록을 사용하여 여러 가지 타입의 예외를 처리할 수 있습니다. 'catch' 블록의 괄호 안에는 잡아낼 예외의 타입과 변수를 지정합니다. 

[예제]

catch (const char* e) {
    // 예외 처리 코드
    std::cout << "Caught an exception: " << e;
}


C++에서의 예외 처리 메커니즘에 대해 기본적인 개념을 알아보았습니다.


8.2. try, catch, throw 사용하기

C++의 기본 예외 처리 메커니즘을 사용하는 방법을 자세히 배우게 됩니다. 간단한 'try', 'catch', 'throw'의 사용법부터 다양한 예외 타입을 처리하는 방법, 다중 'catch' 블록을 사용하는 방법까지를 살펴봅니다. 이 섹션을 통해 개발자들은 안전하고 견고한 프로그램을 작성하기 위해 필요한 예외 처리 기법을 자세히 알게 됩니다.

8.2.1. try 블록과 throw 문의 사용

예외 처리에서 'try', 'throw' 문은 예외를 발생시키고 그 예외를 감지하는 데 꼭 필요한 요소입니다.

 

첫째, 'try' 블록은 예외가 발생할 수 있는 코드를 포함하는 부분입니다. 'try' 블록 안에 있는 코드는 실행 중에 언제든지 예외를 발생시킬 수 있습니다. 예외가 발생하면 프로그램의 제어 흐름은 바로 'catch' 블록으로 전달됩니다. 이를테면, 다음과 같은 코드를 생각해 봅시다. 

[예제]

try {
  // 예외가 발생할 수 있는 코드
} catch (/* 예외 타입 */) {
  // 예외 처리 코드
}


둘째, 'throw' 문은 예외를 발생시키는 데 사용됩니다. 'throw' 문은 예외를 발생시키는 데 사용되는 표현식을 포함하며, 이 표현식은 'catch' 블록에서 처리할 예외 타입을 결정합니다. 예외가 발생하면 'throw' 문 다음의 코드는 실행되지 않습니다. 예를 들어, 다음 코드는 0으로 나누려고 하면 예외를 발생시킵니다.  

[예제]

int divide(int numerator, int denominator) {
  if (denominator == 0) {
    throw "Division by zero is undefined!";
  }
  return numerator / denominator;
}

 

이 예제에서 'throw' 문은 문자열 리터럴 "Division by zero is undefined!"를 예외로 던집니다. 이 문자열은 후속 'catch' 블록에서 처리할 수 있습니다. 이러한 방식으로, 'try' 블록과 'throw' 문을 사용하면 예외 발생 가능성이 있는 코드를 안전하게 관리할 수 있습니다. 이는 프로그램의 안정성을 향상하고, 예상치 못한 오류로부터 프로그램을 보호하는 데 도움이 됩니다.

 

8.2.2. catch 블록에서의 예외 처리

앞서 'try'와 'throw'에 대해 알아보았으니 이제 'catch' 블록에 대해 살펴보겠습니다. 'catch' 블록은 'try' 블록에서 발생한 예외를 처리하는 부분입니다. 여기서 중요한 것은 'catch' 블록이 예외의 타입에 따라 다르게 작동한다는 것입니다. 이해를 돕기 위해 다음 예제를 보겠습니다. 

 

[예제]

try {
  // 예외 발생 가능 코드
} catch (int e) {
  std::cout << "Integer exception: " << e << std::endl;
} catch (const char* e) {
  std::cout << "String exception: " << e << std::endl;
}

 

이 코드에서 볼 수 있듯이, 각 'catch' 블록은 특정 타입의 예외를 처리합니다. 첫 번째 'catch' 블록은 정수형 예외를 처리하고, 두 번째 'catch' 블록은 문자열 예외를 처리합니다. 예외가 발생하면 해당 타입의 'catch' 블록이 실행됩니다. 만약 던진 예외와 일치하는 타입의 'catch' 블록이 없다면 프로그램은 종료됩니다.

 

또한, 여러 예외를 처리하는 방법에 대해 살펴보겠습니다. 아래 코드를 보면, 모든 예외를 처리하는 범용 'catch' 블록이 있습니다.

 

[예제]

try {
  // 예외 발생 가능 코드
} catch (int e) {
  std::cout << "Integer exception: " << e << std::endl;
} catch (const char* e) {
  std::cout << "String exception: " << e << std::endl;
} catch (...) {
  std::cout << "Unknown exception caught!" << std::endl;
}

 

'catch(...)' 블록은 어떤 타입의 예외든 처리할 수 있습니다. 그러나 이 블록은 마지막에 위치해야 합니다. 왜냐하면 첫 번째로 일치하는 'catch' 블록이 실행되고 나면 다른 'catch' 블록은 검사하지 않기 때문입니다. 따라서 'catch(...)' 블록이 앞에 오면 다른 'catch' 블록은 무시됩니다.

 

이렇게 예외를 처리하는 방법을 이해하면 프로그램의 안정성과 가독성을 높일 수 있습니다. 'try', 'catch', 'throw'는 강력한 도구이며, 이를 활용하여 코드의 예외를 관리하고 제어할 수 있습니다. 

 

1) catch 블록의 동작 원리

'catch' 블록의 동작 원리를 이해하기 위해, 먼저 C++ 프로그램이 예외를 어떻게 처리하는지 살펴봅시다. C++에서는 프로그램 실행 중에 예외가 발생하면, 해당 예외를 처리할 수 있는 'catch' 블록을 찾습니다. 그리고 이 'catch' 블록이 발견되면, 해당 블록의 코드가 실행되고, 그 후 프로그램의 제어는 'try/catch' 구조 바로 다음의 코드로 이동합니다. 

 

[예제]

try {
    throw 20;
} catch (int e) {
    std::cout << "An integer exception occurred. Exception number: " << e << '\n';
}

std::cout << "This is after the try-catch block.\n";

 

위 예제에서, 'throw 20;' 문은 int 타입의 예외를 발생시킵니다. 이 예외는 바로 아래의 'catch' 블록에서 처리되며, 그 후에 'try/catch' 구조 다음의 코드가 실행됩니다. 

 

'catch' 블록의 동작 원리를 이해하는 데 중요한 한 가지 개념은 예외 처리의 범위입니다. 'try' 블록 내에서 발생한 예외는 해당 'try' 블록에 대응하는 'catch' 블록에서만 처리될 수 있습니다. 즉, 각 'try' 블록은 해당하는 'catch' 블록을 가지며, 이것이 범위를 형성합니다. 

 

예를 들어, 다음과 같은 코드를 살펴봅시다.

 

[예제]

try {
    // code that may throw an exception
} catch (int e) {
    // handle int exception
}

try {
    // another piece of code that may throw a different exception
} catch (char e) {
    // handle char exception
}

 

위 코드에서 첫 번째 'try/catch' 블록은 int 타입의 예외를 처리하고, 두 번째 'try/catch' 블록은 char 타입의 예외를 처리합니다. 이렇게 각 'try/catch' 블록은 독립적인 범위를 형성하며, 각각의 범위 내에서 발생한 예외만 처리할 수 있습니다.

 

또한 'catch' 블록은 가장 먼저 일치하는 타입의 예외를 처리합니다. 예를 들어, 여러 'catch' 블록이 있고 각각 다른 타입의 예외를 처리할 때, 'throw' 문은 가장 먼저 일치하는 'catch' 블록을 선택합니다. 따라서 순서가 중요합니다.

 

2) 다양한 타입의 예외를 처리하는 방법

예외 처리는 프로그램의 안정성과 신뢰성을 높이는 데 중요한 도구입니다. 특히 C++에서는 다양한 타입의 예외를 처리하는 방법을 제공합니다. 각 예외 타입에 맞는 catch 블록을 사용하면, 여러 타입의 예외를 각각 다른 방식으로 처리할 수 있습니다.

 

'catch' 블록은 try 블록에서 발생한 예외를 처리합니다. 예외의 타입은 throw 문에 의해 결정되며, 이 타입은 'catch' 문에서 지정한 타입과 일치해야 합니다. 'catch' 문의 파라미터 타입이 throw 문에서 발생한 예외와 일치하면, 해당 'catch' 블록이 실행됩니다.

[예제]

try {
    throw 'a';  // throw a char exception
} catch(int e) {
    std::cout << "An integer exception occurred.\n";
} catch(char e) {
    std::cout << "A character exception occurred. Exception: " << e << '\n';
}

 

위 코드에서는 'try' 블록에서 'a' 문자를 throw합니다. 이 예외는 char 타입이므로, char 타입의 'catch' 블록에서 처리됩니다. int 타입의 'catch' 블록은 이 예외를 처리할 수 없습니다.

 

여러 'catch' 블록을 사용하면, 다양한 타입의 예외를 처리할 수 있습니다. 이 경우, 'catch' 블록은 위에서 아래로 차례대로 검사 되며, 첫 번째로 일치하는 'catch' 블록이 선택됩니다.

 

[예제]

try {
    throw 3.14;  // throw a double exception
} catch(int e) {
    std::cout << "An integer exception occurred.\n";
} catch(char e) {
    std::cout << "A character exception occurred.\n";
} catch(double e) {
    std::cout << "A double exception occurred. Exception: " << e << '\n';
}

 

위 코드에서는 'try' 블록에서 3.14를 throw합니다. 이 예외는 double 타입이므로, double 타입의 'catch' 블록에서 처리됩니다. int 타입과 char 타입의 'catch' 블록은 이 예외를 처리할 수 없습니다.

 

이러한 방식으로, 'catch' 블록을 사용하여 프로그램의 다양한 부분에서 발생할 수 있는 다양한 타입의 예외를 개별적으로 처리할 수 있습니다. 이렇게 하면 각 예외에 가장 적합한 방법으로 응답할 수 있습니다, 또한 예외 처리는 프로그램의 동작을 보다 예측 가능하게 만들며, 이는 버그를 줄이고 프로그램의 전반적인 품질을 향상합니다.

 

8.2.3. 다중 catch 블록 사용하기

C++에서는 하나의 try 블록에 대해 여러 개의 catch 블록을 사용할 수 있습니다. 이를 '다중 catch 블록'이라고 하며, 이를 통해 다양한 종류의 예외를 한 번에 처리할 수 있습니다. 

 

다중 catch 블록은 각각 다른 종류의 예외를 처리하기 위해 사용됩니다. 각 catch 블록은 고유한 예외 타입을 가지며, 각 타입에 해당하는 예외가 발생했을 때 해당 catch 블록이 실행됩니다. 

 

여러 개의 catch 블록을 사용하면, 발생 가능한 각각의 예외 조건에 따라 서로 다른 처리를 수행할 수 있습니다. 이는 코드의 유연성을 높이며, 각 예외에 대한 처리를 분명하게 구분하도록 도와줍니다. 

 

다음은 다중 catch 블록의 사용 예입니다.

 

[예제]

try {
    // 일부 코드...
    throw "Error message"; // 문자열 예외 발생
}
catch (int e) {
    std::cout << "Integer exception: " << e << std::endl;
}
catch (char e) {
    std::cout << "Char exception: " << e << std::endl;
}
catch (...) {
    std::cout << "Unknown exception occurred." << std::endl;
}

 

위 코드에서는 try 블록 내부에서 문자열 예외가 발생합니다. 그러나 해당 예외에 대한 catch 블록은 존재하지 않습니다. 따라서 가장 마지막에 위치한 '...'을 사용한 catch 블록이 실행됩니다. 이 catch 블록은 어떤 예외 타입도 받을 수 있는 'catch-all' 블록으로, 위의 코드에서는 알 수 없는 예외가 발생했을 때 이를 처리합니다. 

 

이처럼 다중 catch 블록을 사용하면, 특정 예외에 대한 처리와 더불어 빠뜨릴 수 있는 예외도 함께 처리할 수 있습니다. 이를 통해 코드의 안정성을 높이고, 다양한 예외 상황에 대응할 수 있게 됩니다. 다만, 여러 개의 catch 블록을 사용할 때는 각 블록이 처리하는 예외 타입이 명확해야 하며, 상위 타입의 예외를 먼저 처리하는 경우 하위 타입의 예외는 절대로 잡히지 않으므로 이 부분에 유의해야 합니다. 


8.3. 사용자 정의 예외 클래스 생성하기

C++에서는 사용자 정의 예외 클래스를 생성하여, 특정한 상황에 맞는 예외를 던질 수 있습니다. 사용자 정의 예외 클래스는 std::exception 클래스를 상속받아 구현합니다. what() 함수를 오버라이드하여, 예외 발생시 반환할 메시지를 지정합니다. 사용자 정의 예외 클래스를 사용하면, 더욱 세밀한 예외 처리가 가능해지고 프로그램의 안정성과 유지보수성을 높일 수 있습니다.  

8.3.1. 예외 클래스의 정의와 구조

C++에서는 예외 상황에 대응하기 위해 사용자 정의 예외 클래스를 생성할 수 있습니다. 이를 통해 문제가 발생한 구체적인 상황을 잘 표현할 수 있고, 디버깅을 훨씬 쉽게 만듭니다. 사용자 정의 예외 클래스는 표준 예외 클래스인 std::exception을 상속받아 생성합니다. 

 

다음은 간단한 사용자 정의 예외 클래스의 예입니다.

 

[예제]

#include <exception>

class MyException : public std::exception
{
public:
    const char * what () const throw ()
    {
        return "My exception occurred";
    }
};

 

이 클래스는 std::exception을 상속받고 what() 함수를 오버라이딩합니다. what() 함수는 예외가 발생했을 때 출력될 메시지를 반환합니다. 이 함수는 throw() 지정자를 가지고 있는데, 이는 이 함수가 예외를 던지지 않음을 보장합니다.

 

이제 이 사용자 정의 예외를 throw 문을 사용해 던질 수 있습니다.

 

[예제]

try
{
    throw MyException();
}
catch(MyException& e)
{
    std::cerr << e.what() << '\n';
}
catch(std::exception& e)
{
    // Other errors
}

 

이 코드에서 try 블록 안에서 MyException 예외가 발생합니다. catch 블록에서는 이 예외를 잡아서 처리합니다.

  • 첫 번째 catch 블록에서는 MyException을 잡고, 이 예외의 what() 함수를 호출하여 에러 메시지를 출력합니다.
  • 두 번째 catch 블록에서는 그 외의 모든 std::exception을 잡습니다. 

사용자 정의 예외 클래스를 사용하면, 발생 가능한 여러 가지 예외 상황에 대응하는 예외 타입을 생성할 수 있습니다. 이를 통해 각 예외 상황에 대한 정보를 더욱 세밀하게 제공할 수 있고, 예외 처리를 더욱 명확하게 할 수 있습니다. 또한, 프로그램의 안정성과 유지보수성도 향상할 수 있습니다.

 

1) 사용자 정의 예외 클래스의 속성

사용자 정의 예외 클래스를 정의할 때, 클래스는 일반적으로 다음과 같은 세 가지 주요 속성을 가져야 합니다.

  • 예외 메시지 : 예외 클래스는 문제를 설명하는 문자열 메시지를 저장해야 합니다. 이 메시지는 보통 what() 멤버 함수를 통해 접근할 수 있습니다.
  • 예외 타입 : 각기 다른 예외 상황에 대해 서로 다른 예외 타입을 사용하면, 예외 핸들러에서 각기 다른 예외 타입에 대한 적절한 처리를 구현할 수 있습니다.
  • 예외 계층 : 예외 타입을 구조화하여 계층을 형성할 수 있습니다. 이를 통해 보다 일반적인 예외 핸들러에서 특정 타입의 예외를 처리할 수 있습니다.

다음은 예외 메시지와 타입을 가지는 간단한 사용자 정의 예외 클래스입니다.

 

[예제]

#include <exception>
#include <string>

class MyException : public std::exception
{
private:
    std::string message;
public:
    MyException(const std::string& msg) : message(msg) {}

    const char * what () const throw ()
    {
        return message.c_str();
    }
};

 

이 예외 클래스는 std::exception을 상속받고, std::string 타입의 message 멤버 변수를 가지고 있습니다. 이 메시지는 생성자를 통해 초기화되며, what() 함수를 통해 접근할 수 있습니다.

 

[예제]

try
{
    throw MyException("Something went wrong!");
}
catch(MyException& e)
{
    std::cerr << e.what() << '\n';
}
catch(std::exception& e)
{
    // Other errors
}

 

이제 try 블록에서 MyException을 던질 때, 특정 메시지를 전달할 수 있습니다. 그리고 이 메시지는 catch 블록에서 what() 함수를 통해 출력될 수 있습니다.

 

이처럼 사용자 정의 예외 클래스를 사용하면 예외가 발생한 상황을 보다 정확하게 표현할 수 있고, 예외를 적절하게 처리할 수 있습니다. 다양한 타입의 예외를 구분하고, 각각의 예외에 대한 적절한 처리를 구현하는 것은 프로그램의 안정성과 유지보수성을 높이는 데 매우 중요합니다.

 

2) 사용자 정의 예외 클래스의 메서드 구현

사용자 정의 예외 클래스를 생성할 때 클래스의 메서드를 어떻게 구현하는지에 대해 알아보겠습니다. 일반적으로 C++의 사용자 정의 예외 클래스는 std::exception 클래스를 상속받고 필요한 메서드를 추가합니다. 

 

이러한 예외 클래스의 중요한 메서드 중 하나는 what()입니다. what() 메서드는 예외가 발생했을 때 출력될 메시지를 반환합니다. 이 메서드는 std::exception에서 상속되며, 사용자 정의 예외 클래스에서 이를 재정의할 수 있습니다.

 

[예제]

class MyException : public std::exception
{
private:
    std::string message_;
public:
    MyException(const std::string& message) : message_(message) {}

    virtual const char* what() const throw()
    {
        return message_.c_str();
    }
};

 

위의 코드에서, MyException 클래스는 std::exception 클래스를 상속받고 있습니다. what() 메서드를 재정의하여, message_ 문자열을 반환하도록 하였습니다. throw() 키워드는 이 메서드가 예외를 던지지 않을 것임을 보증합니다.

 

이렇게 정의한 MyException 클래스는 다음과 같이 사용할 수 있습니다.

 

[예제]

try {
    throw MyException("This is a custom exception!");
} catch (const MyException& e) {
    std::cout << e.what() << std::endl;
} catch (const std::exception& e) {
    std::cout << "Other exceptions" << std::endl;
}

 

이 코드에서, try 블럭 내에서 MyException 객체를 던지고, 이를 catch 블록에서 잡아냅니다. 그리고 e.what()을 통해 예외 메시지를 출력합니다.

 

기억해야 할 것은 사용자 정의 예외 클래스에서 std::exception의 what() 메서드를 재정의하여 자신만의 메시지를 반환하도록 설정할 수 있다는 점입니다. 이렇게 하면 코드의 가독성이 높아지며 디버깅 과정에서 문제를 쉽게 파악할 수 있습니다.

 

8.3.2. 예외 클래스를 활용한 예외 처리

사용자 정의 예외 클래스를 만든다면 그것을 어떻게 사용하는지를 이해하는 것이 중요하겠죠. 아래에서 MyException 클래스를 사용하는 방법을 더욱 자세히 살펴보도록 하겠습니다.

 

이전에 정의한 MyException 클래스를 이용해 예외를 던지고 처리해보겠습니다.

사용자 정의 예외를 사용하는 것은 std::exception과 같은 표준 예외를 사용하는 것과 비슷합니다. 단지, 표준 예외가 아닌 우리가 정의한 예외를 던지는 것뿐입니다.

 

[예제]

try {
    throw MyException("An error occurred!");
} catch (const MyException& e) {
    std::cout << "Caught an exception: " << e.what() << std::endl;
} catch (...) {
    std::cout << "Caught an unknown exception" << std::endl;
}

 

위의 코드에서, try 블럭블록 내에서 MyException을 던지고 있습니다. catch 블록에서는 이를 잡아냅니다. MyException 객체를 catch 하는 부분에서 e.what()를 사용하여 예외 메시지를 출력하고 있습니다.

 

그런데 여기서 주목할 점은 catch (...)입니다. 이것은 모든 타입의 예외를 잡을 수 있는 일종의 '만능' 예외 처리기입니다. 이것이 있다면, 위에서 처리하지 못한 예외가 있다면 이곳에서 잡힐 것입니다. 따라서 보통은 여러 catch 블록이 있는 경우 마지막에 위치하게 됩니다.

 

사용자 정의 예외 클래스를 사용하면, 프로그램의 예외 처리를 더욱 세밀하게 할 수 있습니다. 예를 들어, 여러분이 파일을 열거나 네트워크 연결을 설정하는 등의 작업을 수행하는 클래스를 작성하고 있다면, 각각의 작업에 대한 고유한 예외 클래스를 만들 수 있습니다. 이렇게 하면 어떤 작업에서 예외가 발생했는지 쉽게 파악할 수 있습니다.


8.4. 예외의 전파와 예외 명세

예외의 전파(exception propagation)는 함수나 메서드에서 발생한 예외가 호출자로 전달되는 과정을 말합니다. 즉, 함수 내에서 발생한 예외가 잡히지 않으면, 이 함수를 호출한 곳으로 예외가 '전파'됩니다. 

예외 명세(exception specification)는 C++에서 함수가 던질 수 있는 예외를 지정하는 기능입니다. 하지만 이 기능은 C++11 이후로 비권장(deprecated)되었고, C++17에서는 제거되었습니다. 이로 인해 현대 C++에서는 예외 명세를 사용하지 않습니다. 

8.4.1. 예외의 전파 이해하기

"예외의 전파"는 발생한 예외가 처리되지 않으면 호출된 함수에서 호출한 함수로 계속 전달되는 것을 의미합니다. 이러한 동작은 우리가 함수 스택을 거슬러 올라가면서 문제를 해결할 수 있도록 돕습니다. 

[예제]

#include <iostream>

void f1() {
    throw "exception occurred in f1()";
}

void f2() {
    f1();
}

void f3() {
    f2();
}

int main() {
    try {
        f3();
    } catch(const char* e) {
        std::cerr << e << std::endl;
    }
    return 0;
}

 

위 코드에서는 f1() 함수에서 예외가 발생하고, f2()와 f3()는 이를 처리하지 않아, 예외는 main() 함수의 catch 블록으로 전달됩니다.

 

위와 같이, 예외를 처리하지 않으면 호출 스택을 거슬러 올라가게 됩니다. 이러한 점이 예외 처리의 중요한 부분이며, 프로그램의 어느 부분에서든 예외를 잡아 처리할 수 있음을 보장합니다. 

 

이해를 돕기 위해, 각 함수에 예외 처리 코드를 추가해 봅시다. 각 함수에서 예외를 잡아 다른 예외를 던지도록 할 수 있습니다. 이렇게 함으로써 각 단계에서 추가 정보를 예외에 포함시킬 수 있습니다. 

 

[예제]

#include <iostream>
#include <stdexcept>

void f1() {
    throw std::runtime_error("exception occurred in f1()");
}

void f2() {
    try {
        f1();
    } catch(const std::exception& e) {
        throw std::runtime_error(std::string("f2() caught an exception: ") + e.what());
    }
}

void f3() {
    try {
        f2();
    } catch(const std::exception& e) {
        throw std::runtime_error(std::string("f3() caught an exception: ") + e.what());
    }
}

int main() {
    try {
        f3();
    } catch(const std::exception& e) {
        std::cerr << "main() caught an exception: " << e.what() << std::endl;
    }
    return 0;
}

 

이제 각 함수가 예외를 잡아 새로운 예외를 던지고 있습니다. 이를 통해 함수 간 예외의 전파를 명확하게 확인할 수 있습니다. 이렇게 하는 것이 바람직한 방법은 아닙니다. 그러나 예외의 전파와 관련된 개념을 이해하는 데 도움이 됩니다. 

 

각 함수에서 예외를 잡고 다른 예외를 던지는 것은 좋지 않은 방법이라고 말씀드렸는데, 그 이유는 이렇게 하면 중요한 정보가 손실되기 쉽고 예외의 본래 목적을 흐릴 수 있기 때문입니다. 보통은 예외를 잡아서 처리하거나, 잡지 못한 예외는 상위 함수로 전달하는 것이 일반적인 패턴입니다.

 

이제 예외가 전파되는 방식을 이해했으니, 실제로 발생한 예외에 대한 처리 방법을 더 잘 이해할 수 있습니다. 예외를 던지는 것은 예외 상황이 발생했음을 표시하는 데 사용되며, 이를 던지는 코드는 예외를 처리하는 방법을 결정하지 않습니다. 대신, 예외를 처리하는 것은 예외를 받는 코드의 책임입니다.

 

따라서, 예외를 던지는 코드와 예외를 처리하는 코드를 분리하면서, 예외의 전파를 통해 호출 스택을 통한 예외 처리를 가능하게 합니다. 이러한 구조는 예외 처리의 독립성을 제공하며, 또한 소프트웨어의 결함을 줄이고 프로그램의 안정성을 향상시키는 데 기여합니다.

 

이해를 돕기 위한 마지막 예제로, 함수 f1()에서 발생한 예외를 f3()에서 처리하는 경우를 살펴봅시다. f2()에서 예외를 처리하지 않아, 이 예외는 f3()까지 전파되며 f3()에서 처리됩니다.

 

[예제]

#include <iostream>
#include <stdexcept>

void f1() {
    throw std::runtime_error("exception occurred in f1()");
}

void f2() {
    f1();
}

void f3() {
    try {
        f2();
    } catch(const std::exception& e) {
        std::cerr << "f3() caught an exception: " << e.what() << std::endl;
    }
}

int main() {
    f3();
    return 0;
}


이처럼 C++에서는 예외의 전파를 통해 효과적인 예외 처리 전략을 구축할 수 있습니다. 이러한 접근 방식은 코드의 각 부분이 자신의 책임 범위 내에서 문제를 해결하도록 하여 코드의 유지 보수성과 안정성을 높입니다.

 

1) 예외 전파의 이해

"예외 전파"는 한 함수에서 예외가 발생하고 이를 처리하지 않으면, 해당 예외가 그 함수를 호출한 함수로 "전파"되는 것을 의미합니다. 이런 방식으로, 예외는 적절한 catch 블록을 찾아 함수 호출 스택을 거슬러 올라갑니다.

[예제]

#include <iostream>

void funcA() {
    throw "Exception occurred in funcA()";
}

void funcB() {
    funcA();
}

int main() {
    try {
        funcB();
    } catch(const char* e) {
        std::cerr << e << std::endl;
    }
    return 0;
}

 

이 예제에서 funcA()에서 예외가 발생했지만 funcA() 내에서는 그 예외를 처리하지 않습니다. 대신, 예외는 funcB()로 전파되며, funcB()도 예외를 처리하지 않기 때문에 예외는 마침내 main() 함수의 catch 블록에 도달합니다.

 

이것이 예외 전파의 기본적인 원리입니다. 함수 호출 스택을 거슬러 올라가면서 적절한 예외 처리기를 찾는 것이죠.

 

이러한 특성은 런타임 에러를 우아하게 처리하는 데 매우 유용합니다. 어느 함수에서든 예외가 발생할 수 있지만, 모든 함수가 그 예외를 처리할 필요는 없습니다. 대신, 예외는 종종 더 높은 수준에서 처리됩니다.

 

함수 간에 예외를 전파하는 방법에 대해 더 알아보기 위해, 각 함수가 예외를 잡아 다른 예외를 던지는 코드를 추가해보겠습니다.

 

[예제]

#include <iostream>
#include <stdexcept>

void funcA() {
    throw std::runtime_error("Exception occurred in funcA()");
}

void funcB() {
    try {
        funcA();
    } catch(const std::exception& e) {
        throw std::runtime_error(std::string("funcB() caught an exception: ") + e.what());
    }
}

int main() {
    try {
        funcB();
    } catch(const std::exception& e) {
        std::cerr << "main() caught an exception: " << e.what() << std::endl;
    }
    return 0;
}

 

이 예제에서, funcB()는 funcA()에서 발생한 예외를 잡아 새로운 예외를 던지고 있습니다. 이를 통해 함수 간 예외의 전파를 명확하게 확인할 수 있습니다. 이렇게 함으로써 각 단계에서 추가 정보를 예외에 포함시킬 수 있습니다.

 

2) 예외 전파를 제어하는 방법

예외 전파를 제어하는 방법은 두 가지가 있습니다. 첫째는 예외를 잡아 처리하는 것이고, 둘째는 예외를 더 상위의 함수로 전달하는 것입니다. 

 

첫 번째 방법인 예외를 잡아 처리하는 것은 catch 블록을 사용하여 수행됩니다.

 

[예제]

#include <iostream>

void funcA() {
    throw "Exception occurred in funcA()";
}

void funcB() {
    try {
        funcA();
    } catch(const char* e) {
        std::cerr << "funcB() caught an exception: " << e << std::endl;
    }
}

int main() {
    funcB();
    return 0;
}

 

이 예제에서 funcB()는 funcA()에서 발생한 예외를 잡습니다. 따라서 예외는 main() 함수로 전파되지 않습니다.

 

하지만 때로는 예외를 그냥 잡는 것이 아니라, 처리를 한 후에 다시 던지는 것이 좋을 수 있습니다. 이를 통해 예외 정보에 추가적인 컨텍스트를 제공하거나, 호출 스택을 거슬러 올라가는 도중에 필요한 정리 작업을 수행할 수 있습니다. 이런 경우에는 throw 문을 사용하여 예외를 다시 던질 수 있습니다.

 

[예제]

#include <iostream>
#include <stdexcept>

void funcA() {
    throw std::runtime_error("Exception occurred in funcA()");
}

void funcB() {
    try {
        funcA();
    } catch(const std::exception& e) {
        std::cerr << "funcB() caught an exception: " << e.what() << std::endl;
        throw;  // rethrow the exception
    }
}

int main() {
    try {
        funcB();
    } catch(const std::exception& e) {
        std::cerr << "main() caught an exception: " << e.what() << std::endl;
    }
    return 0;
}

 

이 예제에서 funcB()는 funcA()에서 발생한 예외를 잡은 후, 그 예외를 다시 던지고 있습니다. 따라서 예외는 main() 함수의 catch 블록까지 전파됩니다. throw 문에 예외 객체를 명시하지 않으면, 현재 처리 중인 예외가 재발생합니다. 이 기능을 사용하여 예외 처리기에서 처리하지 못한 예외를 다른 곳에서 처리하도록 할 수 있습니다.

 

종합하면, 예외를 제어하는 방법은 두 가지입니다. 하나는 예외를 catch로 잡아서 처리하는 것이고, 다른 하나는 예외를 재발생시켜서 상위 함수로 전파하는 것입니다. 이 두 가지 방법을 잘 사용하면 프로그램의 예외 처리를 더욱 효과적으로 할 수 있습니다. 

 

예외를 잘 처리하는 것은 프로그램의 안정성과 가독성, 유지 관리성에 큰 영향을 미칩니다. try-catch 블록을 적절하게 사용하여 예외 상황을 처리하면 프로그램이 예외 상황에 더욱 강력하게 대응할 수 있습니다. 예외를 재발생시키는 것은 프로그램의 동작을 더욱 통제할 수 있게 해주는 강력한 도구입니다.

 

하나의 예외 처리기에서 처리할 수 없는 예외가 발생하면, 그 예외를 재발생시켜서 다른 예외 처리기에 처리하도록 할 수 있습니다. 이는 프로그램의 다양한 부분에서 동일한 예외를 처리할 수 있게 해 주며, 예외 처리의 중복을 줄이고 코드의 가독성을 향상합니다.

 

또한, 예외를 재발생시키는 것은 예외의 원인을 더욱 명확하게 해줍니다. 예외가 발생한 함수 내에서 예외를 잡아서 처리하면, 그 예외의 원인이 무엇인지 명확하게 알기 어려울 수 있습니다. 그러나 예외를 재발생시키면, 그 예외의 원인이 무엇인지 더욱 명확하게 파악할 수 있습니다.

 

결론적으로, 예외 전파를 제어하는 방법은 프로그램의 예외 처리를 더욱 효과적으로 하고, 프로그램의 안정성과 가독성, 유지 관리성을 향상하는 중요한 도구입니다. 이를 통해 우리는 프로그램에서 발생할 수 있는 다양한 예외 상황에 대응할 수 있습니다.

 

8.4.2. 함수에서 예외 명세하기

"함수에서 예외 명세하기"란 함수 선언 부분에서 해당 함수가 어떤 종류의 예외를 던질 수 있는지 명시하는 것을 의미합니다. 이를 통해 함수의 사용자는 함수 호출에서 발생할 수 있는 예외에 대비할 수 있습니다. C++에서는 이를 지원하기 위해 'throw' 키워드를 사용합니다.

 

C++에서의 예외 명세는 다음과 같이 작성합니다:

 

[예제]

void function() throw(int);

 

위의 함수 선언은 'function'이 int 형태의 예외를 던질 수 있음을 나타냅니다. 이는 호출하는 측이 이 함수에서 int 예외를 처리할 준비를 해야 함을 의미합니다. 또한, 이 함수가 int 이외의 예외를 던지게 되면 std::unexpected() 함수가 호출됩니다.

 

다른 예로, 어떤 예외도 던지지 않는 함수는 다음과 같이 명세할 수 있습니다:

 

[예제]

void function() throw();

 

이 경우, 함수가 어떤 예외도 던지지 않음을 보증합니다.

 

하지만 C++11 이후부터는 예외 명세가 deprecated 되었으며, 'noexcept'가 그 역할을 대체하게 되었습니다. 'noexcept'는 해당 함수가 예외를 던지지 않음을 보증합니다.

 

[예제]

void function() noexcept;

 

여기서 중요한 점은, 예외 명세는 함수의 실행 도중에 발생할 수 있는 예외를 미리 알리는 역할을 하는 것이지만, 실제로 예외가 발생하면 그 처리는 호출자의 책임입니다. 함수의 세부 구현부에서 예외를 던지면, 그 예외는 함수를 호출한 코드로 전파되며, 호출한 코드에서 해당 예외를 처리해야 합니다.

 

함수에서 예외를 명세하는 것은 코드의 안정성을 높이고, 함수의 사용자가 예외 처리에 대해 고려해야 할 부분을 명확하게 해줍니다. 이는 특히 라이브러리를 작성하거나, 여러 사람이 함께 작업하는 큰 프로젝트에서 매우 유용합니다.

 

8.4.3. 예외 명세의 유용성과 주의점

"예외 명세의 유용성과 주의점"에 대해 살펴보겠습니다.

 

예외 명세는 함수가 던질 수 있는 예외의 종류를 명시하는 기능이며, C++에서 throw 또는 noexcept 키워드를 통해 명세합니다. 이는 함수의 사용자에게 예외에 대비할 수 있는 정보를 제공하며, 코드의 가독성과 안정성을 높이는 데 도움이 됩니다.

 

[예제]

void function() throw(int);

 

위 예제에서 function 함수는 int 타입의 예외를 던질 수 있음을 나타냅니다. 이를 통해 함수 사용자는 해당 함수를 호출할 때 int 타입의 예외 처리를 준비할 수 있습니다.

 

그러나, 예외 명세에는 주의해야 할 점들이 있습니다.

 

첫째, 예외 명세는 함수가 던질 수 있는 예외를 보장하는 것이지만, 실제로 그 예외를 처리하는 것은 호출하는 측의 책임입니다. 예외 명세는 예외의 발생 가능성에 대해 알려주는 것이지, 예외를 처리하는 방법을 제공하지는 않습니다. 예외 처리에 대한 책임은 여전히 함수를 호출하는 코드에 있습니다.

 

둘째, C++11 이후로는 예외 명세가 deprecated 되었으며, 대신 noexcept 키워드가 도입되었습니다. noexcept는 해당 함수가 예외를 던지지 않음을 보증합니다. 예외 명세와는 반대로, noexcept는 예외를 던지지 않음을 보증하므로, 함수가 예외를 던지게 되면 std::terminate()가 호출되어 프로그램이 종료됩니다. 따라서 noexcept를 사용할 때는 해당 함수가 정말로 예외를 던지지 않음을 확신할 수 있어야 합니다.

 

[예제]

void function() noexcept;

 

이 함수는 예외를 던지지 않음을 보증합니다. 만약 이 함수가 예외를 던진다면, std::terminate() 함수가 호출되어 프로그램이 종료됩니다.

 

예외 명세는 코드의 가독성과 안정성을 높이는 데 도움을 주지만, 적절한 예외 처리는 함수를 호출하는 측에서 이루어져야 함을 기억해야 합니다. 또한, C++11 이후로는 예외 명세보다는 noexcept를 사용하는 것이 권장되므로, 예외를 던지지 않는 함수에는 noexcept를 사용하는 것이 바람직합니다.


8.5. 예외 안전성과 예외 중립성

"예외 안전성(exception safety)"은 예외가 발생했을 때도 프로그램이 정상적인 상태를 유지하도록 하는 프로그래밍의 방식입니다. 이를 위해 메모리 누수, 자원 누수, 데이터 손상 등을 방지하는 작업이 필요합니다.

"예외 중립성(exception neutrality)"은 함수나 메서드가 자신이 처리할 수 없는 예외를 호출자에게 전달하는 것입니다. 이는 예외의 전파를 허용함으로써, 예외 처리의 책임을 함수나 메서드에서 그 호출자로 이동시키는 방식입니다. 이렇게 함으로써 각 책임 수준에서 적절한 예외 처리가 가능합니다.

8.5.1. 예외 안전성의 레벨과 원칙

예외 안전성은 소프트웨어 설계와 구현에 있어 핵심적인 원칙 중 하나로, 다양한 레벨에 따라 이해할 수 있습니다. C++에서는 특히 네 가지의 레벨을 다룹니다:

1) 실패 불가능 (No-fail) / 신뢰할 수 있는 최종 동작 (Commit-or-rollback) 원칙

이 원칙은 함수가 예외를 던지지 않도록 하는 최고 레벨의 예외 안전성을 의미합니다. 함수가 신뢰할 수 있는 최종 동작을 보장하거나, 아니면 아무 일도 일어나지 않게끔 해야 합니다. 즉, 함수는 항상 성공하거나, 실패하면 원래 상태로 롤백해야 합니다.

[예제]

void noFailFunction() noexcept {
    // 예외를 던지지 않는 동작
}


2) 강력한 안전성 (Strong exception safety)

이 원칙은 함수가 예외를 던질 경우에도 프로그램의 상태가 변경되지 않도록 하는 것입니다. 예외가 발생하더라도, 함수 호출 전 상태로 롤백하는 것을 보장합니다.

[예제]

void strongExceptionSafetyFunction() {
    int temp = value; // value를 보호하기 위해 임시 변수에 저장
    // 도중에 예외가 발생할 수 있는 코드
    value = temp; // 복구 코드
}


3) 기본 안전성 (Basic exception safety)

이 원칙은 함수가 예외를 던지더라도 프로그램이 불안정한 상태에 빠지지 않도록 보장하는 것입니다. 리소스 누수 없이 안전하게 예외를 처리할 수 있습니다.

[예제]

void basicExceptionSafetyFunction() {
    // 도중에 예외가 발생할 수 있는 코드
    // 자원을 반납하는 코드
}


4) 무안전성 (No exception safety)

이 원칙은 함수가 예외를 던질 경우 프로그램의 상태에 대해 아무런 보장도 하지 않는 것을 의미합니다. 함수가 예외를 던지면 프로그램 상태가 예측불가능해질 수 있습니다.

[예제]

void noExceptionSafetyFunction() {
    // 예외를 던지는 코드
    // 이후의 코드는 실행되지 않음
}

 

이 네 가지 레벨의 예외 안전성을 이해하고 적절히 활용하는 것은 강력한 예외 관리 전략을 수립하는 데 있어 중요한 요소입니다. 함수나 메소드가 어떤 예외 안전성 수준을 제공하는지 명시적으로 지정하고, 이에 따라 적절하게 예외를 처리하면 예외 발생 시 프로그램의 안정성을 유지할 수 있습니다.

각 레벨에서의 예외 처리 방법


1) 실패 불가능 (No-fail) / 신뢰할 수 있는 최종 동작 (Commit-or-rollback) 원칙

이 레벨의 예외 안전성을 보장하려면, 예외를 발생시키지 않는 코드만 사용해야 합니다. 이는 noexcept 키워드를 사용하여 함수가 예외를 던지지 않음을 명시적으로 지정하는 것을 포함합니다.

 

[예제]

void noFailFunction() noexcept {
    // 예외를 발생시키지 않는 코드
}


2) 강력한 안전성 (Strong exception safety)
강력한 예외 안전성을 보장하려면, 예외가 발생하더라도 프로그램의 상태가 바뀌지 않도록 해야 합니다. 일반적으로 이는 모든 동작을 수행하기 전에 변경을 위한 임시 복사본을 만들고, 모든 동작이 성공적으로 완료된 후에만 실제 값을 변경하는 방식으로 달성됩니다.

[예제]

void strongExceptionSafetyFunction() {
    int temp = value; // 임시 복사본 만들기
    // 여기서 예외가 발생할 수 있는 코드
    value = temp; // 성공적으로 완료된 후에만 실제 값을 변경
}


3) 기본 안전성 (Basic exception safety)

기본 예외 안전성을 보장하려면, 예외가 발생하더라도 리소스 누수 없이 안전하게 예외를 처리할 수 있어야 합니다. 이는 대개 자원 해제 코드를 적절히 배치하여 달성합니다.

 

[예제]

void basicExceptionSafetyFunction() {
    // 여기서 예외가 발생할 수 있는 코드
    // 필요한 경우 자원 해제 코드
}

 

4) 무안전성 (No exception safety)

무안전성 수준에서는 함수가 예외를 던질 경우 프로그램의 상태에 대해 아무런 보장도 하지 않습니다. 이 경우, 예외 처리 방법에 대한 명확한 가이드라인은 없으나, 가능한 한 빠르게 문제를 해결하거나 프로그램을 종료하는 것이 일반적입니다.

[예제]

void noExceptionSafetyFunction() {
    // 예외를 발생시키는 코드
    // 프로그램 상태는 불안정해질 수 있음
}

 

이러한 각 예외 안전성 레벨에서의 예외 처리 방법을 이해하면, 예외 발생 시 프로그램의 안정성을 유지하고, 예상치 못한 문제에 효과적으로 대응할 수 있습니다. 그럼에도 불구하고, 항상 특정 작업에 가장 적합한 예외 안전성 레벨을 선택하는 것이 중요합니다.

 

8.5.2. 예외 중립성의 필요성

예외 중립성이란 무엇일까요? 이것은 예외를 던질 수 있는 코드가 예외를 던지지 않는 코드에 영향을 미치지 않도록 하는 원칙입니다. 다시 말해, 함수가 예외를 직접 처리하고, 그 결과를 반환 값으로 제공하거나, 실패를 나타내는 다른 방법을 사용하여 호출자에게 전달하는 것을 의미합니다.

 

C++에서 이것은 특히 중요한데, 예외를 던질 수 있는 코드와 그렇지 않은 코드가 혼합될 수 있기 때문입니다. 예외 중립적인 함수는 예외를 던지는 함수를 호출하더라도 호출자에게 예외를 전파하지 않습니다. 이것은 호출자가 예외를 처리할 준비가 되어 있지 않거나, 예외를 던지지 않는 코드에서 호출되는 경우에 특히 유용합니다.

 

예를 들어, 아래 C++ 코드는 예외 중립적입니다:

 

[예제]

int exceptionNeutralFunction() noexcept {
    try {
        // 여기서 예외가 발생할 수 있는 코드
    } catch (...) {
        // 예외를 처리하고, 호출자에게 예외 없이 실패를 알립니다.
        return -1;
    }

    // 성공적으로 완료되면 0을 반환합니다.
    return 0;
}

 

이 예제에서 exceptionNeutralFunction 함수는 예외를 던지지 않는다는 것을 noexcept 키워드를 통해 명시합니다. 그 안에서 예외가 발생할 수 있는 코드를 try 블록으로 감싸서 예외를 즉시 잡아냅니다. 만약 예외가 발생하면, 함수는 예외를 처리하고 실패를 호출자에게 알리는 방법으로 -1을 반환합니다. 이렇게 하면, 함수의 호출자는 예외를 걱정하지 않고 이 함수를 안전하게 호출할 수 있습니다.

 

이렇게 하면 어떤 이점이 있을까요? 첫째, 예외 중립적인 함수는 예외를 던지는 코드와 안전하게 혼용될 수 있습니다. 즉, 호출자가 예외 처리에 대해 걱정할 필요가 없습니다. 둘째, 예외 중립적인 함수는 예외를 던지지 않으므로, 예외에 대한 오버헤드 없이 성능을 최적화할 수 있습니다. 이는 특히 성능에 민감한 애플리케이션에서 중요합니다.

 

따라서 예외 중립성은 함수를 보다 안정적이고 예측 가능하게 만들어 줍니다. 하지만, 이 원칙을 적용하려면 코드를 신중하게 설계하고 관리해야 하며, 예외를 적절히 처리하고 실패를 적절하게 전달하는 방법을 찾아야 합니다. 이것이 바로 예외 중립성의 필요성입니다.

 

8.5.3. 예외 안전성과 예외 중립성을 유지하는 전략

우리는 이미 예외 안전성과 예외 중립성에 대해 배웠습니다. 이제 이 두 가지 원칙을 유지하기 위한 몇 가지 전략에 대해 알아봅시다.

 

  • 리소스 관리를 위한 RAII 사용 : C++에서 가장 효과적인 방법 중 하나는 Resource Acquisition Is Initialization (RAII) 기법을 사용하는 것입니다. 이 기법은 객체가 리소스를 소유하고, 객체가 범위를 벗어날 때 소멸자에서 리소스를 자동으로 해제하도록 합니다. 이렇게 하면 예외가 발생하더라도 리소스 누수를 걱정할 필요가 없습니다.

 

[예제]

void foo() {
    std::ifstream file("example.txt"); // 파일 열기
    // ... 파일을 사용하는 코드 ...
} // 여기서 file 객체가 범위를 벗어나면서 소멸자가 호출되고, 파일이 자동으로 닫힙니다.

 

  • 예외를 던지지 않는 연산을 사용 : 가능하면 예외를 던지지 않는 연산을 사용하는 것이 좋습니다. 이렇게 하면 코드가 더 안정적이며 예외 안전성을 보장하는 데 도움이 됩니다.
  • 스마트 포인터 사용 : 스마트 포인터를 사용하면 메모리 누수를 방지하고, 코드의 안전성과 예외 안전성을 높일 수 있습니다.

[예제]

void foo() {
    std::unique_ptr<MyObject> ptr(new MyObject());
    // ... ptr을 사용하는 코드 ...
} // 여기서 ptr 객체가 범위를 벗어나면서 소멸자가 호출되고, MyObject가 자동으로 삭제됩니다.

 

  • 예외 중립적인 함수 설계 : 함수를 설계할 때는 예외 중립적이도록 해야 합니다. 즉, 함수 내에서 예외를 적절히 처리하고, 실패를 나타내는 적절한 반환 값을 사용해야 합니다.
  • 예외 명세 제공 : 함수가 던질 수 있는 예외를 명시적으로 지정하는 것이 좋습니다. 이렇게 하면 함수의 사용자는 어떤 예외를 처리해야 하는지 명확히 알 수 있습니다.

이러한 전략들을 통해 C++ 코드는 예외가 발생하더라도 안정적으로 동작하고, 예외를 적절히 처리하여 프로그램의 안정성과 신뢰성을 높일 수 있습니다.


8.6. 오류 처리 기법

오류 처리 기법은 프로그램의 안정성과 신뢰성을 보장하는 데 필수적입니다. C/C++에서는 다양한 오류 처리 기법이 사용됩니다.

 

  • 리턴 코드 사용 : 함수는 오류를 나타내는 특정 값을 반환할 수 있습니다.
  • 예외 사용 : C++에서는 예외를 던져서 오류를
  • 알릴 수 있습니다.오류 코드 설정 : 전역 오류 코드를 설정하여 오류를 표시할 수 있습니다.
  • 오류 처리 함수 호출 : 특정 오류 처리 함수를 호출하여 오류를 알릴 수 있습니다.

이러한 기법 중 어떤 것을 사용할지는 프로그램의 요구 사항과 특성에 따라 달라집니다.

 

8.6.1. 오류 코드와 오류 반환

먼저, C/C++에서 오류 코드와 오류 반환에 대해 이야기하기 전에, 이 두 용어에 대해 이해해야 합니다.

  • 오류 코드(Error Code)는 특정 오류 상황을 나타내는 일종의 '표식'입니다. 함수나 메서드가 예상되는 작업을 수행하지 못했을 때, 이를 호출한 코드에게 어떤 문제가 발생했는지 알려주는 방법 중 하나입니다.
  • 오류 반환(Error Return)은 함수가 예상치 못한 문제를 발견했을 때, 오류 코드를 반환하여 이 문제를 호출자에게 알리는 것을 말합니다.

오류 코드와 오류 반환은 상호 보완적입니다. 함수가 오류를 발견했을 때, 오류 코드를 설정하고, 이 오류 코드를 반환하는 것이 바로 오류 반환입니다.

 

예를 들어, C에서 파일을 열어서 작업을 수행하는 함수가 있다고 가정해 보겠습니다. 이 함수는 파일을 성공적으로 열었는지 여부를 확인할 필요가 있습니다. 그렇지 않으면, 오류 코드를 설정하고 반환해야 합니다.

 

[예제]

#include <stdio.h>

// 함수는 오류 코드를 반환합니다.
int open_file(const char *filename) {
    FILE *file = fopen(filename, "r");
    if (file == NULL) {
        // 파일을 열지 못했습니다.
        // 오류 코드를 반환합니다.
        return -1;
    }

    // 파일 작업을 수행합니다.
    // ...

    fclose(file);
    // 모든 작업이 성공적으로 수행되면 0을 반환합니다.
    return 0;
}

int main() {
    int result = open_file("non_existent_file.txt");
    if (result == -1) {
        printf("Failed to open file.\n");
    }
    // ...
    return 0;
}

 

위 예제에서는, open_file 함수가 파일을 성공적으로 열지 못했을 때 -1이라는 오류 코드를 반환합니다. 이는 호출자에게 "파일을 열지 못했습니다"라는 정보를 제공합니다. 호출자는 이 오류 코드를 확인하고 적절한 대응을 할 수 있습니다.

 

C++에서는 예외를 사용하여 오류를 처리하며, 오류 코드 반환 방식을 사용하는 경우가 덜합니다. 하지만, 예외를 사용하지 않거나 사용할 수 없는 상황에서는 오류 코드와 오류 반환 방식이 유용하게 사용됩니다.

 

8.6.2. C++에서의 assert 사용하기

C++에서 assert는 코드의 특정 조건이 참이라고 가정할 때 사용합니다. 이것은 주로 디버깅 과정에서 사용되며, 조건이 거짓이면 프로그램은 즉시 종료되고 오류 메시지가 출력됩니다. 이렇게 함으로써 프로그래머는 프로그램에서 무엇이 잘못되었는지 빠르게 파악할 수 있습니다.

 

C++에서 assert를 사용하려면 먼저 <cassert> 헤더를 포함해야 합니다.

 

[예제]

#include <cassert>

 

assert는 매크로 함수이며, 사용법은 아래와 같습니다.

[예제]

assert(표현식);

 

여기서 '표현식'은 평가한 결과가 참이어야 하는 표현식입니다.

 

이제 assert를 사용하는 간단한 예제를 보겠습니다.

 

[예제]

#include <cassert>

int main() {
    int x = 5;
    x += 5;

    // x가 10인지 검사합니다. 만약 아니라면, 프로그램은 여기에서 죽습니다.
    assert(x == 10);

    return 0;
}

위의 예제에서는 x가 10인지 검사하는데, 만약 x가 10이 아니라면, 프로그램은 즉시 종료되고 오류 메시지를 출력합니다.

그러나 assert는 프로그램의 정상적인 오류 처리 방법으로 사용되어서는 안 됩니다. 왜냐하면 assert는 디버깅을 위한 도구로, 릴리스 빌드에서는 비활성화되기 때문입니다. 즉, 프로그램이 고객에게 배포되면 assert는 작동하지 않습니다. 따라서 오류 처리를 위한 로직에서는 assert를 사용하지 않는 것이 좋습니다.

그럼에도 불구하고, assert는 코드가 정확하게 작동하는지 검증하고, 잠재적인 버그를 빠르게 찾아낼 수 있으므로 매우 유용한 도구입니다. 당신이 작성하는 코드의 조건을 명확하게 이해하고, 그 조건을 assert를 사용하여 검증함으로써, 코드의 안정성과 품질을 크게 향상할 수 있습니다.

 

8.6.3. 예외 처리와 오류 처리의 차이점

예외 처리와 오류 처리는 개발자가 코드 내에서 발생할 수 있는 문제를 어떻게 다룰지 결정하는 중요한 방법입니다. 이들은 비슷해 보일 수 있지만, 핵심적인 차이점이 있습니다.

 

오류 처리는 코드의 실행 도중 문제가 발생했을 때, 프로그램이 계속 실행될 수 있도록 하는 방법입니다. 오류 처리는 일반적으로 오류 코드를 반환하거나, 오류 상태를 변경하거나, 오류 로그를 작성하는 등의 방법으로 이루어집니다. C 언어는 예외 처리 메커니즘이 없으므로 오류 코드 반환 방식을 주로 사용합니다.

 

[예제]

#include <stdio.h>

int divide(int a, int b) {
    if (b == 0) {
        printf("Error: Division by zero is undefined.\n");
        return -1;
    }
    return a / b;
}

int main() {
    int result = divide(10, 0);
    if (result == -1) {
        // 오류 처리...
        return -1;
    }
    printf("Result: %d\n", result);
    return 0;
}


반면에 예외 처리는 프로그램의 정상적인 흐름에서 벗어난 상황을 처리하는 방법입니다. 이는 예외가 발생했을 때 프로그램의 흐름을 중단하고, 예외를 처리하는 코드로 제어를 이동시키는 방식으로 작동합니다. C++에서는 try, catch, throw를 이용한 예외 처리 방식을 제공합니다.

[예제]

#include <iostream>
#include <stdexcept>

int divide(int a, int b) {
    if (b == 0) {
        throw std::invalid_argument("Error: Division by zero is undefined.");
    }
    return a / b;
}

int main() {
    try {
        int result = divide(10, 0);
        std::cout << "Result: " << result << std::endl;
    } catch (const std::exception& e) {
        // 예외 처리...
        std::cerr << e.what() << std::endl;
        return -1;
    }
    return 0;
}

 

이 두 가지 접근 방식은 각각의 장점과 단점이 있으며, 상황에 따라 적절한 방법을 선택해야 합니다. 오류 처리는 코드의 흐름을 방해하지 않지만, 오류가 발생한 후에 즉시 처리해야 하는 경우에는 부적합할 수 있습니다. 예외 처리는 오류 상황을 즉시 처리할 수 있지만, 예외를 던지는 것은 리소스를 많이 소모하므로 너무 남발하지 않아야 합니다. 

 

1) 예외 처리와 오류 처리의 사용 시나리오

예외 처리와 오류 처리는 서로 다른 사용 시나리오에 따라 적절하게 사용될 수 있습니다. 상황에 따라 어떤 것을 사용할지 결정하는 것이 중요합니다. 

 

예외 처리의 사용 시나리오

예외 처리는 예기치 못한 오류나 예외 상황에 대응하기 위해 사용됩니다. 예외 처리는 예외가 발생할 가능성이 있는 코드 블록을 try 문으로 감싸고, catch 문을 사용하여 예외를 잡아 처리합니다.

 

C++에서는 다음과 같이 예외를 처리할 수 있습니다:

 

[예제]

#include <iostream>
#include <stdexcept>

void mightGoWrong() {
    bool error = ...; // 어떤 조건
    if (error) {
        throw std::runtime_error("Something went wrong!");
    }
}

int main() {
    try {
        mightGoWrong();
    } catch(const std::exception& e) {
        std::cerr << e.what() << std::endl;
        // 여기서 예외를 처리합니다.
    }
    return 0;
}

 

이런 경우, 예외 처리는 코드의 실행을 중단하고 즉시 예외를 처리하도록 해주므로, 특히 문제가 발생하면 즉시 대응해야 하는 상황에서 유용합니다.

 

오류 처리의 사용 시나리오

오류 처리는 프로그램이 오류 상태를 식별하고 대응할 수 있도록 하는 방법입니다. 오류 처리는 보통 함수에서 오류 코드를 반환하거나, 오류 상태를 변경하거나, 로그를 남기는 등의 방법을 통해 이루어집니다.

 

C에서는 다음과 같이 오류를 처리할 수 있습니다:

 

[예제]

#include <stdio.h>

int mightGoWrong() {
    bool error = ...; // 어떤 조건
    if (error) {
        return -1; // 오류 코드 반환
    }
    return 0; // 성공
}

int main() {
    int result = mightGoWrong();
    if (result == -1) {
        printf("An error occurred!\n");
        // 여기서 오류를 처리합니다.
        return -1;
    }
    return 0;
}

 

이런 경우, 오류 처리는 함수 호출자에게 오류를 알려주면서도 프로그램의 실행을 계속하게 해주므로, 오류 발생 후 프로그램이 계속 실행되어야 하는 상황에서 유용합니다.

 

결국, 예외 처리와 오류 처리 중 어느 것을 사용할지는 상황에 따라 달라집니다. 오류 처리는 코드의 흐름을 방해하지 않지만, 오류가 발생한 후에 즉시 처리해야 하는 경우에는 부적합할 수 있습니다. 예외 처리는 오류 상황을 즉시 처리할 수 있지만, 예외를 던지는 것은 리소스를 많이 소모하므로 너무 남발하지 않아야 합니다. 프로그램의 요구 사항과 상황을 고려하여 적절한 방법을 선택해야 합니다.

 

2) 예외 처리와 오류 처리의 장단점 비교

예외 처리와 오류 처리는 각각의 장단점이 있습니다. 이들의 특징을 이해하면 프로그래밍에서 어떤 상황에서 어떤 처리 방식을 사용할지 결정하는데 도움이 됩니다.

 

예외 처리의 장점

일관된 오류 처리: 예외는 일반적으로 프로그램의 실행 흐름을 방해하지 않고, 오류 상황을 처리하는 데 효과적인 방법을 제공합니다.

 

구조화된 오류 처리: 예외는 구조화된 방식으로 오류를 처리할 수 있게 해줍니다. try-catch 블록을 사용하여 예외를 처리하면 오류가 발생한 지점에서 바로 오류를 처리할 수 있습니다.

 

예외 처리의 단점
  • 비용 : 예외를 던지는 것은 리소스를 많이 사용하므로, 성능에 영향을 줄 수 있습니다.
  • 예측하기 어려움 : 예외는 언제 던져질지 예측하기 어렵습니다. 따라서 예외 처리는 코드의 복잡성을 높일 수 있습니다.

 

오류 처리의 장점
  • 성능 : 오류 코드를 반환하는 것은 비용이 많이 들지 않습니다.
  • 예측 가능성 : 오류 코드는 명시적이므로 오류 발생 가능성이 있는 코드를 쉽게 식별할 수 있습니다.

 

오류 처리의 단점
  • 일관성 없음 : 함수마다 다른 오류 코드를 반환할 수 있으므로, 일관성 있는 오류 처리를 위해 추가적인 노력이 필요합니다.
  • 흐름 방해 : 오류 코드를 확인하고 처리하기 위해 프로그램의 흐름이 중단될 수 있습니다.

이런 장단점을 고려하여, 각 상황에 가장 적합한 방법을 선택해야 합니다. 상황에 따라 예외 처리와 오류 처리를 적절히 혼용하는 것도 좋은 전략일 수 있습니다.

 

예를 들어, C++에서 예외와 오류 코드를 혼용하는 방법은 다음과 같습니다.

 

[예제]

#include <iostream>
#include <stdexcept>

int mightGoWrong() {
    bool error = ...; // 어떤 조건
    if (error) {
        throw std::runtime_error("Something went wrong!");
    }
    return 0;
}

int main() {
    try {
        int result = mightGoWrong();
        if (result != 0) {
            std::cerr << "An error occurred!\n";
        }
    } catch(const std::exception& e) {
        std::cerr << e.what() << std::endl;
    }
    return 0;
}

 

이 코드에서 mightGoWrong() 함수는 오류가 발생하면 예외를 던지거나 오류 코드를 반환할 수 있습니다. 이렇게 코드를 작성하면 예외 처리와 오류 처리의 장점을 모두 활용할 수 있습니다.

 

3) 효과적인 오류 처리 전략 구축하기

효과적인 오류 처리 전략을 구축하는 것은 중요합니다. 다음은 이를 위한 몇 가지 방법입니다.

 

  • 예외와 오류 코드를 적절하게 혼용하기 : 예외는 복잡한 오류 상황을 처리하는 데 효과적이지만, 자원이 많이 필요한 연산에는 부적합할 수 있습니다. 이런 상황에서는 오류 코드를 사용하는 것이 더 효과적일 수 있습니다. 반대로, 일반적인 오류 상황에서는 예외를 사용하는 것이 더 좋습니다. 적절하게 혼용하는 것이 중요합니다.


[예제]

#include <iostream>
#include <stdexcept>

enum ErrorCode {
    SUCCESS = 0,
    FAILURE = 1
};

ErrorCode mightGoWrong() {
    bool error = ...; // 어떤 조건
    if (error) {
        throw std::runtime_error("Something went wrong!");
    }
    return SUCCESS;
}

int main() {
    try {
        ErrorCode result = mightGoWrong();
        if (result != SUCCESS) {
            std::cerr << "An error occurred!\n";
        }
    } catch(const std::exception& e) {
        std::cerr << e.what() << '\n';
    }
    return 0;
}

 

  • 오류 처리는 미루지 않는다 : 오류가 발생하면 즉시 처리해야 합니다. 오류를 무시하거나 나중에 처리하려는 시도는 프로그램에 더 많은 문제를 초래할 수 있습니다.
  • 오류는 문서화한다 : 함수나 메서드는 어떤 오류를 발생시킬 수 있는지 명시적으로 문서화해야 합니다. 이는 오류 처리에 대한 명확한 이해를 제공하고, 예외를 처리하는 코드를 작성하는 데 도움이 됩니다.
  • 예외 안전성을 확보한다 : 예외가 발생할 때 프로그램의 상태가 손상되지 않도록 하기 위해 예외 안전성을 고려해야 합니다. 이를 위해 RAII(Resource Acquisition Is Initialization) 패턴을 사용하면 자원을 안전하게 관리할 수 있습니다.

[예제]

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

void processFile(const std::string& fileName) {
    std::ifstream file(fileName);
    if (!file.is_open()) {
        throw std::runtime_error("Failed to open the file!");
    }
    // 파일을 처리하는 코드...
}

 

이 방법들을 통해 효과적인 오류 처리 전략을 구축할 수 있습니다. 핵심은 오류를 적절히 처리하고, 프로그램의 안정성을 유지하는 것입니다. 

 

  • 오류 처리에 대한 실제 사례 분석 : '오류 처리에 대한 실제 사례 분석'은 핵심적인 개념을 실제 코드를 통해 이해하는 데 큰 도움이 됩니다. 아래에는 파일을 열고 읽는 간단한 C++ 프로그램에 대한 오류 처리의 실제 사례를 제시하겠습니다.

 

C++에서 파일을 열어 작업을 수행하는 경우, 여러 가지 오류가 발생할 수 있습니다. 파일이 존재하지 않거나, 읽기 권한이 없거나, 파일을 열었지만 도중에 문제가 발생할 수 있습니다. 이런 오류들을 어떻게 처리할 수 있는지 살펴보겠습니다.

[예제]

#include <iostream>
#include <fstream>
#include <string>

void readFile(const std::string& fileName) {
    std::ifstream file(fileName);
    
    // 파일이 제대로 열렸는지 확인
    if (!file.is_open()) {
        throw std::runtime_error("Unable to open the file.");
    }

    std::string line;
    while (getline(file, line)) {
        // 파일에서 라인을 읽는 도중 오류가 발생하면 예외를 던짐
        if (file.fail()) {
            throw std::runtime_error("An error occurred while reading the file.");
        }
        std::cout << line << '\n';
    }
}

int main() {
    try {
        readFile("test.txt");
    } catch (const std::exception& e) {
        std::cerr << "Caught an exception: " << e.what() << '\n';
    }
    return 0;
}

 

이 예제에서는 파일을 열고 그 내용을 읽는 작업에서 발생할 수 있는 두 가지 주요 오류를 처리합니다. 먼저 std::ifstream의 is_open 메서드를 사용하여 파일이 제대로 열렸는지 확인합니다. 만약 파일이 제대로 열리지 않았다면 std::runtime_error 예외를 발생시킵니다.

 

다음으로, 파일의 각 라인을 읽으면서 std::ifstream의 fail 메서드를 사용하여 읽기 작업 중에 오류가 발생했는지 확인합니다. 오류가 발생하면 역시 std::runtime_error 예외를 발생시킵니다.

 

마지막으로, 모든 파일 읽기 작업은 try 블록 내에서 수행되며, catch 블록에서 발생할 수 있는 예외를 처리합니다. 이렇게 함으로써, 오류가 발생하면 적절한 메시지를 출력하고 프로그램을 안전하게 종료할 수 있습니다.

 

이러한 실제 사례를 통해 오류 처리의 중요성과 구현 방법을 실제로 확인할 수 있습니다. 이와 같은 기법들을 다양한 상황에 적용하여 프로그램의 안정성과 신뢰성을 향상할 수 있습니다.

 

 

 

2023.05.30 - [GD's IT Lectures : 기초부터 시리즈/C, C++ 기초부터 ~] - [C/C++ 프로그래밍 : 중급] 7.가상 함수와 추상 클래스

 

[C/C++ 프로그래밍 : 중급] 7.가상 함수와 추상 클래스

Chapter 7. 가상 함수와 추상 클래스 가상 함수는 C++의 객체 지향 프로그래밍의 중요한 개념 중 하나입니다. 이는 기반 클래스에서 선언되고 파생 클래스에서 재정의 될 수 있는 함수를 가리킵니다

gdngy.tistory.com

 

반응형

댓글