상당수의 코드 기반은 전적으로 오류처리 코드에 좌우된다
따라서 아래와 같은 고려사항들을 파악할 필요가 있음
오류 코드를 사용하는 대부분의 방법은 다음과 같다.
public class DeviceController {
...
public void sendShutDown() {
DeviceHandle handle = getHandle(DEV1);
// 디바이스 상태를 점검
if (handle != DeviceHandle.INVALID) {
// 레코드 필드에 디바이스 상태를 저장
retrieveDeviceRecord(handle);
// 디바이스가 일시정지 상태가 아니라면 종료
if (record.getStatus() != DEVICE_SUSPENDED) {
pauseDevice(handle);
clearDeviceWorkQueue(handle);
closeDevice(handle);
} else {
logger.log("Device suspended. Unable to shut down");
}
} else {
logger.log("Invalid handle for: " + DEV1.toString());
}
}
...
}
위 형태의 단점은 아래와 같다
1. 호출한 즉시 오류를 확인해야하기 때문에 호출자 코드가 복잡하다
2. 오류 확인을 잊어버리기 쉽고, 오류 케이스 추가시 매번 코드를 확인해야한다
따라서 오류 발생시 예외를 던지는? 것이 훨씬 낫다.
public class DeviceController {
...
public void sendShutDown() {
try {
tryToShutDown();
} catch (DeviceShutDownError e) {
logger.log(e);
}
}
private void tryToShutDown() throws DeviceShutDownError {
DeviceHandle handle = getHandle(DEV1);
DeviceRecord record = retrieveDeviceRecord(handle);
pauseDevice(handle);
clearDeviceWorkQueue(handle);
closeDevice(handle);
}
private DeviceHandle getHandle(DeviceID id) {
...
throw new DeviceShutDownError("Invalid handle for: " + id.toString());
...
}
...
}
위와 같이 caller 함수에서 catch로 예외를 받는? 역할을 맡고, callee 함수에서 예외를 throw 하는 형태로 가는 것이 좋다.
try-catch-finally 문에서 try 스코프에 들어가는 코드를 실행하면 어느 시점에서든 실행이 중단된 후 catch, finally 순으로 진행된다.
// JUnit5 + assert를 활용하자
@Test(expected = StorageException.class)
public void retrieveSectionShouldThrowOnInvalidFileName() {
sectionStore.retrieveSection("invalid - file");
}
public List<RecordedGrip> retrieveSection(String sectionName) {
// TODO - ...
return new ArrayList<RecordedGrip>();
}
public List<RecordedGrip> retrieveSection(String sectionName) {
try {
FileInputStream stream = new FileInputStream(sectionName);
} catch (Exception e) {
throw new StorageException("retrieval error", e);
}
return new ArrayList<RecordedGrip>();
}
checked 예외는 컴파일 단계에서 확인되며 반드시 처리해야 하는 예외이다.
IOException
SQLException
Unchecked 예외는 런타임에서 확인되며 명시적인 처리를 강제하지 않는 예외들이다.
NullPointerException
IllegalArgumentException
IndexOutOfBoundException
SystemException
오류가 발생한 원인과 위치를 찾기 쉽도록 호출스택만으로 부족한 정보를 충분히 덧붙이자
ACMEPort 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");
} finally {
...
}
// caller
LocalPort port = new LocalPort(12);
try {
port.open();
} catch (PortDeviceFailure e) {
reportError(e);
logger.log(e.getMessage(), e);
} finally {
...
}
// callee
public class LocalPort {
private ACMEPort innerPort;
public LocalPort(int portNumber) {
innerPort = new ACMEPort(portNumber);
}
public void open() {
try {
innerPort.open();
} catch (DeviceResponseException e) {
throw new PortDeviceFailure(e);
} catch (ATM1212UnlockedException e) {
throw new PortDeviceFailure(e);
} catch (GMXError e) {
throw new PortDeviceFailure(e);
}
}
...
}
위와 같이 실행하면 장점은 아래와 같이 생각해 볼 수 있다.
클래스나 객체가 예외적인 상황을 캡슐화해 처리하여 클라이언트 코드가 예외적인 상황 처리할 필요가 없도록 할 수 있다.
// caller
try {
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();
} catch(MealExpencesNotFound e) {
m_total += getMealPerDiem();
}
// callee
public class PerDiemMealExpenses implements MealExpenses {
public int getTotal() {
// 기본값으로 일일 기본 식비를 반환한다.
// (예외가 아닌)
// throw new (...)
}
}
null 반환하는 습관은 좋지 않다. 이유는 아래와 같다.
차라리 예외를 던지거나 특수 사례 객체를 반환하는 것이 훨씬 좋다.