C++ - exception

mohadang·2022년 9월 26일
0

C++

목록 보기
13/48
post-thumbnail
post-custom-banner

Exception

  • c++에도 예외 처리가 있지만 중요성이 떨어짐

  • 자바는 모든게 exception

  • 예외 사용은 모든 언어에서 너무 남용되고 있다

    • if로 처리될 수 있는데도 불구하고
    • 예외로 부터 안전한 코드를 짜기 힘들다
    • 사람의 생각은 선형적이다.
  • C++ 자체에서 예외는 없다, 단 프로그래머가 만드것이다.

    • 프로그래머가 만든 표준 라이브러리에서 많은 예외를 던지기도 함
  • 자바나 C#에 있는 예외가 c++에는 없다

  • EX : 범위 이탈, 0으로 나누기, NULL 객체

//[out of range]
#include <exception>
int main()
{
  std::string str = "coco";
  try
  {
    char ch = str.at(5);
  }
  catch(const std::out_of_range& e)
  {

  }
  catch(const std::exception& e)
  {

  }
}
  • 위의 exception 처리는 사실 사용되면 안되는 것이다. 사람이 얼마든지 컨트롤 할 수 있는 예외이기에 직접 if로 예외를 처리해야 한다.
    • 예외 처리는 정말 내가 컨트롤 할 수 없을 때만 처리 해야 한다..
    • Ex
    const size_t index = 5;
    if(index < str.size())
    {
      char ch = str.at(index);
    }

Divide 0

  • C++ 언어 단에서 처리하지 않음
  • 0으로 나누는 것은 각 컴파일러 회사마다 처리를 다르게 한다.
  • 0으로 나눈는 것은 윈도우 os 에서 익셉션을 발생 시킨다(인터럽트 발생)
  • 이 역시 코드로 예외 처리 가능하기에 exception 처리 할 필요 없다.

NULL 참조

  • Divide 0 익셉션과 같다
  • 이 역시 코드로 예외 처리 가능하기에 exception 처리 할 필요 없다.

OS Exception vs C++ Exception

  • OS Exception
    • 비 동기적 : 프로그램은 프로그램대로 실행되고, 익셉션 처리는 윈도우 단에서 따로 돌면서 익셉션 발생하면 처리
    • 윈도우 : 구조적 예외 처리(SEH), 느림
    • 리눅스 : POSIX 신호, Fault, Traps, Aborts
      • 윈도우에서 예외처리 작성한 코드를 리눅스로 옴기면 프로그램 실행 안 될 수도 있다.
    • 플랫폼 마다 다름
    • 오버헤드 비교적 많음, 느림
  • std::exception
    • 동기적
    • C++ STL
    • 모든 플랫폼에 공통
    • 오버헤드가 비교적 적음.

생성자에서의 예외 처리

  • 대부분의 예외는 불필요하지만 단 한곳 필요한 곳은 있다. 그것은 생성자...
Invectory::Invectory(int count)
{
  mSlots = new int[count];
}
  • mSlots가 NULL이면 어떻게 해야 할까?
    • 에러코드 반환 ??
      • 당연히 생성자에서는 못함.
    • 자기 자신을 NULL로 Settting??
      • this = NULL; ???
      • 이미 개체가 생성한 거다.
  • 생성자에서 예외로 처리 해야 한다.
struct SlotNullException : public std::exception
{
  const char* what() const throw()
  {
    retrun "Slot is NULL";
  }
};// 사용자 정의 예외

Invectory::Invectory(int count)
{
  mSlots = new int[count];
  if(mSlots == NULL)
  {
    throw SlotNullException(); // 예외 발생
  }
}
  :
Inventory* myInventory = nullptr;
try
{
  myInventory = new Inventory(5);
}
catch(const SlotNullException& e)
{
  std::cerr << e.what() << std::endl;
}
catch(const std::exception& e)
{
  다른 에러
}
  • 그런데 생각해 보자, 위의 코드가 올바른 예외 처리가 맞을 까??, mSlots 할당이 실패
    했으니 이후 코드에서 못쓰게 만들어 줘야 하는것 아닌가??

  • 생성자에서 쓰는 예외는 정말 괜찮을까??

    • 기본적으로 대부분의 C++ 컴파일러의 예외처리 기능은 꺼져있다 왜 ?? 이유는 느리니까.
    • 익셉션 발생하면 가장 로우 단에서 스택 정리 발생한다.
    • Exception을 꼭 써야 한다면 C++을 사용하지 않을 것이다. 이유는 성능 때문
  • 메모리 부족 때문에 예외가 발생하면 어떻게 하지?

    • 프로그램 종료 해야함?
      • 크래시랑 뭐가 다름
    • 생성자 호출 재시도
      • 하지만 이미 메모리가 동났는걸??
      • 모든 new마다 NULL 나오는 것을 핸들링 한다?? 어려울 껄??, 상식적으로 어렵다.
  • C++ vs 다른 언어들

    • 다른 언어들은 지금까지 예외를 광범위하게 남용해 왔음
  • 역사적으로 Exception

    • 아직도 업계에서는 Exception을 제대로 사용하지 못한다.
    • C는 에러 코드 기반으로 프로그래밍을 한다. 그래서 에러코드를 반환하는 함수를 만든다. 근데 결과값을 반환하는 함수는 리턴을 어떻게 해야하지 ??
  • C

int result = PrintAllRecord();
GetErrorCode();// 괴랄한 방식, 그리고 Errorcode를 전역 변수로 사용, 뭔 문제가 발생할 지 모름
  • C++
int number;
cin >> number;
if(cin.fail()) 에러 코드 일종
{

}
  • 파이썬 같이 리턴 여러개 반환할 수 있는 언어는 익셉션 사용 안한다.

에러 코드보다는 예외 처리가 좋다 ??

  • 에러 코드는 가독성이 떨어져서 실수하기가 쉽다 ?
    if(handle != INVALID)
    {
      if(getStatus() != SUSPENDED)
      {
        if(!result)
        {
          :
          DO SOMETHING
          :
        }
      }
    }
    • 이렇게 중첩하면 보기 어렵겠지 그러나 이것은 코딩 스타일 문제지 에러 코드 반환 문제가 아니다.
    if(handle == INVALID)
      return;
    if(getStatus() == SUSPENDED)
      return;
    if(result == false)
      return;
      :
    DO SOMETHING
      :
    • 다음과 같이 "Early Exist" 방식을 사용 했다.
    • 에러 코드는 예외 처리 못지 않게 가독성이 높음, 혹은 더 높을 수도 있음.
  • 예외는 코드의 유지 보수를 쉽게 만든다 ?
  • 예외는 코드를 더 안전하고 탄탄하게 만든다 ?
    • 예외 처리 진영
      • 예외 처리를 사용하면 예외가 발생하는 경우에도 프로그램이 계속 돌아간다.
      • 그래서 서비스가 다운되지 않고 계속 실행된다.
        -> 문제가 발생 했는데 계속 돌리는 것은 SW를 좀비로 만드는 것이다
    • 예외 처리가 없는 운영체제(Windows, Android, Linux, IOS)
      • 대부분 C로 짜여져 있음
      • 익셉션 사용 안함
      • 그런데도 신뢰적인 소프트웨어임.
  • 예외 안전성
    • 예외가 나더라도 프로그램 상태가 예외가 발생하기 이전 상태로 돌아가야 한다.
    • DB의 트랜잭션이란 개념과 비슷
  • 예외 안정성이 보장되지 않은 코드
public class CoffeeShop
{
    :
  void SetWithPoint()
  {
    throws EmptyItemException
    {
      deductPoint(customer, points);// 고객 포인트를 깍는다.

      if(isEmpty(itemID))
      {
        throw new EmptyItemException();// 고객 포인트는 까졌는데 이제와서
      }                                // 고객이 없다고 Exceptino 발생
    }
  }
    :
};
  • 프로그램이 계속 실행된다고 하더라도 정상이 아니다.
  • 예외 안전성 보장하는 코드
public class CoffeeShop
{
    :
  void SetWithPoint()
  {
    {
      if(isEmpty(itemID))
      {
        throw new EmptyItemException();
      }
      deductPoint(customer, points);

    }
  }
    :
};
  • 위의 코드는 간단해서 그렇제 좀만 복잡해 지면 예외 상황 처리하기가 정말 힘들어 진다.

  • 만약 위의 함수가 5가지 예외 상황 던진다고 하면 함수 호출 할 때 5가지 예외 상황이 발생할 수 있는 것이다.

  • 5번 호출하면 25가지 예외 상황 처리 코드가 들어간다.

  • 근데 그 함수가 또 다른 5가지 예외 상황 발생 할 수 있는 코드를 호출하면??? X5

  • 100% 예외 안전성을 가지는 프로그램을 짜는 게 쉬운 일이 아님

    • 사람은 무엇을 읽어도 위에서부터 아래로 차례대로 읽음
    • 예외 처리 프로그램 방식으로 100줄짜리 프로그램을 짠다면 100개의 리턴문이 생기는 것과 마찬가지.
      • GOTO는 나쁘다라는 말을 들어본적 있을 것이다...
  • 어떤 함수가 예외를 처리하지 ?

    • 수많은 언어들에서 어떤 함수가 무슨 예외를 던지는지 알기 힘듦,

    • 함수 헤더에 그 함수에서 던지는 예외를 표기하지 않음

      • Java 는 예외, Java에서는 함수 작성시 시그니처에 어떤 Exception 발생하는지 표기 해야함.
      viud Function4()
        throw classNotFoundException
      {
      
      }
    • 예외 처리는 보기 힘들다.

    try
    {
      Function1(); <-- 도대체 어디서 Exception이 튀어나오는지 알 수 있을까??
    }
    catch(const SampleException& e)
    {
      ...
    }
    Function1()
    {
      Function2();
    }
    Function2()
    {
      Function3();
    }
    Function3()
    {
      throw SampleException();
    }
    • 함수 트리를 뒤지는 작업이 쉽지많은 않다.
    • 정말 괜찮은 해결책이 나올 뻔 했는데...
      • WCF
        • 언어단에서 트랜잭션 지원
        • 공식 : 서비스 지향 애플리케이션 개발을 위한 프레임워크
        • 비공식 : .NET 프레임워크의 계승자(더 이상 일어나지는 않겠지만)
        • 일단 예외를 던진 개체를 사용하거나 그 개체에 접근할 수 없음
          • 접근을 시도하면 또 다른 예외 발생

웹 방식의 에러코드 처리

  • 웹은 에러코드로 돌아갔다.
    • 웹 요청은 상태코드(status code)와 바디(body)를 반환
    • 만약 상태코드가 20X라면, 바디가 있음.
    • 만약 상태코드가 에러 코드라면(4xx, 5xx)
      바디가 비어 있을 수 있음.
  • 다음을 이용하여 C++에서도 웹과 같은 처리를 할 수 있다.
    • Struct
    • Class
    • 실리콘 밸리 어떤 회사에서도 많이 사용하는 방식
enum EError  {    SUCCESS, ERRROR  }

struct ErrorCode
{
  EError Status;
  int Code;
}

template<typename T> struct result
{
  ErrorCode Error;
  T Value;
}

  :

Result<const char*> result;

result = record.GetStudentIDByName("Pope Kim");

if(result.Error.Status == ERROR)
{
  cout << "Error Codee " << result.Error.Code << endl;
}
else
{
  cout << result.Value << endl;
}

적절한 예외 처리

  • 예외 처리를 완전히 사용 못한다는 것은 불가능 하다.

  • 내가 만든 프로그램, 소스코드, 수정할 수 있는 소스 코드

    • 내 안의 경계
  • 경계 상황

    • 외부 라이브러리, 파일 시스템, "내가 Control 할 수 없는 부분"
  • 1 유효성 검사/예외는 오직 경계에서만

    • 밖에서 오는 데이터를 제어할 수 없기 때문
      • 밖에서 짠 Code를 내가 짰는가??
      • EX) 외부에서 들어오는 웹 요청, 파일 읽기/쓰기, 외부 라이브러리
      • 조엘이 네트워크 리소스를 사용하는 API의 인터페이스가 복잡해야만 하는 이유가 생각난다. API 인터페이스가 간결 하면 당장은 사용하기 편하겠지만 예외 처리를 하는데 어려움을 겪을 것이다.
      • EX)
        1. 파일을 버퍼로 읽는 중이다
        2. 그런데 누군가 파일을 지웠다.
        • 누군가 파일을 지우는 상황은 내가 컨트롤 할 수 없는 경계 상황이기 떄문에 파일을 버퍼로 읽는 것을예외 처리 하는 것이 맞는 방법이다.
  • 2 일단 시스템에 들어온 데이터는 다 올바르다고 간주할 것

    • EX)
      1. 웹으로 데이터를 요구했다.
      2. 그러나 웹에서는 NULL을 반환했다.
    • 웹에서 들어온 데이터 경계 밖의 데이터임으로 유효성 검증을 해야 한다.
    • 유효성 검증이 완료되면 그 순간부터는 내 System에 데이터가 들어오가 완전히 올바른 데이터라고 간주해야 한다.
    • 그래서 내 System안의 코드에서는 적어도 웹에서 들어온 데이터때문에 예외 처리를 할 필요 없이 코드가 깔끔해 지고 로직에만 집중 할 수 있다.
    • 다른 개발자가 실수로 잘못된 데이터를 반환한다면
      • assert를 사용하여 개발 및 Debuging 중에 문제를 잡아내고 고칠 것
  • 3 예외 상황이 발생할 떄는 NULL을 능동적으로 사용할 것

    • 함수가 제대로 돌지 않을때 웹 방식의 리턴을 이용하여 예외를 처리 할 수 있다. 그러나 그것이 귀찮다면 NULL을 반환해서 표현 할 수도 있다.
    • 하지만 기본적으로 함수가 NULL을 반환하거나 받는 일은 없어야 함
    • 코딩 표준 : 만약 NULL을 반환하거나 받는다면 함수의 이름을 잘 지을 것.
  • 요약

    • 경계에서의 유효성 검사를 정말 정말 확실하게 검사
    • 예외 던질려면 정말 경계 상황에서만 잡아야 한다.
    • 경계 내부에서는 예외 없이 짜는것이 정말 깔끔하다.
    • 예외 상황 생각하지 못하고 실수때문에 나오는 것은 assert로 잡아야 한다.
    • Test의 품질은 Exception을 얼마만큼 잘 쓰는것이 좋은 것이 아니라 얼마만큼 Test를 하는가이다.
  • EX 1

string ReadFileOrNull(string filename) // null 반환 한다고 표시
{
  if(!File.Exists(filename)) // 경계에서 유효성 검사.
  {
    return null;
  }

  try
  {
    return File.LoadAllText(filename); // 경계밖에서 일어날 수 있는 예외 잡음.
  }
  catch(Exception e)
  {
    return null;
  }
}
  • EX 2
int ConvertToHumanAge(const Animal* pet)// or NULL 조건이 아닌 반드시 제대로된 데이터가 들어온다.
{
  Assert(pet != NULL);
    :
/*
  pet이 들어올 데이터가 반드시 유효할 것이라고 간주,
  NULL이 들어오면 함수 사용한 사람에게 알려서 고치라고 지시
  pet을 위한 예외 처리 뿐만 아니라 이 함수를 호출한 쪽에게 어떻게 문제를 처리할 지, 아니면
  이 함수내에서 무었인가 처리해야 할 지 고민할 필용도 없음.

  릴리즈 버전에서는 Assert는 자동으로 삭제 된다.
*/
}

예외는 만병통치약이 아니다.

  • 동일한 프로그래머가 로직과 예외를 모두 작성??
    • 로직이 잘못돼 있으면 예외도 틀렸을 가능성이 높음
      • 그 프로그래머가 잘못된 이해를 바탕으로 로직을 짬
      • 잘못된 이해를 바탕으로 만든 로직을 또 잘못된 이해를 바탕으로 예외 처리함.
    • 동일한 프로그래머가 작성한 유닛 테스트가 한계를 갖는 이유
      • 잘못된 이해를 바탕으로 로직을 짜고 그 잘못된 로직이 정상인지 검사하는 테스트를 한다.??
      • 로직도 실수 했는데 테스트도 실수 안한다는 보장 있나??
    • Tester 인력을 따로 두는 이유...
  • 양질의 소프트웨어는 예외가 아니라 철저한 테스트 계획에서 만들어진다.

개발 중 버그 잡기

  • 이걸 라이브 서버에서 해서는 안 됨
    • 하지만 예외 때문에 그런 유혹을 잡음
  • 개발 중 코드에서 버그를 잡기 위해서는 assert를 사용
    • 전제 조건, 사후 조건, 그리고 불변값 확인
    • 또한 assert가 실패하면 올바른 호출 스택을 볼 수 있음
  • 품질 관리(QA)가 제대로 이루어지지 않았다면 차라리 소프트웨어가 뻗어 버리게 만드는 게 좋다.
    • 문제가 바로 드러나기에 바로 고칠 수 있기 때문
    • 하지만, 안전과 생명에 관련되는 소프트웨어라면 예외
  • 시체보다는 좀비가 낫지 않나요??
    • 예외가 없는 경우
      • 프로그램에서 크래시 발생
      • 크래시에서 메모리 덤프를 얻을 수 있음
      • 개발자는 덤프 파일을 열어 디버깅 할 수 있음.
      • 시나리오
        • 게임중에 예외 발생
        • 사용자에게 Error를 개발자에게 보내겠습니까?(실제로는 메모리 덤프)
        • 개발자는 받은 메모리 덤프에서 바로 디버깅 가능
    • 예외가 있는 경우
      • 왼쪽에 언금한 어떠한 것도 안 할 가능성이 높음
      • 아마 어딘가에서 printf로 찍은 로그(log)나 보며 디버깅 하겠지
      • 시나리오
        • 에외가 발생
        • 그러나 메모리 덤프를 사용하지 않고 로그를 찍음
        • 그러나 로그를 찍었는데 정보가 충분하지 않음. 그리고 로그가 너무 복잡해서 담당 개발자가 아니면 분석이 불가능
        • 그래서 로그에 정보를 더 찍음
        • 그리고 다시 예외가 발생할 때까지 기다림...
profile
mohadang
post-custom-banner

0개의 댓글