# Effective 5장 ## 각 장별 요약 - 1장 : C# 언어 요소 - 2장 : .NET 리소스 관리 - 3장 : 제네릭 활용 - 4장 : LINQ 활용 - 5장 : 예외 처리 --- ## 5 장 : 예외 처리 > 현대적인 C#프로그램에서 예외와 오류를 관리하는 방법에 대하여 ### 아이템 45 : 메서드가 실패했음을 알리기 위해서 예외를 이용하라 - 메서드가 요청된 작업을 제대로 수행할 수 없는 경우, 예외를 발생시켜 실패가 발생했음을 알려야 한다. - 다른 개발자가 사용할 라이브러리를 작성하는 경우에는 정상적인 운영 환경에서 예외가 발생할 가능성을 최소화하는 것이 좋다. - 또 예외가 발생할 수 밖에 없는 상황에서는 개발자가 try / catch 블록을 작성하지 않고도 정상적으로 메서드가 수행될 수 있는지를 확인할 수 있는 API 를 함께 제공하는 것이 좋다. - 메서드 내부의 에러를 반환코드로 나타내는 것 보다는 예외로써 알려주는 것이 좋다. - 반환코드로 나타낼 때, 이는 메서드 호출자에 의해서 처리된다. - 반면 예외는 적절한 catch 문이 구성된 위치까지 콜 스택을 통해 전파된다. - 따라서 개발자는 에러를 발생시키는 위치와 에러를 처리하는 부분을 여러 수준에서 분리하여 개발할 수 있다. - 또한 예외는 쉽게 무시하기 어렵기 때문에 오류가 발생한 상태로 프로그램이 정상적으로 수행되기 어렵다. - 메서드 내부에서 의도하지 않은 모든 상황을 예외로 다룰 필요는 없다. - 가령 `Files.Exits()` 메서드의 경우 파일이 존재하지 않았을 때는 `false` 를 반환하면 된다. - 반면 `Files.Open()` 메서드의 경우 파일이 존재하지 않았을 때 예외를 발생시켜야 한다. - 이러한 차이는 메서드 명명 방법에도 중대한 영향을 끼친다. - 이렇듯 예외는 일반적인 흐름 제어 메커니즘으로 사용해서는 안된다. - 또한 요청한 작업이 성공적으로 수행될 수 있을지를 사전에 테스트할 수 있는 메서드를 같이 제공하는 것이 좋다. --- ### 아이템 46 : 리소스 정리를 위해 using과 try / finally 를 활용하라 - 관리되지 않은 리소스를 사용하는 모든 타입은 `IDisposable` 인터페이스가 제공하는 `Dispose()` 메서드를 반드시 구현해야 한다. - 더불어 사용자들이 `Dispose()` 메서드를 호출하는 것을 잊는 경우에도 리소스가 해제될 수 있도록 방어적으로 `finalizer` 를 작성해야한다. - 사용자의 입장에서는 `IDisposable` 인터페이스를 상속받은 객체를 선언한 후 `Dispose()` 메서드를 호출해야한다. - 하지만 `using` 문이나 `try / finally` 블록을 활용하면 항상 `Dispose()` 메서드가 호출될 수 있다. - ex ) ```csharp= public void ExecuteCommand(string connString, string commandString) { SqlConnection myConnection = new SqlConnection(connString); var mySqlCommand = new SqlCommand(commandString, myConnection); myConnection.Open(); mySqlCommand.ExecuteNonQuery(); } ``` - `SqlConnection` 과 `SqlCommand` 는 둘다 `Dispose()` 를 구현한 객체지만 사용자가 `Dispose()` 메서드를 호출하지 않았기 때문에 두 객체는 `finalizer` 가 호출될 때까지 메모리에 남게 된다. - 문제 해결을 위해서는 `Dispose()` 메서드를 호출해주면 되지만, SQL 명령을 수행하는 도중에 예외가 발생할 경우 리소스가 해제되지 않는다. - 따라서 `using` 구문이나 `try / finally` 를 통해 자동으로 `Dispose()` 되게 하자 ```csharp= public void ExecuteCommand(string connString, string commandString) { using ( SqlConnection myConnection = new SqlConnection(connString) ) { using ( var mySqlCommand = new SqlCommand(commandString, myConnection) ) { myConnection.Open(); mySqlCommand.ExecuteNonQuery(); } } } ``` --- ### 아이템 47 : 사용자 지정 예외 클래스를 완벽하게 작성하라 - 예외란 오류 보고 메커니즘이다. - 이는 상당히 떨어진 위치에서조차 발생된 예외를 처리할 수 있다. - 따라서 예외의 정확한 정보를 예외 객체 내에 포함해야한다. - `catch` 문을 작성할 때, 예외의 런타임 타입에 따라 서로 다른 작업을 수행하게끔 구성해놓는게 좋다. - 발생한 예외가 다른 작업이나 메커니즘으로 이어질 가능성이 있을때가 새로운 예외 타입을 만들어야 할 때다. - 모든 예외 클래스의 이름은 Exception 으로 끝나야 한다. - 또, System.Exception 클래스나 더 적절한 클래스를 상속해서 구현해야 한다. - 예외를 생성할 때는 오류상황을 세부적으로 나타내는 고유한 정보를 포함시키는 것이 좋다. - 해당 예외에 앞서 발생한 예외가 있다면 `innerException` 속성에 저장하는 것이 좋다. - `toString()` 메서드를 적절히 사용하면 문제 증상을 설명하는 세부적인 문장을 가져올 수 있다. --- ### 아이템 48 : 강력한 예외 보증을 준수하는 것이 좋다 - 예외를 발생시키는 것은 응용프로그램 입장에서는 상당히 파괴적인 동작을 요청하는 것과 다르지 않다. - 응용프로그램의 제어 흐름이 크게 바뀌기 때문에 수행될 것으로 예상한 작업이 제대로 수행되지 않을 수 있다. - 데이브 에이브람스는 예외에 대한 보증을 기본 보증 / 강력한 보증 / 예외 없음 보증으로 나눴다. - 기본 보증 : 특정 함수 내에서 발생한 예외가 이 함수를 빠져나오더라도 어떤 리소스도 누수되지 않으며 모든 객체의 상태가 유효한 상태를 유지함 - 해당 보증의 문제 상당수가 강력한 보증을 준수함으로써 해결될 수 있다. - 강력한 보증 : 기본 보증에 더하여 예외 발생 시에도 프로그램의 상태가 변경되지 않음을 추가로 보증하는 것 - 데이터 교환시 원본을 유지한 채로 복사본에 대한 작업이 완료되면 복사본을 원본과 교환하는 방식을 적용하는 방어적인 프로그래밍 도입. - 이때 원본과 교환하는 작업이 원자적이지 않으므로 예외가 발생할 수 있다. - 따라서 이를 위해 봉투-편지 패턴을 도입할 수 있다. - 예외 없음 보증 : 작업이 결코 실패하지 않으며 따라서 예외가 발생하지도 않음을 보증하는 것 - ex ) `finalizer` 와 `Dispose()` 는 절대로 예외가 일어나서는 안된다. --- ### 아이템 49 : catch 후 예외를 다시 발생시키는 것보다 예외 필터가 낫다 - 표준 `catch` 절은 예외의 타입에 따라 그에 부합하는 예외만을 잡으며, 다른 타입의 예외에 대해서는 신경쓰지 않는다. - 응용프로그램의 상태나 객체의 상태 혹은 예외 객체가 가진 각종 속성등을 다루는 코드는 반드시 해당 `catch` 문 안에 작성해야했다. - 이러한 한계 때문에 우선 예외를 잡은 후 분석 과정을 수행한 다음, 그 내용을 기반으로 예외를 다시 발생시키는 코드를 작성하곤 했다. - 하지만 이러한 코딩 방식은 분석을 상당히 어렵게 만들 뿐 아니라, 추가적인 런타임 비용이 발생한다. - ex ) ```csharp= static void Method1 () { try { SomeException(); } catch (MyExecption e) { throw ... ; // # 1 } } static void Method1 () { try { SomeException(); // # 2 } catch (MyExecption e) where (false) { Console.WriteLine("Error!"); } } ``` - `Method1` 의 경우 해당 `catch` 문이 콜 스택상에 예외 발생지점으로 남아서 `try` 구문 이전까지의 지역변수 등을 잃어버린다. - 하지만 `Method2` 의 경우 `try` 문의 이전 예외 발생 위치가 콜 스택에 남기 때문에 이전까지 작업했던 지역변수 등에 접근할 수 있다는 장점이 있다. --- ### 아이템 50 : 예외 필터의 다른 활용 예를 살펴보라 - 예외 로그 처리 예제 ```csharp= public static bool ConsoleLogException(Exception e) { var oldColor = Console.ForegroundColor; Console.ForegroundColor = ConsoleColor.Red; WriteLine("Error : {0}", e); Console.ForegroundColor = oldColor; return false; } ... try { data = MakeWebReqeust(); } catch (Exception e) when (ConsoleLogException(e)) { } catch (TimeoutException e) when (failures++ < 10) { WriteLine("Timeout!!"); } ``` - 항상 `false` 를 반환하는 조건의 필터를 사용함으로써 모든 예외에 대한 미들웨어처럼 동작하는 메서드를 작성했다. ```csharp= try { data = MakeWebReqeust(); } catch (TimeoutException e) when (failures++ < 10) { WriteLine("Timeout!!"); } catch (Exception e) when (ConsoleLogException(e)) { } ``` - 또한 이러한 메서드를 뒤쪽에 배치하면 특정 타입의 예외에 대해서만 로그를 볼 수도 있다. - 또 다른 예외 필터의 활용으로는 디버깅 중 예외처리 루틴을 수행하지 않도록 하는 기능이다. ```csharp= try { data = MakeWebReqeust(); } catch (Exception e) when (ConsoleLogException(e)) { } catch (TimeoutException e) when (failures++ < 10) && (!System.Diagnostics.Debugger.IsAttached) { WriteLine("Timeout!!"); } ``` - 예외 필터를 잘 활용하면 예외가 발생했을 때 원하는 방향으로 예외를 핸들링하고 디버깅할 수 있다. --- ### 느낀점 > 이것으로 Effective C# 의 1회독을 완료했다. > > 마지막 단원인 예외 처리 단원이었다. > 예외 처리는 서버로의 API 요청시에 실패를 대응하기위해 사용했던 기억이 있다. > 그 외에도 작성하는 메서드에서 오류를 발생했을 때에도 예외를 발생하여 > 여러수준에서 에러를 관리하기 위해 예외를 사용하면 > 한결 견고한 프로그램을 만들 수 있을 것 같다. > 예외 필터부분에서 항상 false를 반환하는 필터의 경우 > 전체 또는 특정 타입의 예외에 대해 메소드를 수행하는 것을 보고 > express의 미들웨어같이 동작하는 방식이어서 재밌었다.