Clean Code · Ch.07

오류 처리

외부 API는 감싸라

외부 라이브러리의 예외를 그대로 노출하면 구현 세부사항이 상위로 누출된다.

❌ BEFORE 반복되는 catch
var port = new ACMEPort(12);
try
{
    port.Open();
}
catch (DeviceResponseException e)
{
    ReportPortError(e);
    _logger.Log("Device response exception", e);
}
catch (ATM1212UnlockedException e)
{
    ReportPortError(e);
    _logger.Log("Unlock exception", e);
}
catch (GMXError e)
{
    ReportPortError(e);
    _logger.Log("Device response exception", e);
}
✅ AFTER 감싼 LocalPort
var port = new LocalPort(12);
try
{
    port.Open();
}
catch (PortDeviceFailureException e)
{
    ReportError(e);
    _logger.Log(e.Message, e);
}
LocalPort 구현
public class LocalPort
{
    private readonly ACMEPort _innerPort;

    public LocalPort(int portNumber)
    {
        _innerPort = new ACMEPort(portNumber);
    }

    public void Open()
    {
        try
        {
            _innerPort.Open();
        }
        catch (DeviceResponseException e)
        {
            throw new PortDeviceFailureException(e);
        }
        catch (ATM1212UnlockedException e)
        {
            throw new PortDeviceFailureException(e);
        }
        catch (GMXError e)
        {
            throw new PortDeviceFailureException(e);
        }
    }
}
🧱

단일 예외 타입: 호출부는 PortDeviceFailureException 하나만 처리하면 된다.

🔒

의존성 감소: 상위 코드가 ACMEPort, ATM1212, GMX 등 외부 라이브러리에 직접 의존하지 않는다.

🔄

교체 용이: 외부 라이브러리를 교체해도 LocalPort 내부만 바꾸면 된다.

Special Case Pattern

catch에서 예외 케이스를 처리하는 대신, 정상 객체처럼 보이는 대체 객체를 리턴하라.

❌ BEFORE try-catch로 비즈니스 로직 처리
try
{
    var expenses = expenseReportDAO.GetMeals(employee.Id);
    total += expenses.GetTotal();
}
catch (MealExpensesNotFoundException)
{
    total += GetMealPerDiem();
}
🔄

반복 발생: GetMeals()가 실패할 가능성을 항상 고려해야 하고 try-catch가 반복된다.

🌀

책임 혼재: 비즈니스 로직(합산)과 예외 처리가 한 곳에 뒤섞여 있다.

📉

가독성 저하: 정상 흐름과 예외 흐름을 따로 읽어야 한다.

✅ AFTER Special Case Object
public class PerDiemMealExpenses : IMealExpenses
{
    public int GetTotal()
    {
        return 25; // 디폴트 식비
    }
}
호출부 — 예외 없이 동일 인터페이스
// expenseReportDAO.GetMeals()는 항상 IMealExpenses를 반환한다
// 식비 기록이 없으면 PerDiemMealExpenses를 반환
var expenses = expenseReportDAO.GetMeals(employee.Id);
total += expenses.GetTotal(); // 예외 신경 안 써도 됨!
GetMeals(id)
(정상)
MealExpenses
GetTotal()
(없을 때)
PerDiemMealExpenses
GetTotal() → 25
Special Case Pattern — 예외 발생 시 catch에서 처리하는 대신, 동일 인터페이스를 구현한 "정상처럼 동작하는 대체 객체"를 반환하라. 상위 호출자는 예외를 신경 쓰지 않아도 된다.

Ch.07 핵심 원칙 요약

오류 처리를 깔끔하게 만드는 네 가지 원칙

🎁

예외 감싸기

외부 API 예외를 내부 예외로 변환해 의존성을 낮춰라.

🎭

Special Case Pattern

예외 상황을 정상 객체처럼 보이게 만들어 호출부를 깔끔하게.

🚫

null 반환 금지

null을 반환하면 NullReferenceException 지뢰밭이 된다. 빈 객체나 Optional을 사용하라.

📋

checked vs unchecked

일반적으로 unchecked exception이 더 유연하다. 호출 스택 전체에 예외를 선언하지 않아도 된다.