본문 바로가기
Homo Coding

예외에 대한 비용

by javauser 2011. 4. 28.
아래 내용은 http://blog.dynatrace.com/2011/04/12/the-cost-of-an-exception/ 를 번역한 부분입니다.

최근에 예외 비용에 대해서 dynaTrace에서 더 많은 토론이 있었다. 고객과 같이 일할 때 고객이 알지 못하는 많은 예외들을 매우 자주 발견하곤 한다. 이러한 예외를 없앤 후에, 코드는 이전 보다 상당히 더 빨라진다. 이는 코드에서 예외를 사용하는 것은 심각한 성능 오버헤드를 가지고 온다는 가정을 만든다. 아마도 예외를 사용하는 것을 피하는 것이 더 낫다라고 생각할 수도 있다. 예외가 에러 상황을 처리하는데 중요한 뼈대를 이루기 때문에 예외를 피하는 것은 좋은 방법이 아닌 것처럼 보인다. 무엇보다도 이는 예외를 던지는 비용에 대해서 더 자세히 들여달 볼 충분한 이유이다.

한가지 실험

예외를 임의로 던지는 간단한 코드를 가지고 실험을 해보았다. 이는 실제 심도있는 실험 측정치는 아니며 HotSpot 컴파일러가 해당 코드가 실행될 때 무엇을 하는지에 대해서는 잘 모른다. 하지만, 몇가지 기본적인 영감을 제공하고 있다.

public class ExceptionTest {
 
  public long maxLevel = 20;
 
  public static void main (String ... args){
 
    ExceptionTest test = new ExceptionTest();
 
    long start = System.currentTimeMillis();
    int count = 10000;
    for (int i= 0; i < count; i++){
      try { 
        test.doTest(2, 0);
      }catch (Exception ex){
//        ex.getStackTrace();
      }
    }
    long diff = System.currentTimeMillis() - start;
    System.out.println(String.format("Average time for invocation: %1$.5f",((double) diff)/count));
  }
 
  public void doTest (int i, int level){
      if (level < maxLevel){
        try {
          doTest (i, ++level);
        }
        catch (Exception ex){
          throw new RuntimeException ("UUUPS", ex);
        }
      }
      else {
        if (i > 1) {
          throw new RuntimeException("Ups".substring(0, 3));
        }
      }
  }
}


결과
결과는 매우 흥미로왔다. 예외를 던지고 잡는 비용은 오히려 낮은 것처럼 보인다. 예제에서 하나의 예외당 약 0.002ms가 걸렸다. 이는 실제로 너무나 많은 예외를 던지지(10만건 이상) 않는다면 다소 무시할 수 있을 것이다.

이러한 결과가 예외 처리 그 자체는 코드 성능에 영향을 미치지 않는 것을 보여주지만, 다음과 같은 의문을 남긴다. 예외에 대한 엄청난 성능 영향에 대한 부분은 어떤 것이 문제인가? 분명 내가 어떤 중요한 것을 놓쳤다.

다시 생각해본 후에, 예외 처리에 대해 중요한 부분을 놓쳤다고 깨달았다. 예외가 발생할 때 어떤 것을 수행하는지에 대한 부분을 놓친 것이다. 대부분의 경우 예외를 단순히 잡지 않는다. 보통 문제에 대한 보상을 하려고 하며 최종 사용자들에게 애플리케이션이 계속해서 기능을 유지하도록 한다. 따라서 내가 놓진 점은 예외 처리에 대해 실행되는 보상 코드였다. 이러한 코드가 무엇을 하는지에 따라서 성능에 대한 문제는 매우 중대해질 수 있다. 어떤 경우에 이는 서버에 접속을 재시도하는 것을 의미할 수도 있으며 다른 경우에는 매우 성능이 나오지 않는 솔루션을 제공하는 기본 예비 시스템을 사용한다는 것을 의미할 수도 있다.

이는 많은 시나리오에서 보았던 행위를 잘 설명한 것처럼 보일 수 있지만, 분석에 대해 아직 수행하지 않았다고 생각한다. 여기에서 놓쳤던 다른 것이 있다는 느낌이 들었다.

스택 트레이스
스택 트레이스를 종합해볼 때 상황이 어떻게 바뀌는지를 조사했던 이러한 문제에 대해 여전히 의문점이 남는다. 이는 매우 자주 발생하는 것이다. 어떠한 부분이 문제인지를 해결하기 위해 예외와 스택 트레이스를 로그를 남긴다.

따라서 코드를 이제는 예외의 스택 트레이스를 가지고 오도록 수정했다. 이는 극적인 반전이었다. 예외에 대한 스택 트레이스를 얻어오는 것은 단지 예외를 잡고 던지는 것보다 성능에 대해 10배 이상의 더 높은 영향을 미쳤다. 따라서 스택 트레이스가 문제가 어디서 그리고 왜 발생했는지를 이해하는데 도움이 되겠지만 성능에 지대한 영향을 미친다.

여기에서 영향은 단일 스택 트레이스에 대해 거론하지 않았지만 매우 높다. 대부분의 경우 예외는 여러 수준으로 던지고 잡는다. 서버에 연결하는 웹 서비스 클라이언트에 대한 단순한 예제를 살펴보자. 먼저 연결 실패에 대한 자바 라이브러리 수준의 예외가 있다. 그 다음에 실패한 클라이언트에 대한 프레임워크 수준의 예외가 있으며 특정 비즈니스 로직 호출을 실패했다는 애플리케이션 수준의 예외가 있을 수 있다. 여기에는 세가지 수준의 스택 트레이스가 종합되어 있다.

대부분의 경우, 이러한 예외들을 로그 파일이나 애플리케이션 출력을 통해 보아야 한다. 이러한 긴 스택 트레이스를 기록한다는 것은 다소 성능에 영향을 미친다. 최소한 로그 파일을 통상 볼 때 보통 살펴보고 그에 대한 반응을 할 수 있다.

내가 목격했던 더 안좋은 행위는 잘못된 로깅 코드 때문인 경우도 있다. log.isxxEnabled()를 먼저 호출함으로써 로그 레벨을 점검하는 대신에 개발자들은 단지 로깅 메소드를 호출한다. 이러한 경우, 로깅 코드는 항상 예외에 대한 스택 트레이스를 가지고 오는 행위를 포함하는 것을 실행한다. 어찌되었든 로그 레벨이 너무 낮게 세팅되어 있다면 이러한 내용을 볼 수 없으며 이에 대해서 인식하지 못할 수도 있다. 로그 레벨을 먼저 점검하는 것은 불필요한 객체 생성을 방지하는 보편적인 규칙이어야 한다.

결론
잠재적인 성능 영향으로 인해 예외를 사용하지 않는 것은 좋지 않은 생각이다. 예외는 실행시 문제에 대처할 수 있는 보편적인 방식을 제공하는데 도움이 되며 깨끗한 코드를 작성하는데 도움이 된다. 하지만 코드에서 던지는 예외의 수를 추적할 필요는 있다. 비록 예외를 잡을 수도 있지만 여전히 심각한 성능 영향을 준다. dynaTrace에서 예외를 추적하고 있으며 많은 경우, 코드에서 어떤 일이 진행되고 이를 해결함으로써 어떠한 성능 영향이 있는지를 보고 다들 놀란다.

예외를 사용하는 것은 좋은 것이지만 너무 많은 스택 트레이스를 잡는 것은 회피해야 한다. 문제를 이해할 필요가 없는 경우조차도 많이 있다. 특히 이미 예견되는 문제를 다루는 경우에는 더 그렇다. 따라서 예외 메시지는 충분한 정보가 된다는 것이 검증될 수도 있다. 연결 실패 메시지로부터 충분한 정보를 얻을 수 있기 때문에 java.net 호출 스택의 내부로 전체 스택 트레이스를 필요로 하지 않는다.
반응형