이론/일반

예외(Exception) 써야할까?

tsyang 2022. 6. 19. 23:42

예외처리의 동작방식


 

우선 예외에는 대안이 있다. 바로 에러 코드를 함수에서 리턴하는 것이다. 즉, 예외를 쓰려면 에러 코드를 리턴하는 것보다 더 좋아야 한다.

 

그렇다면 예외가 어떻게 동작하는지 간단히 보자.

 

void bar()
{
	throw std::runtime_error("some exception");
}

void foo()
{
	bar();
}

int main()
{
	try
	{
		foo();
	}
	catch(...)
	{
	}

}

 

처음에 main함수에 대한 스택 프레임이 올라가고, 그다음 foo 스택 프레임이 올라가고, 그다음에 bar 함수에 대한 스택 프레임이 올라간다. 

 

 

bar함수에서 예외가 던져지면 exception 객체가 힙에 생성되고 bar함수의 스택 프레임은 스택에서 pop이 된다. 

 

이어서 foo 스택도 pop이 되고 main함수의 catch 구문에서 Exception 객체를 가리켜 처리한다.

 

 

이런식으로 Exception이 throw되면 try/catch 문을 만날때까지 스택을 거슬러 올라가며 pop을 한다. 이것을 stack unwinding 이라고 한다.

 

만약 main()에 이르러도 try/catch가 없다면 프로그램이 종료된다.

 

만약 에러 코드를 리턴했다면 이런 리턴값을 하나 하나 체크하면서 스택을 거슬러 올라가야 하기 때문에 더 복잡해 질 수 있다. 함수의 호출 스택이 엄청 많다고 생각한다면 예외 처리가 간편해보인다.

 

 

 

예외의 퍼포먼스


 

예외 처리의 퍼포먼스는 다음의 두 가지로 나눠 생각해봐야 한다.

 

  • 예외가 발생하지 않음 : 오버헤드가 거의 없다.
  • 예외가 발생함 : 오버헤드가 약간 있다. 왜냐하면 Exception 객체가 생성되기 때문이다. 이는 개발 환경마다 오버헤드가 거의 없다고 말할수도 , 크다고 말할 수도 있다.

 

참고로 Zero Cost Exception 이라는 개념도 있는데, Program Counter(PC)와 Exception Table 등을 이용하여 예외가 발생하지 않은 경우 예외처리가 전혀 없는 로직을 따라가도록 코드가 생성된다. 그러나 예외가 발생한 경우는 더 느릴수도 있다고 한다. 
참고) https://mortoray.com/2013/09/12/the-true-cost-of-zero-cost-exceptions/
https://devblogs.microsoft.com/oldnewthing/20220228-00/?p=106296

 

아무튼 사람마다 예외처리가 비싼가? 에 대한 대답은 다 다르지만, 예외가 발생하지 않은 경우 오버헤드는 무시할만하다라는 대에는 이견이 딱히 없는듯..

 

 

그렇다면 예외가 발생한 경우는? 이 경우에도 오버헤드는 무시할만 하지만, 만약에 루프안에서 예외가 지속적으로 발생하는 경우에는 성능에 악영향을 미칠 수 있으므로 주의해야 한다는 것은 확실하다.

 

이게 어느정도인지 궁금해서 대충 코드를 짜서 돌려봤다.

public static void Main(string[] args)
{
    Stopwatch sw = new Stopwatch();
    long timeNoCatch, timeNoException, timeException;


    sw.Reset();
    sw.Start();
    for (int i = 0; i < 10e4; ++i)
    {
        int sum = 0;
        checked
        {
            for (int j = 0; j < 10000; ++j)
                sum += 214748;
        }
    }
    sw.Stop();
    timeNoCatch = sw.ElapsedMilliseconds;

    sw.Reset();
    sw.Start();
    for (int i = 0; i < 10e4; ++i)
    {
        try
        {
            int sum = 0;
            checked
            {
                for (int j = 0; j < 10000; ++j)
                    sum += 214748;
            }
        }
        catch
        {
            // ignored
        }
    }
    sw.Stop();
    timeNoException = sw.ElapsedMilliseconds;

    sw.Reset();
    sw.Start();
    for (int i = 0; i < 10e4; ++i)
    {
        try
        {
            int sum = 214748;
            checked
            {
                for (int j = 0; j < 10000; ++j) //10000번째 예외발생
                    sum += 214748;
            }
        }
        catch
        {
            // ignored
        }
    }
    sw.Stop();
    timeException = sw.ElapsedMilliseconds;

    Console.WriteLine($"No Try/Catch Block : {timeNoCatch}");
    Console.WriteLine($"Try/Catch Block but no Exception : {timeNoException}");
    Console.WriteLine($"Try/Catch Block And Exception : {timeException}");
}

 

대충 덧셈 1만번을 수행하며 예외 처리를 발생시키거나 발생시키지 않는 코드를 작성했다.

결과는 위와 같이 나왔는데, 사실 코드 실행 순서에 따라 성능 차이가 좀 있기에 Try/Catch문을 사용 했을때와 안 했을때의 시간 차이는 아주 미미하다고 할 만 하다. (비록 위에서는 Try/Catch문을 쓴게 오히려 빨랐지만) 그렇지만 확실한건 예외가 발생했을때 실제로 오버헤드가 꽤 컷다는 점은 명백하다. (대충 for루프에서 오버플로 체크하는 덧셈 5천번 수행한 정도)

 

물론, 위 실험은 실제 하드웨어의 사용률과 컴파일러의 최적화 방식에 따라 차이가 있으므로 매우 신뢰도가 낮지만 어쨋든 예외가 발생하면 코스트가 꽤 크다는건 관측할 수 있었다.

 

 

예외 처리의 safety guarantee


이건 가비지 컬렉터가 없는 C++등의 언어에서 준수해야 할 사양인데, 어떤 함수 f()가 예외를 던졌다면 그 함수를 호출하는 모든 함수들은 exception safety guarantee를 준수해야 한다는 것이다. 

 

이런 safety guarantee는 대충 resource leak을 방지하기 위해 쓰인다. 

 

void foo()
{
	auto someClass = new SomeClass();
	
	throw std::runtime_error("some exception");
	
	delete someClass;
}

즉 위와 같은 함수에서 예외가 던져지고 나면 someClass에 할당된 메모리가 해제되지 않기 때문에 memory leak이 발생한다. 

 

따라서 코드를 다음과 같이 작성해야 한다.

void foo()
{
	auto someClass = std::make_unique<SomeClass>();
	throw std::runtime_error("some exception");
}

 

이런 safety guarantee에도 3가지 종류가 있는데 알아서 찾아볼 것.

 

 

 

그래서 Exception 쓸까 말까?


 

결론부터 말하면 정답은 없다. 취향 차이. 그리고 케바케.

 

그래도 이유를 좀 알아보자면, 구글의 C++ 스타일 가이드의 Exception 항목에 그 이유가 잘 나와 있다.

 

우선 구글의 스타일 가이드에는 명료하게  "We do not use C++ exceptions." 라고 적혀있다.

 

그 이유는 익셉션이 물론 장점이 있지만 다음의 단점이 있다는 것이다.

 

  • 어떤 함수가 익셉션을 던지면 그 함수를 호출하는 모든 함수들이 exception safety guarantee를 준수하는지 체크해야 한다.
  • 프로그램의 가독성을 떨어트린다. (이건 구글의 견해이고.. 아니라고 하는 사람도 있다.)
  • 컴파일 시간이 약간 늘어난다.
  • 익셉션을 허용하면 개발자들이 적절하지 않은 상황에서도 익셉션을 던질 수 있음. (즉, 남용)

 

그러나 구글 스타일 가이드의 결론에도 일부 상황에서는 쓰는게 좋을 수도 있다고 인정하고 있다.

 

 

 

만약 익셉션을 사용하기로 했다면 주의해야 할 점이 있는데 ,

 

  • 예상치 못한 실패에 대해서만 사용해라. (예 : nullptr, outofrange같은 익셉션은 던지지 마라. 예기치 못한 실패가 아니라 그냥 프로그래머의 실수임)
  • 루프 안에서 익셉션을 자주 던지면 오버헤드가 크다.
  • 비관리 언어의 경우 resource leak을 주의해야 한다.
  • 어떤 함수 안에서 예외를 처리할 수 있다면 굳이 익셉션을 throw 할 필요도 없다.
  • 절대 일어나지 않을 일을 가정하고 익셉션을 던지지 마라. 이건 그냥 절대 안 일어나는 거니까 코드만 복잡해진다.
  • destructor, swap, move, default constructor 에서 예외 던지면 예외가 꼬일 수 있음.

요정도 되시겠다.

 

 

익셉션이 유용한 대표적인 사례는 바로 생성자인데, 생성자는 값을 리턴할 수 없으므로 에러 코드를 리턴할 수 없기 때문에 오로지 익셉션을 통해서만 예외 처리가 가능하다.

 

 

 

 

'이론 > 일반' 카테고리의 다른 글

N번째 난수 값 얻어오기  (0) 2023.06.18
클로저 (Closure)  (3) 2022.06.25
객체 메모리, Object Alignment  (0) 2021.04.18
멀티플레이 게임과 동기화  (0) 2020.10.11