프로그램 사용자는 프로그래머가 생각하는대로만 프로그램을 다루지 않습니다.
숫자만 입력해야하는데 문자열을 입력하기도 하고 순서를 무시하고 명령어를 내리기도 합니다.
파일을 잘 전송하고 있는데 누군가가 와이파이 허브의 전원을 내리는 바람에 네트워크가 다운되고 데이터를 저장하려 했더니 용량이 부족 할 수도 있습니다.
이처럼 프로그래머가 생각하는 시나리오에서 벗어나는 사건, 이것을 예외(Exception) 라고 부릅니다. 그리고 예외가 프로그램의 오류나 다운으로 이어지지 않도록 적절하게 처리하는 것을 예외 처리(Exception Handing) 라고 합니다.
다음 예제는 길이가 3인 배열을 만들고,4번째 요소에 접근을 시도하는 코드를 통해 발생시킵니다.
이 예외를 처리하는 코드는 없으며, 바로 프로그램이 다운되고 맙니다.
namespace KillingProgram
{
internal class Program
{
static void Main(string[] args)
{
int[] arr = { 1, 2, 3 };
for (int i = 0; i < 5; i++)
{
Console.Write(arr[i]); //배열의 크기(5)를 넘어서면 예외를 일으키고 종료됩니다.
}
Console.WriteLine("종료"); //이 코드는 실행되지 않습니다.
}
}
}
출력
1 2 3 Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.
at KillingProgram.Program.Main(String[] args) in C:\Users\GeonHa\source\repos\KillingProgram\Program.cs:line 11
오류 메시지는 CLR이 출력한 것입니다.
잘못된 인덱스를 통해 배열의 요소에 접근하려 들면 배열 객체가 이 문제에 대한 상세 정보를 IndexOutOfRangeException객체에 담은 후 Main() 메소드에 던지는데, 이 예제의 main() 메소드는 이 예외를 처리할 방도가 없기 때문에 다시 CLR에 던집니다.
CLR까지 전달된 예외는 "처리되지 않은 예외"가 되고 CLR은 이것을 받으면 예외 객체에 담긴 내용을 사용자에게 출력한 후 프로그램을 강제로 종료합니다. 그래서 문제를 일으키는 13행 뒤에 있는 코드들도 실행 되지 않은 것입니다.
C#은 예외를 받을때 다음과 같이 try~catch문을 이용합니다.
try
{
//실행하고자 하는 코드
}
catch(예외 객체 1)
{
//예외가 발생했을 때의 처리
}
catch(예외 객체 2)
{
//예외가 발생했을 때의 처리
}
try~catch문으로 앞에서 만들었던 예제프로그램을 수정해 보겠습니다.
namespace KillingProgram
{
internal class Program
{
static void Main(string[] args)
{
int[] arr = { 1, 2, 3 };
try
{
for (int i = 0; i < 5; i++)
{
Console.Write(arr[i] + " ");
}
}
catch (IndexOutOfRangeException e)
{
Console.WriteLine($"예외가 발생 했습니다 : {e.Message}");
}
Console.WriteLine("종료");
}
}
}
출력
1 2 3 예외가 발생 했습니다 : Index was outside the bounds of the array.
종료
C#의 모든 예외는 System.Exception를 상속 받아야합니다.
이 상속 관계로 인해 모든 예외를 System.Exception로 간주 할수있기 때문에
System.Exception 클래스를 이용하면 다음과 같이 하나의 catch 절로 처리할 수 있습니다.
try
{
//실행 코드
}
catch (Exception e)
{
//예외 처리
}
하지만 섬세한 예외처리가 필요한 코드에서는 Exception 클래스만으로 대응이 어렵고,
Exception 형식은 프로그래머가 발생할 것으로 계산한 것이 아닌 다른 예외도 받아버리기 때문에
Exception 예외를 사용할 때는 코드를 면밀히 검토해야 합니다.
try~catch문으로 예외를 받는다는 것은 어디선가 예외를 던진다는 것입니다.
예외는 throw 문을 이용해서 던집니다.
try
{
// ...
throw new Exception("예외를 던집니다.");
}
catch(Exception e) // throw문을 통해 던져진 예외 객체는 catch문을 통해 받습니다.
{
Console.WriteLine(e.Message);
}
또 다른 예제입니다.
메소드 안에서 특정 조건을 만족하면 예외를 던지고,
이렇게 던져진 예외는 메소드를 호출하는 try~catch문에서 받아냅니다.
static void DoSomthing(int arg)
{
if (arg < 10)
Console.WriteLine($"arg : {arg}");
else
throw new Exception("arg가 10보다 큽니다.");
//예외를 던졌지만 DoSomething메소드 안에는 catch문이 없습니다.
//이 예외는 DoSomething메소드 호출자에게 던져집니다.
}
static void Main()
{
try
{
DoSomething(13);
//DoSomething메소드에서 던진 호출자의 try~catch블록에서 받습니다.
}
catch (Exception e)
{
Console.WrteLine(e.Message);
}
}
다음은 throw문을 이용하는 예제 프로그램입니다.
namespace Throw
{
internal class Program
{
static void DoSomething(int arg)
{
if (arg < 10)
Console.WriteLine($"arg : {arg}");
else
throw new Exception("arg가 10보다 큽니다.");
}
static void Main(string[] args)
{
try
{
DoSomething(1);
DoSomething(3);
DoSomething(5);
DoSomething(9);
DoSomething(11); //여기서 예외가 발생하면 다음 코드는 실행되지 않습니다.
DoSomething(13);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
}
}
출력
arg : 1
arg : 3
arg : 5
arg : 9
arg가 10보다 큽니다.
throw는 보통 문(Statement)으로 하지만 C#7.0부터는 식(expression)으로도 사용할 수 있도록 개선 되었습니다.
int? a = null;
//a가 null이라면, b에 a를 할당하지 않고 throw 식을 실행합니다.
int b = a ?? throw new ArgumentNullExcption();
다음 예제 코드와 같이 조건 연산자 안에서도 사용할 수 있습니다.
int[] array = new[] { 1,2,3 };
int index = 4;
//index가 0이상3이하라면 array[index]를 val안에 넣고,
//그렇지 않다면 IndexOutOfRangeExcption()예외를 던진다.
int val = array[
index >= 0 && index < 3
? index : throw new IndexOutOfRangeExcption()
];
앞서 살펴봤던 두 가지 throw 식의 예제 코드를 실제로 동작하는 프로그램에 넣어 테스트 해보겠습니다.
namespace ThrowExpression
{
internal class Program
{
static void Main(string[] args)
{
try
{
int? a = null;
int b = a ?? throw new ArgumentNullException();
}
catch (ArgumentNullException e)
{
Console.WriteLine(e);
}
try
{
int[] array = new[] { 1, 2, 3 };
int index = 4;
int val = array[
index >= 0 && index < 3
? index : throw new IndexOutOfRangeException()
];
}
catch (IndexOutOfRangeException e)
{
Console.WriteLine(e);
}
}
}
}
출력
System.ArgumentNullException:
Value cannot be null.(값은 null일 수 없습니다.)
System.IndexOutOfRangeException:
Index was outside the bounds of the array.(인덱스가 배열 범위를 벗어났습니다.)
try 블록에서 코드를 실행 하다가 예외가 던져지면 프로그램의 실행이 catch 절로 바로 뛰어넘어옵니다.
만약 예외 때문에 try블록의 자원 해제같은 중요한 코드를 미처 실행하지 못한다면 이는 곧 버그를 만들어내는 원인이 됩니다.
C#에서는 예외 처리를 할때 자원 해제 같은 뒷마무리를 우아하게 실행할 수 있도록 finally절을 try~catch문과 함께 제공합니다.
자신이 소속된 try절이 실행 된다면 finally절은 어떤 경우라도 실행됩니다.
심지어 try 절안에서 return 문이나 throw문이 사용되더라도 finally 절은 꼭 실행됩니다.
(return 문이나 throw문은 프로그램의 흐름제어를 외부 코드로 옮깁니다. )
using System;
using System.Collections.Generic;
public class Program
{
static int Divide(int dividend,int divisor)
{
try
{
Console.WriteLine("Divide() 시작");
return dividend / divisor; //예외가 일어나지 않고 정상적으로 return을 하더라도 finally 절은 실행 됩니다.
}
catch (DivideByZeroException e)
{
Console.WriteLine("Devide()예외 발생");
throw e; //예외가 일어나도 finally절은 실행됩니다.
}
finally
{
Console.WriteLine("Divide() finally 실행[Divide() 종료]");
}
}
public static void Main()
{
try
{
Console.Write("제수를 입력하세요 :");
String temp = Console.ReadLine();
int dividend = Convert.ToInt32(temp);
Console.Write("피제수를 입력하세요 :");
temp = Console.ReadLine();
int divisor = Convert.ToInt32(temp);
Console.WriteLine($"{dividend}/{divisor} = {Divide(dividend,divisor)}");
}
catch(FormatException e)
{
Console.WriteLine("에러 : " + e.Message);
}
catch(DivideByZeroException e)
{
Console.WriteLine("에러 : " + e.Message);
}
finally
{
Console.WriteLine("Main() finally 실행[프로그램을 종료합니다.]");
}
}
}
출력(정상적인 경우)
제수를 입력하세요 :28
피제수를 입력하세요 :2
Divide() 시작
Divide() finally 실행[Divide() 종료]
28/2 = 14
Main() finally 실행[프로그램을 종료합니다.]
출력(예외가 나온 경우)
제수를 입력하세요 :0
피제수를 입력하세요 :0
Divide() 시작
Devide()예외 발생
Divide() finally 실행[Divide() 종료]
에러 : Attempted to divide by zero.
Main() finally 실행[프로그램을 종료합니다.]
출력(인풋단계에서의 예외 발생)
제수를 입력하세요 :안녕하세요
에러 : The input string '안녕하세요' was not in a correct format.
Main() finally 실행[프로그램을 종료합니다.]
[ finally문 안에서 예외가 또 일어나면 어떻게 해야 하나요? ]
finally 블록에서 예외가 일어나면 받아주거나 처리해주는 코드가 없으므로 이 예외는 "처리되지 않은 예외"가 됩니다.
코드를 면밀히 살펴 예외가 일어나지 않도록 하거나 현재 수준의 코드에서 예외가 일어날 가능성을 완전히 배제할 수 없다면 이안에서 다시한번 try~catch 절을 사용하는것도 방법입니다.
모든 예외 객체는 System.Exception 클래스로 부터 파생되어야 합니다. 이 규칙에 의거하여, 우리도 Exception 클래스를 상속 하기만 하면 새로운 예외 클래스를 만들 수 있습니다.
예제는 4개의 8비트 정수를 받아 하나의 32비트 정수 안에 병합하는 MergeARGB() 메소드를 갖고 있습니다. 매개변수에 입력하는 각 정숫값은 0~255여야 하며 매개변수의 값이 이 범위를 벗어나면 MergeARGB() 메소드는 InvalidArgumentException 예외를 일으킵니다.
using System;
using System.Collections.Generic;
//새로운 클래스(InvalidArgumentException)를 만들어서
//Exception을 상속하여줌으로써 사용자 정의 예외를 만든다.
class InvalidArgumentException : Exception
{
public InvalidArgumentException()
{}
public InvalidArgumentException(string message) : base(message)
{}
public object Argument{ get; set; }
public string Range{ get; set; }
}
public class Program
{
static uint MergeARGB(uint alpha, uint red, uint green, uint blue)
{
uint[] args = new uint[] {alpha, red, green, blue };
foreach(uint arg in args)
{
if(arg > 255)
throw new InvalidArgumentException() //사용자 정의 예외 던지기
{
Argument = arg, Range = "0~255"
};
}
return (alpha << 24 & 0xFF000000) |
(red << 16 & 0x00FF0000) |
(green << 8 & 0x0000FF00) |
(blue & 0x000000FF);
}
public static void Main()
{
try
{
Console.WriteLine("0x{0:X}", MergeARGB(255,111,111,111));
Console.WriteLine("0x{0:X}", MergeARGB(1,65,192,128));
Console.WriteLine("0x{0:X}", MergeARGB(0,255,255, 300));
}
catch(InvalidArgumentException e) //사용자 정의 예외 받기
{
Console.WriteLine(e.Message);
Console.WriteLine($"Argument:{e.Argument}, Range:{e.Range}");
}
}
}
출력
0xFF6F6F6F
0x141C080
Exception of type 'MyException.InvalidArgumentException' was thrown.
Argument:300, Range:0~255
C# 6.0부터는 catch절이 받아들일 예외 객체에 제약사항을 명시해서 해당 조건을 만족하는 예외 객체에 대해서만 예외 처리 코드를 실행할 수 있도록 하는 예외 필터(Exception filter)가 적용되었습니다.
아래 예제는 예외필터를 구현한 예제입니다.
(when을 if로 생각하면 이해하기 쉽습니다.)
namespace ExceptionFiltering
{
class FIlterableException : Exception
{
public int ErrorNo { get; set; }
}
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("Enter Number Between 0~10");
string input = Console.ReadLine();
try
{
int num = Int32.Parse(input);
if (num < 0 || num > 10)
throw new FIlterableException() { ErrorNo = num };
else
Console.WriteLine($"Output : {num}");
}
catch (FIlterableException e) when (e.ErrorNo < 0)
{
Console.WriteLine("Negative input is not allowed");
}
catch (FIlterableException e) when (e.ErrorNo > 10)
{
Console.WriteLine("Too Big number is not allowed");
}
}
}
}
출력(정상적인 프로그램)
Enter Number Between 0~10
9
Output : 9
출력(예외를 발생한 프로그램)
Enter Number Between 0~10
-1923
Negative input is not allowed
예외 처리를 할때 try~catch를 꼭 써야만 하는 이유가 무엇일까요?
다음예제는 Stacktrace를 사용하여 해당 에러가 어느 부분에서 발생했는지 추적하는 코드입니다.
catch (FIlterableException e) when (e.ErrorNo > 10)
{
Console.WriteLine("Too Big number is not allowed");
Console.WriteLine(e.StackTrace); //에러 발생 위치를 추적합니다.
}
출력
Too Big number is not allowed
at ExceptionFiltering.Program.Main(String[] args) in C:\Users\GeonHa\source\repos\ExceptionFiltering\Program.cs:line 19