본문 바로가기
연구하기/Computer Engineering

예외 (Exception) - 2부 -

by 썰렁황제 2006. 3. 28.
2007-02-14 : C# 에서 throw 시 StackTrace 를 초기화하지 않는 방법이 있어 이 부분을 추가했습니다.

예외 (Exception) - 2부 -
 
2006년 3월 28일 강현신


C++ 의 예외 처리

  C++ 에서는 예외 발생을 묘사하기 위하여 throw 를, 예외 흐름을 묘사하기 위해 try ~ catch 블록을 사용하고 있다.

기본적인 표현 방식은, try 블록 안에 예외가 발생할 수 있을 가능성이 있는 주 흐름 명령어들을 위치시키고, catch 블록 안에 이러한 주 흐름에서 발생한 예외를 처리하는 예외 흐름을 기술한다. try 블록 안에 위치한 주 흐름은 많은 형태의 명령어로 기술될 수 있으므로, 발생할 수 있는 예외 또한 여러 가지 형태가 될 수 있다. C++ 은 catch 구문 뒤에 변수 형식 (type) 을 지시하여, throw 사용 시 인자로 들어가는 데이터의 형에 따라 각 예외에 대한 예외 처리 흐름을 구분할 수 있는 수단을 마련하고 있다.

  C++ 예외 처리가 가지는 최대의 결점은, 예외 상황의 발생이 오직 프로그래머가 직접 기술해주는 것으로만 이루어진다는 것이다. 즉 메인 시스템에서 발생하는 각종 오류, 외부 서브 시스템에서 발생하는 오류 등 각종 다양한 형태의 오류들에 대해서 C++ 은 기본적인 오류 처리를 해 주지 않는다. 따라서 사용자는 이러한 상황의 발생을 모두 일일이 체크하여 하나 하나 기술해주어야 한다.

  이 점은 모든 것을 하나 하나 기술해 주어야 하는 C 라는 언어의 특성상 어쩔 수 없는 것이기도 하고, 적절한 모듈화를 통하여 편의성을 높일 수 있는 여지가 존재하지만, 근본적으로 언어 자체의 기본 개념에서 기술하기 어렵다는 점은 매우 아쉬운 일이다.
 

Java 의 예외 처리

  Java 는 겉보기로는 C++ 과 매우 유사한 형태의 예외 처리 구조를 지원한다. 하지만 Java 는 객체 지향 기반의 언어이며, 또한 Virtual Machine 이라는 독특한 구동 플랫폼을 사용하고 있기 때문에 실질적인 내부 예외 처리 형식은 C++ 과 굉장히 큰 차이를 가지고 있다.

  프로그래머가 모든 예외 상황에 대해 직접 기술해주어야만 하는 C++ 과는 달리 Java 는 Ada 와 같은 예외 개념을 도입한 언어들과 마찬가지로 언어상에서 기본적으로 처리해 주는 예외 상황이 있다. 게다가 Java 라는 언어가 가지는 독특한 특성인 Virtual Machine 의 개념은 Java 의 기본 예외 상황 기술 능력을 크게 확대시켰다. 즉 Virtual Machine 이 한 번 시스템을 추상화시킴으로써 언어가 취급하는 시스템의 범주를 한정시켜, 기본적으로 언어가 구동하는 시스템에서 발생하는 모든 오류 및 특별한 상황의 발생을 예외로서 기술할 수 있게 된 것이다.

   Java 는 예외의 기술을 명확하게 하기 위하여 예외 기술을 처리하는 데이터 형을 C++ 과는 달리 하나의 형태로 정의하였다. 이 목적으로 탄생한 클래스가 Throwable 클래스이며, 이 클래스를 이용해서만 Java 는 예외를 기술할 수 있다. 즉 C++ 에서 throw 를 통하여 예외를 기술할 때 어떠한 형식의 데이터를 첨부하여도 상관없는 것과는 달리 Java 는 오직 Throwable 에서 파생한 클래스만 가능하다.

  C++ 에 비해 오히려 표현 방식이 줄었다고도 생각해 볼 수 있으나, 우리가 주목해야 할 점은 예외를 표현하기 위한 수단으로서 데이터형이 필요한 것이지, 다양한 데이터 형을 표현하기 위한 예외 표현 수단이 필요한 것이 아니라는 점이다. 과연 int 나 float 같은 일반적인 데이터형이 오류를 기술하는 데에 있어서 얼마나 의미있는 내용을 전달해 줄 수 있을까? 그렇다면 차라리 예외에서 발생할 수 있는 공통적인 정보를 묶어 이러한 정보를 기술할 수 있는 수단으로 하나의 데이터 형을 만드는 것이 오히려 유리할 것이다.

  이렇게 예외의 기술을 하기 위한 별도의 데이터형을 만들어냄으로써, Java 는 C++ 이나 기존의 다른 언어에 비해 오류에 대해 더욱 다양한 표현 요소를 가질 수 있는 가능성을 가지게 되었다. 실제로 Java 는 1.4 에 들어와 뒤에서 언급할 예외 계층 표현이라는 새로운 예외 표현 요소를 추가할 수 있게 되었고, 이것은 에외 처리에 대한 데이터형을 별도로 구상한 것이 충분한 의미를 가지고 있음을 증명하는 좋은 예이기도 하다.

  자바의 예외 표현 개체인 Throwable 은 Error 와 Exception 이라는 두 개의 계층으로 분류된다. 이 두 계층은 프로그램 내부에서 예외를 제어할 수 있는 수준이냐 아니냐에 따라서 구분이 되며, Error 는 프로그램 내부에서 제어되지 않는 예외를, Exception 은 프로그램 내부에서 제어되는 예외를 다룬다. 여기서 프로그램 내부에서 제어되지 않는 예외라는 것은 ThreadDeath 와 같은 프로그램 내부에서 모니터링할 수 없는 상황과, VirtualMachineException 처럼 프로그램 자체에서 도저히 복구 불가능한 상황을 이야기하며 이 경우는 사실상 더 이상의 진행이 어려우므로 일반적인 예외와는 별도로 구분하여 상황을 처리할 수 있는 여지를 마련해 주고 있다.

  자바는 예외 처리에 대해서 일일이 상세한 내역을 모두 기술해 주도록 되어 있다. 첫째, 모든 예외 반환 메소드들은 메소드 인자 기술 끝 부분과 블록 시작 사이에 throws Throwable(또는 그 파생 클래스) 를 기술하여 자신이 예외를 반환함을 명시하여야 한다. 또한 이렇게 예외를 반환하는 것이 명시된 메소드들을 호출할 때에는 반드시 try~catch 를 사용하여 예외 처리를 수행하도록 하고 있다. 반대로 catch 의 인자를 통해 기술된 예외가 try 블록에서 발생할 가능성이 없는 경우에는 해당 예외 기술을 허용하지 않는다. 단 Error 및 Error로부터 파생된 제어할 수 없는 예외에 대해서는, 애당초 제어가 불가능하므로 이러한 규칙을 요구하지 않는다.

  이런 엄격한 규칙은 프로그램 코드 상에서 예외의 발생 경로를 명확하게 하고 각 메소드가 어떠한 예외를 발생시킬 수 있는지를 코드 수준에서 이해할 수 있다는 장점이 있는 반면 (실제로 메소드에 이러한 기술이 없는 C#의 경우, 메소드가 어떠한 예외를 발생시킬 수 있는지를 명시적으로 코드에 기술할 방법이 없으므로 이에 대해서는 전적으로 주석에 의존하고 있다), 잠재적으로 모든 코드가 모든 예외를 발생시킨다는 관점에서 보았을 때에는 기술 방식이 상당히 번거롭다는 결점이 있다.

자바가 C++ 와 비교하여 구문상으로 예외에 새로 추가된 것이 있는데 바로 finally 라는 구문이다. 이 구문은 try 블록이 정상적으로 종료되었든지 (이는 return 등의 메소드 종료 구문을 포함한다) 혹은 try 블록에서 오류가 발생하여 catch 블록으로 이동된 후 catch 블록이 종료되었을 때 (이는 다음 예외 처리 블록으로 예외를 전달하는 것 ? throw 를 포함한다) 호출되는 블록으로서, 예외의 발생 유무를 떠나 해당 예외 처리 블록이 종료되는 상황에서 반드시 수행해 주어야만 하는 일을 기술하는 도구로 사용된다. 예외 처리 블록 어느 곳에서든지 예외 처리 블록을 탈출하는 상황에서 언제든지 호출된다는 장점 때문에 해당 작업 종료 시에 반드시 수행되어야 하는 작업이 존재하는 경우 이 구문은 굉장히 강력한 도구이며, 프로그램의 안정성을 증대시키는 데에 결정적인 역할을 담당한다. (데이터베이스 Connection 획득/반납 등이 그 좋은 예 중 하나이다)

  Java 는 버전 1.4 에 들어와 예외의 계층 구조 도입이라는 새로운 예외 기술 방법을 도입하였다.

  1.4 버전 이후의 Java 는 예외 클래스 내에 또 다른 예외의 참조를 둘 수 있으며, 따라서 프로그래머는 초기 발생한 예외를 바탕으로 자신이 새로 예외를 기술할 수 있다. 그러므로 예외를 최종적으로 처리하는 구문에서는 이러한 예외 계층 구조를 분석하여 현재 발생한 원인이 무엇인지 정확하게 분석할 수 있다.

하나 예를 들어보자.

public int calculateABCD(int a, int b, int c, int d) {

  try {

    return calculateABC(a, b, c) / d;

  } catch (Exception e) {

    System.out.println(e.getMessage());

  }

}

 

public int calculateABC(int a, int b, int c) {

  return calculateAB(a, b) / c;

}

 

public void calculateAB(int a, int b) {

  return a / b;

}

 

  calculateABCD() 메소드를 호출하면, b 가 0 이든 c 가 0이든 d 가 0이든 모두 동일하게 ArithmeticException 이 발생한다. 따라서 StackTrace 를 조사해 보지 않는 한 메시지만으로는 이 오류가 어디에서 문제가 발생하였는지 찾기가 어렵다. 일반적인 디버깅 작업에서 발생하는 것이라면 StackTrace 가 일반적으로 더 큰 비중을 가지므로 이런 상황이 큰 문제가 되지 않을 수 있지만, 만약 어플리케이션을 실제로 구동시켜 사용하는 사용자가 이러한 메시지를 본다면 쉽게 문제를 파악할 수 있을까?

  이 구문을 다음과 같이 바꾼다면 최종 출력 단계에서 좀 더 알아보기 쉬운 메시지로 나타낼 수 있다.

 

public int calculateABCD(int a, int b, int c, int d) {

  try {

    return calculateABC(a, b, c) / d;

  } catch (Throwable e) {

    While (e != null) {

      System.out.println(e.getMessage());
      e = e.getCause();

    }

  }

}
 

public int calculateABC(int a, int b, int c) {

  try {

    return calculateAB(a, b) / c;

  } catch (Exception e) {

    throw new Exception(“세 숫자 연산 중 문제 발생”, e);

  }

}

 

public void calculateAB(int a, int b) {

  try {

    return a / b;

  } catch (Exception e) {

    throw new Exception(“두 숫자 연산 중 문제 발생”, e);

  }

}

  좀 극단적인 예이기는 하지만, 이렇게 하면 최종적으로 얻게 되는 예외에 연결된 추가적인 예외들의 메시지를 분석하는 것 만으로도 실질적으로 무엇이 문제인지를 쉽게 이해할 수 있다.
 

C# 의 예외 처리

  C# 은 Java 와 거의 비슷한 컨셉으로 설계된 언어이다. Java Virtual Machine 과 비슷한 .NET CLR 이라는 플랫폼에서 구동하는 가상 머신 기반 언어라는 것에서부터, interface 를 지원하는 점, 언어 기본 구조의 유사성 등 많은 부분에서 유사한 점을 발견할 수 있다.  워낙 코드 상에서 유사한 점이 많아, API 패키지 사용을 제외한다면 Java 코드의 이식성이 굉장히 높고, 실제로 많은 Java 공개 코드들이 C# 으로 고스란히 이식되고 있다.

  그러나 Java 가 기본 문법을 C 에서 가져왔다고 하더라도 실질적으로는 객체 지향 기반 언어로서 C++ 과 완전히 다른 모습을 보이는 것처럼, C# 도 많은 부분에서 Java 와 많은 부분에서 큰 차이를 보이고 있으며 특히 Java 가 가지고 있었던 형 표현의 모호함과, 흐름의 추상화를 극복하고 있다는 점에서 굉장한 차이가 있다.


  C# 의 예외 처리는 기본적으로 Java 와 거의 동일하지만, 이를 묘사하는 관점에서는 Java 와 다른 점을 보이고 있다.

  첫째이자 눈으로 볼 수 있는 가장 큰 차이점은, C# 의 경우 Java 와는 달리 어느 메소드에서든 예외를 발생시킬 수 있다. Java 의 경우, 사용자가 메소드 안에서 throw 명령을 사용해 Exception 을 예외 처리 구간으로 던지는 상황을 기술하였을 경우, Java 는 반드시 메소드에서 throws Exception 을 사용하여 이를 명시하도록 제한하고 있지만, C# 의 경우에는 이러한 제한이 없다. 즉 어느 순간에도 throw 로 Exception 을 던질 수 있게 된 것이다.

  Java 처럼 예외 발생에 대한 명시적인 기술은 예외 발생 구간이 어디인지 정확히 묘사한다는 점에서 예외 발생 구간을 명확히 인식할 수 있다는 장점이 있지만, 반면 저렇게 묘사하지 않는 구간에 대해서는 예외가 발생하지 않는다고 가정한다. 하지만 실제로는 예외가 발생하지 않는다고 묘사하는 구간에서도 예외가 발생할 가능성이 아예 존재하지 않는 것은 아니며, 더불어 프로그래머 자신이 예외 처리 구간을 누락시킬 가능성도 충분히 존재한다.

  반면 C# 은 모든 언어 구간에서 예외가 발생할 수 있다고 전제하고, 예외를 던지는 행위에 대한 어떠한 명시적 표현을 요구하지 않는다. 그렇기 때문에 사용자는 언제든지 예외적인 상황이 발생하였을 경우 예외를 발생시켜 이를 자신의 외부로 알릴 수 있게 되었다.


  둘째로, C# 은 Java 와는 달리 예외의 기술을 위한 형식을 Exception으로부터 시작한다. 전술했듯이 Java 는 Throwable 로부터 예외 기술을 시작하며, 이곳에서부터 프로그램 내부에서 제어 가능한 예외인 Exception 과, 프로그램 내부에서 제어할 수 없는 Error로 나뉘게 된다. 하지만 C# 의 경우 근본적으로 모든 상황을 Exception 으로 통합하고 이를 시스템상에서 발생할 수 있는 오류인 SystemException 과 프로그램 단에서 발생할 수 있는 ApplicationException 이라는 두 가지 범주로 나누었다. 결과적으로 보았을 때 C# 은 제어할 수 없는 예외가 없다고 (그것은 정말로 없을 수도 있고 아니면 제어될 수 없는 오류는 기술할 필요가 없다고 취급하는 것일 수도 있다. 이 부분은 아직 확실하게 조사하지 못했다.) 가정하고 있다는 점이고, 따라서 C# 은 발생할 수 있는 모든 예외에 대해 예외 처리가 가능하다.


  셋째, C# 은 기본적으로 예외 처리 구문에서 받은 예외 표현 개체를 다시 throw 로 상위 레벨의 예외 처리로 전송할 경우 이전까지의 모든 StackTrace 정보가 throw 를 발생시킨 지점으로 초기화된다. 만약 StackTrace 를 초기화시키지 않고 상위로 전송하려 한다면, throw 후 개체 인스턴스를 명시하지 않으면 된다. Java 의 경우에는 throw 를 사용하여 예외 개체를 전송한다고 해도 기본적으로는 StackTrace 정보가 처음 그대로 유지되며, fillInStackTrace() 라는 메소드를 통하여 현재 지점의 Stack 으로 StackTrace 정보를 설정할 수 있게 되어 있다. 순수하게 Exception 처리라는 관점에서만 본다면, C# 쪽이 조금 더 실질적인 방법이라고 볼 수 있는데, 그 이유는 예외를 핸들링하는 경우 대부분 하위의 예외가 그렇게 중요시되지 않고, 또한 예외 핸들링 코드를 진행하면서 사실상 StackTrace 정보가 더 이상 예외만을 가리키지 않게 되기 때문이다.

  C# 은 Java 와는 달리 처음부터 예외의 계층 구조를 가지고 있었으며, 시기상 C# 의 스펙과 1.4 의 스펙 공개가 비슷했으므로, 예외 계층 표현에 대해서는 두 언어가 서로 영향을 받았을 가능성은 적어 보인다.


  C# 이 가지는 기본적인 컨셉은 .NET CLR 을 기반으로 구동하는 모든 언어에 동일하게 적용되므로, 기술 방법은 다를지언정 Visual Basic .NET 및 Managed C++ 등의 언어에도 동일하게 적용할 수 있으며 실제로 마이크로소프트는 모든 .NET 기반 언어 문서에 위 세 언어의 예를 모두 기술하고 있다.


예외 처리의 확장

  Java 나 C# 과 같이, 예외에 대한 사유를 계층적으로 구성할 수 있는 프로그래밍 언어에서는 예외 처리를 확장하여, 개발자가 아닌 일반 사용자에게도 현재 예외 상황을 구체적으로 전달할 수 있다. 이미 Java 의 예외 처리 방식에서 한 번 언급하였지만, Java 및 C# 의 예외 기술 개체는 자신의 사유를 표현하는 메시지를 내부에 담을 수 있으며, 따라서 이를 역추적하면 현재 발생한 예외에 대한 정확한 이유를 표현하는 것이 가능하다.

  좀 더 진보된 방식으로 기술한다면, 사용자에게 알려야 할 예외와 그렇지 않은 예외를 서로 별도의 예외 클래스로 구분하여 확장함으로써, 사용자에게 알려야 하는 예외 클래스만을 추출하여 그 메시지를 사용자에게 알려주는 지능적인 방법을 사용할 수 있다.

  Java 에서 예를 들었던 코드를 이에 맞추어 기술하면 다음과 같다.

 
public class UserRecognizeException extends RuntimeException {

  public UserRecognizeException(String mes, Exception e) {

    super(mes, e);

  }

}

......

public int calculateABCD(int a, int b, int c, int d) {

  try {

    return calculateABC(a, b, c) / d;

  } catch (Exception e) {

    While (e != null) {

      // 사용자가 인식할 수 있는 예외만 출력한다.

      If (e is UserRecognizeException)
        System.out.println(e.getMessage());

      // 발생한 예외 자체는 모두 오류 출력에 기록해 둔다.
      // 실제로는 e.printStackTrace() 쪽이 더 유효하나 여기서는 비교를 위해
      // 이렇게 기술해 둔다.
      System.err.println(e.getMessage());
      e = e.getCause();

    }

  }

}

public int calculateABC(int a, int b, int c) {

  try {

    return calculateAB(a, b) / c;

  } catch (Exception e) {

    throw new UserRecognizeException (“세 숫자 연산 중 문제 발생”, e);

  }

}

public void calculateAB(int a, int b) {

  try {

    return a / b;

  } catch (Exception e) {

    throw new UserRecognizeException (“두 숫자 연산 중 문제 발생”, e);

  }

}

반응형