2016년 3월 18일 금요일

[Effective C++] 항목 8 : 예외가 소멸자를 떠나지 못하도록 붙들어 놓자

  1. #include <iostream>  
  2. #include <vector>  
  3. using namespace std;  
  4.   
  5. class DBConnection  
  6. {  
  7. public:  
  8.       
  9.     //DBConnection 객체를 반환하는 함수.  
  10.     static DBConnection create()   
  11.     {  
  12.         cout << "DBConnection::create()" << endl;  
  13.   
  14.         DBConnection temp;  
  15.         return temp;  
  16.     }  
  17.   
  18.     //연결을 닫는다. 연결이 실패하면 예외를 던진다.  
  19.     void close(){ cout << "DBConnection::close()" << endl; }      
  20. };  
  21.   
  22. class DBConn      
  23. {  
  24. public:  
  25.     DBConn(DBConnection temp)  
  26.     {  
  27.         cout << "DBConn() 생성자" << endl;  
  28.         db = temp;  
  29.     }  
  30.       
  31.     // 데이터베이스 연결이 항상 닫히도록 확실히 챙겨주는 함수  
  32.     ~DBConn()                
  33.     {  
  34.         cout << "~DBConn() 소멸자" << endl;  
  35.         db.close(); // close()함수에서 에러가 발생하면 문제가 발생한다.   
  36.                       
  37.     }  
  38. private:  
  39.     DBConnection db;  
  40. };  
  41.   
  42.   
  43. int main()  
  44. {  
  45.     // DBConnection 객체를 생성하고 이것을 DBConn 객체로 넘겨서 관리를 맡긴다.  
  46.     DBConn dbc(DBConnection::create());   
  47.   
  48.     return 0;  
  49. }     
 위와 같이 데이터베이스를 연결하는 클래스가 있다고 가정해 봅시다. 보시다시피, 사용자가 DBConnection 객체에 대해 close를 직접 호출해야 하는 설계임을 알 수 있습니다. 이 close 호출만 성공하면 아무 문제될 것이 없는 코드 인데,  여기서 close 호출 시 예외가 발생했다고 가정해 보면 어떨까요? DBConn의 소멸자는 분명히 이 예외를 나가도록 내버려 둘 것입니다. 이게 바로 문제점입니다. 예외를 던지는 소멸자는 문제점을 가지고 있습니다. 그리고 이 문제를 피하는 방법은 두가지 정도로 나뉩니다. 

 문제해결 하나!
 : close에서 예외가 발생하면 프로그램을 바로 끝낸다. 대개 abort를 호출해준다.
  1. DBConn::~DBConn()  
  2. {  
  3.     cout << "~DBConn() 소멸자" << endl;  
  4.   
  5.     try  
  6.     {  
  7.         db.close();  
  8.     }  
  9.     catch(...)  
  10.     {  
  11.         // close 호출이 실패했다는 로그를 작성.  
  12.         abort();  
  13.     }  
  14. }  
 객체 소멸이 진행 되다가 에러가 발생한 후에 프로그램 실행을 계속할 수 없는 상황에서는 괜찮을 선택일 것입니다. 소멸자에서 생긴 예외를 그대로 나가도록 했다가는 정의되지 않은 동작에까지 이를 수 있다면, 그런 불상사를 막아버리는 방법도 괜찮은 방법이겠죠?


 문제해결 둘!
 : close를 호출한 곳에서 일어난 예외를 삼켜 버린다.
  1. DBConn::~DBConn()  
  2. {  
  3.     cout << "~DBConn() 소멸자" << endl;  
  4.   
  5.     try  
  6.     {  
  7.         db.close();  
  8.     }  
  9.     catch(...)  
  10.     {  
  11.         // close 호출이 실패했다믄 로그를 작성.  
  12.     }  
  13. }  
 이 방법은 중요한 정보(무엇이 잘못됐는지)가 묻혀 버리기 때문에 대부분의 경우에는 좋은 방법은 아니지만,  때에 따라서는 불완전한 프로그램 종료 혹은 미정의 동작으로 인해 입는 위험을 감수하는 것보다 그냥 예외를 먹어버리는게 나을 수도 있다는 것입니다. 단 이 방법이 제대로 빛을 보려면, 발생한 예외를 그냥 무시한 뒤라도 프로그램이 신뢰성 있게 실행을 지속할 수 있어야 합니다.

 하지만 위 둘의 방법은 각자의 문제점을 가지고 있습니다. 중요한것은 close가 최초로 예외를 던지게 된 요인에 대해 프로글매이 어떤 조치를 취할 수 있는가인데, 이런 부분에 대한 대책이 없기 때문입니다. 그럼 더 나은 방법은 뭐가 있는지 살펴 보도록 하죠. 

 새로운 문제 해결
 : 그렇다면 DBConn 인터페이스를 잘 설계해서, 발생할 소지가 있는 문제에 대처할 기회를 사용자가 가질 수 있도록 하면 어떨까요? 예를 들어 DBConn에서 Close 함수를 직접 제공하게 하면 이 함수의 실행 중에 발생하는 예외를 사용자가 직접 처리할 수 있을 것입니다.  DBConnection이 닫혔는지의 여부를 유지했다가, 닫히지 않았으면 DBConn의 소멸자에서 닫을 수도 있을 것이고, 이렇게 하면 데이터베이스 연결이 누출 되지 않습니다. 하지만 소멸자에서 호출하는 close마저 실패한다면 이야기가 달라지겠지만 말이죠. 

  1. #include <iostream>  
  2. #include <vector>  
  3. using namespace std;  
  4.   
  5. class DBConnection  
  6. {  
  7. public:  
  8.     // DBConnection 객체를 반환하는 함수.  
  9.     static DBConnection create()  
  10.     {  
  11.         cout << "DBConnection::create()" << endl;  
  12.         DBConnection temp;  
  13.         return temp;  
  14.     }  
  15.   
  16.     // 연결을 닫는다. 연결이 실패하면 예외를 던진다.  
  17.     void close(){ cout << "DBConnection::close()" << endl; }      
  18. };  
  19.   
  20. class DBConn          
  21. {  
  22. public:  
  23.     DBConn(DBConnection temp)  
  24.     {  
  25.         cout << "DBConn() 생성자" << endl;  
  26.         db = temp;  
  27.     }  
  28.   
  29.     void close()              
  30.     {                         
  31.         cout << "close() 함수 호출" << endl;  
  32.         db.close();  
  33.         closed = true;  
  34.     }  
  35.       
  36.     ~DBConn()         
  37.     {  
  38.         cout << "~DBConn() 소멸자" << endl;  
  39.         if (!closed)  
  40.         {  
  41.             try  
  42.             {  
  43.                 cout << "try::close() 함수 호출" << endl;  
  44.                 db.close(); // 사용자가 연결을 안 닫았으면 여기서 닫는다.  
  45.             }  
  46.             catch(...)  
  47.             {  
  48.                 // close 호출이 실패했다는 로그를 작성.  
  49.             }  
  50.         }  
  51.     }     
  52.   
  53. private:  
  54.     DBConnection db;  
  55.     bool closed;  
  56. };  
  57.   
  58.   
  59. int main()  
  60. {  
  61.     DBConn dbc(DBConnection::create());   
  62.     dbc.close();  
  63.   
  64.     return 0;  
  65. }     
 여기에서 어떤 동작이 예외를 일으키면서 실패할 가능성이 있고 또 그 예외를 처리해야 할 필요가 있다면, 그 예외는 소멸자가 아닌 다른 함수에서 비롯된 것이어야 한다는 것이 포인트 입니다. 왜냐하면 예외를 일으키는 소멸자는 시한폭탄과 마찬가지라서 프로그램의 불완전 종료 혹은 미정의 동작의 위험을 내포 하고 있기 때문입니다. 
 위의 예제를 보면 사용자가 호출할 수 있는 close 함수를 둬서, 사용자에게 에러를 처리할 수 있는 기회를 주고 있습니다. 이것마저 없다면 사용자는 예외에 대처할 기회를 포착하지 못하게 될테죠. 사용자가 이 기회를 무시 했다고 해도 DBConn이 close 함수를 호출해 줄것이므로 문제는 없습니다. 

 * 소멸자에서는 예외가 빠져나가면 안됩니다. 만약 소멸자 안에서 호출된 함수가 예외를 던질 가능성이 있다면, 어떤 예외이든지 소멸자에서 모두 받아낸 후에 삼켜 버리든지 프로그램을 끝내든지 해야 합니다. 
 * 어떤 클래스의 연산이 진행되다가 던진 예외에 대해 사용자가 반응해야 할 필요가 있다면, 해당 연산을 제공하는 함수는 반드시 보통의 함수(즉, 소멸자가 아닌 함수)이어야 합니다.

1. abort로 프로그램 종료
2. 해당 함수에서 예외처리
3. 상속 받음 객체의 일반 함수에서 종료 체크후 종료 안되었을 경우 소멸자에서 한번더 종료



댓글 없음:

댓글 쓰기

[Effective C++] 항목 30 : 인라인 함수는 미주알고주알 따져서 이해해 두자.

인라인 함수를 사용하면 컴파일러가 함수 본문에 대해 문맥별 최적화를 걸기가 용이해집니다. 인라인 함수의 아이디어는  함수 호출문을 그 함수의 본문으로 바꿔치기하자는 것  남발했다가는 코드의 크기가 커질 게 뻔하다. 인라인 함수로 부풀려진 ...