클린코드 7장 - 오류 처리
CleanCode 7장 오류 처리
에 대해 정리한 포스트입니다.
Overview
상당수 코드 기반은 전적으로 오류 처리 코드에 좌우되기 때문에 깨끗한 코드와 연관성이 있습니다.
- 여기저기 흩어진 오류 처리 코드 때문에 실제 코드가 하는 일을 파악하기가 어려워 질 수 있음
- 오류 처리 코드로 인해 프로그램 논리를 이해하기 어려워진다면 깨끗한 코드라 부르기 어렵다.
이 장은 오류를 처리하는 기법과 고려 사항 몇가지로 구성되어 있습니다.
- 오류 코드 보다 예외 를 사용하라
- Try-Catch-Finally 문부터 작성하라
- 미확인 ^unchecked 예외를 사용하라
- 예외에 의미 를 제공하라
- 호출자를 고려 해 예외 클래스를 정의하라
- 정상 흐름 을 정의하라
- null을 반환 하지 마라
- null을 전달 하지 마라
오류 코드보다 예외를 사용하라
오류 코드 를 사용하는 기존의 방식은 아래와 같습니다.
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);
}
private DeviceHandle getHandle(DeviceID id) {
...
throw new DeviceShutDownError("Invalid handle for: " + id.toString());
...
}
...
}
Try-Catch-Finally문부터 작성하라
try-catch-finally 문에서 try 블록에 들어가는 코드를 실행하면 어느 시점에서든 실행이 중단된 후 catch 블록으로 넘어갈 수 있습니다.
- try 블록에서 무슨 일이 생기든지 catch 블록은 프로그램 상태를 일관성 있게 유지해야 함
- try-catch-finally 문을 시작으로 코드를 짜면 호출자가 기대하는 상태를 정의하기 쉬워짐
TDD 방식으로 메소드 구현
-
단위 테스트 를 만든다.
@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>(); }
테스트가 성공할 것이다.
-
리펙터링이 가능해졌다.
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>(); }
미확인^unchecked 예외를 사용하라
미확인 예외란?
checked 예외 는 컴파일 단계에서 확인되며 반드시 처리해야 하는 예외입니다.
- IOException
- SQLException
Unchecked 예외 는 실행 단계에서 확인되며 명시적인 처리를 강제하지는 않는 예외입니다.
- NullPointerException
- IllegalArgumentException
- IndexOutOfBoundException
- SystemException
미확인 예외의 단점
- 메서드를 선언할 때 메서드가 반환할 예외를 모두 열거해야 하기 때문에 메서드 유형의 일부 가 됨
- OCP^Open ^Closed ^Principle 을 위반
- 확인된 예외는 예상되는 모든 예외를 사전에 처리할 수 있다는 장점이 있지만, 일반적인 애플리케이션은 의존성이라는 비용이 이익보다 더 크다.
소프트웨어 개발 작업에 이용된 많은 모듈 중에 하나에 수정을 가할 때 그 모듈을 이용하는 다른 모듈을 줄줄이 고쳐야 한다면, 이와 같은 프로그램은 수정하기가 어렵다. 개방-폐쇄 원칙은 시스템의 구조를 올바르게 재조직(리팩토링)하여 나중에 이와 같은 유형의 변경이 더 이상의 수정을 유발하지 않도록 하는 것이다. 개방-폐쇄 원칙이 잘 적용되면, 기능을 추가하거나 변경해야 할 때 이미 제대로 동작하고 있던 원래 코드를 변경하지 않아도, 기존의 코드에 새로운 코드를 추가함으로써 기능의 추가나 변경이 가능하다.
-
아래 코드는 단순한 출력을 하는 메소드이다.
public void printA(bool flag) { if(flag) System.out.println("called"); } public void func(bool flag) { printA(flag); }
-
문득 아 프린트를 안할 때 NotPrintException 을 던지기로 구현을 변경했을 때,
public void printA(bool flag) throws NotPrintException { if(flag) System.out.println("called"); else throw new NotPrintException(); } public void func(bool flag) throws NotPrintException { printA(flag); }
해당 함수 뿐만이 아니라 호출하는 함수도 수정을 해줘야 하기 때문에 OCP 를 위반하게 된다.
예외에 의미를 제공하라
- 오류가 발생한 원인과 위치를 찾기 쉽도록 호출 스택만으로는 부족한 정보를 충분히 덧붙여야 함.
- 오류 메시지에 정보를 담음
- 실패한 연산 이름, 실패 유형 언급
호출자를 고려해 예외 클래스를 정의하라
오류를 잡아내는 방법 은 오류를 정의할 때 고려해야 할 중요한 사항입니다.
-
아래 코드는 외부 라이브러리를 호출하고 모든 예외를 호출자가 잡아내고 있습니다.
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 { ... }
-
호출 라이브러리 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); } } ... }
외부 API를 감싸면 아래와 같은 장점이 있다.
- 에러 처리가 간결해짐
- 외부 라이브러리와 프로그램 사이의 의존성이 크게 줄어듦
- 프로그램 테스트가 쉬워짐
- 외부 API 설계 방식에 의존하지 않아도 됨
정상 흐름을 정의하라
클래스나 객체가 예외적인 상황을 캡슐화해 처리하여 클라이언트 코드가 예외적인 상황을 처리할 필요가 없도록 할 수 있습니다.
아래는 특수 사례 객체를 반환하는 특수 사례 패턴의 예시입니다.
-
총계를 계산하는 코드입니다.
try { MealExpenses expenses = expenseReportDAO.getMeals(employee.getID()); m_total += expenses.getTotal(); } catch(MealExpencesNotFound e) { m_total += getMealPerDiem(); }
-
getTotal 메소드에 예외 시 처리를 넣어 클라이언트 코드를 간결하게 처리합니다.
public class PerDiemMealExpenses implements MealExpenses { public int getTotal() { // 기본값으로 일일 기본 식비를 반환한다. // (예외가 아닌) } }
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID()); m_total += expenses.getTotal();
null을 반환하지 마라
null을 반환하는 습관은 좋지 않습니다.
- 호출자에게 null을 체크할 의무를 줌
- NullPointerException 의 발생 위험이 있음
- null확인이 너무 많아짐
public void registerItem(Item item) {
if (item != null) {
ItemRegistry registry = peristentStore.getItemRegistry();
if (registry != null) {
Item existing = registry.getItem(item.getID());
if (existing.getBillingPeriod().hasRetailOwner()) {
existing.register(item);
}
}
}
}
차라리 예외를 던지거나 특수 사례 객체를 반환하는 것이 좋습니다.
// bad
List<Employee> employees = getEmployees();
if(employees != null) {
for(Employee e : employees) {
totalPay += e.getPay();
}
}
// good
List<Employee> employees = getEmployees();
for(Employee e : employees) {
totalPay += e.getPay();
}
public List<Employee> getEmployees() {
if (..직원이 없다면..)
return Collections.emptyList();
}
null을 전달하지 마라
결론
reference
Clean Code 클린코드 : 애자일 소프트웨어 장인 정신 자바의 예외 처리 java 예외 (Exception) 처리에 대한 작은 생각 | 넥스트리 | NEXTREE