아이템 45. 메서드가 실패했음을 알리기 위해서 예외를 이용하라
어떤 메서드가 실패했음을 알리기위해 반환값으로 오류코드를 사용하기도한다. 반면 예외의 발생은 콜 스택을 통해 적절한 catch문을 만날 때까지 콜스택을 통해 전파된다. 코드 작성자는 에러를 발생시키는 위치와 처리하는 부분을 분리시켜 개발할 수 있다.
특정 메서드는 예상치 못한 상황에 대해 예외를 발생시키도록 설계할 수 도 있고 그러한 상황을 정상적으로 처리할 수 있는 것으로 설계할 수도 있다.
ex) File.Open()는 파일이 없을 때 예외를 발생시킬 수 있다. 있어야하는 파일이 없기 때문이고 더 이상 프로그램을 진행할 수 없기 때문이다. 반면, File.Exists()는 파일이 있는 지만 체크하고 있든 없든 그에 따른 프로그램을 작성할 수있기 때문에 예외를 발생시키지 않는다.
메서드를 작성할 때 예외를 일으키지 않는 방향으로 설계하는 것이 좋다. 되도록이면 테스트 메서드를 통해 해당 작업이 수행 가능한지 판단하고 예외를 일으키지 않는 상황이 보장된 작업을 수행하도록 한다.
class Program
{
private static bool TryWork(out int errorCode)
{
if ((errorCode = CheckCondition()) != 0)
{
return false;
}
Work();
errorCode = 0;
return true;
}
public static void Main(string[] args)
{
if (!TryWork(out int errorCode))
{
// 예외를 발생시키지 않고 로그 정보를 남긴다.
Log.Dump($"TryWork is Failed : {errorCode}");
}
}
}
아이템 46. 리소스 정리를 위해 using과 try/finally를 활용하라
관리되지 않은 리소스를 해제하기위해 IDisposable를 구현한 구현체를 예외가 발생했을 때도 정상적으로 Dispose()가 호출되도록 해야한다.
public void Execute(string sqlConnStr, string sqlCmdStr)
{
using (SqlConnection conn = new SqlConnection(sqlConnStr))
{
using (SqlCommand cmd = new SqlCommand(sqlCmdStr))
{
conn.Open();
cmd.ExecuteNonQuery();
}
}
}
IDisposable 구현체에만 사용할 수 있는 using 키워드는 해당 객체를 생성한 뒤 블락 내에서 예외가 발생하면 자동적으로 try-finally 구문을 이용해 Dispose()를 호출한다.
SqlConnection conn = null;
try
{
conn = new SqlConnection(connStr);
}
finally
{
conn.Dispose();
}
아이템 47. 사용자 지정 예외 클래스를 완벽하게 작성하라
모든 상황을 예외로 던질 필요도 없고 많은 상황이 기존 Exception과 파생 클래스로 표현가능한 점에서 사용자 예외 클래스를 작성하는 것을 조금 생각해 볼 필요가 있다.
사용자 예외 클래스를 정의하고자 하였다면, 개별 예외 클래스의 고유한 책임을 명확히 하고 이름을 Exception으로 끝나는 것으로 System.Exception 혹은 그 파생 클래스를 상속하여 작성한다.
사용자가 만든 예외 클래스를 정의하기 위해 다음과 같은 생성자를 제공해야한다.
// 기본 생성자
public Exception();
// 에러 메세지를 포함하는 생성자
public Exception(string);
// 에러 메세지와 내부 예외를 포함하는 생성자
public Exception(string, Exception);
// 입력 스트림을 이용하는 생성자
protected Exception(SerializationInfo, StreamingContext);
아이템 48. 강력한 예외 보증을 준수하는 것이 좋다.
강력한 예외보증이란 예외로 인해 요청됐던 작업이 중단되는 경우에도 응용프로그램을 변경 이전의 상태로 유지해야함을 의미한다.
강력한 보증을 준수하면 예외를 catch 하더라도 마치 요청이 없었던 것 처럼 프로그램을 진행시킬 수 있다.
강력한 예외 보증을 제공하는 방어적 프로그래밍의 예는 다음과 같다.
1. 원본에 대한 복사본을 생성한다.
2. 복사본에 대하여 요청한 작업을 수행한다. 이때 예외가 발생할 수 있다.
3. 예외가 발생하지 않은 경우에 대해 수정된 복사본과 원본을 교체한다.
또 다른 예로는 델리게이트 내에서 절대 예외를 일으키면 안되므로 강력한 예외 없음 보증을 준수해야할 것이다.
아이템 49. catch 후 예외를 다시 발생시키는 것보다 예외 필터가 낫다.
예외 필터
var retryCount = 0;
var dataString = default(String);
while (dataString == null)
{
try
{
dataString = HTTPRequest();
}
catch(TimeoutException e) when (retryCount++ < 3)
{
WriteLine("Operation Timeout... Try Again");
Task.Delay(1000*tryCount);
}
}
예외 필터는 catch문 이후에 when 키워드를 이용해서 나타내고 catch로 지정한 예외 타입에 대해서만 수행된다.
when 절에서는 예외에 대한 위치를 알고 있기 때문에(dataString = HTTPRequest()), 호출 스택이나 지역변수 등에 대한 정보를 접근할 수 있다. 만약 예외 필터가 false를 반환하면 런타임은 콜 스택을 따라 올라가면서 발생한 예외 타입을 catch할 수 있는 지점을 계속 찾아간다.
catch 문 내에 예외를 다시 던지는 throw를 사용하면 이전에 저장되었던 콜 스택 정보가 전부 유실된다. throw 이후 부터 다시 스택되감기를 진행하며 이때부터 예외 위치와 콜 스택 정보가 쌓이게 된다. 따라서 상위 콜스택에서 catch를 이용해 다시 던져진 예외를 catch했더하다라도 일차적으로 발생한 원시 예외에 대해 아무런 정보를 얻을 수 없다.
var retryCount = 0;
var dataString = default(String);
while (dataString == null)
{
try
{
dataString = HTTPRequest();
}
catch(TimeoutException e) when (retryCount++ < 3)
{
throw; // 이전의 예외 정보는 유실되고 다시 상위 프로시저로 스택이 되감기된다.
}
}
예외 필터를 적극적으로 사용하는 경우
- Task의 AggregateExcpetion : Task 객체의 경우 자신의 작업에서 파생된 자식 Task 객체가 있을 수 있으며 자식 Task가 예외가 발생했을 시 모든 자식 Task에 대한 예외를 AggregateException의 InnerExceptions 속성에 담는다. 해당 AggregateException을 catch하여 InnerExceptions 속성을 살펴보면서 예외 처리가 필요한지 판단할 수 있을 것이다.
- COMException : COMException은 HResult라는 속성을 가지고 있다. COM 객체를 사용하면서 발생하는 예외에 대한 오류 코드로 이 값을 이용하여 예외 처리를 달리할 수 있다.
- HTTPException : GetHttpCode()는 응답 코드를 가져오는데 응답 코드를 보고 어떤 상황인지 판별하여 예외처리를 수행할 수 있다.
아이템 50. 예외 필터의 다른 활용 예를 살펴보라
다음 메서드는 예외를 콘솔로 기록하는 메서드이다. 항상 false를 반환한다.
public static bool ConsoleLogOnException(Exception e)
{
var oldCol = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Red;
WriteLine("Error : {0}", e);
Console.ForegroundColor = oldCol;
return false;
}
항상 false를 반환하는 위 메서드를 when 절에 위치시켜 모든 예외 Exception 클래스에 대해 Console 로그를 남기도록 작성할 수 있다.
public void Execute()
{
try
{
var data = RecvOnCompeleted();
}
catch (Exception e) when(ConsoleLogOnException(e))
{
}
catch (InvalidOperation e)
{
//...
}
catch (ArgumentNullException e)
{
//...
}
}
catch Exception은 모든 예외 클래스를 잡아낼 수 있다. when 절에서 해당 예외를 기록하고 false를 반환하기 때문에 그 이후로의 catch 문을 검사하여 실제 발생한 타입과 일치한 catch 문을 찾는다. 따라서 모든 예외 정보를 콘솔로 남기면서 추가적인 예외 처리를 할 수 있게 된다.
출처 : Effective C# 빌 와그너 저 - 한빛 미디어
'C# > Advanced C#' 카테고리의 다른 글
[More Effective C#] 2장 API 설계 (1) (0) | 2021.12.12 |
---|---|
[More Effective C#] 1장 데이터 타입 (0) | 2021.12.04 |
[Effective C#] 4장 LINQ 활용 (2) (0) | 2021.11.24 |
[Effective C#] 4장 LINQ 활용 (1) (0) | 2021.11.24 |
[Effective C#] 3장 제너릭 활용 (0) | 2021.11.20 |