Creative Commons License

Microsoft .NET

닷넷!시작하기
닷넷! Ver 2.0~
닷넷!스킬업
웹개발
윈폼개발
실용모듈개발
Tip & Tech
하루 한 문법

Microsoft .NET 개발자들을 위한 공간입니다. 기초강의에서 부터 고급 기술 정보 및 팁등을 다루도록 하겠습니다.

.

닷넷!시작하기

Microsoft. NET 을 시작하는 분들을 위한 강좌입니다. 주로 기초적인 내용과 때론 기본적인 내용을 다룹니다

[C# 기초강좌] 18. C# 오류 및 예외처리

작성자 : 박종명
최초 작성일 : 2010-07-02 (금요일)
최종 수정일 : 2010-07-02 (금요일)
조회 수 : 11777

예외처리 삼형제 try, catch, finally 그리고 throw”

 

안녕하세요. 박종명입니다. 닷넷 열여덟 번째 강좌를 진행하도록 하겠습니다

이번 시간에는 프로그램이 비정상적으로 실행되는 오류 및 예외 상황을 C#에서는 어떻게 처리하는지에 대해
알아보도록 하겠습니다

 

 

오류(error)냐 예외(exception)?

우리는 보통 어떤 프로그램을 사용하다가 예기치 않은 상황이 발생하면 에러(error)!라고 합니다

이렇듯 통상 표현하는 에러라는 것은 한글 번역으로 오류라는 뜻으로 그 표현이 적절해 보입니다

 

다만 프로그래밍 언어적 측면에서는 오류와 예외를 조금 구분하는데요

오류는 명백히 문제인 상황, 예외는 예기치 못한 상황에 가깝습니다

다시 말해 예외는 정상적인 코드를 작성했음에도 어떠한 이유로 해서 비 정상적인 실행을 하게 되는 상황을 말합니다 예를 들어 사용자로부터 입력 받은 두 정수를 나누는 논리에서 분모에 0이 입력된 경우를 들 수 있겠습니다

 

다음의 코드는 정상적으로 컴파일 되는 유효한 코드입니다.

그러나 인자 b 0이 입력되는 순간 (0으로는 나눌 수 없기에) 이 논리는 깨어지게 됩니다.

이러한 예외적인 상황은 b에 값이 대입되고 나누려고 하는 실행 시간 즉 런타임 환경에 발생합니다

 

static double Division(int a, int b)

{

    return a / b;

}

 

이에 반해 오류는 다음의 코드와 같이 구문 자체에 문제가 있는 경우입니다.

코드에서는 정수를 0으로 나누려고 합니다. 코드는 명백히 잘못된 코드이며 컴파일 타임에 문제가 발생하게 됩니다

 

static double Division(int a)

{

    return a / 0;

}

 

예기가 조금 길어졌는데요.

중요한 것은 개념적으로 예외(exception)는 오류(error)와는 다른 의미로써 정상적인 코드에서도 발생할 수 있는
예기치 않은 논리적인 오류를 뜻한다
는 것입니다.

 

사실 일상적인 상황에서 오류냐 예외냐 하는 차이를 논하는 것은 어찌 보면 무의미 할 수 있습니다만
프로그래밍 언어적 측면에서는 살짝 구분해 주는 것이 정도(?)에 가깝다고 하겠습니다

 

구조적인 예외처리 기법

닷넷에서는 예외를 보다 효과적으로 처리할 수 있도록 구조적 예외처리 기법을 지원합니다

사실 자바나 C++의 예외처리 기법과 거의 같다고 할 수 있겠습니다. 예외의 개념적인 의미나 처리하는 메커니즘이
별반 차이가 없다는 것이죠

 

프로그램의 코드는 실제 그 프로그램이 수행할 논리를 작성한 코드와 예기치 않게 발생하는 예외를 처리하기 위한
방어 코드가 포함됩니다. 안정적인 프로그램을 위해서 두 가지 요소 모두 필요하지만
그 역할에 있어서는 서로가
명백히 다른 성격의
코드이지요.

 

전통적인 프로그램에서의 예외처리기법은 성격이 다른 이 두 요소가 혼란스럽게 섞이게 되는 구조였습니다

예를 들어 비주얼베이직의 On Error Goto 에 의한 예외처리나 SQL Server 2005 이전 버전에서의 SQL 언어에서의
예외 처리가 여기에 해당합니다

 

다음의 코드는 전통적인 예외처리 방식을 가상으로 작성한 것이며 프로그램 논리와 예외처리 논리가 서로 섞여 있어
혼란스러운 구조를 보여줍니다

 

int errorCode = 0;

FileInfo source = new FileInfo("code.cs");

if (errorCode == -1) goto Failed;

int length = (int) source.Length;

if(errorCode == -2) goto Failed;

char[] contents = new char[length];

if(errorCode == -3) goto Failed;

//Succeeded....

Failed: ...

 

이 코드에서 실제 프로그램 논리를 구현한 코드는 다음과 같이 단 세 줄에 불과합니다

FileInfo source = new FileInfo("code.cs");

int length = (int) source.Length;

char[] contents = new char[length];

 

그러나 파일 액세스에 대한 예외를 처리하기 위해 방어코드가 중간중간에 삽입되어 복잡하고 혼란스러워 보이게 되는 것이죠. 이런 형태의 예외처리 방식을 비구조적 예외처리 기법이라 합니다

 

반면 닷넷이나 자바에서 제공하는 예외처리 방식은 프로그램의 주요 논리코드와 예외처리 코드를 서로 분리해서 정의할 수 있도록 지원하여 기존 비 구조적 예외처리 기법의 단점을 보완한 것이라 하겠습니다

더불어 예외 상황에 대한 상세 정보를 담은 자료구조를 일관되게 이용할 수 있으며 중첩된 예외 처리 및 호출체인을 제어할 수 있는 수단이 제공됩니다. 이러한 닷넷의 예외처리 방식을 구조적 예외처리 기법이라 합니다. 뭔가 말만 계속 길어지네요.

 

그럼 이제 실제 예외처리를 하는 코드를 살펴 보겠습니다

 

기본적인 예외처리

워낙 유명한 try, catch 그리고 finally 3형제(?)가 예외처리를 위한 기본적인 키워드입니다

 

try:     예외가 발생할 만한 코드를 try 블록으로 감싸 줍니다

catch:  try 블록 안의 코드에서 예외가 발생하면 실행되는 영역으로 실제 예외 처리를 수행합니다

finally:  try 블록 안의 코드에서 예외가 발생하든 그렇지 않든 무조건 실행되는 영역입니다

           try 블록에서 사용한 정리되지 않는 자원에 대한 마무리 작업을 하는 용도로 많이 사용됩니다

 

그럼 이제 가장 간단한 예외처리를 보겠습니다

try

{

  int a = 10; int b = 0;

    int c = a / b;

}

catch

{

    Console.WriteLine("0으로 나눌 수 없습니다");

}

finally

{

   Console.WriteLine("finally 영역 실행");

}

 

try 블록에서 예외가 발생하였으며 이후 catch finally 영역이 실행됩니다

(물론 try 블록에서 예외가 발생하지 않게 하더라도 finally 영역은 여전히 실행됩니다)

 

앞서 살펴본 비구조적 예외처리 기법과는 달리 주요 논리코드와 예외처리코드가 명확히 분리되어 보다 잘 구조화 된 형태를 띠고 있습니다

 

예외 클래스

예외가 발생하면 어떤 예외인지 어디서 발생했는지 등의 예외 발생과 관련된 정보를 담은 객체가 시스템에 의해 자동으로 생성됩니다. 앞서 예에서는 0으로 나누기 예외가 발생하였으며 이는 DivideByZeroException 클래스의 객체에 해당합니다catch 구문에서 이 객체를 전달받으려면 입력 매개변수로 지정해 줘야 합니다

 

다음 코드는 catch 에서 예외 객체에 대한 참조를 명시하고 해당 객체의 멤버를 이용하는 예입니다

catch(DivideByZeroException exception)

{

    Console.WriteLine(exception.Message);    //예외를 설명하는 메세지

    Console.WriteLine(exception.StackTrace); //예외가 발생한 위치

}

 

닷넷 프레임워크에는 수 많은 예외 클래스가 미리 정의되어 있는데요

할당되지 않는 객체 참조 예외인 NullReferenceException, 배열의 인덱스 범위를 초과한 예외인 IndexOutOfRangeException, 메모리가 부족한 예외인 OutOfMemoryException 등이 있습니다

 

이들 예외 클래스의 최상위 부모는 Exception 클래스입니다

Exception 클래스에는 모든 예외에서 공통적으로 사용하는 예외 메시지, 예외 발생 위치, 형식, 예외 원인 등을 위한 속성들이 제공됩니다

 

또한 하나의 try 구문에 여러 개의 catch 블록이 존재할 수 있는데 닷넷 런타임(CLR)은 발생한 예외에 해당하는 객체를 생성하여 해당 타입을 매개변수로 취하는 catch블록으로 객체 참조를 전달해 줍니다

만일 해당 형식이 없을 경우 그 형식의 상위 부모 타입으로 전달됩니다

 

아래 코드와 같이 3개의 catch 블록이 정의되어 있을 경우 0으로 나누기 예외는 DivideByZeroException 형식을 매개변수로 받는 catch 블록이 실행됩니다

 

이렇게 try 블록에서 발생할 소지가 있는 몇 가지 예외들을 별도의 catch로 정의해 두면 각 예외 상황에 맞는 적절한 처리를 각기 달리 다룰 수 있다는 장점이 있습니다.

만일 해당하는 직접적인 형식이 정의되지 않은 경우 그 부모 타입을 인자로 받는 catch가 호출되는데 Exception 클래스는 모든 예외 클래스의 최 상위 부모이므로 정의되지 않은 형식을 모두 처리할 수 있게 됩니다
(참고로 catch 에 매개변수를
지정하지 않으면 자동으로 Object 타입을 받을 수 있게 되어 역시나 모두 처리할 수 있게 됩니다)

 

catch (NullReferenceException exception)

{

 Console.WriteLine(exception.Message);

}

catch (DivideByZeroException exception)

{

    Console.WriteLine(exception.Message);

}

catch (Exception exception)

{

    Console.WriteLine(exception.Message);

}

 

여러 개의 catch 블록을 정의할 때 한가지 유의할 사항이 있는데요

catch 블록은 정의된 순서대로 차례차례 검사되기 때문에 예외 클래스의 상속과 충돌할 수 있다는 것입니다

즉 아래와 같이 상위 형식인 Exception 을 하위형식인 DivideByZeroException 보다 위에 정의하게 되면 컴파일 오류가 발생합니다. 순서상 먼저 정의된 catch 절에서 모든 예외를 받게 되므로 다음에 정의된 catch 는 구조적으로 문제가 있다는 것이죠. 이는 컴파일 오류를 발생시키기 때문에 쉽게 알 수 있습니다

 

catch (Exception exception)

{

    Console.WriteLine(exception.Message);

}         

catch (DivideByZeroException exception)

{

    Console.WriteLine(exception.Message);

}

 

예외 던지기

발생한 예외를 호출한 곳으로 다시 전달하기 위해 throw 키워드를 사용합니다

다음의 코드를 보면 MyClass에 나눗셈을 위한 Division 이라는 메서드가 정의되어 있습니다

이 메서드에서는 나눗셈 수행 중 예외가 발생하면 이를 catch 하여 정보를 표시하고 메서드를 호출한 곳으로

예외를 다시 던져 줍니다. 이때 throw 키워드를 사용합니다

 

class MyClass{

  public static void Division(int a, int b){

      try

      {

          int c = a / b;

          Console.WriteLine(c.ToString());

      }

      catch (Exception exception)

      {

          Console.WriteLine("예외 발생! 호출한 곳으로 예외 던지기");

          throw  exception;

      }

  }

}

 

그리고 이를 호출하는 코드는 다음과 같습니다

static void Main(string[] args){

    try

    {

        MyClass.Division(10, 0);

    }           

    catch (Exception exception)

    {

        Console.WriteLine(exception.Message);

    }                                    

}

결과는 다음과 같습니다

이렇게 호출한 곳으로 예외 객체를 다시 던져주는 것이 권장되기도 하는데요

만일 MyClass 가 외부에 정의된 경우라고 가정하면 예외 발생시 자신의 로직을 적절히 처리하고 이를 참조하는 프로그램에도 해당 예외를 던져 주어 처리할 수 있는 기회를 주는 것입니다

만일 예외 처리를 MyClass에서만 처리하고 다시 알려주지(throw) 않는다면 이 메서드를 호출하는 곳에서는 예외가 발생했는지 알 수 없게 되는 것이죠

 

throw 는 이처럼 try, catch 와 함께 사용되는 것이 일반적입니다만 단독으로 사용할 경우도 있습니다

이때의 용도는 예외를 직접 발생시키거나 예외 상황을 알리기 위함입니다

아래 코드는 복사 대상 원본 객체가 null일 경우 해당 예외 객체를 명시적으로 생성하여 이를 호출한 곳으로
예외를 알려 줍니다

 

static void CopyObject(SampleClass original){

    if (original == null){

        throw new System.ArgumentException("Parameter cannot be null", "original");

    }

}

 

예외처리 호출체인

예외가 발생하면 처리를 위한 예외처리기를 찾게 되는데요. 이는 호출 스택을 거슬러 올라가면서 예외처리 블록을 찾는 과정입니다. 만일 모든 호출 스텍을 거슬러 올라갔는데도 예외처리기를 찾지 못한다면 예외는 닷넷 런타임(CLR) 에 까지 전달됩니다

 

즉 아래와 같은 호출 구조에서,

CLR -> A 객체(예외처리 없음) -> B 객체(예외처리 없음) -> C 객체(예외처리 없음)

C객체에서 예외가 발생한다면 CLR 까지 예외가 전달되는 것이죠. 결국 응용프로그램에서는 예외처리를 전혀 하지 않은 것이며 프로그램은 CLR의 예외 보고와 함께 종료될 것입니다

 

만일 B 객체에 예외처리를 위한 try, catch 블록이 존재한다면 B객체에서 해당 예외를 처리하게 됩니다

(B 객체에서 throw 하지 않았다면 예외처리를 위한 호출체인은 멈추게 됩니다)

 

 

사용자 정의 예외 클래스

앞서 닷넷에 미리 정의된 예외 클래스인 Exceptoin 과 그 자식 클래스에 대해 설명했는데요

많은 예외 상황을 대비해 수 많은 예외 클래스가 미리 정의되어 있지만 모든 예외를 정의할 수는 없습니다

이는 예외라는 것이 프로그램의 예기치 않는 상황이라 그 범주의 한계가 없기 때문입니다

 

따라서 해당 프로그램에서 예외로 간주할 만한 독특한 상황이 있다면 (규칙에 따라) 개발자가 예외 클래스를 따로 정의해서 사용할 수 있습니다. 이 때 예외 클래스의 최상위 부모 클래스인 Exception 으로부터 상속받도록 하는 것이 규칙입니다

 

사용자 정의 예외클래스를 사용하는 간단한 예를 보겠습니다

이 프로그램에서 정의하는 예외 상황은 잘 생긴 사람 질투하기’.. 입니다 ^^;

.. 잘생긴 사람이 싫은 삐뚤어진(?) 개발자에겐 잘 생긴 것 자체가 예외일 수 있겠죠

AreYouHandSomeGuy 메서드를 호출할 때 인자로 문자를 받습니다

이 문자가 X이면 프로그램은 정상 실행되지만 이 이외의 문자라면 모두 예외로 간주해 버립니다

 

그러나 이 개발자를 위한 예외 클래스는 닷넷에서 제공 될 리가 없지요

그래서 사용자 정의 예외 클래스를 정의해서 사용하게 됩니다

Exception 클래스를 상속받도록 하며 예외에 대한 설명을 위해 부모객체의 생성자를 호출하도록 합니다

 

class JiRalException: System.Exception

{

public JiRalException() : base("됐거덩요!!!") {}

    public JiRalException(string message) : base(message){}

}

 

그리고 이 예외클래스를 사용하는 논리를 다음과 같이 작성합니다

class MyClass{

   public static void AreYouHandsomeGuy(char c){

       if (c == 'X')

       {

           Console.WriteLine("Good");

       }

       else

       {

    //예외상황으로 간주. 호출한 곳으로 사용자정의예외객체 던지기

           throw new JiRalException();

       }

   }

}

 

이제 이 프로그램을 사용하는 코드를 아래와 같이 작성합니다

static void Main(string[] args){

    try

    {

        MyClass.AreYouHandsomeGuy('O');

    }

    catch (Exception exception)

    {

        Console.WriteLine(exception.Message);

    }                                               

}

 

그리고 결과입니다

.. 억지스러운 설정이 좀 거시기 하죠. --; 그러나 나중에 기억을 잘 나실 거에요 ㅎㅎ

 

마지막으로 MSDN의 예외와 관련한 성능 고려사항을 보겠습니다

 

상당한 양의 시스템 리소스와 실행 시간이 예외를 throw하거나 처리할 때 사용되므로 예측 가능한 이벤트나 흐름 제어를 처리하는 경우가 아니라 정말 비정상적인 조건을 처리하는 경우에만 예외를 throw해야 합니다. 예를 들어, 메서드를 호출할 때는 유효한 매개 변수를 사용해야 하므로 메서드 인수가 잘못된 경우 응용 프로그램에서 예외를 throw하는 것은 적절할 수 있습니다. 여기서 잘못된 메서드 인수는 비정상적인 상황이 발생했음을 의미합니다. 반대로, 사용자가 때때로 잘못된 데이터를 입력할 수도 있으므로 사용자 입력이 잘못된 경우에는 예외를 throw하지 않아야 합니다. 이러한 경우에는 사용자가 올바른 데이터를 입력할 수 있도록 재시도 메커니즘을 제공합니다.

 

비정상적인 조건에 대해서만 예외를 throw한 다음 특정 예외에 적용되는 처리기가 아니라 응용 프로그램 대부분에 적용되는 범용 예외 처리기에서 예외를 catch합니다. 이렇게 해야 하는 이유는 대부분의 오류가 해당 오류와 가까운 곳에 있는 유효성 검사 및 오류 처리 코드로 처리될 수 있으므로 예외를 throw하거나 catch할 필요가 없기 때문입니다. 범용 예외 처리기는 응용 프로그램의 임의의 위치에서 throw정말로 예기치 않은 오류를 catch합니다.

 

또한 반환 코드가 충분하면 예외를 throw하지 않아야 하고, 반환 코드를 예외로 변환하지 않아야 하며, 정기적으로 예외를 catch하고 무시한 다음 처리를 계속하지 않아야 합니다

 

예외를 처리하는 데 비용이 많이 들기 때문에 꼭 필요한 경우를 제외하고는 대안을 사용하라는 의미입니다

개인적인 생각으로는 이 글에 대한 해석의 관점이 조금씩 다를 수 있을 것 같습니다

중요한 것은 상황이죠. 이 글은 권장 수준에서 봐 주시길 바랍니다

 

그럼. 이것으로 강좌를 마치도록 하겠습니다

 

111라는 말이 있습니다

한 장의 글엔 하나의 그림과 하나의 표가 반드시 있어야 한다는 뜻인데요

글을 보는 사람이 이해하기 쉽게 하려면 말로만 풀지 말고 적절한 그림과 표로 기술하라는 의미이지요

요즘 신경 쓸 곳이 많다 보니 글 작성 시 이 철칙(?)을 위배하는 경우가 많네요.

 

이 글만 해도 사실은 밖에 없는 셈이네요.

한 장의 감동적인 그림이 백 마디 말보다 나은데 말이죠. ^^

 

앞으로 더 분발해서 쉬운 글 쓰도록 노력하겠습니다.

그럼. 즐거운 한 주 되세요~~~

∵Commented by 박진경 at 2010-09-23 오후 5:08:08  
싸이트찾아다니다가 참 좋은글 만났습니다. 많이 배우도록 하겠습니다.
∵Commented by 박종명 at 2010-09-25 오전 9:33:35  
반갑습니다~ 자주 들러 주세요 ^.^
이름
비밀번호
홈페이지
AG <- 왼쪽의 문자를 오른쪽 박스에 똑같이 입력해 주세요