오류처리는 중요하다.
하지만, 오류 처리 코드로 인해 프로그램 논리를 이해하기 어려워진다면 깨끗한 코드라 부르기 어렵다.
깨끗한 코드와 오류 처리는 연관성이 있다.
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());
}
}
...
}
호출한 함수가 적절한 결과값 뿐만 아니라 오류처리를 위한 결과값까지 반환을 하기 때문에 복잡해진다.
따라서 예외를 던지는 편이 낫다.
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);
}
예외 처리도 한가지 작업.
한 가지 함수에서 한 가지 작업만 하는게 좋다.
예시) 파일을 열어 직렬화된 객체 몇 개를 읽어 들이는 코드가 필요.
다음은 파일이 없으면 예외를 던지는지 알아보는 단위 테스트다.
@Test(expected = StorageException.class)
public void retrieveSectionShouldThrowOnInvalidFileName() {
sectionStore.retrieveSection("invalid - file");
}
다음 코드는 예외를 던지지 않으므로 단위테스트는 실패한다.
public List<RecordedGrip> retrieveSection(String sectionName) {
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>();
}
catch 블록의 예외 유형을 좁혀 실제로 FileinputStream 생성자가 던지는 FileNotFoundException 을 잡아낸다.
public List<RecordedGrip> retrieveSection(String sectionName) {
try {
FileInputStream stream = new FileInputStream(sectionName);
stream.close();
} catch (FileNotFoundException e) {
throw new StorageException("retrieval error", e);
}
return new ArrayList<RecordedGrip>();
}
'파일을 열어 직렬화된 객체 몇 개를 읽어 들이는 코드가 필요' 했으므로 TDD를 사용해,
FileInputStream 을 생성하는 코드와 close 호출문 사이에 나머지 논리를 채워 넣으면 됨.
애플리케이션에서 오류를 정의할 때 프로그래머에게 가장 중요한 관심사는
'오류를 잡아내는 방법'이 돼야 한다.
아래 코드는 예외 유형과 관계 없이 예외에 대응하는 방식(catch 블럭)이 거의 동일하다.
그래서 코드를 간결하게 고치기 쉽다.
호출하는 라이브러리 API 를 감싸면서 예외 유형 하나를 반환하면 된다.
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 {
...
}
LocalPort port = new LocalPort(12);
try {
port.open();
} catch (PortDeviceFailure e) {
reportError(e);
logger.log(e.getMessage(), e);
} finally {
...
}
// ACMEPort 클래스가 던지는 예외를 잡아 변환하는 감싸기(Wrapper) 클래스
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);
}
}
...
}
때로는 try catch 를 이용하여 예외를 던지고, 중단된 계산을 처리하는게 적합하지 않은 때도 있다.
아래 코드는 예외가 논리를 따라가기 어렵게 만든다.
특수한 상황을 처리할 필요가 없다면 더 좋지 않을까?
try {
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();
} catch(MealExpensesNotFound e) {
// 식비를 비용으로 청구하지 않은 경우
m_total += getMealPerDiem();
}
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();
// 식비를 비용으로 청구하지 않은 경우 반환되는 클래스
public class PerDiemMealExpenses implements MealExpenses {
public int getTotal() {
// 기본값으로 일일 기본 식비를 반환한다.
}
}
null 값을 반환하는 것은 호출자에게 문제를 떠넘기는 나쁜 코드
null 을 반환하고픈 유혹이 든다면 그 대신,
List<Employee> employees = getEmployees();
if (employees != null) {
for(Employee e : employees) {
totalPay += e.getPay();
}
}
List<Employee> employees = getEmployees();
for(Employee e : employees) {
totalPay += e.getPay();
}
public List<Employee> getEmployees() {
if ( .. 직원이 없다면 .. )
return Collections.emptyList();
}
깨끗한 코드는 읽기도 좋아야 하지만 안정성도 높아야 한다.
오류 처리와 프로그램 로직을 분리하면 튼튼하고 깨끗한 코드를 작성할 수 있다.
함수에서 nil vs 예외 처리
에러 케이스가 1가지인 경우는 nil, 여러 가지인 경우에는 예외 처리
https://stackoverflow.com/questions/31077613/optionals-vs-throwing-functions