6. 안전하게 예외 처리하기
in 클린코드 on Clean-code
안전하게 예외 처리하기
1. 예외 처리 방식
오류 코드를 리턴하지 말고, 예외를 던져라
- 옛날에는 오류를 나타낼 때 에러코드를 던졌다.
- 하지만 예외를 던지는 것이 명확하고, 처리 흐름이 깔끔해진다.
// 👎👎
if (handle != DeviceHandle.INVALID) {
...
} else {
logger.log("Invalid handle for: " + DEV1.toString())
}
- 오류가 발생한 부분에서 예외를 던진다. (별도의 처리가 필요한 예외라면 checked exception 으로 던진다.)
- checked exception 에 대한 예외처리를 하지 않는다면 메서드 선언부에 throws 를 명시해야 한다.
- 예외를 처리할 수 있는 곳에서 catch 하여 처리한다.
// 👍👍
public void sendShutDown() {
try {
tryToShutDown();
} catch (DeviceShutDownError e) {
logger.log(e); //3
}
}
private void tryToShutDown() throws DeviceShutDownError {
... //2
}
private DeviceHandle getHandle(DeviceID id) {
...
throw new DeviceShutDownError("Invalid handle for: " + DEV1.toString()") //1
...
}
2. Unchecked Exception 을 사용하라
Checked vs Unchecked Exception
Exception 을 상속하면 Checked Exception
- 명시적인 예외처리가 필요하다.
- ex) IOEception, SQLException
RuntimeExcetion 을 상속하면 UncheckedException
- 명시적인 예외처리가 필요하지 않다
- ex) NullPionterEception, IllegalArgumentException, IndexOtuOfBoundException
Exception에 관한 규약
자바 언어 명세가 요구하는 것은 아니지만, 업계에 널리 퍼진 규약으로
Error 클래스를 상속해 하위 클래스를 만드는 일은 자제하자
즉 사용자가 직접 구현하는 unchecked throwable 은 모두 RuntimeException의 하위 클래스
여야 한다.
Exception, RuntimeExcetion, Error 를 상속하지 않는 throwable 을 만들수도 있지만, 이러한 throwable은 정상적인 사항보다 나을 게 하나도 없으면서 API 사용자를 헷갈리게 할 뿐이므로 절대로 사용하지 말자.
Checked Exception 이 나쁜 이유
- 특정 메소드에서 checked exception 을 throw 하고 상위 메소드에서 그 exception 을 catch 한다면 모든 중간단계 메소드에 exception을 throws 해야 한다.
- OCP (개방 폐쇄 원칙) 위배 : 상위 레벨 메소드에서 하위 레벨 메소드의 디테일에 대해 알아야 하기 때문에 OCP 원칙에 위배된다.
- 필요한 경우 checked exception 을 사용해야 되지만 일반적인 경우 득보다 실이 많다 디미터의 법칙에 어긋나는 상황
- 마치 depth 가 적어보이더라도 그 내부가 여러번이라면 그건 잘못된 것!
Unchecked Exception 을 사용하자
안정적인 소프트웨어를 제작하는 요소로 확인된 예외가 반드시 필요하지는 않다는 사실이 분명해졌다. C#은 확인된 예외를 지원하지 않는다. 영웅적인 시도에도 불구하고 C++ 역시 확인된 예외를 지원하지 않는다. 파이썬이나 루비도 마찬가지다. 그럼에도 불구하고 C#, C++, 파이썬, 루비는 안정적인 소프트웨어를 구현하기에 무리가 없다.
3. Exception 잘 쓰기
예외에 메시지 담기
예외에 의미있는 정보 담기
- 오류가 발생한 원인과 위치를 찾기 쉽도록, 예외를 던질 때는 전후 상황을 충분히 덧붙인다.
- 실패한 연산 이름과 유형 등 정보를 담아 예외를 던진다.
exception wrapper
예외를 감싸는 클래스를 만든다.
// 👎👎 로그를 찍을 뿐 할 수 있는 일이 없다.
try {
port.open()
} catch (PortDeviceFailure e) {
reportError(e)
logger.log("Device response exception", e)
} catch (...)
- port.open() 시 발생하는 checked exception 들을 감싸도록 port 를 가지는 LocalPort 클래스는 만든다.
- port.open() 이 던지는 checked exception 들을 하나의 PortDeviceFailure exception으로 감싸서 던진다.
LocalPort port = new LocalPort(12);
try {
port.open()
} catch (PortDeviceFailure e) {
reportError(e)
logger.log(e.getMessage(), e)
}
// 👍👍
public class LocalPort {
...
public void open() {
try {
innerPort.open()
} catch (DeviceResponseException e) {
throw new PortDeviceFailure(e)
} catch ... {
throw new ...
}
}
}
4. 실무 예외 처리 패턴
getOrElse
- 예외 대신 기본 값을 리턴한다.
- null 이 아닌 기본 값
- 도메인에 맞는 기본값
getOrElseThrow
- null 대신 예외를 던진다 (기본값이 없다면)
- null 체크 지옥에서 벗어나자
// 👎
User user = userRepository.findByUserId(userId)
if (user != null) {
// user를 이용한 처리
}
// 👍
User user = userService.getUserOrElseThrow(userId);
public class UserService {
public User getUserOrElseThrow(Long userId) {
User user = userRepository.findByUserId(userId)
if (user == null) {
throw new IllegalArgumentException("User is not found. userId = " + userId)
}
return user
}
}
- 데이터를 제공하는 쪽에서 null 체크를 하여, 데이터가 없는 경우엔 예외를 던진다.
- 호출부에선 매번 null 체크를 할 필요 없이 안전하게 데이터를 사용할 수 있다.
- 호출부의 가독성이 올라간다.
파라미터의 null을 점검하라
- null을 리턴하는 것도 나쁘지만 null을 메서드로 넘기는 것은 더 나쁘다.
- null을 메서드의 파라미터로 넣어야 하는 API 를 사용하는 경우가 아니면 null을 메서드로 넘기지 마라.
null을 파라미터로 받지 못하게 한다
public class MetricsCalculator {
public double xProjection(Point p1, Point p2) {
if (p1 == null || p2 == null) {
throw InvalidArgumentException("Invalid argument for MetricsCalculator.xProjection")
}
return (p2.x - p1.x) * 1.5
}
}
- null 이 들어오면 unchecked exception을 발생시킨다.
실무에서는 보통 자신의 예외를 정의한다.
public class MyProjectException extends RuntimeException {
private MyErrorCode errorCode;
private String errorMessage;
public MyProjectException(MyErrorCode errorCode) {
...
}
public MyProjectException(MyErrorCode errorCode, String errorMessage) {
...
}
}
public enum MyErrorCode {
private String defaultErrorMessage;
INVALI_REQUEST("잘못된 요청입니다.")
DUPLICATED_REQUEST("기존 요청과 중복되어 처리할 수 없습니다.")
...
INTERNAL_SERVER_ERROR("처리 중 에러가 발생했습니다.")
}
// 호출부
if (request.getUserName() == null) {
throw new MyProjectException(errorCode.INVALID_REQUEST, "userName is null")
}
- 에러 로그에서 stacktrace 해봤을 때 우리가 발생시킨 예외라는 것을 바로 인지할 수 있다.
- 다른 라이브러리에서 발생한 에러와 섞이지 않는다. 우리도 IllegalArgumentException을 던지는 것보다 우리 예외로 던지는게 어느 부분에서 에러가 났는지 파악하기에 용이하다.
- 우리 시스템에서 발생한 에러의 종류를 나열할 수 있다.
5. 오픈소스 속 Exception 살펴보기
apps-android-commons
…
Elastic Search
…