1. 예외(Exception)란 무엇인가?
예외는 프로그램 실행 중에 발생하는 비정상적인 상황을 의미합니다. 예를 들어, 배열의 유효 범위를 벗어난 인덱스에 접근하거나, 0으로 숫자를 나누려 할 때 예외가 발생할 수 있습니다. 예외가 발생하면 해당 지점에서 프로그램의 정상적인 흐름이 중단되고, 별도의 처리가 없다면 프로그램이 비정상적으로 종료됩니다.
소스의 예시처럼, 크기가 3인 int
배열 arr
에서 i
가 3 이상일 때 arr[i]
에 접근하려 하면 예외가 발생하며, 13번째 줄에서 예외가 발생했기 때문에 그 이후의 코드는 더 이상 실행되지 않고 프로그램이 종료됩니다.
2. try-catch
를 이용한 예외 처리
C#에서는 try-catch
문을 사용하여 예외를 처리합니다.
try
블록: 예외가 발생할 가능성이 있는 코드를 작성하는 영역입니다.catch
블록: try
블록 안에서 특정 타입의 예외가 발생했을 때 해당 예외를 잡아서 처리하는 영역입니다.예를 들어, 배열 범위 초과 예외(IndexOutOfRangeException
)를 처리하려면 다음과 같이 작성할 수 있습니다:
try
{
// 예외 발생 가능성 있는 코드 (예: arr[i] 접근)
Console.WriteLine(arr[i]); // i가 범위를 넘으면 여기서 예외 발생
}
catch (IndexOutOfRangeException e) // IndexOutOfRangeException 예외를 잡습니다.
{
// 예외 발생 시 처리할 코드
Console.WriteLine($"예외가 발생했습니다: {e.Message}");
}
// 예외가 잡히면 이 코드는 실행됩니다.
Console.WriteLine("종료");
소스에서처럼, try
블록 안에서 IndexOutOfRangeException
이 발생하면 18번째 줄의 catch (IndexOutOfRangeException e)
블록이 이 예외를 잡아서 처리합니다. 예외가 처리되었으므로, 그 이후 코드인 "종료" 메시지도 정상적으로 출력됩니다.
하나의 try
블록에 여러 개의 catch
블록을 붙여서 다양한 종류의 예외를 다르게 처리할 수 있습니다. 가장 구체적인 예외 타입부터 먼저 catch
하고, 마지막에 가장 일반적인 Exception
타입을 catch
하는 것이 좋습니다. catch (Exception e)
는 모든 종류의 예외를 잡을 수 있습니다.
3. System.Exception
클래스
C#의 모든 예외는 System.Exception
클래스를 상속받습니다. 이 클래스는 예외에 대한 정보를 담고 있으며, Message
속성은 예외에 대한 설명 메시지를 제공합니다. StackTrace
속성은 예외가 발생하기까지 호출된 메서드들의 순서(호출 스택)를 보여주어 문제의 원인을 파악하는 데 도움을 줍니다.
4. 예외 던지기 (throw
)
프로그램 코드에서 특정 조건이 만족되지 않거나 잘못된 상황이 발생했을 때, 프로그래머가 직접 예외를 발생시킬 수 있습니다. 이를 예외 던지기(throw
)라고 합니다. throw new Exception("예외 메시지");
와 같이 사용합니다.
메서드 안에서 throw
문을 만나면 해당 메서드의 실행은 즉시 중단되고, 예외가 발생한 사실이 호출자에게 전달됩니다. 호출자는 이 예외를 try-catch
문으로 잡아서 처리할 수 있습니다. 소스의 DoSomething
메서드는 arg
가 10보다 크면 예외를 던지고, Main
메서드에서 DoSomething
을 호출한 후 이 예외를 catch
블록에서 잡습니다.
C# 7.0부터는 throw
문을 식의 일부로 사용할 수 있는 throw
식(Throw Expression) 기능이 추가되었습니다. 예를 들어 a ?? throw new ArgumentNullException();
와 같이 사용하여 null 검사와 예외 발생을 간결하게 표현할 수 있습니다.
5. finally
블록
try-catch
문과 함께 finally
블록을 사용할 수 있습니다. finally
블록 안에 있는 코드는 예외 발생 여부나 catch
블록의 실행 여부에 관계없이 항상 실행됨을 보장합니다.
finally
블록은 주로 파일을 닫거나, 네트워크 연결을 끊거나, 데이터베이스 연결을 해제하는 등 자원을 정리하는 코드를 작성하는 데 사용됩니다. try
블록에서 예외가 발생하든 안 하든, 심지어 try
블록이나 catch
블록에서 return
문을 만나 메서드를 빠져나가더라도 finally
블록은 실행됩니다.
try
{
// 예외 발생 가능성 있는 코드
}
catch (Exception e)
{
// 예외 처리 코드
}
finally
{
// 예외 발생 여부와 상관없이 항상 실행되는 코드
// (예: 자원 해제)
}
소스는 예외가 발생했을 때 자원 해제 코드를 각 catch
블록마다 반복하는 비효율적인 상황을 보여주며, finally
블록이 이를 해결해준다는 것을 시사합니다.
!주의! finally
블록 안에서 또 다른 예외가 발생하면 어떻게 될까요? 소스에 따르면, finally
블록 안에서 발생한 예외는 처리되지 않은 예외가 되며, 이는 프로그램의 비정상 종료로 이어질 수 있습니다 (이는 소스에 직접 명시되지는 않았으나, 일반적인 C#의 예외 처리 동작입니다). 아주 중요한 자원 해제 코드가 실패할 가능성이 있다면, finally
안에서 또 다른 try-catch
로 감싸는 것을 고려해볼 수 있지만, 이는 드문 경우에만 사용해야 합니다.
6. 사용자 정의 예외 클래스 만들기
.NET에서 제공하는 기본 예외 클래스 외에, 프로그램의 특정 오류 상황을 더 명확하게 나타내기 위해 개발자가 직접 예외 클래스를 정의할 수 있습니다. 사용자 정의 예외 클래스는 System.Exception
클래스를 상속받아 만듭니다.
class MyException : Exception // Exception 클래스 상속
{
// ... 필요에 따라 생성자나 속성 등을 추가
}
사용자 정의 예외 클래스에 오류와 관련된 추가 정보(예: 잘못된 값, 허용 범위 등)를 담는 속성을 추가하면 예외 발생 시 문제 해결에 더 도움이 됩니다. 소스에서는 InvalidArgumentException
클래스에 Argument
와 Range
속성을 추가한 예시를 보여주고, 소스에서는 이 예외를 던지고 잡아서 속성 값을 출력하는 코드를 보여줍니다.
7. 예외 필터링
C# 6.0부터는 catch
문에 when
키워드를 사용하여 예외 필터링을 적용할 수 있습니다. 예외 필터링을 사용하면 특정 조건이 만족될 때만 해당 catch
블록이 예외를 처리하도록 지정할 수 있습니다.
catch (FilterableException e) when (e.ErrorNo < 0) // 조건: ErrorNo가 0보다 작을 때만 처리
{
// ... 처리 코드 ...
}
catch (FilterableException e) when (e.ErrorNo > 10) // 조건: ErrorNo가 10보다 클 때만 처리
{
// ... 처리 코드 ...
}
소스는 FilterableException
의 ErrorNo
속성 값에 따라 다른 catch
블록에서 처리하는 예시를 보여줍니다. 예외 필터링을 통해 예외 처리 로직을 더욱 세분화하고 가독성을 높일 수 있습니다.
8. 예외 처리 다시 생각하기
모든 비정상적인 상황에 대해 반드시 예외를 던져야 하는 것은 아닙니다. 소스에서는 나누기 연산에서 제수(divisor)가 0인 경우를 예로 들며, 예외를 던지는 대신 오류 코드를 반환하거나 (return -5;
와 같이) out
매개변수를 사용하여 결과를 반환하고 성공 여부를 나타내는 방식을 고려해 볼 수 있다고 제시합니다. 제수 0처럼 발생 가능하고 예상 가능한 오류는 예외 처리 대신 다른 방식으로 처리하는 것이 더 적합할 수도 있다는 관점을 제시합니다. 하지만 배열 범위 초과와 같이 예상하기 어려운 오류는 예외 처리가 더 적합합니다.