[C#] 25. 예외 처리(try ~ catch)하는 방법


Study/C#  2021. 9. 9. 14:58

안녕하세요. 명월입니다.


이 글은 C#에서 예외 처리(try ~ catch)하는 방법에 대한 글입니다.


우리가 프로그램을 작성하다보면 예상하지 못한 에러가 발생할 때가 있습니다.

예를 들면, 프로그램을 작성하는 데 변수에 값이 없다던가(null 에러), 정수의 값에 나누셈을 하는 데 값을 나누는 값(제수)이 0의 값으로 값을 나눈다거나의 문제가 발생할 수 있습니다.

using System;

namespace Example
{
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 피제수의 변수
      int dividend = 10;
      // 제수의 변수
      int divisor = 0;
      
      // 나눈 값을 출력한다.
      Console.WriteLine(dividend / divisor);
      // 아무 키나 누르시면 종료합니다.
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

프로그램 실행 중에 에러가 발생했습니다.

보통 프로그램 실행 중에 이렇게 에러가 발생하면, 에러가 발생한 부분을 건너 띄고 프로그램이 진행되는 것이 아니고, 그대로 프로그램이 멈추게 됩니다.


물론 위의 예제처럼 선언된 변수의 값이 잘못되면 변수의 값을 고치면 해결이 되겠지만, 만약에 유저로부터 받은 값이 잘못되거나 데이터 베이스의 값이 다를 경우에는 프로그램이 정지되는 것이 아니고 예외 처리를 해야 합니다.

즉, 에러가 발생해도 프로그램이 멈추거나 종료가 되면 안됩니다.

using System;

namespace Example
{
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 피제수의 변수
      int dividend = 10;
      // 제수의 변수
      int divisor = 0;
      // 예외 처리
      try
      {
        // try 스택 영역에서 에러가 발생하면 catch 영역으로 넘어간다.
        Console.WriteLine(dividend / divisor);
      }
      catch (Exception e)
      {
        // 에러가 발생하면 에러 내용을 콘솔 출력한다.
        Console.WriteLine(e);
      }
      // 아무 키나 누르시면 종료합니다.
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

결과를 보면 나누는 영역에서 여러가 발생했지만 에러 메시지를 콘솔에 출력하고 "Press any key..."의 메시지까지 콘솔에 출력이 됩니다.

즉, 에러가 발생하게 되어도 프로그램이 멈추지 않고 끝까지 실행이 되는 것을 확인 할 수 있습니다.


에러처리는 에러의 구분에 따라 넘길 수 있는 catch의 스택 영역을 달리 할 수 있습니다.

using System;

namespace Example
{
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 피제수의 변수
      int dividend = 10;
      // 제수의 변수
      int divisor = 0;
      // 예외 처리
      try
      {
        // try 스택 영역에서 에러가 발생하면 catch 영역으로 넘어간다.
        Console.WriteLine(dividend / divisor);
      }
      // 나눗셈 에러 Exception
      catch(DivideByZeroException e)
      {
        // 콘솔 출력
        Console.WriteLine("DivideByZeroException");
      }
      // 최상위 에러 Exception
      catch (Exception e)
      {
        // 콘솔 출력
        Console.WriteLine("Exception");
      }
      // 아무 키나 누르시면 종료합니다.
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

위 try 영역에서는 나눗셈 에러가 발생했기 때문에 DivideByZeroException의 스택 영역으로 넘어갑니다.

만약 해당 영역에서 DivideByZeroException가 아닌 에러가 발생하면, 모든 에러를 잡아내는 Exception 영역으로 넘어갑니다.

(여기서 에러 클래스의 종류를 알아내는 것은 처음 Exception을 만들고 에러를 발생 시키면 어떤 에러가 발생하는 지 메시지로 나옵니다. 첫번째 예제 참고)


보통의 일반 코드라면 모두 Exception으로 처리를 해도 무관합니다만, 발생하는 에러 별로 처리를 달리할 때는 위처럼 에러 별로 구분하는 작업이 필요합니다.

즉, Exception으로 모든 에러를 처리한다고 해도 특별히 성능 상의 문제는 없습니다.


그렇다면 우리가 클래스를 작성할 때, 클래스의 특성항 일부러 에러를 발생해야 할 경우도 있습니다.

using System;

namespace Example
{
  // 에러 클래스
  class CharacterCountOver : Exception
  {
    // 생성자. 에러가 발생할 때 내보내는 에러 메시지 설정
    public CharacterCountOver() : base("Exceeded character count")
    {

    }
  }
  // 예제 클래스
  class InputText
  {
    // 멤버 변수
    private string data;
    // 멤버 변수 파라미터
    public string Data
    {
      set
      {
        // 입력된 글자가 10글자 초과되면
        if (value.Length > 10)
        {
          // CharacterCountOver 에러를 발생한다.
          throw new CharacterCountOver();
        }
        // 그렇지 않으면 값을 맴버 변수에 넣는다.
        data = value;
      }
      get
      {
        // 맴버 변수를 반환
        return data;
      }
    }
  }
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 클래스 인스턴스 생성
      InputText p = new InputText();
      try
      {
        // 값을 입력, 총 12글자
        p.Data = "abcdefghijkl";
        // 콘솔 출력
        Console.WriteLine("OK!!");
      }
      // 에러가 발생하면
      catch (Exception e)
      {
        // 콘솔 출력
        Console.WriteLine(e);
      }
      // 콘솔 출력
      Console.WriteLine(p.Data);
      // 아무 키나 누르시면 종료합니다.
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

위 예제에서는 제가 에러 클래스를 만들었습니다. 이 에러 클래스는 기본적으로 Exception 클래스를 상속받았습니다. 상속을 받아야 try ~ catch에서 사용할 수 있습니다.

그리고 InputText 클래스의 프로퍼티에서 throw를 사용할 때 인스턴스를 넘기게 됩니다. 참고로 throw 키워드를 사용하면 강제 에러가 발생하게 되고 실행되는 Call Stack history의 가장 가까운 try ~ catch에 걸립니다. 만약 try ~ catch가 없으면 첫번째 예제처럼 프로그램이 멈추겠네요.

저는 구조상 Main의 try ~ catch에서 에러가 잡히게 되어 있습니다.

콘솔 결과를 보면 CharacterCountOver가 발생했다는 것을 알 수 있습니다.


여기서 또 보면 에러는 발생했지만 콘솔 출력 OK!!는 출력이 되지 않았습니다. 그 뜻은 try의 영역에서는 에러가 발생하면 에러가 발생한 지점에서 스택은 멈추고 catch로 점프를 하게 됩니다. 그래서 콘솔에 OK!!이 출력되지 않았습니다.

그 다음 catch 영역부터는 정상적으로 실행이 되는 것을 확인할 수 있습니다.


그러면 여기서 또 궁금증이 하나 생기게 됩니다. 프로그램의 제어문(if~else)처럼 사용하게 되면 프로그램을 좀 더 유용하게 개발 할 수 있지 않을까?

using System;
using System.Diagnostics;

namespace Example
{
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 홀수의 함
      int sum = 0;
      // 성능 시간 측정 클래스
      Stopwatch sw = new Stopwatch();
      // 측정 시작
      sw.Start();
      // 0에서 999까지 반복
      for (int i = 0; i < 1000; i++)
      {
        // 짝수의 경우는 더하지 않는다.
        if (i % 2 == 0)
        {
          continue;
        }
        // 홀수의 값은 더한다.
        sum += i;
      }
      // 측정 중지
      sw.Stop();
      // 콘솔 출력 - 처리 시간
      Console.WriteLine("Control time - " + sw.ElapsedMilliseconds);
      // 결과 출력
      Console.WriteLine("Result - " + sum);
      // 개행
      Console.WriteLine();
      // 결과 변수 초기화
      sum = 0;
      // 에러 클래스 인스턴스 생성
      Exception e = new Exception();
      // 측정 시작
      sw.Start();
      // 0에서 999까지 반복
      for (int i = 0; i < 1000; i++)
      {
        try
        {
          // 짝수의 경우 에러 발생
          if (i % 2 == 0)
          {
            throw e;
          }
          // 홀수의 값을 더한다.
          sum += i;
        }
        catch (Exception)
        {

        }
      }
      // 측정 중지
      sw.Stop();
      // 콘솔 출력 - 처리 시간
      Console.WriteLine("Exception time - " + sw.ElapsedMilliseconds);
      // 결과 출력
      Console.WriteLine("Result - " + sum);
      // 개행
      Console.WriteLine();
      // 아무 키나 누르시면 종료합니다.
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

위 예제는 try ~ catch를 제어문처럼 사용해서 코드를 작성했습니다. 결과는 성능이 압도적으로 느려지네요..

즉, 이 try ~ catch는 에러를 잡아내는 역할을 하지만 역시 성능 저하에 많은 영향을 끼치기 때문에 가능하면 모든 에러는 제어문으로 걸러내는 것이 주요 포인트입니다.

참고로 try 영역을 감싸는 것으로는 성능에는 영향이 없고, throw가 발생하여 catch로 넘어갈 때, 인터럽트가 걸리기 때문에 느려지는 것입니다.


여기까지 C#에서 예외 처리(try ~ catch)하는 방법에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.