7장. 오류 처리
오류 코드보다 예외를 사용하라
Try-Catch-Finally 문부터 작성하라
try 블록은 트랜잭션과 비슷하다.
try 블록에서 무슨 일이 생기든지 catch 블록은 프로그램 상태를 일관성 있게 유지해야 한다.
- 일단 테스트코드를 작성한다.
@Test(expected = StoargeException.class)
public void retrieveSectionShouldThrowOnInvalidFileName() {
sectionStore.retrieveSection("invalid - file")
}
- 테스트코드에 맞춰 다음 코드를 구현한다.
public List<RecordedGrip> retrieveSection(String sectionName) {
return new ArrayList<RecordedGrip>();
}
- 예외를 던지게 코드를 수정한다.
public List<RecordGrip> retrieveSection(String sectionName) {
try {
FileinputStream stream = new FileInputStream(sectionName)
} catch (Exception e) {
throw new StorageException("retrieval error", e);
}
return new ArrayList<RecordedGrip>()
}
- catch의 예외 유형을 좁혀 실제로 FileInputStream 생성자가 던지는 예외를 잡아내자.
public List<RecordGrip> retrieveSection(String sectionName) {
try {
FileinputStream stream = new FileInputStream(sectionName)
} catch (FileNotFoundException e) {
throw new StorageException("retrieval error", e);
}
return new ArrayList<RecordedGrip>()
}
이렇게 하면 try문에 들어가야하는 필수 로직을 명확히 분리하여 구현할 수 있다.
미확인 예외를 사용하라
예외를 상위로 throw할 경우, 하위 단계의 코드를 수정하면 상위에서 잡아야 할 예외가 늘어난다.
만약에 확인된 예외를 하나하나 전부 잡게 해놓았는데 하위 코드를 수정한다면, 새로운 예외를 추가해야할 수도 있다.
결과적으로 최하위에서 최상위 단계까지 연쇄적인 수정이 일어난다.
즉, 최하위 함수에서 던지는 예외를 모두 알아야 하므로 캡슐화가 깨진다.
이럴 경우에는 확인된 오류를 수정 혹은 추가하는 비용이 커질 수도 있다.
오류를 확인하는 비용이 커질지, 코드 수정 비용이 커질지는 그때그때 생각해봐야 한다.
예외에 의미를 제공하라
예외를 던질 때는 전후 상황을 충분히 덧붙인다.
즉, 오류 메시지에 정보를 담아 예외와 함께 던지자.
호출자를 고려해 예외 클래스를 정의하라
프로그래머에게 가장 중요한 관심사는 오류를 잡아내는 방법이 되어야 한다.
try/catch문을 다른 클래스(외부 API)로 감싸주면 의존성이 크게 줄어든다.
- before
LocalPort 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
public class LocalPort {
private ACMEPort innerPort;
public LocalPort(int portNumber) {
innerPort = new ACMEPort(portNumber);
}
public void open() {
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)
}
}
}
LocalPort port = new LocalPort(12);
try {
port.open();
} catch (PortDeviceFailure e) {
reportError(e);
logger.log(e.getMessage(), e);
}
이렇게 감싸면 특정 업체가 설계한 API 방식에 발목 잡히지 않는다.
정상 흐름을 정의하라
특수 사례 패턴
클라이언트가 예외처리를 할 필요 없게 클래스나 객체가 예외상황을 캡슐화한다.
try {
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expanses.getTotal()
} catch(MealExpansesNotFound e) {
m_total += getMealPerDiem();
}
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expanses.getTotal()
////////////////////////////////////////////////////////
public class PerDiemMealExpenses implemens MealExpenses {
public int getTotal() {
// 기본값으로 일일 기본 식비를 반환한다.
}
}
null을 반환하지 마라
null 대신 빈 리스트를 반환하던가 하는 방식으로 NullPointerException을 방지할 수 있다.
null을 전달하지 마라
메서드로 null을 전달하는 방식은 null을 반환하는 것보다 더 나쁘다.
애초에 null을 넘기지 못하도록 금지하는 정책이 가장 합리적이다.
이런 정책을 따르면 그만큼 부주의한 실수를 저지를 확률도 작아진다.
결론
오류 처리를 프로그램 논리와 분리하면 독립적인 추론이 가능해지며 코드 유지보수성도 크게 높아진다.