---
# System prepended metadata

title: Effective 5장

---

# 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의 미들웨어같이 동작하는 방식이어서 재밌었다.