|
| 1 | +# Error |
| 2 | + |
| 3 | +오류 처리는 프로그램에 반드시 필요한 요소 중 하나다. 뭔가 실패할 가능성은 늘 존재한다. 또한 그 잘못된 것을 바로 잡는 책임도 프로그래머에게 있다. |
| 4 | + |
| 5 | +클린 코드와 오류 처리 또한 연관이 있다. 여기저기 흩어진 오류 처리 코드들 때문에 실제 코드가 하는 일을 파악하기 어렵다면, 클린 코드라고 할 수 없다. |
| 6 | + |
| 7 | +이번 챕터에서는 오류 처리를 우아하고 고상하게 만드는 기법과 고려 사항들을 다룬다. |
| 8 | +<br><br> |
| 9 | + |
| 10 | +## 목차 |
| 11 | +1. [오류 코드보단 예외를 사용하라](#예외) |
| 12 | +2. [Try-Catch-Finally 문부터 작성하라](#폼부터) |
| 13 | +3. [미확인 예외를 사용하라](#미확인) |
| 14 | +4. [예외에 의미를 제공하라](#의미) |
| 15 | +5. [호출자를 고려해 예외 클래스를 정의하라](#호출자) |
| 16 | +6. [정상 흐름을 정의하라](#정상) |
| 17 | +7. [null을 반환하지 마라](#null반환) |
| 18 | +8. [null을 전달하지 마라](#null전달) |
| 19 | +9. [결론](#결론) |
| 20 | +<br><br> |
| 21 | + |
| 22 | +## <div id="예외">오류 코드보단 예외를 사용하라 </div> |
| 23 | + |
| 24 | +대부분의 언어는 예외 처리를 지원한다. **오류 플래그나 코드를 반환하는 함수는 호출자로 하여금, 함수를 호출한 즉시 오류를 확인하도록 강제한다.** 또한 이 단계는 무의식적으로 넘어가기 쉽다. 때문에 오류가 발생하면 예외를 던지는 편이 낫다. |
| 25 | + |
| 26 | +예외를 던지도록 하면, 호출자 코드가 더 깔끔해진다. **오류 처리 코드와 비즈니스 로직이 뒤섞이지 않는다. (바꿔 말하면, 둘이 뒤섞이면 이해하기 힘든 코드가 된다.)** 즉, 각 개념을 독립적으로 살펴보고 이해할 수 있게 된다! |
| 27 | + |
| 28 | +**Bad:** |
| 29 | +```java |
| 30 | +public class DeviceController { |
| 31 | + ... |
| 32 | + public void sendShutDown() { |
| 33 | + DeviceHandle handle = getHandle(DEV1); |
| 34 | + // 디바이스 상태 점검 |
| 35 | + if (handle != DeviceHandle.INVALID) { |
| 36 | + // 레코드 필드에 디바이스 상태 저장 |
| 37 | + retrieveDeviceRecord(handle); |
| 38 | + |
| 39 | + // 디바이스가 일시정지 상태가 아니라면 종료 (즉, 오류 상황) |
| 40 | + if (record.getStatus() != DEVICE_SUSPENDED) { |
| 41 | + pauseDevice(handle); |
| 42 | + clearDeviceWorkQueue(handle); |
| 43 | + closeDevice(handle); |
| 44 | + } else { |
| 45 | + logger.log("Device suspended. Unable to shut down"); |
| 46 | + } |
| 47 | + } else { |
| 48 | + logger.log("Invalid handle for: " + DEV1.toString()); |
| 49 | + } |
| 50 | + } |
| 51 | +} |
| 52 | +``` |
| 53 | + |
| 54 | +**Good:** |
| 55 | +```java |
| 56 | +public class DeviceController { |
| 57 | + ... |
| 58 | + public void sendShutDown() { |
| 59 | + try { |
| 60 | + tryToShutDown(); |
| 61 | + } catch (DeviceShutDownError e) { |
| 62 | + logger.log(e); |
| 63 | + } |
| 64 | + } |
| 65 | + |
| 66 | + private void tryToShutDown() throws DeviceShutDownError { |
| 67 | + DeviceHandle handle = getHandle(DEV1); |
| 68 | + DeviceRecord record = retrieveDeviceRecord(handle); |
| 69 | + pauseDevice(handle); |
| 70 | + clearDeviceWorkQueue(handle); |
| 71 | + closeDevice(handle); |
| 72 | + } |
| 73 | + |
| 74 | + private DeviceHandle getHandle(DeviceID id) { |
| 75 | + ... |
| 76 | + throw new DeviceShutDownError("Invalid handle for: " + id.toString()); |
| 77 | + ... |
| 78 | + } |
| 79 | + ... |
| 80 | +} |
| 81 | +``` |
| 82 | + |
| 83 | +확실히 코드가 깔끔해지고, 메인 로직이 어떤 일을 하는지 한 눈에 파악하기 쉬워졌다. 디바이스를 종료하는 알고리즘과 오류 처리 알고리즘의 분리만으로도 가독성이 크게 좋아진다. |
| 84 | + |
| 85 | +<br><br> |
| 86 | + |
| 87 | +## <div id="폼부터">Try-Catch-Finally 문부터 작성하라 </div> |
| 88 | + |
| 89 | +예외가 발생할 수 있는 코드를 짜는 경우 try-catch-finally 문으로 시작하자. try-catch-finally 구문은 하나의 트랜잭션과도 같아서 try 블록에서 무슨 일이 일어나든지 catch 블록이 프로그램 상태를 일관성있게 유지해준다. 따라서 try 내부에서 어떤 일이 발생하더라도, expect result의 type이나 상태를 정의하기 쉬워진다. |
| 90 | + |
| 91 | +아래 예제를 살펴보자. |
| 92 | + |
| 93 | +**파일을 열어서 직렬화된 객체 몇 개를 읽어 들이는 코드가 필요하다는 요구사항**을 가정하자. 아래 코드는 파일이 없으면 예외를 던지는지 확인하는 단위 테스트 코드이다. |
| 94 | + |
| 95 | +```java |
| 96 | +@Test(expected = StorageException.class) |
| 97 | + public void retrieveSectionShouldThrowOnInvalidFileName() { |
| 98 | + sectionStore.retrieveSection("invalid - file"); |
| 99 | +} |
| 100 | +``` |
| 101 | + |
| 102 | +테스트에 맞춰 아래 코드를 구현했다. |
| 103 | + |
| 104 | +```java |
| 105 | +public List<RecordedGrip> retrieveSection(String sectionName) { |
| 106 | + // 실제로 구현할 때까지 비어 있는 더미를 반환한다. |
| 107 | + return new ArrayList<RecordedGrip>(); |
| 108 | +} |
| 109 | +``` |
| 110 | + |
| 111 | +반면 해당 함수는 예외를 던지지 않기 때문에 단위 테스트가 실패로 나온다. 잘못된 파일 접근을 시도하도록 구현을 아래처럼 변경하자. |
| 112 | + |
| 113 | +```java |
| 114 | +public List<RecordedGrip> retrieveSection(String sectionName) { |
| 115 | + try { |
| 116 | + FileInputStream stream = new FileInputStream(sectionName); |
| 117 | + } catch (Exception e) { |
| 118 | + throw new StorageException("retrieval error", e); |
| 119 | + } |
| 120 | + return new ArrayList<RecordedGrip>(); |
| 121 | +} |
| 122 | +``` |
| 123 | + |
| 124 | +이제는 코드가 예외를 던지기 때문에 테스트가 성공한다. **이 시점부터 리팩토링이 가능해진다.** catch 블록에서 예외 유형을 좁혀 실제로 FileInputStream 생성자가 던지는 FileNotFoundException을 잡아내자. |
| 125 | + |
| 126 | +```java |
| 127 | +public List<RecordedGrip> retrieveSection(String sectionName) { |
| 128 | + try { |
| 129 | + FileInputStream stream = new FileInputStream(sectionName); |
| 130 | + stream.close(); |
| 131 | + } catch (FileNotFoundException e) { |
| 132 | + throw new StorageException("retrieval error", e); |
| 133 | + } |
| 134 | + return new ArrayList<RecordedGrip>(); |
| 135 | +} |
| 136 | +``` |
| 137 | + |
| 138 | +이제 try-catch 구조를 통해 범위를 정의했으므로, TDD를 사용해 필요한 나머지 논리들을 추가하면 된다. 나머지 논리는 FileInputStream을 생성하는 코드와 close 문 사이에 추가될 것이고, 오류나 예외를 발생시키지 않는다는 가정 하에 위 구조가 유지된다. |
| 139 | + |
| 140 | +지금까지 살펴봤듯, 강제로 예외를 일으키는 테스트케이스를 먼저 작성하고, 테스트를 통과하게 코드를 작성하자. 이렇게 되면 자연스럽게 try 블록의 트랜잭션 범위부터 구현하게 되므로 트랜잭션의 본질을 유지하기 쉬워진다. |
| 141 | +<br><br> |
| 142 | + |
| 143 | +## <div id="미확인">미확인 예외를 사용하라 </div> |
| 144 | + |
| 145 | +자바 첫 버전에서 선보인 checked exception은 멋진 아이디어처럼 보였다. 메소드 선언시 메소드가 반환할 예외를 모두 열거했다. 또한 코드가 메소드를 사용하는 방식이 이 선언과 일치하지 않으면 컴파일도 안됐다. |
| 146 | + |
| 147 | +반면 이제는 안정적인 소프트웨어를 제작하는데 checked exception가 반드시 필요하지 않다는 사실이 분명해졌다. 그러므로 checked exception가 그것을 사용하는 비용 이상의 가치를 제공하는지 다시 한 번 생각해보자. |
| 148 | + |
| 149 | +가장 먼저 checked exception은 OCP(Open Closed Principle)을 위반한다. 하위단계에서 코드를 변경하면, 그것을 사용하는 모든 함수의 선언부에 올라가서 전부 exception을 추가해줘야 한다. |
| 150 | + |
| 151 | +또한, checked exception은 캡슐화를 깨뜨린다. throws 경로에 위치하는 모든 함수가 최하위 함수의 예외를 파악하고 있어야 하기 때문이다. (하나라도 빠뜨리면 컴파일 에러가 뜨게 된다!) |
| 152 | + |
| 153 | +```java |
| 154 | +//checked exception을 사용하는 경우 아래 상위 코드인 func에서도 FileNotFoundException을 선언해야 한다. |
| 155 | + public void func(String filename) throws FileNotFoundException { |
| 156 | + FileInputStream fis = openFile(filename); |
| 157 | + ... |
| 158 | + } |
| 159 | + public void openFile(String filename) throws FileNotFoundException { |
| 160 | + try { |
| 161 | + FileInputStream stream = new FileInputStream(sectionName); |
| 162 | + stream.close(); |
| 163 | + } catch (FileNotFoundException e) { |
| 164 | + throw new StorageException("retrieval error", e); |
| 165 | + } |
| 166 | + } |
| 167 | +``` |
| 168 | + |
| 169 | +아주 중요한 라이브러리를 만드는 경우 모든 예외를 잡아서 checked exception으로 명시하는 것은 유용할 수 있다. 하지만 **일반적인 application에서는 이익보다는 checked exception으로 인한 의존성이라는 비용이 훨씬 더 커지게 된다.** |
| 170 | +<br><br> |
| 171 | + |
| 172 | +## <div id="의미">예외에 의미를 제공하라 </div> |
| 173 | + |
| 174 | +아주 중요한 부분인데, 예외를 던질 때는 오류 메세지에 전후 맥락과 상황을 충분히 쉽게 이해할 수 있도록 작성해주자. 자바는 모든 예외에 call stack을 제공하지만 이 정보만으로는 부족한 상황이 있다. |
| 175 | + |
| 176 | +오류 메세지를 정확히 명시해준다면, 오류가 발생한 원인과 위치를 찾기 용이해진다. 실패한 연산의 이름과 실패 유형 등을 기록해주고, application에서 logging을 사용한다면, catch 블록에서 더 자세한 정보를 로그로 남겨 오류에 대한 충분한 정보를 기록해주자. |
| 177 | + |
| 178 | +<br><br> |
| 179 | + |
| 180 | +## <div id="호출자">호출자를 고려해 예외 클래스를 정의하라 </div> |
| 181 | + |
| 182 | +프로그래머가 application에서 오류를 정의할 때(혹은 예외 코드를 작성할 때) 가장 중요한 신경써야하는 것은 **오류를 잡는 방법이 되어야 한다.** |
| 183 | + |
| 184 | +아래 코드를 보자. |
| 185 | + |
| 186 | +**Bad:** |
| 187 | +```java |
| 188 | +ACMEPort port = new ACMEPort(12); |
| 189 | + |
| 190 | + try { |
| 191 | + port.open(); |
| 192 | + } catch (DeviceResponseException e) { |
| 193 | + reportPortError(e); |
| 194 | + logger.log("Device response exception", e); |
| 195 | + } catch (ATM1212UnlockedException e) { |
| 196 | + reportPortError(e); |
| 197 | + logger.log("Unlock exception", e); |
| 198 | + } catch (GMXError e) { |
| 199 | + reportPortError(e); |
| 200 | + logger.log("Device response exception"); |
| 201 | + } finally { |
| 202 | + ... |
| 203 | + } |
| 204 | +``` |
| 205 | + |
| 206 | +위와 같이 에러를 잡고 있었다면 해당 레포지토리를 더 열심히 읽자. 위 코드에서 반복되는 중복 코드들은 그렇게 놀랍지도 않다. 그렇다면 이를 어떻게 개선해야 할까? |
| 207 | + |
| 208 | +사실 생각해보면, 대부분의 상황에서 우리가 에러를 처리하는 방식은 비교적 일정하다. 보통 아래와 같은 절차를 밟는다. |
| 209 | + |
| 210 | +> 1. 오류를 기록한다. |
| 211 | +> 2. 프로그램을 계속 진행해도 좋은지 확인한다. |
| 212 | +
|
| 213 | +위 코드의 경우 특히 예외에 대응하는 방식이 완전히 동일하기 때문에 코드를 간결히 고치기 더 쉬워진다. 호출하는 라이브러리 API를 감싸면서 예외 유형 하나를 반환하면 된다. |
| 214 | + |
| 215 | +**Good:** |
| 216 | +```java |
| 217 | + LocalPort port = new LocalPort(12); |
| 218 | + try { |
| 219 | + port.open(); |
| 220 | + } catch (PortDeviceFailure e) { |
| 221 | + reportError(e); |
| 222 | + logger.log(e.getMessage(), e); |
| 223 | + } finally { |
| 224 | + ... |
| 225 | + } |
| 226 | +``` |
| 227 | +```java |
| 228 | +public class LocalPort { |
| 229 | + private ACMEPort innerPort; |
| 230 | + |
| 231 | + public LocalPort(int portNumber) { |
| 232 | + innerPort = new ACMEPort(portNumber); |
| 233 | + } |
| 234 | + |
| 235 | + public void open() { |
| 236 | + try { |
| 237 | + innerPort.open(); |
| 238 | + } catch (DeviceResponseException e) { |
| 239 | + throw new PortDeviceFailure(e); |
| 240 | + } catch (ATM1212UnlockedException e) { |
| 241 | + throw new PortDeviceFailure(e); |
| 242 | + } catch (GMXError e) { |
| 243 | + throw new PortDeviceFailure(e); |
| 244 | + } |
| 245 | + } |
| 246 | + ... |
| 247 | +} |
| 248 | +``` |
| 249 | + |
| 250 | +위의 LocalPort 클래스는 단순히 ACMEPort 클래스가 던지는 예외를 잡아 변환해주는 Wrapper 클래스이다. 이 방법은 실제로 외부 API를 사용할 때는 최적의 방법이다. 아래와 같은 장점들을 제공해주기 때문이다. |
| 251 | + |
| 252 | +> 1. 외부 라이브러리와 프로그램 사이의 의존성이 크게 줄어든다. |
| 253 | +> 2. 나중에 다른 라이브러리로의 교체가 용이하다. (코드를 크게 뜯어고칠 필요가 없어진다.) |
| 254 | +> 3. Wrapper 클래스에서 외부 API를 호출하는 대신, 테스트 코드를 넣어주는 방법으로 프로그램을 테스트하기도 쉬워진다. |
| 255 | +> 4. 특정 업체가 API를 설계한 방식에 발목 잡히지 않는다. 프로그램에 적합하고, 사용하기 편리한 API로 정의하면 된다. |
| 256 | +
|
| 257 | +당장 위 예제만 봐도 port 디바이스 실패를 표현하는 예외 유형을 하나만 정의해도 이렇게 프로그램이 깔끔해진 모습을 볼 수 있다. |
| 258 | + |
| 259 | +예외 클래스에 포함된 정보로 오류를 구분해도 괜찮은 경우는 예외 클래스가 하나만 있어도 충분하다. 반면, 한 예외는 잡아내고 어떤 예외는 무시해도 괜찮은 경우라면, 여러 예외 클래스를 사용하자. |
| 260 | +<br><br> |
| 261 | + |
| 262 | +## <div id="정상">정상 흐름을 정의하라 </div> |
| 263 | + |
| 264 | + |
| 265 | +<br><br> |
| 266 | + |
| 267 | +## <div id="null반환">null을 반환하지 마라 </div> |
| 268 | + |
| 269 | + |
| 270 | + |
| 271 | +<br><br> |
| 272 | + |
| 273 | +## <div id="null전달">null을 전달하지 마라 </div> |
| 274 | + |
| 275 | + |
| 276 | + |
| 277 | +<br><br> |
| 278 | + |
| 279 | +## <div id="결론">결론 </div> |
| 280 | + |
| 281 | + |
| 282 | + |
| 283 | +<br><br> |
0 commit comments