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

[C/C++ 프로그래밍] 14. 예외 처리

by GDNGY 2023. 5. 16.

Chapter 14. 예외 처리

C/C++에서 예외 처리는 프로그램에서 예기치 않은 이벤트나 오류가 발생했을 때 이를 효과적으로 처리하는 방법을 말합니다. 이런 오류들은 파일을 열 수 없거나, 메모리를 할당할 수 없는 경우 등 다양한 상황에서 발생할 수 있습니다. C 언어는 내장된 예외처리 메커니즘이 없기 때문에, 일반적으로 오류코드를 반환하거나 전역 오류 변수인 'errno'를 설정하여 오류를 처리합니다. 반면에 C++에서는 'try', 'catch', 'throw' 키워드를 사용하여 예외를 던지고, 이를 잡아내는 구조화된 방식의 예외 처리를 지원합니다. 이를 통해 예외가 발생하면 적절한 처리를 수행하거나, 프로그램을 안전하게 종료할 수 있습니다. 이렇게 예외 처리를 통해 프로그램의 안정성과 신뢰성을 향상할 수 있습니다.

 

반응형

 


[Chapter 14. 예외 처리]


14.1. 예외 처리의 이해
14.1.1. 예외 처리의 개념
14.1.2. 예외 처리의 필요성

14.2. C언어에서의 예외 처리
14.2.1. 반환값을 이용한 예외 처리
14.2.2. setjmp, longjmp 함수를 이용한 예외 처리
14.2.3. errno, perror, strerror 함수를 이용한 에러 처리

14.3. C++에서의 예외 처리
14.3.1. try, catch, throw를 이용한 예외 처리
14.3.2. 예외 클래스 정의 및 사용

14.4. 예외 처리의 활용
14.4.1. 예외 처리를 활용한 안전한 코드 작성
14.4.2. 예외 처리의 성능과 효율

14.5. 예외 처리의 고급 주제
14.5.1. 예외의 재발생과 예외 명세
14.5.2. 사용자 정의 예외 처리

14.6. 예외 처리의 주의점
14.6.1. 예외 처리 시 주의할 사항
14.6.2. 예외 안전성


14.1. 예외 처리의 이해

예외 처리의 기본 개념과 그 필요성에 대해 설명합니다. 예외 처리는 프로그램 실행 중 발생할 수 있는 오류를 대비하여 프로그램의 안정성을 유지하고 예상치 못한 문제로부터 시스템을 보호하는 방법입니다. 이러한 예외 처리가 왜 필요한지, 그리고 이를 통해 어떻게 더 안정적인 소프트웨어를 만들 수 있는지에 대해 설명합니다.

14.1.1. 예외 처리의 개념

예외 처리의 개념은 프로그래밍에서 매우 중요한 주제입니다. 프로그래밍을 하다 보면 예상치 못한 상황이 발생할 수 있습니다. 예를 들어, 파일을 열려고 하는데 그 파일이 존재하지 않는 경우나, 메모리를 할당하려고 하는데 시스템에서 더 이상 메모리를 제공할 수 없는 경우 등이 있습니다. 이런 상황들을 '예외'라고 부르며, 이 예외 상황을 '처리'하는 것이 '예외 처리'입니다.

 

예외 처리는 프로그램의 안정성과 신뢰성을 보장하는 데 매우 중요합니다. 예외 처리 없이 프로그램을 작성하면, 예외 상황이 발생했을 때 프로그램이 예기치 않게 종료될 수 있습니다. 반면, 예외 처리를 잘해두면 예외 상황이 발생했을 때도 프로그램은 안정적으로 계속 실행될 수 있습니다.

 

예외 처리는 기본적으로 두 단계로 이루어집니다: 예외 탐지와 예외 처리. 예외 탐지는 문제가 발생한 것을 시스템이 감지하는 과정이며, 예외 처리는 이를 적절히 다루는 과정입니다.

 

이제 예외 처리의 개념에 대해 알아보았으니, 실제 예제를 통해 이해해 보겠습니다. 먼저 C언어의 예외 처리를 보겠습니다. C언어에서는 반환값을 이용해 예외를 처리합니다.

[예제]

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

int main() {
    FILE *fp = fopen("non_existent_file.txt", "r");
    if (fp == NULL) {
        printf("파일을 열 수 없습니다.\n");
        exit(1);
    }
    // 파일 처리 코드
    fclose(fp);
    return 0;
}


위 코드에서 'fopen' 함수는 파일을 열고 그 파일의 포인터를 반환합니다. 만약 파일이 존재하지 않아 열 수 없는 경우에는 NULL을 반환합니다. 따라서 'if (fp == NULL)' 문장은 파일이 제대로 열렸는지 확인하는 예외 처리 코드입니다.

이어서 C++의 예외 처리를 보겠습니다. C++에서는 try, catch, throw를 이용해 예외를 처리합니다.

[예제]

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

int main() {
    std::ifstream file;
    try {
        file.open("non_existent_file.txt");
        if (!file) {
            throw std::runtime_error("파일을 열 수 없습니다.");
        }
        // 파일 처리 코드
    } catch (const std::exception& e) {
        std::cerr << e.what() << '\n';
    }
    return 0;
}


위 코드에서는 'try' 블록 안에서 파일을 열고 있습니다. 만약 파일을 열지 못하면, 'throw' 문을 이용해 'std::runtime_error' 예외를 발생시킵니다. 이 예외는 'catch' 블록에서 잡아서 처리하고 있습니다. 이처럼 C++에서는 예외를 직접 던지고 잡는 방식으로 예외를 처리합니다.

이처럼 예외 처리는 프로그램의 안정성을 보장하는 데 중요한 역할을 합니다.

 

14.1.2. 예외 처리의 필요성

모든 프로그램은 예상하지 못한 문제에 직면할 수 있습니다. 이런 상황은 다양한 원인으로 발생할 수 있습니다. 파일이 예상과 다르게 없을 수 있고, 사용자로부터의 입력이 잘못될 수 있으며, 시스템 자체에서 문제가 발생할 수도 있습니다. 이렇게 프로그램 실행 도중 발생한 오류 상황을 예외(exception)라고 부르며, 이러한 예외를 잘 처리하는 것이 중요합니다.

예외 처리의 주요 목표는 프로그램의 안정성을 유지하고, 예상치 못한 상황에서도 프로그램이 적절하게 반응하는 것입니다. 예외 처리를 잘 구현하면, 문제가 발생했을 때 프로그램이 중단되는 대신, 적절한 조치를 취하거나 사용자에게 유용한 오류 메시지를 제공할 수 있습니다.

예외 처리가 없는 경우, 프로그램은 예외가 발생했을 때 정지하거나 예측 불가능한 동작을 보일 수 있습니다. 이는 사용자에게 혼란을 주며, 시스템의 안정성을 해칠 수 있습니다. 또한, 예외 처리를 하지 않으면 오류의 원인을 찾아내는 데 어려움을 겪을 수 있습니다.

다음은 C언어에서 예외 처리를 하지 않았을 때의 예입니다.

[예제]

#include <stdio.h>

int main() {
    FILE *fp = fopen("non_existent_file.txt", "r");
    // 파일 처리 코드
    fclose(fp);
    return 0;
}


위 코드에서는 파일을 열지만, 이 파일이 실제로 존재하는지 아닌지 확인하지 않습니다. 만약 이 파일이 존재하지 않는다면, 'fclose' 함수가 NULL 포인터를 참조하게 되어 프로그램이 비정상 종료하게 됩니다.

다음은 이에 대한 C++ 코드 예시입니다.

[예제]

#include <fstream>

int main() {
    std::ifstream file("non_existent_file.txt");
    // 파일 처리 코드
    return 0;
}


이 C++ 코드도 마찬가지로 파일이 존재하는지 확인하지 않습니다. 이 경우에도 파일이 없으면 예외가 발생하고 프로그램이 비정상 종료하게 됩니다.

반면, 앞서 살펴본 예외 처리가 있는 코드는 파일이 없는 경우에도 프로그램이 안정적으로 동작합니다. 따라서 예외 처리는 프로그램의 신뢰성을 높이는 데 중요한 역할을 합니다.

물론, 모든 예외를 예측하고 처리하는 것은 쉽지 않습니다. 하지만 가능한 한 많은 예외를 처리함으로써, 프로그램의 안정성을 높이고 사용자에게 더 나은 경험을 제공할 수 있습니다.


14.2. C언어에서의 예외 처리

C언어의 예외 처리 방법에 대해 다룹니다. C언어는 내장된 예외 처리 메커니즘이 없어 반환값 확인, setjmp/longjmp 함수, errno, perror, strerror 함수를 이용해 예외를 처리합니다. 이들을 이용해 안정적인 코드를 작성하는 방법에 대해 배워봅시다.

14.2.1. 반환값을 이용한 예외 처리

C언어는 예외 처리 메커니즘을 내장하고 있지 않습니다. 따라서 대부분의 경우 함수의 반환값을 확인하여 예외 상황을 처리합니다. 함수가 정상적으로 실행되었을 때와 문제가 발생했을 때 반환하는 값이 다르기 때문입니다. 예를 들어, 표준 라이브러리 함수인 fopen은 파일을 열지 못했을 때 NULL을 반환하고, malloc 함수는 메모리 할당에 실패하면 NULL을 반환합니다.

 

이런 방식으로 예외를 처리하는 것은 간단하지만, 프로그래머가 반환값을 항상 체크해야 하고, 실패 시에 적절한 조치를 취해야 한다는 단점이 있습니다. 하지만 이는 프로그램의 안정성을 높이는 데 중요한 역할을 합니다.

 

다음은 fopen 함수의 반환값을 확인하여 파일 열기 실패를 처리하는 예제입니다.

 

[예제]

#include <stdio.h>

int main() {
    FILE *fp = fopen("non_existent_file.txt", "r");
    if (fp == NULL) {
        perror("Error opening file");
        return -1;
    }
    // 파일 처리 코드
    fclose(fp);
    return 0;
}


위의 코드에서 fopen 함수가 NULL을 반환하면, 즉, 파일을 열지 못했을 때 perror 함수로 오류 메시지를 출력하고 프로그램을 종료합니다. 이처럼 반환값을 이용한 예외 처리는 C언어에서 가장 기본적이면서도 흔하게 사용되는 방법입니다.

그러나 이 방법은 모든 함수의 반환값을 체크해야 하기 때문에 코드가 복잡해질 수 있습니다. 또한 함수가 여러 가지 오류를 반환할 수 있는 경우, 반환값만으로는 정확한 오류 원인을 파악하기 어려울 수 있습니다. 이러한 문제를 해결하기 위해 C언어는 setjmp와 longjmp 함수를 제공합니다. 

 

14.2.2. setjmp, longjmp 함수를 이용한 예외 처리

setjmp와 longjmp는 C 언어의 비표준 함수로, C++에서는 사용할 수 없습니다. 이들 함수는 프로그램의 흐름을 비선형적으로 제어할 수 있게 해주는 기능을 합니다. 더 구체적으로 말하면, setjmp는 현재 실행 위치를 저장하고, longjmp는 setjmp로 저장된 위치로 제어 흐름을 되돌립니다. 이를 통해 예외 상황 발생 시 직접 호출 스택을 건너뛰고 에러 처리 코드로 점프할 수 있습니다.

 

하지만 이 함수들을 사용할 때는 주의가 필요합니다. 이 함수들은 호출 스택을 건너뛰어 점프하므로, 해당 범위에서 선언된 지역 변수들은 정리되지 않습니다. 따라서 메모리 누수 등의 문제가 발생할 수 있습니다.

다음은 setjmp와 longjmp 함수를 이용한 예외 처리 예제입니다.

 

[예제]

#include <stdio.h>
#include <setjmp.h>

jmp_buf env;

void function_that_might_fail() {
    if (/* 오류 발생 조건 */) {
        longjmp(env, 1);  // setjmp가 저장한 위치로 점프
    }
    // 정상 처리 코드
}

int main() {
    if (setjmp(env) == 0) {
        function_that_might_fail();
    } else {
        printf("Error occurred!\n");
    }
    return 0;
}


위의 코드에서 setjmp(env)는 현재 실행 위치를 env에 저장하고, 0을 반환합니다. function_that_might_fail 함수에서 오류가 발생하면 longjmp(env, 1)을 호출하여 setjmp(env)의 위치로 되돌아갑니다. 이때 setjmp(env)는 longjmp가 전달한 값을 반환하게 됩니다.

 

이렇게 setjmp와 longjmp 함수를 이용하면 예외 처리를 더욱 강력하게 할 수 있지만, 코드의 복잡성과 위험이 증가한다는 단점이 있습니다.

 

위 코드는 다소 복잡해 보일 수 있지만, 한 번에 이해하려고 노력하지 않아도 괜찮습니다. 처음 보는 개념이므로 조금 복잡하게 느껴질 수 있습니다. 그렇기 때문에 코드를 몇 번 읽어보고, 필요하다면 직접 실행해보는 것을 권장합니다.

 

setjmp(env) 함수는 jmp_buf 타입의 변수 env에 현재 컨텍스트를 저장합니다. 이 콘텍스트에는 CPU 레지스터 상태, 스택 포인터 등의 정보가 포함되어 있습니다. 이후에 longjmp(env, 1) 함수를 호출하면, 이 콘텍스트로 복원되고 setjmp 함수에서 1을 반환하게 됩니다.

 

여기서 흥미로운 부분은 setjmp 함수가 두 번 호출되지 않았음에도 불구하고 두 번째 반환 값이 나온다는 점입니다. 이렇게 되는 이유는 longjmp 함수가 setjmp가 저장한 컨텍스트로 복원하기 때문입니다. longjmp를 호출하면 setjmp 호출 지점으로 돌아가서 두 번째 반환 값을 생성합니다. 이를 이용해 예외 발생 시점에서 직접 에러 처리 코드로 점프하는 것이 가능합니다.

 

하지만 이런 방식은 코드 흐름을 끊고, 호출 스택이 정리되지 않는 등의 이유로 문제가 발생할 수 있습니다. 따라서 setjmp와 longjmp 함수를 사용할 때는 반드시 주의해야 합니다. 이러한 이유로 C++에서는 이 함수들 대신 예외 처리 구문을 제공하고, C 언어에서도 가능한 이 기능을 적극적으로 사용하기보다는 다른 예외 처리 방법을 선호합니다.

 

마지막으로, 모든 프로그래머는 프로그램에서 발생할 수 있는 예외 상황을 대비해 항상 준비해야 합니다. 이런 준비 없이는 작은 문제도 큰 문제로 확대될 수 있습니다. 때문에 우리는 예외 처리에 대해 신중하게 고려해야 합니다.

 

14.2.3. errno, perror, strerror 함수를 이용한 에러 처리

C 언어에서 일반적으로 사용되는 에러 처리 함수들인 errno, perror, strerror에 대해 알아보겠습니다.

 

먼저 errno는 표준 라이브러리에서 정의된 전역 변수로, 여러 표준 C 함수들이 내부적으로 이 변수의 값을 변경하며, 그 변경된 값은 가장 최근에 발생한 에러를 나타냅니다. 예를 들어 파일을 열려고 시도했지만 그 파일이 존재하지 않는 경우, errno의 값은 ENOENT가 됩니다.

 

[예제]

#include <stdio.h>
#include <errno.h>

int main() {
    FILE *fp;

    fp = fopen("nonexistent.txt", "r");
    if (fp == NULL) {
        printf("Error: %d\n", errno);
    }

    return 0;
}


위 코드에서 파일을 열 수 없는 경우 errno 변수를 사용해 오류 코드를 출력합니다. nonexistent.txt 파일이 존재하지 않기 때문에, 프로그램은 "Error: 2"를 출력합니다. 여기서 2는 ENOENT에 해당하는 값입니다.

 

하지만 errno는 단지 숫자로, 이 숫자가 실제로 어떤 문제를 나타내는지를 우리는 알 수 없습니다. 이러한 상황에서 perror와 strerror 함수가 유용하게 사용됩니다.

 

perror 함수는 errno에 대응하는 문자열 메시지와 함께 사용자가 제공한 메시지를 출력합니다.

 

[예제]

#include <stdio.h>
#include <errno.h>

int main() {
    FILE *fp;

    fp = fopen("nonexistent.txt", "r");
    if (fp == NULL) {
        perror("Error");
    }

    return 0;
}


위 코드를 실행하면, "Error: No such file or directory"라는 메시지를 출력합니다. "Error"는 우리가 perror에 전달한 문자열이고, "No such file or directory"는 errno에 해당하는 에러 메시지입니다.

 

strerror 함수는 errno 값을 인자로 받아 해당하는 에러 메시지를 반환합니다.

 

[예제]

#include <stdio.h>
#include <string.h>
#include <errno.h>

int main() {
    FILE *fp;

    fp = fopen("nonexistent.txt", "r");
    if (fp == NULL) {
        printf("Error: %s\n", strerror(errno));
    }

    return 0;
}

 

strerror를 사용해 errno에 해당하는 에러 메시지를 출력합니다. 이것도 마찬가지로 "Error: No such file or directory"라는 메시지를 출력합니다.

 

이렇게 C 언어에서는 errno, perror, strerror를 통해 다양한 에러 상황을 처리할 수 있습니다. 이 함수들을 이용하면 프로그램의 안정성을 높이고 사용자에게 정확한 에러 정보를 제공할 수 있습니다. 

 


14.3. C++에서의 예외 처리

'C++에서의 예외 처리'는 코드에서 예상치 못한 사건을 감지하고 적절히 대응하는데 필요한 기법을 다룹니다. C++은 이를 위해 특별한 문법을 제공하는데, try, catch, throw가 그것입니다. 이 구조를 사용하여 개발자는 예외를 '던지고', '잡고', 그리고 '처리'할 수 있습니다. C++에서의 예외 처리는 프로그램의 안정성과 가독성을 높이는 데 크게 도움이 됩니다.

14.3.1. try, catch, throw를 이용한 예외 처리

C++에서 예외 처리는 try, catch, throw라는 세 가지 키워드를 이용해 이루어집니다. 이 섹션에서는 이 세 가지 키워드를 어떻게 사용하는지, 그리고 그들이 예외 처리에서 어떻게 상호 작용하는지에 대해 자세히 알아보겠습니다.

 

try 키워드는 예외가 발생할 가능성이 있는 코드 블럭을 정의하는 데 사용됩니다. 이 try 블록 내에서 문제가 발생하면 프로그램은 즉시 현재 블록을 벗어나고, 해당 예외를 처리할 수 있는 첫 번째 catch 블록으로 이동합니다.

 

[예제]

try {
    // 예외가 발생할 수 있는 코드
}

throw 키워드는 실제 예외를 "던지는" 데 사용됩니다. 이 키워드를 사용하면 특정 오류 조건에 도달했을 때 프로그램에서 예외를 생성하고 처리할 수 있습니다.

 

[예제]

if (divisor == 0) {
    throw "Division by zero condition!";
}


catch 키워드는 try 블럭에서 발생한 예외를 잡아내는 데 사용됩니다. catch 블록은 특정 유형의 예외를 처리하도록 설계되어 있으며, 이 유형은 catch문의 괄호 안에 지정됩니다.

 

[예제]

catch (const char* msg) {
    cerr << msg << endl;
}


이 세 가지 요소를 함께 사용하여 전체 예외 처리 시스템을 만들 수 있습니다.

 

[예제]

double division(int a, int b) {
    if (b == 0) {
        throw "Division by zero condition!";
    }
    return (a / b);
}

int main () {
    int x = 50;
    int y = 0;
    double z = 0;

    try {
        z = division(x, y);
        cout << z << endl;
    } catch (const char* msg) {
        cerr << msg << endl;
    }

    return 0;
}

 

위 코드에서, 우리는 division 함수 내에서 0으로 나누는 상황을 검사하고 있다. 만약 이런 상황이 발생하면, 함수는 문자열 예외를 던집니다. 이 예외는 main 함수 내의 try 블록에서 잡히며, 그 후에 적절한 오류 메시지가 출력됩니다.

 

이렇게 C++에서는 try, catch, throw를 사용하여 코드에서 발생할 수 있는 예외 상황을 효과적으로 처리할 수 있습니다. 예외 처리를 사용함으로써, 프로그램의 안정성을 향상시키고 예상치 못한 오류로 인한 프로그램의 중단을 방지할 수 있습니다.

 

14.3.2. 예외 클래스 정의 및 사용

C++에서 예외를 처리하는 또 다른 방법은 사용자 정의 예외 클래스를 생성하는 것입니다. 이를 통해 우리는 더욱 구조화된 방법으로 예외를 처리하고, 특정 예외 유형에 대한 추가 정보를 제공할 수 있습니다.

 

먼저, 사용자 정의 예외 클래스를 만들어 봅시다. 이 클래스는 표준 예외 클래스(std::exception)를 상속받아야 합니다. 다음은 간단한 사용자 정의 예외 클래스의 예입니다.

 

[예제]

class DivisionByZeroException : public std::exception {
public:
    const char* what() const throw() {
        return "Division by zero!";
    }
};

이 DivisionByZeroException 클래스는 std::exception 클래스로부터 상속받고, what() 멤버 함수를 오버라이드합니다. what() 함수는 예외가 발생했을 때 반환할 메시지를 지정합니다.

 

다음으로, 이 클래스를 사용하는 방법을 알아보겠습니다.

 

[예제]

double division(int a, int b) {
    if (b == 0) {
        throw DivisionByZeroException();
    }
    return (a / b);
}

int main () {
    int x = 50;
    int y = 0;
    double z = 0;

    try {
        z = division(x, y);
        cout << z << endl;
    } catch (DivisionByZeroException& e) {
        cerr << "Caught exception: " << e.what() << endl;
    } catch (...) {
        cerr << "Caught unknown exception." << endl;
    }

    return 0;
}

 

이 코드에서는 division 함수에서 0으로 나누려고 할 때 DivisionByZeroException 클래스의 인스턴스를 생성하여 던집니다. 이 예외는 main 함수 내의 try 블록에서 잡혀 처리되며, catch 블록에서 what() 함수를 호출하여 예외 메시지를 출력합니다.

 

catch (...) 구문은 모든 다른 예외 유형을 처리하며, 알려지지 않은 예외 유형이 발생하면 이 블록이 실행됩니다. 이렇게 사용자 정의 예외 클래스를 사용하면 더욱 구조화되고 특정한 예외 처리가 가능해집니다.

 

이러한 방식으로 사용자 정의 예외 클래스를 만들고 사용하면, 다양한 유형의 예외를 세밀하게 처리할 수 있으며, 예외 발생 원인에 대한 더 많은 정보를 제공할 수 있습니다. 이를 통해 코드의 가독성과 유지보수성을 크게 향상할 수 있습니다.

 


14.4. 예외 처리의 활용

예외 처리의 활용에서는 예외 처리의 중요성과 실제 활용 방법에 대해 살펴봅니다. 예외 처리는 프로그램의 안정성과 신뢰성을 보장하는 데 큰 역할을 합니다. 불가피한 에러 상황에서도 프로그램이 안전하게 실행될 수 있도록 보장하며, 문제가 발생했을 때 적절하게 대응하여 사용자에게 알릴 수 있습니다. 예외 처리는 C에서는 함수의 반환값, setjmp, longjmp, errno, perror, strerror 등을 이용하며, C++에서는 try, catch, throw 구문과 사용자 정의 예외 클래스를 활용하여 처리할 수 있습니다.

14.4.1. 예외 처리를 활용한 안전한 코드 작성

예외 처리란 프로그램에서 발생할 수 있는 예외 상황을 미리 예측하고, 해당 상황이 발생했을 때 적절하게 대응하는 기법을 말합니다. 이는 프로그램이 예상치 못한 문제에 직면했을 때, 그 문제를 해결하거나 적절히 처리하여 프로그램이 중단되는 것을 막는 역할을 합니다.

 

예외 처리를 통해 안전한 코드를 작성하는 방법을 살펴보겠습니다. C 언어에서는 함수의 반환값을 이용하거나, setjmp와 longjmp, errno, perror, strerror 등의 함수를 이용하여 예외를 처리합니다.

 

예를 들어, 함수의 반환값을 이용한 예외 처리는 다음과 같습니다.

 

[예제]

#include <stdio.h>

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

int main() {
    int result = divide(10, 0);
    if (result == -1) {
        // 에러 처리
    }
    return 0;
}

 

이 코드에서 divide 함수는 나눗셈을 수행하지만, 나눗셈이 불가능한 경우 (나누는 수가 0인 경우)에는 -1을 반환하며 에러 메시지를 출력합니다. 이처럼 함수의 반환값을 이용하면 함수가 실행되는 동안 발생할 수 있는 예외 상황을 미리 예측하고, 그에 따라 적절히 대응할 수 있습니다.

 

C++에서는 try, catch, throw 구문을 사용하여 예외를 처리합니다. 특정 코드 블록에서 예외가 발생할 가능성이 있는 경우, 그 코드 블록을 try 구문 안에 넣고, catch 구문을 사용하여 예외를 잡아 처리할 수 있습니다. 이렇게 하면 프로그램이 예외 때문에 갑자기 중단되는 것을 막을 수 있습니다.

 

다음은 C++의 try, catch, throw를 이용한 예외 처리 예제입니다.

 

[예제]

#include <iostream>

double divide(double a, double b) {
    if (b == 0) {
        throw "Division by zero condition!";
    }
    return a / b;
}

int main() {
    try {
        std::cout << divide(10.0, 0.0);
    }
    catch (const char* msg) {
        std::cerr << msg << std::endl;
    }
    return 0;
}

 

이 코드에서 divide 함수는 나눗셈을 수행하지만, 나누는 수가 0인 경우에는 "Division by zero condition!"이라는 메시지를 throw하며 예외를 발생시킵니다. 이 예외는 main 함수에서 try, catch 구문을 이용하여 잡아 처리되며, 예외 메시지는 표준 에러 스트림에 출력됩니다.

 

이처럼 예외 처리는 프로그램의 안정성을 보장하고, 예상치 못한 문제를 적절히 처리하는데 큰 역할을 합니다. 이를 통해 프로그램은 더욱 안전하고 신뢰성 있게 동작할 수 있습니다.

 

14.4.2. 예외 처리의 성능과 효율

예외 처리는 오류를 감지하고 처리하는 데에 있어 매우 유용한 도구입니다. 하지만 이는 추가적인 시스템 자원을 사용하게 됩니다. 때문에 예외 처리의 활용은 프로그램의 성능과 효율성에 영향을 미칩니다.

먼저, 예외 처리는 프로그램의 흐름을 변경하는 비용이 있습니다. 예외가 발생하면 프로그램의 제어 흐름이 예외를 처리하는 코드로 즉시 이동하게 됩니다. 이런 제어 흐름의 변경은 비용이 드는 작업이므로, 너무 많은 예외 처리는 프로그램의 성능을 저하시킬 수 있습니다.

[예제]

#include <iostream>
#include <stdexcept>

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

 

위 예제에서, "throw" 문은 제어 흐름을 "catch" 블록으로 즉시 변경합니다. 이 변경은 시스템 자원을 사용하므로, 예외 처리를 너무 자주 사용하면 프로그램의 성능을 저하시킬 수 있습니다.

 

또한, 예외 처리는 프로그램의 코드 복잡성을 증가시킵니다. 예외를 처리하는 코드는 프로그램의 주요 기능과 분리되어 있을 수 있으므로, 코드를 이해하고 유지하는 데에 추가적인 노력이 필요합니다.

 

[예제]

#include <stdio.h>
#include <setjmp.h>

jmp_buf jump_buffer;

void my_function() {
    longjmp(jump_buffer, 1);
}

int main() {
    if (setjmp(jump_buffer) == 0) {
        my_function();
    } else {
        printf("An error occurred!\n");
    }
    return 0;
}

 

 

위 C 언어 예제에서, "setjmp"와 "longjmp" 함수를 사용하여 예외를 처리하고 있습니다. 이런 방식의 예외 처리는 프로그램의 복잡성을 증가시킬 수 있습니다.

 

그러므로, 예외 처리를 사용할 때는 이러한 비용과 복잡성을 고려해야 합니다. 예외 처리는 프로그램의 안정성을 향상시키지만, 적절히 사용하지 않으면 프로그램의 성능과 효율성을 저하시킬 수 있습니다. 예외 처리를 효과적으로 사용하려면, 예외가 정말로 발생할 수 있는 상황에서만 사용해야 하며, 예외를 불필요하게 많이 사용하지 않는 것이 중요합니다.

 


14.5. 예외 처리의 고급 주제

'예외 처리의 고급 주제'는 C++에서 예외 처리를 사용할 때 고려해야 할 몇 가지 고급 주제에 대해 다룹니다. 여기에는 예외 사양(exception specifications), 예외 안전성(exception safety)과 Unwinding, 그리고 사용자 정의 예외 클래스의 설계와 구현 등이 포함됩니다. 이런 주제들을 통해 프로그래머는 예외 처리를 더 깊이 있고 효율적으로 사용하는 방법을 배울 수 있습니다. 이들 주제는 중급 이상의 C++ 프로그래밍 기술을 요구합니다.

 

14.5.1. 예외의 재발생과 예외 명세

예외 처리의 고급 주제 중 하나인 "예외의 재발생과 예외 명세"는 더욱 복잡한 예외 상황을 처리하는데 사용됩니다.

 

때로는 예외를 적절히 처리한 후에도, 같은 예외를 다시 발생시켜야 할 경우가 있습니다. C++에서는 'throw' 문장을 사용하여 현재 처리 중인 예외를 다시 발생시킬 수 있습니다. 이는 특히 상위 수준의 함수에서 예외를 더 자세히 처리하거나 로깅하려는 경우 유용합니다.

[예제]

try {
    // 예외를 발생시키는 코드
} catch (std::exception& e) {
    // 예외 처리
    // ...
    throw; // 예외 재발생
}

 

위 코드에서 throw; 문장은 현재 catch 블록에서 처리 중인 예외를 다시 발생시킵니다.

 

예외 명세

 

C++에서는 함수가 던질 수 있는 예외의 타입을 선언하는 것이 가능합니다. 이를 '예외 명세(exception specification)'라고 부릅니다. 예외 명세는 함수의 프로토타입에 'throw' 키워드를 사용하여 지정합니다. 하지만 이 기능은 C++11 이후로는 추천되지 않으며, 'noexcept' 키워드가 대신 사용됩니다.

 

[예제]
void myFunction() throw(int); // int 타입의 예외만 던질 수 있음

 

예외 명세를 사용하면 함수가 던질 수 있는 예외의 범위를 제한하게 되지만, 실제로는 코드 복잡성을 증가시키며, 예외 처리를 더 어렵게 만들 수 있으므로, 사용을 자제하는 것이 좋습니다.

 

위 주제들은 예외 처리의 중요성을 더욱 강조하며, 안정성 있는 코드를 작성하는 데 도움을 줍니다. 또한, 이들을 이해하고 활용하는 것은 C++의 고급 개념을 이해하는 데 큰 도움이 됩니다. 이어서 더 깊은 주제들을 다루며, 예외 처리의 전체적인 개념을 완성해 보겠습니다.

 

14.5.2. 사용자 정의 예외 처리

'사용자 정의 예외 처리'는 프로그래머가 직접 예외 클래스를 정의하여 프로그램의 특정 상황에 맞는 예외를 처리하는 방법입니다.

 

사용자 정의 예외 클래스

 

C++에서는 사용자 정의 예외 클래스를 만들어 특정한 예외 상황을 대응할 수 있습니다. 이는 C++의 표준 예외 클래스가 제공하는 것 이상의 정보를 예외와 함께 전달할 필요가 있을 때 유용합니다.

 

사용자 정의 예외 클래스를 만들 때는 보통 std::exception 클래스를 상속받아 만듭니다. 이는 예외 처리 코드가 표준 예외와 사용자 정의 예외를 동일하게 처리할 수 있게 해 줍니다.

 

[예제]

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

 

위 코드는 std::exception 클래스를 상속받는 MyException 클래스를 정의하고 있습니다. what() 함수는 예외 정보를 문자열로 반환합니다. 이 함수는 noexcept 키워드가 붙어 있어 이 함수 내에서 예외가 발생하지 않음을 보장합니다.

 

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

 

[예제]

try {
    // 예외를 발생시키는 코드
    throw MyException();
} catch (MyException& e) {
    std::cout << e.what() << std::endl;
}

위 코드에서 throw MyException(); 문장은 MyException 타입의 예외를 발생시킵니다. 이 예외는 catch (MyException& e) 블록에서 잡히고, e.what()을 호출하여 예외 정보를 출력합니다.


사용자 정의 예외 처리는 코드의 안정성을 높이고, 오류 발생 시 디버깅을 쉽게 해 줍니다. 그러나 예외 클래스를 적절히 설계하고 사용하는 것이 중요하므로, 예외 처리에 대한 이해를 바탕으로 신중하게 접근해야 합니다. 다음 섹션에서는 더 고급 예외 처리 기법을 다루겠습니다.

 


14.6. 예외 처리의 주의점

C++의 예외 처리에는 몇 가지 주의해야 할 점이 있습니다. 첫째, 모든 예외를 잡는 catch(...)는 가능한 한 피해야 합니다. 이유는 이것이 잡아내는 예외의 타입을 알 수 없기 때문에, 적절한 처리를 할 수 없습니다. 둘째, 예외 처리 블록에서 다른 예외를 던지지 않도록 해야 합니다. 만약 그렇게 되면 프로그램이 예상치 못한 방식으로 종료될 수 있습니다. 마지막으로, 예외 처리는 에러 처리를 대체하는 것이 아니라 보완하는 것입니다.

14.6.1. 예외 처리 시 주의할 사항

예외 처리는 강력한 도구일 수 있지만, 잘못 사용하면 코드의 가독성과 유지 관리성을 해칠 수 있습니다. 따라서 아래와 같은 주요 사항들에 주의해야 합니다.

 

불필요한 예외 처리는 피하자: 예외 처리는 코드에 복잡성을 추가합니다. 따라서 필요한 경우에만 사용해야 합니다. 모든 상황에 대해 예외 처리를 추가하는 것은 성능을 저하시키고, 불필요한 코드 복잡성을 증가시킬 수 있습니다.

 

[예제] : Bad Practice

try {
    // code that might not throw an exception
} catch (...) {
    // handle all exceptions
}

 

[예제] : Good Practice

try {
    // code that might throw an exception
} catch (SpecificException& e) {
    // handle specific exception
}


예외는 예외적인 상황에서만 던지자: 예외는 이름에서 알 수 있듯이 정상적인 흐름에서 벗어난 상황에서만 사용해야 합니다. 예외를 통해 프로그램 흐름을 제어하려는 시도는 혼란을 야기하고 코드의 가독성을 저하시킵니다.

 

[예제] : Bad Practice

try {
    if (condition) {
        throw Exception();
    }
} catch (Exception& e) {
    // handle exception as normal flow control
}


[예제] : Good Practice

if (condition) {
    // handle condition normally
}

 

예외를 던질 때는 충분한 정보를 제공하자: 예외를 처리할 때 필요한 모든 정보를 제공하는 것이 중요합니다. 이는 예외를 처리하는 코드가 예외의 원인을 이해하고 적절히 대응할 수 있게 합니다.

 

[예제] : Bad Practice

if (error) {
    throw std::runtime_error("An error occurred");
}


[예제] : Good Practice

if (error) {
    throw std::runtime_error("An error occurred: " + detailed_description);
}


예외가 발생하더라도 자원 누수가 발생하지 않도록 하자: 예외가 발생하면 함수가 즉시 종료되므로, 예외가 발생하기 전에 할당한 모든 자원을 적절히 해제해야 합니다. C++에서는 이를 위해 RAII(Resource Acquisition Is Initialization) 패턴을 사용합니다. 이 패턴은 객체가 소멸될 때 자원을 자동으로 해제하도록 합니다.

 

[예제] : Good Practice with RAll

try {
    Resource resource; // Resource is acquired here
    // use resource
} catch (...) {
    // Resource is automatically released when the exception is thrown
}


이러한 원칙들을 지키면서 예외 처리를 사용하면, 코드는 더욱 견고하고 유지 보수하기 쉬워질 것입니다. 

 

14.6.2. 예외 안전성

C++에서 예외 안전성(exception safety)은 예외가 발생하더라도 프로그램이 일관성을 유지하는 능력을 의미합니다. 이는 또한 프로그램이 예외를 안전하게 처리하고, 예외가 발생하더라도 리소스 누수가 없도록 하는 데 필요한 기법들을 포함합니다.

 

예외 안전성은 크게 세 가지 수준으로 분류할 수 있습니다:

 

기본 예외 안전성(Basic Exception Safety): 예외가 발생하더라도 프로그램의 상태는 유효한 상태를 유지합니다. 하지만 프로그램의 상태가 예외가 발생하기 전과 동일하다는 보장은 없습니다. 메모리 누수와 같은 문제가 발생하지 않도록 하는 가장 기본적인 수준의 예외 안전성입니다.

 

강한 예외 안전성(Strong Exception Safety): 예외가 발생하더라도 프로그램의 상태는 예외가 발생하기 전 상태를 유지합니다. 즉, 연산이 성공적으로 완료되거나, 아무 일도 일어나지 않은 것처럼 프로그램의 상태를 원복 합니다.

 

무조건 예외 안전성(No-throw Exception Safety): 프로그램이 예외를 발생시키지 않습니다. 이 수준의 예외 안전성을 보장하는 연산은 예외가 발생하지 않음을 보장합니다.

 

C++에서 예외 안전성을 확보하는 기법 중 하나는 "리소스 획득은 초기화(Resource Acquisition Is Initialization, RAII)"이라는 패턴을 사용하는 것입니다. 이 패턴은 자원을 할당받는 것과 초기화를 동시에 수행하며, 자원의 해제는 소멸자에서 자동으로 처리하도록 설계되어 있습니다. 이러한 방식으로 프로그램의 예외 안전성을 향상할 수 있습니다.

 

다음은 RAII를 이용한 예외 안전성 확보의 예입니다.

[예제]

class SafeResource {
public:
    SafeResource() {
        // acquire resource
    }

    ~SafeResource() {
        // release resource
    }
};

void foo() {
    SafeResource res;  // Resource is acquired and initialized

    // If an exception is thrown here, the destructor of res
    // will still be called, releasing the resource.

    // use res

}  // res is automatically released here, even if an exception is thrown

 

이 예에서, SafeResource의 소멸자에서 자원이 해제되므로, foo 함수에서 예외가 발생하더라도 자원 누수는 발생하지 않습니다.

 

이렇게, 예외 안전성은 프로그램이 예외를 적절하게 처리하고, 예외 발생 후에도 일관된 상태를 유지할 수 있도록 돕는 중요한 요소입니다. 다양한 수준의 예외 안전성을 이해하고, 각 상황에 맞게 적절하게 적용할 수 있어야 합니다.

 

 

 

2023.05.16 - [GD's IT Lectures : 기초부터 시리즈/C, C++ 기초부터 ~] - [C/C++ 프로그래밍] 13. 파일 입출력

 

[C/C++ 프로그래밍] 13. 파일 입출력

Chapter 13. 파일 입출력 이 장에서는 C/C++에서 파일 입출력에 대해 상세하게 다루게 됩니다. 먼저 파일과 스트림에 대한 개념을 이해하고, 그 후에 파일 입출력의 기본적인 방법에 대해 알아보게

gdngy.tistory.com

 

반응형

댓글