외부 라이브러리의 예외를 그대로 노출하면 구현 세부사항이 상위로 누출된다.
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); }
var port = new LocalPort(12); try { port.Open(); } catch (PortDeviceFailureException e) { ReportError(e); _logger.Log(e.Message, e); }
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 내부만 바꾸면 된다.
catch에서 예외 케이스를 처리하는 대신, 정상 객체처럼 보이는 대체 객체를 리턴하라.
try { var expenses = expenseReportDAO.GetMeals(employee.Id); total += expenses.GetTotal(); } catch (MealExpensesNotFoundException) { total += GetMealPerDiem(); }
반복 발생: GetMeals()가 실패할 가능성을 항상 고려해야 하고 try-catch가 반복된다.
책임 혼재: 비즈니스 로직(합산)과 예외 처리가 한 곳에 뒤섞여 있다.
가독성 저하: 정상 흐름과 예외 흐름을 따로 읽어야 한다.
public class PerDiemMealExpenses : IMealExpenses { public int GetTotal() { return 25; // 디폴트 식비 } }
// expenseReportDAO.GetMeals()는 항상 IMealExpenses를 반환한다 // 식비 기록이 없으면 PerDiemMealExpenses를 반환 var expenses = expenseReportDAO.GetMeals(employee.Id); total += expenses.GetTotal(); // 예외 신경 안 써도 됨!
오류 처리를 깔끔하게 만드는 네 가지 원칙
외부 API 예외를 내부 예외로 변환해 의존성을 낮춰라.
예외 상황을 정상 객체처럼 보이게 만들어 호출부를 깔끔하게.
null을 반환하면 NullReferenceException 지뢰밭이 된다. 빈 객체나 Optional을 사용하라.
일반적으로 unchecked exception이 더 유연하다. 호출 스택 전체에 예외를 선언하지 않아도 된다.