////
Search
Duplicate
🚉

7장. 오류 처리

: 깨끗한 코드와 오류 처리는 확실히 연관성이 있다. 상당수 코드 기반은 전적으로 오류 처리 코드에 그 가독성이 좌우된다고 할 수 있다.
: 이 장에서는 깨끗하고 튼튼한 코드에 한걸음 더 다가가는 단계로 우아하고 고상하게 오류를 처리하는 기법과 고려 사항 몇 가지를 소개한다.

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

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
복사
: 좌측 코드는 오류 플래그를 설정하거나 호출자에게 오류 코드를 반환하는 방식으로 구현된 코드다.
: 우측 코드는 오류를 발견하면 예외를 던지는 코드다.
: 우측 코드로 개선되면서 가독성도 좋아졌으며 코드 품질도 좋아졌다. 한 메소드에서 뒤섞여있던 디바이스를 종료하는 알고리즘과 오류를 처리하는 알고리즘이 분리되었다.

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

: 예외에서 프로그램 안에다 범위를 정의한다는 사실은 매우 흥미롭다.
⇒ 프로그램 안에다 범위를 정의한다는 건, catch 블록에서 잡는 예외의 범위를 정의한다는 것 같다 내 생각엔
: try 블록은 트랜잭션과 비슷하다. catch 블록은 try 블록에서 무슨 일이 생기든지 다시 프로그램을 정상 흐름으로 되돌려 놓거나 정상 흐름으로 되돌리지 못한다면 예외를 던지는 역할을 수행한다.
: 그러므로 예외가 발생할 여지가 있는 코드를 짤 때는 try-catch-finally 문으로 시작하는 편이 낫다. 그러면 try 블록에서 무슨 일이 생기든지 호출자가 기대하는 상태를 정의해주기 쉬워진다.

Unchecked 예외를 사용하라

: Java에서 Checked Exception는 컴파일러가 검사하는 항목이기 때문에 명시가 필요한 부분이다.
: 이 부분 때문에 저자는 Checked Exception를 지양하고 Unchecked Exception를 사용하라고 얘기한다.
: Checked 예외 관련한 변경사항이 발생할 경우, try-catch 에서 해당 예외들에 대한 처리를 해준다거나 메소드의 선언부에 선언을 해두어야한다.
⇒ OCP, 캡슐화 위반

예외에 의미를 제공하라

: 예외를 던질 때는 전후 상황을 충분히 덧붙여라
: 오류 객체의 네이밍만으로도 어떤 작업을 수행하다가 어떤 이유로 예외가 발생했는지 확인할 수 있으면 더욱 좋다. 이는 우리의 디버깅을 더 편하게 해주지 않을까?
: 자바는 모든 예외에 호출 스택을 제공한다. 하지만 오류가 발생한 코드의 의도를 파악하려면 호출 스택만으로는 부족하다.
: 오류 메시지에 정보를 담아 예외를 던지자, 실패한 연산 이름과 실패 유형도 언급하자, 로깅 기능을 사용해서 catch 블록이 오류를 기록하도록 충분한 정보를 넘겨주자.

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

: 오류를 분류하는 방법은 너무도 많다. 오류가 발생한 위치, 오류가 기인된 곳이 디바이스인지, 네트워크인지 등이다. 그러나 애플리케이션에서 오류를 정의할 때 가장 중요해야하는 관심사는 오류를 잡아내는 방법이어야 한다.
: 오류를 잘 분리해보기 위해서 오류를 형편없이 분류한 사례를 살펴보자.
: 이 코드는 외부 라이브러리를 호출하는 try-catch-finally를 포함한 코드로 외부 라이브러리가 던질 예외를 모두 잡아 낸다.
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 { ... }
Java
복사
: 일반적으로 프로그래머들이 오류를 처리하는 방식은 오류를 일으킨 원인에 종속적이지 않은 경우가 많고 비교적 일정하다.
1.
오류를 기록한다.
2.
프로그램을 계속 수행해도 무리가 없는지 확인한다.
: 위 경우는 예외에 대응하는 방식이 예외 유형과 무관하게 거의 동일하다. 그래서 코드를 간결하게 고치기가 아주 쉽다.
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); } }} LocalPort port = new LocalPort(12); try { port.open(); } catch (PortDeviceFailure e) { // 하나의 예외로 처리 reportError(e); logger.log(e.getMessage(), e); } finally {}
Java
복사
: 호출하는 라이브러리 API를 감싸면서 예외 유형 하나를 반환하게끔 수정하고 ACMEPort 클래스가 던지는 예외를 잡아 단순히 변환해주는 Wrapper 클래스인 LocalPort 클래스를 만들어주면 된다.

정상 흐름을 정의하라

: 때로는 예외를 처리하지 않고 던지는 방식이 적합하지 않은 경우도 있다. 무조건 예외를 던지지 말고 처리하여 정상 흐름으로 회복할 수 있는 정도라면 그렇게 처리하자.

null을 반환하지 마라

: 우리가 흔히 저지르는 실수로 인해 오류가 발생하는 경우에 대해서도 독자는 다뤄야한다고 생각했다.
: 그 중 첫 번째가 흔히 null을 반환하는 습관이다. null을 반환하는 코드는 null 체크에 대한 기능을 추가해야할 뿐만 아니라 호출자에게 문제를 떠넘긴다.
: null을 반환하고 싶은 유혹이 든다면 대신 예외를 던지거나 특수 사례 객체를 반환하자, 사용하려는 외부 API가 null을 반환할 가능성이 있다면 감싸기 메서드를 구현해 예외를 던지거나 특수 사례 객체를 반환하라는 것이다.
: 많은 경우에 특수 사례 객체가 그 해결책이 될 수 있다.

null을 전달하지 마라

: 메서드의 리턴값으로 null을 반환하는 것이 나쁘듯이, 메서드의 매개변수로 null을 전달하는 것도 나쁘다.
: null을 무책임하게 넘겼을 때 처리하는 것이 매우 까다롭기 때문, 애초에 null을 넘기지 않도록 금지하는 것이 합리적이다.

결론

: 클린코드는 가독성도 좋아야 하지만 안정성도 높아야한다, 이 둘은 trade-off 관계에 있지 않다.
: 오류 처리는 프로그램 논리와 분리해 독자적인 사안으로 고려한다면 튼튼하고 깨끗한 코드를 작성할 수 있다.