[C#] 예외(Exception)

Running boy·2023년 8월 9일
0

컴퓨터 공학

목록 보기
25/36

예외(Exception)란?

일반적으로 말하는 컴파일 에러, 런타임 에러를 예외라고 한다.
하지만 컴파일 에러는 IDE가 바로 알려주기 때문에 이 포스트에서 예외는 런타임 에러라고 가정한다.


예외의 구조

checked
{
  int a = int.MaxValue;
  a++;
}

위 코드를 실행하면 아래의 로그를 남기며 프로그램이 비정상적으로 종료된다.

System.OverflowException: Arithmetic operation resulted in an overflow.
    at Program.Main(String[] args) in C:\Users\wjdgh9577\Desktop\TestProject\TestProject\Program.cs:line 13

여기서
'System.OverflowException'은 예외 타입의 이름
'Arithmetic operation resulted in an overflow.'은 예외에 대한 설명
'at Program.Main(String[] args) in C:\Users\wjdgh9577\Desktop\TestProject\TestProject\Program.cs:line 13'은 예외가 발생한 경로
를 뜻한다.


예외 타입(Exception Type)

C#에서 모든 예외는 클래스로 정의돼 있으며 Exception 클래스를 상속받는다.

Exception 클래스에 다양한 멤버가 정의돼 있지만 중요한 몇가지만 정리하면 다음과 같다.

Message: 프로퍼티로 구현. 예외에 대한 설명을 string으로 반환한다.
Source: 프로퍼티로 구현. 예외가 발생한 응용 프로그램명을 string으로 반환한다.
StackTrace: 프로퍼티로 구현. 예외가 발생한 경로(호출 스택)를 string으로 반환한다.
ToString: 메서드로 구현. 예외의 내용을 요약하여 string으로 반환한다.

런타임 에러가 발생했을 때 나오는 예외 로그는 예외의 내용을 ToString 메서드로 반환한 것이다.
이를 통해 ToString은 예외 타입명, Message, StackTrace를 한 번에 반환하도록 정의됐음을 알 수 있다.


예외 처리기(Exception Handler)

C#은 런타임에 예외가 발생할 경우 이를 처리하여 프로그램이 비정상적으로 종료되는 것을 방지할 예외 처리기를 제공한다.
try, catch, finally, when 네가지의 예약어가 사용된다.


try

예외가 발생할 가능성이 있는 코드를 실행할 구문이다.
반드시 catch나 finally와 같이 사용돼야 한다.

try문에서 발생하는 예외는 개발자가 따로 처리할 수 있지만 그렇지 않은 경우 프로그램이 비정상적으로 종료된다.
이를 '처리되지 않은 예외(Unhandled Exception)'라고 한다.


catch

try문에서 예외가 발생할 경우 남은 코드는 실행되지 않고 catch문의 코드가 실행된다.

try
{
	string nullString = null;
    Console.WriteLine(nullString == null);
    Console.WriteLine(nullString.ToString());
    Console.WriteLine(nullString != null); // 실행되지 않는다.
}
catch
{
	Console.WriteLine("Catch");
}

// 출력:
// True
// Catch

하지만 예외를 처리해야 되는 catch문에서도 예외가 발생할 수 있음을 유의해야 한다.
이를 방지하려면 catch문에서 추가로 try/catch 구문을 사용해야 한다.

예외의 종류는 다양하다.
만약 개발자가 예외에 따라 처리를 다르게 하고 싶다면 catch문을 다중으로 정의해야 된다.

try
{
    string nullString = null;
    Console.WriteLine(nullString.ToString());
}
catch (NullReferenceException) // 타입만 적어도 되지만
{
    Console.WriteLine("Catch");
}
catch (Exception e) // 타입의 인자를 추가하면 예외를 직접 처리할 수 있다.
{
    Console.WriteLine(e.ToString());
}

// 출력:
// Catch

예외 타입의 인자를 추가해서 개발자가 예외 인스턴스를 직접 다룰 수 있음에 주목하자.


finally

예외 발생 여부에 상관없이 실행되는 코드 블록이다.
일반적으로 중단된 작업의 자원을 해제하는 용도로 사용된다.

try
{
	// 파일 열기
    // 파일 작업
}
catch
{
	// 파일 작업 중 발생한 예외 처리
}
finally
{
	// 파일 닫기
}

when

'예외 필터'라고 하며 C# 6.0에서 추가되었다.
catch문에 조건식을 추가할 수 있다.

string nullString = null;
try
{
    Console.WriteLine(nullString == null);
    Console.WriteLine(nullString.ToString());
}
catch (Exception) when (nullString != null)
{
    Console.WriteLine("Catch1");
}
catch (NullReferenceException) // when이 없었다면 원래는 컴파일 에러가 발생한다.
{
    Console.WriteLine("Catch2");
}
catch (Exception) // 같은 예외를 중복으로 정의할 수 있다. when이 없으면 컴파일 에러 발생.
{
    Console.WriteLine("Catch3");
}

// 출력:
// Catch2

예외 필터의 특징은 바로 조건식을 실행하는 시점은 예외 처리 핸들러가 실행되기 전이라는 것이다.
그렇기 때문에 아래와 같은 코드도 가능하다.

bool Filter(Exception e)
{
    Console.WriteLine(e.GetType().FullName);
    return false;
}

string nullString = null;
try
{
    Console.WriteLine(nullString == null);
    Console.WriteLine(nullString.ToString());
}
catch (Exception e) when (Filter(e))
{
    Console.WriteLine("Catch1");
}
catch (NullReferenceException e) when (Filter(e))
{
    Console.WriteLine("Catch2");
}
catch (Exception e) when (Filter(e) == false)
{
    Console.WriteLine("Catch3");
}

// 출력:
// True
// System.NullReferenceException
// System.NullReferenceException
// System.NullReferenceException
// Catch3

try문에 try/catch 구문을 중첩했을 때 예외 처리는?

try문에 try/catch 구문이 중첩됐을 때 예외 처리의 순서가 궁금해서 실험해봤다.

try
{
    string nullString = null;
    try
    {
        Console.WriteLine(nullString == null);
        Console.WriteLine(nullString.ToString());
    }
    catch
    {
        Console.WriteLine("Catch1");
        //Console.WriteLine(nullString.ToString());
    }
}
catch
{
    Console.WriteLine("Catch2");
}

// 출력:
// Catch1
try
{
    string nullString = null;
    try
    {
        Console.WriteLine(nullString == null);
        Console.WriteLine(nullString.ToString());
    }
    catch
    {
        Console.WriteLine("Catch1");
        Console.WriteLine(nullString.ToString());
    }
}
catch
{
    Console.WriteLine("Catch2");
}

// 출력:
// Catch1
// Catch2
  1. try문 내부의 try문은 외부의 catch문에 영향을 주지 않는다.
  2. try문 내부의 catch문에서 예외가 발생할 경우 외부의 catch문으로 넘어간다.

의도적인 예외 발생 - throw

개발자가 의도적으로 예외를 발생시키고 싶을 때 throw 예약어를 사용할 수 있다.

try
{
    throw new IndexOutOfRangeException("Test Exception."); // 예외 인스턴스가 필요하다.
}
catch (Exception e)
{
    Console.WriteLine(e.ToString());
}

// 출력:
// System.IndexOutOfRangeException: Test Exception.
//   at Program.Main(String[] args) in C:\Users\wjdgh9577\Desktop\TestProject\TestProject\Program.cs:line 10

단 catch문에 사용될 경우 인스턴스를 생략할 수 있다.

try
{
    throw new IndexOutOfRangeException("Test Exception."); // 예외 인스턴스가 필요하다.
}
catch (Exception e)
{
    throw; // 인스턴스 생략 가능. 단 프로그램이 비정상적으로 종료된다.
}

catch문에 throw를 사용할 경우 반드시 인스턴스를 생략하자.
인스턴스를 명시할 경우 실제 예외가 발생한 호출 스택이 사라진다.

6  static void Main(string[] args)
7  {
8     try
9     {
10        Test();
11    }
12    catch (Exception e)
13    {
14        Console.WriteLine(e);
15        throw e; // 또는 throw;
16    }
17 }
18
19 static void Test()
20 {
21     throw new IndexOutOfRangeException("Test Exception");
22 }

// throw e 결과:
// System.IndexOutOfRangeException: Test Exception
//    at Program.Test() in C:\Users\wjdgh9577\Desktop\TestProject\TestProject\Program.cs:line 21
//    at Program.Main(String[] args) in C:\Users\wjdgh9577\Desktop\TestProject\TestProject\Program.cs:line 10
// Unhandled exception. System.IndexOutOfRangeException: Test Exception
//    at Program.Main(String[] args) in C:\Users\wjdgh9577\Desktop\TestProject\TestProject\Program.cs:line 15

// throw 결과:
// System.IndexOutOfRangeException: Test Exception
//    at Program.Test() in C:\Users\wjdgh9577\Desktop\TestProject\TestProject\Program.cs:line 21
//    at Program.Main(String[] args) in C:\Users\wjdgh9577\Desktop\TestProject\TestProject\Program.cs:line 10
// Unhandled exception. System.IndexOutOfRangeException: Test Exception
//    at Program.Test() in C:\Users\wjdgh9577\Desktop\TestProject\TestProject\Program.cs:line 21
//    at Program.Main(String[] args) in C:\Users\wjdgh9577\Desktop\TestProject\TestProject\Program.cs:line 10

위 예시를 보면 인스턴스를 생략할 경우 예외가 발생한 호출 스택이 제대로 출력된다.
하지만 인스턴스를 명시한 경우 예외가 발생한 호출 스택이 아닌 throw한 호출 스택이 출력된다.

사실 조금만 더 생각해보면 이해가 가능하다.
throw는 예외를 발생시킨다.
21행을 보면 throw로 IndexOutOfRangeException 예외를 발생시킨다. 그래서 호출 스택은 21행을 가리킨다.
21행에서 throw한 예외를 12행에서 catch한다. 이 예외 인스턴스는 호출 스택을 제대로 담고 있다.
15행에서 throw로 다시 예외를 발생시킨다.
이 때 예외 인스턴스 e의 호출 스택이 15행을 가리키게 된다. 왜냐하면 새로운 예외를 발생시켰기 때문이다.
하지만 인스턴스를 생략할 경우 예외 인스턴스 e를 throw한게 아니다.
그렇기 때문에 15행의 예외는 21행의 예외에 영향을 주지 않는다.

throw가 컴퓨터에게 예외를 알리는 방식

throw 인스턴스; - 예외가 발생했으니 인스턴스에 예외 정보를 담아서 넘겨줄게!
throw; - 뭔진 모르겠지만 아무튼 예외가 발생으니 너가 알아봐!


사용자 정의 예외 타입

System.Exception을 상속받아서 원하는 예외 타입을 만들 수 있다.

class TestException : Exception
{
    TestException(string message) : base(message) { }
}

위와 같이 생성자의 일부만 재정의하여 인스턴스를 생성할 때 매개변수를 강제할 수도 있다.


예외 처리는 언제 사용해야 되는가?

예외 처리는 상당히 무거운 작업이다.
이를 남용할 경우 프로그램이 비정상적으로 종료되는 것은 막을 순 있겠으나 성능에 많은 영향을 미친다.

Stopwatch sw = new Stopwatch();

sw.Start();
for (int i = 0; i < 100; i++)
{
    try
    {
        int j = int.Parse("20"); // 예외가 발생하지 않음.
    }
    catch
    {
    	
    }
}
sw.Stop();
Console.WriteLine(sw.Elapsed.TotalMilliseconds + "ms");

sw.Restart();
for (int i = 0; i < 100; i++)
{
    try
    {
        int j = int.Parse("2a"); // 예외 발생.
    }
    catch
    {
		
    }
}
sw.Stop();
Console.WriteLine(sw.Elapsed.TotalMilliseconds + "ms");

// 출력:
// 0.5867ms
// 479.7748ms

위 예시를 보면 예외를 100번 발생시켰을 뿐인데 상당한 시간 차이가 남을 알 수 있다.

성능적인 문제를 제외하고도 무분별한 예외 처리는 오히려 개발자가 버그를 발견하기 힘든 상황으로 만들 수도 있다.
예외라는건 어쨌든 프로그램이 오작동한다는 뜻인데 프로그램을 종료시키지 않으니 개발자가 주의를 갖지 않는 이상 문제를 발견할 수가 없다.

예외 처리를 할 때 아래 규칙을 지키자.

예외 처리 적용시 권장사항

  1. 적어도 public 메서드에 한해서는 넘겨받는 인자값이 올바른지 확인하고 예외 처리를 한다.
  2. 예외 타입을 명시하지 않은 catch문은 스레드마다 한 번만 전역적으로 둔다. 그 외에는 예외 타입을 명시한다.
  3. 성능상 문제가 발생할 수 있는 예외 처리 메서드는 예외 처리가 없는 메서드를 함께 제공한다.
  4. try/finally는 언제나 사용해도 괜찮다.

참고 자료
시작하세요! C# 10 프로그래밍 - 정성태

profile
Runner's high를 목표로

0개의 댓글