|
| 1 | +# 오류처리 |
| 2 | +깨끗한 코드와 오류 처리는 확실히 연관성이 있다. 오류 처리는 중요하지만 오류 처리 코드로 인해 프로그램 논리를 이해하기 어려워 진다면 깨끗한 코드라 부르기 어렵다. |
| 3 | + |
| 4 | +## 1. 오류 코드보다 예외를 사용하라 |
| 5 | +오류코드를 사용할시 코드가 복잡해진다. 함수를 호출한 즉시 오류를 확인해야 하기 때문이다. |
| 6 | +예외처리를 할시 코드도 더 깔끔해지고 논리가 오류처리 코드와 뒤섞이지 않게 된다. |
| 7 | + |
| 8 | +### 1-1. 오류코드 sample |
| 9 | +``` |
| 10 | +public class DeviceController { |
| 11 | + ... |
| 12 | + public void sendShutDown() { |
| 13 | + DeviceHandle handle = getHandle(DEV1); |
| 14 | + // 디바이스 상태를 점검한다 |
| 15 | + if (handle != DeviceHandle.INVALID) { |
| 16 | + // 레코드 필드에 디바이스 상태를 저장한다 |
| 17 | + retrieveDeviceRecord(handle); |
| 18 | + // 디바이스가 일시정지 상태가 아니라면 종료한다. |
| 19 | + if (record.getStatus() != DEVICE_SUSPENDED) { |
| 20 | + pauseDevice(handle); |
| 21 | + clearDeviceWorkQueue(handle); |
| 22 | + closeDevice(handle); |
| 23 | + } else { |
| 24 | + logger.log("Device suspended. Unable to shut down"); |
| 25 | + } |
| 26 | + } else { |
| 27 | + logger.log("Invalid handle for: " + DEV1.toString()); |
| 28 | + } |
| 29 | + } |
| 30 | + ... |
| 31 | +} |
| 32 | +
|
| 33 | +``` |
| 34 | + |
| 35 | +### 1-2. 예외처리 sample |
| 36 | +``` |
| 37 | +public class DeviceController { |
| 38 | + ... |
| 39 | + public void sendShutDown() { |
| 40 | + try { |
| 41 | + tryToShutDown(); |
| 42 | + } catch (DeviceShutDownError e) { |
| 43 | + logger.log(e); |
| 44 | + } |
| 45 | + } |
| 46 | + |
| 47 | + private void tryToShutDown() throws DeviceShutDownError { |
| 48 | + DeviceHandle handle = getHandle(DEV1); |
| 49 | + DeviceRecord record = retrieveDeviceRecord(handle); |
| 50 | + |
| 51 | + pauseDevice(handle); |
| 52 | + clearDeviceWorkQueue(handle); |
| 53 | + closeDevice(handle); |
| 54 | + } |
| 55 | +
|
| 56 | + private DeviceHandle getHandle(DeviceID id) { |
| 57 | + ... |
| 58 | + throw new DeviceShutDownError("Invalid handle for: " + id.toString()); |
| 59 | + ... |
| 60 | + } |
| 61 | + ... |
| 62 | +} |
| 63 | +
|
| 64 | +``` |
| 65 | + |
| 66 | +## 2. Try-Catch-Finally 문부터 작성하라 |
| 67 | +try 블록에 무슨일이 생기든지 catch 블록은 프로그램 상태를 일관성 있게 유지해야 한다. |
| 68 | +그러므로 예외가 발생할 코드를 짤때는 try-catch-finally 문으로 시작하는 편이 낫다. |
| 69 | +그러면 try블록에서 무슨 일이 생기든지 호출자가 기대하는 상태를 정의하기 쉬워진다. |
| 70 | +먼저 강제로 예외를 일으키는 테스트 케이스를 작성한 후 테스트를 통과하게 코드를 작성하는 방법을 권장한다. |
| 71 | +그러면 자연스럽게 try블록의 트랜잭션 범위부터 구현하게 되므로 범위 내 트랜잭션 본질을 유지하기 쉬워진다. |
| 72 | +``` |
| 73 | +//파일이 없으면 예외를 던지는지 알아보는 단위 테스트 |
| 74 | +@Test(expected = StorageException.class) |
| 75 | +public void retrieveSectionShouldThrowOnInvalidFileName() { |
| 76 | + sectionStore.retrieveSection("invalid - file"); |
| 77 | +} |
| 78 | +//try catch로 예외를 던지므로 테스트가 성공한다 |
| 79 | +public List<RecordedGrip> retrieveSection(String sectionName) { |
| 80 | + try { |
| 81 | + FileInputStream stream = new FileInputStream(sectionName) |
| 82 | + } catch (Exception e) { |
| 83 | + throw new StorageException("retrieval error", e); |
| 84 | + } |
| 85 | + return new ArrayList<RecordedGrip>(); |
| 86 | +} |
| 87 | +
|
| 88 | +``` |
| 89 | +그런뒤 예외 유형을 좁혀 실제 예외를 찾아내면서 리펙토링한다. |
| 90 | +``` |
| 91 | +public List<RecordedGrip> retrieveSection(String sectionName) { |
| 92 | + try { |
| 93 | + FileInputStream stream = new FileInputStream(sectionName); |
| 94 | + stream.close(); |
| 95 | + } catch (FileNotFoundException e) { |
| 96 | + throw new StorageException("retrieval error", e); |
| 97 | + } |
| 98 | + return new ArrayList<RecordedGrip>(); |
| 99 | +} |
| 100 | +
|
| 101 | +``` |
| 102 | + |
| 103 | +## 3. 미확인(unchecked) 예외를 사용하라 |
| 104 | +확인된 예외는 OCP(Open Closed Principle, 개방폐쇄원칙 - '소프트웨어 개체는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다'는 프로그래밍 원칙)를 위반한다. |
| 105 | +하위 단계에서 코드를 변경하면 상위 단계 메서드 선언부를 전부 고쳐야 한다. |
| 106 | +throw 경로에 위치하는 모든 함수가 최하위 함수에서 던지는 예외를 알아야 하므로 캡슐화가 깨진다. |
| 107 | + |
| 108 | +| | Checked Exception | UnChecked Exception | |
| 109 | +|---|:---:|:---:| |
| 110 | +| 확인 시점 | 컴파일 시점 | 런타임 시점 | |
| 111 | +| 처리 여부 | 반드시 처리 | 명시적으로 처리하지 않아도 됨 | |
| 112 | +| 트랜잭션 처리 | roll-back 하지 않음 | roll-back 함 | |
| 113 | +| 예시 | IOException, ClassNotFoundException | NullPointerException, ArithmeticException | |
| 114 | +아래 코드는 단순한 출력을 하는 메소드이다. |
| 115 | +``` |
| 116 | + public void printA(bool flag) { |
| 117 | + if(flag) |
| 118 | + System.out.println("called"); |
| 119 | + } |
| 120 | +
|
| 121 | + public void func(bool flag) { |
| 122 | + printA(flag); |
| 123 | + } |
| 124 | +
|
| 125 | +``` |
| 126 | +문득 프린트를 안할 때 NotPrintException 을 던지기로 구현을 변경했을 때 |
| 127 | +``` |
| 128 | +public void printA(bool flag) throws NotPrintException { |
| 129 | + if(flag) |
| 130 | + System.out.println("called"); |
| 131 | + else |
| 132 | + throw new NotPrintException(); |
| 133 | +} |
| 134 | +
|
| 135 | +public void func(bool flag) throws NotPrintException { |
| 136 | + printA(flag); |
| 137 | +} |
| 138 | +``` |
| 139 | +해당 함수 뿐만이 아니라 호출하는 함수도 수정을 해줘야 하기 때문에 OCP 를 위반하게 된다. |
| 140 | + |
| 141 | + |
| 142 | + |
| 143 | +## 4. 예외에 의미를 제공하라 |
| 144 | +예외에 정보를 충분히 담아서 던지면, 오류가 발생한 원인과 위치를 찾기 쉬워진다. |
| 145 | + |
| 146 | +## 5. 호출자를 고려해 예외 클래스를 정의하라 |
| 147 | +오류를 분류할 수 있는 방법은 많다. |
| 148 | +- 발생한 위치 |
| 149 | +- 발생한 컴포넌트 |
| 150 | +- 유형 : 네트워크 실패, 디바이스 실패, 프로그래밍 오류 등 |
| 151 | + |
| 152 | +하지만 프로그래머에게 가장 중요한 관심사는 오류를 잡아내는 방법이 되어야 한다. |
| 153 | +외부 라이브러를 그대로 사용한 경우에는 외부 라이브러리가 던질 예외를 모두 잡아야 한다. |
| 154 | + |
| 155 | +``` |
| 156 | +ACMEPort port = new ACMEPort(12); |
| 157 | +
|
| 158 | +try { |
| 159 | + port.open(); |
| 160 | +} catch (DeviceResponseException e) { |
| 161 | + reportPortError(e); |
| 162 | + logger.log("Device response exception", e); |
| 163 | +} catch (ATM1212UnlockedException e) { |
| 164 | + reportPortError(e); |
| 165 | + logger.log("Unlock exception", e); |
| 166 | +} catch (GMXError e) { |
| 167 | + reportPortError(e); |
| 168 | + logger.log("Device response exception"); |
| 169 | +} finally { |
| 170 | +... |
| 171 | +} |
| 172 | +``` |
| 173 | +대다수 상황에서 오류를 처리하는 방식은 오류 종류와 무관하게 비교적 일정하다 |
| 174 | +1. log를 남긴다 |
| 175 | +2. 계속 수행가능한지 확인한다 |
| 176 | +위 경우에도 예외 유형과 무관하게 모두 동일했다. 이 경우 외부 라이브러리를 호출하는 API를 감싸면서 예외 유형을 하나만 던지게 수정해보자. |
| 177 | + |
| 178 | + |
| 179 | +``` |
| 180 | +LocalPort port = new LocalPort(12); |
| 181 | +
|
| 182 | +try { |
| 183 | + port.open(); |
| 184 | +} catch (PortDeviceFailure e) { |
| 185 | + reportError(e); |
| 186 | + logger.log(e.getMessage(), e); |
| 187 | +} finally { |
| 188 | +... |
| 189 | +} |
| 190 | +
|
| 191 | +public class LocalPort { |
| 192 | + private ACMEPort innerPort; |
| 193 | +
|
| 194 | + public LocalPort(int portNumber) { |
| 195 | + innerPort = new ACMEPort(portNumber); |
| 196 | + } |
| 197 | +
|
| 198 | + public void open() { |
| 199 | + try { |
| 200 | + innerPort.open(); |
| 201 | + } catch (DeviceResponseException e) { |
| 202 | + throw new PortDeviceFailure(e); |
| 203 | + } catch (ATM1212UnlockedException e) { |
| 204 | + throw new PortDeviceFailure(e); |
| 205 | + } catch (GMXError e) { |
| 206 | + throw new PortDeviceFailure(e); |
| 207 | + } |
| 208 | + } |
| 209 | +} |
| 210 | +``` |
| 211 | +외부 API를 감싸는 클래스는 매우 유용하다. 외부 API와 프로그램 사이에 의존성이 크게 줄어든다. |
| 212 | + |
| 213 | +## 6. 정상 흐름을 정의하라 |
| 214 | +catch문에서 예외를 처리하는 경우 코드가 지저분해지는 일이 발생할 수 있다. |
| 215 | + |
| 216 | +``` |
| 217 | +try { |
| 218 | + MealExpenses expenses = expenseReportDAO.getMeals(employee.getID()); |
| 219 | + //식비를 비용으로 청구시 총계에 더한다 |
| 220 | + m_total += expenses.getTotal(); |
| 221 | +} catch(MealExpensesNotFound e) { |
| 222 | + //그게 아니면 일일 기본 식비를 총계에 더한다 |
| 223 | + m_total += getMealPerDiem(); |
| 224 | +} |
| 225 | +``` |
| 226 | +식비를 비용으로 청구했다면 그걸 더하고, 아니면 일일 기본 식비를 더하는 코드이다. |
| 227 | +만약 청구식비가 없으면 일일 기본 식비를 반환하도록 DAO를 수정하면 아래와 같이 간결하게 된다. |
| 228 | +``` |
| 229 | + public class PerDiemMealExpenses implements MealExpenses { |
| 230 | + public int getTotal() { |
| 231 | + // 기본값으로 일일 기본 식비를 반환한다. |
| 232 | + } |
| 233 | + } |
| 234 | +``` |
| 235 | +``` |
| 236 | + MealExpenses expenses = expenseReportDAO.getMeals(employee.getID()); |
| 237 | + m_total += expenses.getTotal(); |
| 238 | +``` |
| 239 | +이와같은 경우를 특수사례패턴(Special Case Pattern) 이라 한다. |
| 240 | +클래스를 만들거나 객체를 조작해 특수 사례를 처리하는 방식이다. |
| 241 | + |
| 242 | +## 7. null을 반환하지마라 |
| 243 | +null을 반환하면 호출자가 null을 확인해야 한다. null확인 코드로 가득한 화면을 계속봐야 한다. |
| 244 | +이건 호출자에게 문제를 떠넘기는 행위이다. |
| 245 | +null 대신 예외를 던지거나 특수 사례 객체(ex. Collections.emptyList())를 반환하라. |
| 246 | +``` |
| 247 | +List<Employee> employees = getEmployees(); |
| 248 | +
|
| 249 | +if (employees != null) { |
| 250 | + for(Employee e : employees) { |
| 251 | + totalPay += e.getPay(); |
| 252 | + } |
| 253 | +} |
| 254 | +``` |
| 255 | +위 예의 경우 null이 아닌 빈 리스트를 반환한다면 더 깨끗해진다. |
| 256 | +``` |
| 257 | +List<Employee> employees = getEmployees(); |
| 258 | +
|
| 259 | +for(Employee e : employees) { |
| 260 | + totalPay += e.getPay(); |
| 261 | +} |
| 262 | +
|
| 263 | +public List<Employee> getEmployees() { |
| 264 | + if (..직원이 없다면..) |
| 265 | + return Collections.emptyList(); |
| 266 | +} |
| 267 | +``` |
| 268 | + |
| 269 | +## 8. null을 전달하지 마라 |
| 270 | +인수로 null을 전달하면 함수 내에서 예외를 던지거나 assert 문을 사용할 수는 있다. |
| 271 | +하지만 NullPointException 문제를 해결해 줄 수는 없다. |
| 272 | +애초에 null을 넘기지 못하도록 금지하는 정책이 합리적이다. |
| 273 | + |
| 274 | +``` |
| 275 | +double xProjection(Point p1, Point p2) { |
| 276 | + if(p1 == null || p2 == null){ |
| 277 | + throw InvalidArgumentException("Invalid argument for MetricsCalculator.xProjection"); |
| 278 | + } |
| 279 | + return (p2.x - p1.x) * 1.5; |
| 280 | +} |
| 281 | +``` |
| 282 | + |
0 commit comments