개인적 감상평
일정에 쫒겨 마구잡이로 개발을하다보면, 수많은 예외를 보기 마련이다. 이런 예외를 처리하기위해 에러 코드로 분기하기도하고 예외로 처리하기도하고 많은 방법들을 사용했지만, 뭐가 더 낫고 뭘 어째서 써야했는지에 고민하진 않았는데 그에 대한 가이드를 해주는 느낌이다.
확실히 null 핸들링에 대한 고민은 항상 하던 문제였다. 매번 로직에 null 검사를 하는 조건문을 쓰기도 번거로웠다. 7장에서 제안하는 방법들을 사용한다면 확실히 코드의 양도 줄고 퀄리티는 높아질 것 같다.
1. 오류 코드보다 예외를 사용하라.
•
오류를 코드로 처리하게되면 코드의 가독성은 낮아지고 계층의 깊이는 심하게 높아진다.
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());
}
}
...
}
Java
복사
매번 반환값을 검사해야하기 때문에 코드가 복잡해지고 조건문 내의 블록내에서 다음 코드를 진행해야 하기 때문에 계층도 깊어진다.
그리고 이런 처리는 개발자가 손수 작성해서 하는 것이기에 실수로 놓친다면 바로 프로그램이 뻗을 수 있다. 그렇기에 예외를 던지는 편이 낫다.
•
예외를 던지는 코드
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());
}
...
}
Java
복사
⇒ 예외를 던지고 코드를 분리함으로써 각 개념을 독립적으로 살펴보고 이해할 수 있다.
2. Try-Catch-Finally 문부터 작성하라
•
try 블록은 무슨 일이 생기던지 catch 블록은 프로그램 상태를 일관성 있게 유지해야 한다.
•
try 블록에서 무슨 일이 생기든지 호출자가 기대하는 상태를 정의하기 쉬워진다.
TDD 작성 권장 방법
먼저 강제로 예외를 일으키는 테스트 케이스를 작성 후 테스트를 통과하게 코드를 작성하는 방법.
그러면 자연스럽게 try 블록의 트랜잭션 범위부터 구현하게 되기에 범위 내에서 트랜잭션 본질을 유지하기 쉬워진다.
3. 미확인(Unchecked) 예외를 사용하라.
•
확인된 예외는 OCP를 위반한다.
◦
메소드에서 예외를 던졌는데 catch 블록이 세 단계 위에 있다면 그 사이 메소드 모두가 예외를 정의해야 한다.
◦
모든 함수에서 최하위 함수에서 던지는 예외를 알아서 캡슐화가 깨진다.
◦
아주 중요한 라이브러리를 작성할 때를 제외하면 확인된 예외는 비용소모가 더 크다.
4. 예외에 의미를 제공하라
•
자바는 모든 예외에 호출 스택을 제공하지만 그것만으로는 부족하다.
•
오류 메세지에 정보를 담아 예외와 함께 던진다.
5. 호출자를 고려해 예외 클래스를 정의하라
1.
오류가 발생한 컴포넌트로 분류한다.
2.
오류 유형으로 분류한다.
•
호출하는 라이브러리 API를 감싸면서 예외 유형 하나를 반환하게 한다.
LocalPort port = new LocalPort(12);
try {
port.open();
} catch (PortDeviceFailure e) {
reportError(e);
logger.log(e.getMessage(), e);
} finally {
...
}
...
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);
}
}
...
}
Java
복사
•
외부 API를 사용할 때는 감싸기 기법이 최선이다. 외부 API를 감싸면 외부 라이브러리와 프로그램 사이에서 의존성이 크게 줄어든다.
6. 정상 흐름을 정의하라.
•
예외가 발생하는 특수상황자체가 없도록 구현을 하자.
try {
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();
} catch (MealExpensesNotFound e) {
m_total += getMealPerDiem();
}
Java
복사
위와 같이 getMeals 메소드에서 예외가 발생하지 않았다면 반환된 인스턴스에서 getTotal()을 호출하여 더하고 만약 getMeal에서 이러가 MealExpensesNotFound예외가 발생한다면 getMealPerDien() 메소드를 호출해 반환된 값을 더해주는데, 차라리 특수한 예외상황 자체가 안나도록 만들어주면 코드가 더 간결해질 수 있다.
public class PerDiemMealExpenses implements MealExpenses {
public int getTotal(){
//기본값으로 일일 기본 식비를 반환한다.
}
}
Java
복사
이와 같이 기본적으로 기본 식비를 반환하는 객체를 반환하게 한다면 예외는 발생하지 않고 더 간단하게 로직을 짤 수 있다. 그러면 클라이언트 코드가 예외적인 상황을 처리할 필요가 없어지는데 이를 특수 사례 패턴(SPECIAL CASE PATTERN)이라 부른다.
7. null을 반환하지 마라.
1번 항목과 이어지는 내용인데, 메소드가 null을 반환하게 된다면 메소드를 호출하는 모든 로직에서는 null 검사를 해줘야 하는데 개발자는 사람이기에 놓칠 수 있고 이는 곧 이슈로 변한다.
•
null 대신 예외를 던지거나 특수 사례 객체를 반환한다.
•
(Java 8 이후) Optional 객체를 통해 구현할 수 있다.
8. null을 전달하지 마라.
null 은 반환 뿐 아니라 인수 값으로 전달 하는 방식도 피해야 한다.
인수값을 null을 전달하면 결국 메소드위치에서 null을 검사하는 로직을 넣어야 한다.