'이것이 C#이다 개정판'의 chapter 12를 정리한 글입니다.
예외 처리 (Exception Handling)
프로그램을 개발하다보면, 원래 개발하던 시나리오에서 벗어나는 예외(Exception)가 발생하곤 합니다.
using System;
namespace KillingProgram
{
class MainApp
{
static void Main(string[] args)
{
int[] arr = {1, 2, 3};
for (int i = 0; i < 5; ++i)
{
Console.WriteLine(arr[i]);
}
Console.WriteLine("종료");
}
}
}

해당 코드를 실행하면, 위와 같이 "Unhandled exception"이라는 처리되지 않은 예외에 관한 메시지가 출력이 됩니다.
이 메시지는 CLR이 출력한 것입니다.
for 문에서 arr[4]에 접근할 때, 잘못된 인덱스를 통해 배열의 요소에 접근하는 것이기에 문제가 발생하고,
해당 문제에 대한 상세 정보를 IndexOutOfRangeException의 객체에 담은 후 Main() 메서드에 던집니다.
하지만, 해당 코드에서는 Main() 메서드에서 이 예외를 처리할 방도가 없기 때문에 다시 CLR에 던집니다.
※ Main() 메서드에서 던진 예외를 받기 위해서는 try~catch 문이 필요합니다.
CLR까지 전달된 예외는 "처리되지 않은 예외(Unhandled exception)"가 되고 CLR은 이것을 받으면 예외 객체에 담긴 내용을 사용자에게 출력한 후 프로그램을 강제 종료합니다.
try ~ catch
C#에서는 예외를 받을 때 try~catch 문을 이용할 수 있습니다.
try
{
// 실행하고자 하는 코드
}
catch ( 예외_객체_1 )
{
// 예외가 발생했을 때의 처리
}
catch ( 예외_객체_2 )
{
// 예외가 발생했을 때의 처리
}
try 절의 코드 블록에는 예외가 일어나지 않을 경우에 실행되어야 할 코드들이 들어가고, catch 절에는 예외가 발생했을 때의 처리 코드가 들어갑니다.
이때 catch 절은 try 블록에서 던질 예외 객체와 형식이 일치해야 합니다. 그렇지 않으면 던져진 예외는 아무도 받지 못해 "처리되지 않은 예외"로 남게 됩니다.
[예제]
using System;
namespace TryCatch
{
class MainApp
{
static void Main(string[] args)
{
int[] arr = { 1, 2, 3 };
try
{
for (int i = 0; i < 5; ++i)
{
Console.WriteLine(arr[i]);
}
}
catch (IndexOutOfRangeException e)
{
Console.WriteLine($"예외가 발생했습니다 : {e.Message}");
}
Console.WriteLine("종료");
}
}
}

System.Exception 클래스
System.Exception 클래스는 .NET에서 모든 예외의 최상위 기본 클래스입니다.프로그램 실행 중에 예상치 못한 오류 상황이 발생하면, .NET 런타임 환경이나 현재 실행 중인 코드는 이 Exception 클래스 또는 이를 상속받는 특정 예외 클래스의 인스턴스를 생성하여 던집니다.
[특징]
- 모든 예외의 근원
- C#에서 발생하는 모든 종류의 예외는 결국 System.Exception 클래스를 직간접적으로 상속받습니다.
- 오류 정보 컨테이너
- 예외 발생 시 오류 관련 정보를 담는 표준화된 방법을 제공합니다.
- 예외 처리 메커니즘의 기반
- try~catch 구문에서 catch 블록이 특정 예외를 잡을 때, catch 구문은 System.Exception 타입으로 모든 하위 예외들을 포함하여 어떤 종류의 예외든 잡을 수 있습니다.
[System.Exception 하위 예외 클래스]
| 클래스 (System. 생략) | 설명 |
| ArgumentException | 메소드에 전달된 인자 중 하나가 유효하지 않을 때 발생하는 예외입니다. |
| ArgumentNullException | 메소드에 전달된 인자가 null이어서는 안되는데, null이 전달되었을 때 발생하는 예외입니다. |
| ArgumentOutOfRangeException | 메소드에 전달된 인자의 값이 허용 범위를 벗어났을 때 발생하는 예외입니다. |
| InvalidOperationException | 현재 객체의 상태가 특정 작업을 수행하기에 유효하지 않을 때 발생하는 예외입니다. |
| NullReferenceException | null인 객체 참조 변수를 통해 멤버(필드, 메소드, 프로퍼티 등)에 접근하려고 할 때 발생하는 예외입니다. 가장 흔하게 발생하는 런타임 오류 중 하나입니다. |
| IndexOutOfRangeException | 배열이나 컬렉션에 접근할 때 유효하지 않은 인덱스(범위를 벗어난 인덱스)를 사용했을 때 발생하는 예외입니다. |
| InvalidCastException | 명시적 형 변환 또는 언박싱 작업이 런타임에 실패했을 때 발생하는 예외입니다. |
| FormatException | 인자의 형식이 올바르지 않아서 메소드를 완료할 수 없을 때 발생하는 예외입니다. |
| DivideByZeroException | 정수 나누기 연산에서 0으로 나눌 때 발생하는 예외입니다. 부동 소수점 나누기에서는 발생하지 않습니다. |
[System.Exception 대표 프로퍼티]
| 프로퍼티 이름 | 설명 |
| Message | 예외가 발생한 이유를 사람이 읽을 수 있는 텍스트 메시지로 가져옵니다. 예외 객체를 생성할 때 생성자의 인자로 전달되는 경우가 많습니다. |
| StackTrace | 예외가 발생했을 때의 호출 스택 정보를 문자열로 가져옵니다. 오류가 발생하기까지 어떤 메소드들이 순소대로 호출되었는지 보여주므로 디버깅에 매우 유용합니다. |
| InnerException | 현재 예외를 유발한 이전 예외에 대한 참조를 가져옵니다. 한 예외가 다른 예외를 포착하고 다시 새로운 예외를 던질 때 사용됩니다. 여러 단계의 오류 원인을 추적하는 데 도움이 됩니다. |
| Data | 예외에 대한 사용자 정의 정보를 저장할 수 있는 키 / 값 쌍 컬렉션(IDictionary)을 가져옵니다. 표준 속성에 없는 추가 정보를 담을 때 사용됩니다. |
| Source | 예외를 발생시킨 애플리케이션 또는 개체의 이름을 가져오거나 설정합니다. |
| TargetSite | 예외를 던진 메소드를 나타내는 MethodBase 객체를 가져옵니다. |
[주의할 점]
Exception 클래스만으로도 예외 처리가 가능은 하지만, 무조건 Exception 클래스를 사용하는 것은 금물입니다.
System.Exception 형식은 프로그래머가 발생할 것으로 계산한 예외 말고도 다른 예외까지 받아낼 수 있습니다.
만약 그 예외가 현재 코드가 아닌 상위 코드에서 처리해야 할 예외라면, 이 코드는 예외를 처리하지 못하고 원치 않는 흐름으로 갈 수 있습니다.
예외 던지기 : throw 문
예외는 throw 문을 통해 던지고, 던져진 예외 객체는 catch 문을 통해 받습니다.
[예제1]
using System;
namespace Throw
{
class MainApp
{
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.ToString() : {e.ToString()}");
Console.WriteLine($"e.Message : {e.Message}");
Console.WriteLine($"e.StackTrace : {e.StackTrace}");
Console.WriteLine($"e.InnerException : {e.InnerException}");
Console.WriteLine($"e.Data : {e.Data}");
Console.WriteLine($"e.Source : {e.Source}");
Console.WriteLine($"e.TargetSite : {e.TargetSite}");
}
}
}
}

[예제2]
using System;
namespace ThrowExpression
{
class MainApp
{
static void Main(string[] args)
{
try
{
int? a = null;
int b = a ?? throw new ArgumentException();
}
catch (ArgumentException e)
{
Console.WriteLine(e);
}
try
{
int[] array = new[] { 1, 2, 3 };
int index = 4;
int value = array[
index >= 0 && index < 3
? index : throw new IndexOutOfRangeException()
];
}
catch (IndexOutOfRangeException e)
{
Console.WriteLine(e);
}
}
}
}

try~catch와 finally
try 블록에서 코드를 실행하다가 예외가 던져지면 프로그램의 실행이 catch 절로 바로 뛰어넘게 됩니다.
finally 절은 예외로 인해 try 블록의 자원 해제 같은 중요한 코드를 미처 실행하지 못할 경우에 대비할 수 있게 해줍니다.
finally 절은 자신이 소속된 try 절이 실행될 경우, 어떠한 경우라도 finally 블록의 코드를 실행합니다.
심지어 try 절 안에서 return 문이나 throw 문이 사용되더라도 finally 절은 꼭 실행됩니다.
[예제]
using System;
namespace Finally
{
class MainApp
{
static int Divide(int dividend, int divisor)
{
try
{
Console.WriteLine("Divide() 시작");
return dividend / divisor;
}
catch (DivideByZeroException e)
{
Console.WriteLine("Divide() 예외 발생");
throw e;
}
finally
{
Console.WriteLine("Divide() 끝");
}
}
static void Main(string[] args)
{
try
{
Console.Write("제수를 입력하세요. : ");
String temp = Console.ReadLine();
int dividend = Convert.ToInt32(temp);
Console.Write("피제수를 입력하세요 : ");
temp = Console.ReadLine();
int divisor = Convert.ToInt32(temp);
Console.WriteLine("{0} / {1} = {2}", dividend, divisor, Divide(dividend, divisor));
}
catch (FormatException e)
{
Console.WriteLine("에러 : " + e.Message);
}
catch (DivideByZeroException e)
{
Console.WriteLine("에러 : " + e.Message);
}
finally
{
Console.WriteLine("프로그램을 종료합니다.");
}
}
}
}



[finally 절에서의 예외 발생]
finally 블록에서 예외가 일어나면 받아주거나 처리해주는 코드가 없으므로 이 예외는 "처리되지 않은 예외"가 됩니다.
예외가 발생할 가능성을 완전히 배제할 수 없다면 이 안에서 다시 한번 try~catch 절을 사용하는 것도 방법입니다.
사용자 정의 예외 클래스
C#에서 사용하는 모든 예외 객체는 System.Exception 클래스로부터 파생되어야 합니다.
그렇기에 Exception 클래스를 상속하기만 하면 새로운 예외 클래스를 만들 수 있습니다.
[문법]
class MyException : Exception
{
// ...
}
사용자 정의 예외는 그렇게 자주 필요하지는 않습니다. .NET이 100여 가지가 넘는 예외 형식을 제공하고 있기 떄문입니다.
하지만 특별한 데이터를 담아서 예외 처리 루틴에 추가 정보를 제공하고 싶거나 예외 상황을 더 잘 설명하고 싶을 때는 사용자 정의 예외 클래스가 필요합니다.
[예제]
using System;
namespace MyException
{
class InvalidArgumentException : Exception
{
public InvalidArgumentException() { }
public InvalidArgumentException(string message) : base(message) { }
public object Argument { get; set; }
public string Range { get; set; }
}
class MainApp
{
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);
}
static void Main(string[] args)
{
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}");
}
}
}
}

예외 필터 (Exception Filter)
C# 6.0부터는 catch 절이 받아들일 예외 객체에 제약 사항을 명시해서 해당 조건을 만족하는 예외 객체에 대해서만 예외 처리 코드를 실행할 수 있도록 하는 예외 필터가 도입되었습니다.
[문법]
try
{
// 실행하고자 하는 코드
}
catch ( 예외_객체 ) when ( 제약_조건 )
{
// 예외가 발생했을 때의 처리
}
[예제]
using System;
namespace ExceptionFiltering
{
class FilterableException : Exception
{
public int ErrorNo { get; set; }
}
class MainApp
{
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.");
}
}
}
}



예외 처리의 용이성
- try ~ catch 문을 이용해 코드가 깔끔하게 분리가 됩니다.
- try ~ catch 문 없이 if 문으로 예외를 방지하는 등의 방법을 사용할 수는 있지만, 한 눈에 예외 처리임을 확인할 수는 없습니다. 이에 반해 try ~ catch 문은 한 눈에 예외 처리임을 확인할 수 있습니다.
- 디버깅이 아주 용이합니다.
- 예외 객체의 StackTrace 프로퍼티를 통해 문제가 발생한 부분의 소스 코드 위치를 알려주기 때문에 디버깅이 아주 용이합니다.
- 여러 문제점을 하나로 묶거나 코드에서 발생할 수 있는 오류를 종류별로 정리해줄 수 있습니다.
- 프로그램 실행 중 오류가 발생하면, 해당 오류에 대한 다양한 정보(오류 메시지, 발생 위치, 호출 스택 등)가 하나의 '예외 객체(Exception Object)' 안에 캡슐화됩니다. 즉, 예외 객체로 여러 문제점이 하나로 묶이게 되는 것입니다.
- .NET의 예외 처리 시스템은 System.Exception을 최상위로 하는 예외 클래스의 계층 구조를 가지고 있습니다.
즉, 여러 문제점에 대해 catch 블록으로 발생할 수 있는 오류를 종류별로 구분하여 처리할 수 있습니다.- System.ArgumentException :메소드 인자 관련 오류 (포괄적)
- System.ArgumentNullException : 인자가 null인 경우 (ArgumentException의 하위)
- System.ArgumentOutOfRangeException : 인자 값이 범위를 벗어난 경우 (ArgumentException의 하위)
- System.IO.IOException : 파일 입출력 관련 오류 (포괄적)
- System.IO.FileNotFoundException : 파일을 찾을 수 없는 경우 (IOException의 하위)
- System.Net.WebException : 웹 요청 관련 오류 (포괄적)
- Sysetm.Net.Wockets.SocketException : 소켓 통신 관련 오류 (WebException의 하위 또는 관련)
- 등등
'C#' 카테고리의 다른 글
| [C#] 람다식, 식 트리 (0) | 2025.05.12 |
|---|---|
| [C#] 대리자와 이벤트 (0) | 2025.05.09 |
| [C#] 일반화 프로그래밍 (0) | 2025.05.05 |
| [C#] 컬렉션, 인덱서 (1) | 2025.05.03 |
| [C#] 배열 (0) | 2025.05.02 |