I'm FanJae.

[C++] Exception 본문

C++/Basic

[C++] Exception

FanJae 2024. 8. 29. 12:29

1. Error Handling

void db_backup()
{
     // 서버에 접속해서 DB를 백업하는
     // 기능 수행하다가 오류가 발생했다면?
}

void db_clear()
{
}

int main()
{
    db_backup();
    
    db_clear();
}

- 함수에서 특정 기능을 수행하는 도중 오류가 발생하여, 처리해야 하는 경우 어떻게 할 것인가?

① 함수 내에서 프로그램(프로세스)를 종료 

- std::exit() 등의 함수 사용

- 보통, 심각한 경우 사용한다.

② 호출자에게 오류가 발생했음을 알린다.

- 일반적인 상황에서 많이 사용한다.

 

1-2. 호출자에서 함수가 실패 했음을 알리는 방식

① 약속된 함수 반환 값(-1 등)으로 실패 전달.

- C 언어 및 대부분의 언어에서 사용

② 예외(Exception) 사용

- 대부분의 객체지향 언어의 대표적인 방법(C++, java, C#, Python 등)

③ 성공시 결과값 + 실패를 모두 담을 수 있는 타입을 반환하는 방식

- 함수형 언어 및 일부 최신 언어가 사용

 

※ 1개만 사용하는 것이 아니라 오류의 심각성에 따라 위 방식을 섞어서 사용한다.

 

1-1-1. 함수 반환 값으로 오류를 전달하는 방식

#define ERROR -1

void db_backup()
{
     // 서버에 접속해서 DB를 백업하는
     // 기능 수행하다가 오류가 발생했다면?
     
     if ( 실패 )
        return ERROR;
        
     return 0; 
}

void db_clear()
{
}

int main()
{
    db_backup();
    
    db_clear();
}

- 가장 널리 사용되던 방식(C언어)

- 오류를 나타내는 값을 약속 한다. (ex : -1)

- 오류 발생시 약속된 값을 반환한다.

- 함수 반환 타입이 void 라는 것은 함수가 절대 실패하지 않는다는 것을 의미한다. 

 

※ 이 방식의 단점

① 반환 값과 오류가 완벽히 분리되지 않는다.

- -1이 실패를 의미 하는지 연산의 결과인지 파악이 모호하다.

② 호출자가 발생된 오류를 무시할 수 있다.

- 이것이 상당히 심각한 문제이다.

int main()
{
    db_backup(); // 오류 처리를 하지 않았다.
    
    db_clear();
}

- 지금 main()에서는 오류를 처리 하지 않았다.

- 즉, DB 백업이 실패했는데 호출자가 무시하고 DB를 지워버렸다면, 어떻게 되겠는가?

※ 즉, 심각한 오류는 호출자가 반드시 처리를 강제해야 한다.


2. Exception

#include <iostream>

int db_backup()
{
    if ( 실패 )
        return -1;
    
    std::cout << "continue db_backup" << std::endl;
    
    return 0;
}

int main()
{
    int ret = db_backup();
    
    std::cout << "continue main" << std::endl;
}

- 예외(Exception) 이란 객체지향 언어에서 많이 사용하는 오류 처리 방식이다.

 

2-1. 예외(Exception) 문법을 사용하는 방법

함수 실패시 trhow 키워드를 사용해서 예외를 전달한다. 보통은 던진다(throw)라고 표현하기도 한다.

 throw를 하면 함수는 더 이상 실행되지 않고 호출자의 catch 문으로 이동한다.

#include <iostream>

int db_backup()
{
    if ( 실패 )
        throw -1;
    
    std::cout << "continue db_backup" << std::endl;
    
    return 0;
}

int main()
{
    int ret = db_backup();
    
    std::cout << "continue main" << std::endl;
}

③ 호출자는 try, catch 키워드를 사용해서 던져진 예외를 잡는다.

④ 호출자가 던져진 예외를 처리하지 않으면 프로그램은 종료된다. -> abort() 함수가 호출된다.

 

실패라는 문구를 1로 바꿔서 visual studio에서 실행하면 아래와 같은 창이 뜬다.

abort() 호출 여부를 보여준다.

※ Exception의 특징

- 함수 반환 값과 오류의 전달이 완벽히 분리된다.

- 호출자는 반드시 오류를 처리해야만 한다.

 

2-2. 던져진 예외를 처리하는 방법

try
{
      // 예외 발생 가능성이 있는 함수를 호출 할 때는
      // try { } 안에서 호출한다. 예외 발생시 catch로 이동한다.
      int ret = db_backup();
 
      // 예외 발생이 해당 문구 아래쪽은 실행되지 않을 것이다.
}
catch ( int e )
{
      // 예외가 발생된 경우만 실행된다.
}

- 던져진 예외를 처리하는 가장 기본적인 형태는 이와 같다.

- 예외 발생 가능성이 있는 함수를 호출 할때는 try { }안에서 호출 한다. 예외 발생시 catch로 이동한다.

- 즉, int ret =  db_backup(); 에서 예외가 발생했다면 그 아래 문구는 실행이 되지 않는 것이다.

 

2-2-1. 적용

#include <iostream>

int db_backup()
{
    if ( 1 )
        throw 1;
    
    std::cout << "continue db_backup" << std::endl;
    
    return 0;
}

int main()
{
    try
    {
         // 0
         
         int ret = db_backup();
         
         // 1
         std::cout << "DB backup complete." << std::endl;
    }
    catch(int e)
    {
         // 2
         std::cout << "exception!!" << std::endl;
    }
    // 3
    std::cout << "continue main" << std::endl;
}

- 코드의 주석 상 0번 부분부터 차례대로 실행된다. 예외 발생 가능성 있는 함수 호출시 try { } 안에서 호출될 것이다.

- db_backup()은 오류 발생시 throw를 하고 있다. 예외 발생시 1번 이후 문장은 실행(DB backup complete)되지 않는다.

- 예외 발생시 2번으로 이동하여, exception이 출력된다.

- 예외 여부 상관 없이 3번은 정상적으로 실행된다.

즉, try 블록 안에서 예외 가능성이 있는 함수를 호출하고, 발생되는 예외는 catch로 처리하면 된다.

 

2-3. 오류에 대한 정보 전달

#include <iostream>

int db_backup()
{
    if ( 1 )
        throw 1;
    
    std::cout << "continue db_backup" << std::endl;
    
    return 0;
}

int main()
{
    try
    {
         int ret = db_backup();
         
         std::cout << "DB backup complete." << std::endl;
    }
    catch(int e)
    {
         std::cout << "exception!!" << std::endl;
    }
    std::cout << "continue main" << std::endl;
}

- 오류 발생시, 오류에 대한 자세한 정보를 호출자에게 전달하는 것이 좋다.

- DB 연결 실패 사유는 다양하다. (파일 정보 없음, 네트워크 연결, DB 인증 등)

- 즉, 오류에 대한 자세한 정보를 담은 타입을 설계해서 객체를 반환한다.

- 아래는 그 예시이다. (구체적 구현은 생략했다.)

class file_not_found
{
      // 오류의 정보를 담는 멤버.
};

 

#include <iostream>

class file_not_found {};
int db_backup()
{
    if ( 1 )
        throw file_not_found();
    
    std::cout << "continue db_backup" << std::endl;
    
    return 0;
}

int main()
{
    try
    {
         int ret = db_backup();
         
         std::cout << "DB backup complete." << std::endl;
    }
    catch(const file_not_found& e)
    {
         std::cout << "file_not_found!!!" << std::endl;
    }
    std::cout << "continue main" << std::endl;
}

- 이와 같이 객체를 반환하는 것이 일반적이다.

- 자세한 정보가 없다고 하더라도 클래스 이름만을 보더라도 가독성이 좋아진다

- int를 받았을때 보다 어떤 문제가 있는지 알아보기 좋다. (file_not_found라는 것을 보고 파일이 없음을 확신할 수 있다.)

- C++ 표준도 일부 함수(멤버 함수)가 실패하면 예외를 던진다.

 

※ 즉, C++ 표준의 예외 전용 클래스가 있다. cppreference.com 사이트에서 이를 확인할 수 있다.

 

2-4. 여러 오류의 처리

- 함수는 여러 가지의 사유로 실패할 수 있다.

- 이에 따라서 catch도 여러개를 만들 수 있다.

#include <iostream>

class file_not_found {};
class network_disconnected {};
class authentication_failed {};
int db_backup()
{
    if ( 1 )
        throw file_not_found();
    
    std::cout << "continue db_backup" << std::endl;
    
    return 0;
}

int main()
{
    try
    {
         // 0
         
         int ret = db_backup();
         
         // 1
         std::cout << "DB backup complete." << std::endl;
    }
    catch(const file_not_found& e)
    {
         // 2
         std::cout << "file_not_found!!!" << std::endl;
    }
    // 3
    std::cout << "continue main" << std::endl;
}

- 현재 본 예제에서는 file_not_found에 대한 catch만이 존재한다.

- 이 경우 다른 예외가 발생한 경우 프로그램은 비정상 종료. 즉, abort()를 호출한다.

catch(const file_not_found& e)
{
     // 2
     std::cout << "file_not_found!!!" << std::endl;
}
catch(const network_disconnected& e)
{
     // 2
     std::cout << "network_disconnected" << std::endl;
}

- 이런 방식으로 사용할 수 있다.

 

2-4-1. 예외가 많아서 모두 적기 어려운 경우 // catch(...)

catch(const file_not_found& e)
{
     std::cout << "file_not_found!!!" << std::endl;
}
catch(const network_disconnected& e)
{
     std::cout << "network_disconnected" << std::endl;
}
catch(...) // 다른 모든 예외에 대한 처리
{
     std::cout << "..." << std::endl;
}
catch(...)
{

}

- 이 문장은 가급적 다른 모든 예외의 마지막에 처리되도록 권장하고 있다.

- 버전에 따라서 이것이 다른 예외보다 앞에 있을때 오류를 발생 시키는 경우도 있고, 그렇지 않은 경우도 있다.

- 따라서, 반드시 마지막에 놓아야 한다.


3. Standard Exception

3-1. 예외와 성능

- 예외처리를 위해 생성되는 기계어 코드가 오버헤드가 존재한다.

- 성능이 중요한 분야에서는 예외를 사용하지 않는 경우도 있다.

- C#, Java, Python 등의 언어에서는 예외를 많이 사용한다.

- C++의 일부 함수가 예외를 발생 시킨다.

 

3-1-1. std::stoi()

- <string> 헤더

- 인자로 전달된 문자열을 정수(int)로 변경해서 반환하는 함수이다.

#include <iostream>
#include <string>

int main()
{
    int n = std::stoi("10");
    
    std::cout << "result : ", n;
 }

- 이는 문제없이 실행된다.

 

#include <iostream>
#include <string>

int main()
{
    int n = std::stoi("FanJae");
    
    std::cout << "result : " << n;
 }

- 이 문장은 abort()를 호출한다고 실행이 나오지만, Debug를 해보면 실제로 어떤 Exception을 반환하는지 알 수 있다.

- 실행했을때는 알 수 없지만 std::invalid_argument라는 타입의 예외를 발생 시킨다.

 

#include <iostream>
#include <string>

int main()
{
    try
    {
         int n = std::stoi("FanJae");
         
         std::cout << "result : " << n;
    }
    catch ( const std::invalid_argument& e)
    {
       std::cout << e.what() << std::endl;
    }
 }

- std::invalid_arguement에 멤버 함수에는 what이라는 것이 존재한다.

- 이를 실행해보면, 다음과 같은 정보를 제공해준다.

3-2. 어떤 예외를 발생시키는지 판단이 어려운 경우

① catch(...)을 사용하는 방법

#include <iostream>
#include <string>

int main()
{
    try
    {
         int n = std::stoi("FanJae");
         
         std::cout << "result : " << n;
    }
    catch(...)
    {
         std::cout << "Exception" << std::endl;
    }
 }

std::exception을 사용하는 방법

#include <iostream>
#include <string>

int main()
{
    try
    {
         int n = std::stoi("FanJae");
         
         std::cout << "result : " << n;
    }
    catch(const std::exception& e)
    {
         std::cout << e.what << std::endl;
    }
 }

- 모든 C++ 예외 클래스는 std::exception 이라는 클래스로 부터 파생된다.

- 모든 예외 타입에는 what이라는 가상함수가 존재한다.

 

3-3. std::vector의 예외

#include <iostream>
#include <vector>

int main()
{
    std::vector v = {1, 2};
    
    v[100] = 0; // 잘못된 인덱스인 경우 미정의 동작
    v.at(100) = 0; // 잘못된 인덱스인 경우 Exception
    
    std::cout << "continue main" << std::endl;
}

- std::vector의 요소 접근

[] 잘못된 인덱스 전달시 undefine behavior
at() 잘못된 인덱스 전달시 std::out_of_range 타입의 예외 발생

 

#include <iostream>
#include <vector>

int main()
{
    std::vector v = {1, 2};
    
    try
    {
        v.at(100) = 0; // 잘못된 인덱스인 경우 Exception
    }
    catch( const std::out_of_range& e)
    {
        std::cout << "what : " << e.what() << std::endl;
    }
    
    std::cout << "continue main" << std::endl;
}

- 이처럼, C++ 표준을 확인해보면 어떤 경우는 예외가 있으나 어떤 경우는 예외가 없는 것을 확인할 수 있다.

- C++ Reference에서 이런 함수를 찾아보면, 설명에 예외가 나오는지 여부를 확인할 수 있다.

'C++ > Basic' 카테고리의 다른 글

[C++] stack unwinding  (0) 2024.08.29
[C++] Algorithm  (3) 2024.08.28
[C++] Iterator  (0) 2024.08.28
[C++] STL Container II  (0) 2024.08.27
[C++] STL Container I  (0) 2024.08.27
Comments