Post

Chapter 07 - 오류 처리

오류 코드보다 예외를 사용하라

  • 오류 코드를 사용하면 호출자 코드가 복잡해지고, 잊어버리기 쉽다.
  • 오류가 발생하면 예외를 던지는 편이 낫다. (호출자 코드가 더 깔끔해진다.)
    • 논리와 오류 처리 코드가 뒤섞이지 않기 때문이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Bad
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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Good
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());
    ...
  }
  ...
}

Try-Catch-Finally 문부터 작성하라

  • 어떤 면에서 try 블록은 트랜잭션과 비슷하다.
    • try 블록에서 무슨 일이 생기든지 catch 블록은 프로그램 상태를 일관성 있게 유지해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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>();
}

// Test 성공
@Test(expected = StorageException.class)
public void retrieveSectionShouldThrowOnInvalidFileName() {
  sectionStore.retrieveSection("invalid - file");
}

미확인(unchecked) 예외를 사용하라

  • checked 예외는 OCP(Open Closed Principle)를 위반한다.
    • 하위 메서드에서 던진 checked 예외를 상위 메소드들에서 catch로 정의해야하기 때문이다.
    • 또는 함수 선언부에 throw 절을 추가해야 한다.
    • 결과적으로 최하위 단계에서 최상위 단계까지 연쇄적인 수정이 일어난다.
  • 또한 checked 예외는 하위 함수가 던지는 예외 타입을 알아야 하므로 캡슐화가 깨진다.

예외에 의미를 제공하라

  • 예외를 던질 때는 전후 상황을 충분히 덧붙인다.
  • 자바는 모든 예외에 호출 스택을 제공하지만, 의도를 파악하기에는 부족하다.
  • 따라서 오류 메시지에 정보를 담아 예외와 함께 던진다.
    • 실패한 연산 이름, 실패 유형 등
  • catch 블록에서 오류를 로깅하도록 충분히 정보를 념겨주자

호출자를 고려해 예외 클래스를 정의하라

  • 아래와 같은 코드는 호출 함수가 던질 예외를 모두 잡아 낸다.
  • catch 문의 내용이 거의 같은 것을 주목 (중복이 심하다.)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Bad

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 {
  ...
}
  • 함수가 던지는 예외를 잡아 변환하는 wrapper 클래스를 사용해보자.
  • 특히 외부 라이브러리를 사용하는 경우에 wrapper 클래스는 많은 이점을 가져온다.
    • 라이브러리 교체와 변경에 대응하기 쉽다.
    • 라이브러리를 목킹하여 테스트하기 쉽다.
    • 특정 업체가 설계한 방식에 발목 잡히지 않는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Good
  
LocalPort port = new LocalPort(12);
try {
  port.open();
} catch (PortDeviceFailure e) {
  reportError(e);
  logger.log(e.getMessage(), e);
} finally {
  ...
}

// LocalPort은 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);
    }
  }
  ...
}

정상 흐름을 정의하라

  • 비즈니스 로직 중간에 예외 처리를 넣으면 정상 흐름을 파악하는데 어려울 수 있다.
  • Special Case Pattern을 사용하자
1
2
3
4
5
6
7
8
// Bad
// 로직을 이해하는데 어렵다.
try {
  MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
  m_total += expenses.getTotal();
} catch(MealExpensesNotFound e) {
  m_total += getMealPerDiem();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Good
// 단순히 expenses.getTotal()만 더해주면 된다.
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();


public class PerDiemMealExpenses implements MealExpenses {
  public int getTotal() {
    // getMealPerDiem() 내용을 여기에 작성한다.
  }
}

public class ExpenseReportDAO {
  public MealExpenses getMeals(int employeeId) {
    MealExpenses expenses;
    try {
      expenses = expenseReportDAO.getMeals(employee.getID());
    } catch(MealExpensesNotFound e) {
      expenses = new PerDiemMealExpenses();
    }
    
    return expenses;
  }
  ...
}

null을 반환, 전달하지 마라

  • 메서드에서 null을 반환하지 말고, 예외를 던지거나 Special Case 객체를 반환하라
  • 메서드 인수로 null을 전달하지 마라.
    • 정상적인 인수로 null을 기대하는 API가 아니라면, null을 전달하지 마라
This post is licensed under CC BY 4.0 by the author.