자바 라이브러리는 API에서 쓰기에 충분한 수의 예외를 제공한다.
IllegalArgumentException(아이템 49)
호출자가 인수로 부적절한 값을 넘길 때 던지는 예외
ex) 반복 횟수를 지정하는 매개변수에 음수를 건널 때
IllegalStateException
대상 객체의 상태가 호출된 메서드를 수행하기에 적합하지 않을 때
ex) 제대로 초기화되지 않은 객체를 사용하려 할 때 발생
UnsupportedException
클라이언트가 요청한 동작을 대상 객체가 지원하지 않을 때
ex) 구현하려는 인터페이스의 메서드 일부를 구현할 수 없을 때 사용한다.
즉, 원소를 바꿀 수 없는 List에 원소를 바꾸려고 할 때
NullPointerException
null을 허용하지 않는 메서드에 null을 건널 때 IllegalArgumentException이 아닌 NPE발생
IndexOutOfBoundsException
시퀀스의 허용 범위를 넘는 값을 넘길 때도 IllegalArgumentException 해당 예외 발생
private Car getMaxPositionCar() {
return cars.stream()
.max(Car::compareTo)
.orElseThrow(() -> new IllegalArgumentException("차량이 존재하지 않습니다"));
...
}
이 경우에는 인수를 따로 받지 않으므로 IllegalArugmentException 보다는 IllegalStateException이 더 적절하다.
이는 지양해야 하는 패턴이다.
추가적으로, API 문서를 참고해 그 예외가 어떤 상황에서 던져지는지 확인하자.
이후에, 예외가 던져지는 맥락이 부합할 때 사용하자
이후 상황에 부합한다면 표준 예외를 사용하자
더 많은 정보를 제공하길 원한다면 표준 예외를 확장해도 된다.
하지만, 예외는 직렬화할 수 있다는 사실을 기억해야 한다.(12장)
직렬화에는 많은 부담이 따르니 나만의 예외를 새로 만들지 않아야 할 근거로 충분하다.
+) RuntimeException -> Exception -> Throwable -> Serializable
직렬화는 객체를 데이터 스트림으로 만드는 것으로, 타입 정보등의 클래스 메타 정보를 가지고 있기 때문에 성능이 안 좋아진다.
public class Name {
private final String value;
public Name(String name) {
if(name.length() > 5) {
throw new InvalidNameLengthException(5);
}
this.value = name;
}
}
public class InvalidNameLengthException extends IllegalArugumentException {
public static final String DEFAULT_MESSAGE = "%d자 이상은 유효하지 않습니다.";
public InvalidNameLengthException(int limit) {
super(DEFAULT_MESSAGE.formatted(limit));
}
}
IllegalArgumentException을 발생시키지 않고 커스텀 예외를 발생시켰다.
하지만, 이 챕터에서는 예외 확장보다는 기존 예외 사용을 권장 하고 있는데,
나는 커스텀 예외를 계속 사용할 것 같다.
그 이유는, 클래스명만으로도 이름의 길이가 잘못되었다는 예외를 명확하게 전달할 수 있기 때문이다.
+) 추가적으로 어떤 예외 메세지를 던질지에 대한 책임 분리도 가능
내가 방금 Custom Exception의 장점으로 예외 클래스명 만으로도 어떤 예외인지 알기 쉽다.로 들었다.
public class UserNameEmptyException extends RuntimeException {
public UserNameEmptyException(String message) {
...
}
}
이 경우도 예외 클래스명 만으로도 사용자 이름의 입력값이 비어있구나..를 생각할 수는 있다.
하지만, 이 이유 때문에 Custom Exception을 발생시키는 것은 지나치다.
이 같은 경우에는 IllegalArgumentException을 발생시키는 게 더 낫다.
in github 코드리뷰
1. CustomException을 받는곳에서 처리 하기 쉽기 위해 추가로 에러에 관한 정보(Exception이 발생한 메소드에 어떤 파라미터로 들어왔는지 등)를 넣어주거나
2. 여러 타입의 Exception이 발생할 수 있는 코드에서 한가지 Exception 타입으로 묶어서 처리 할때
주로 사용한다.
IndexOutOfBoundsException에 메시지만 넘겨줄 수 있지만,
전체 범위가 얼마인지, 요청한 index가 몇인지 메시지를 통해서 넘겨줄 수 있다.
public class IllegalIndexException extends IndexOutOfBoundsException {
private static final String message = "범위를 벗어났습니다.";
public IllegalIndexException(List<?> target, int index) {
super(message + " size: " + target.size() + " index: " + index);
}
}
클래스를 만드는 행위는 관련된 정보를 해당 클래스에서 최대한 관리하겠다 라는 이야기이다.
즉, custom 예외 클래스를 정의하면 예외에 필요한 메시지, 전달할 정보의 데이터 등등을 한 곳에서 관리가 가능하다.
예외 생성 비용을 절감한다. 예외를 생성할 때 stackTrace 때문에 비용이 많이 발생한다. 이 stackTrace는 부모클래스 중 Throwable.fillInStackTrace()에서 발생하는데, 이걸 Override해서 간략하게 하거나 아예 생성하지 않을 수 있다.
예외의 상속 관계 때문에 Exception, RuntimeException을 잡으면 프로그램 내에서 발생하는 거의 모든 예외에 대한 처리가 가능하다.
따라서 여기서 IllegalArgumentException이 발생했다면 이가 some()내부에서 발생한 것인지 아니면 다른 곳에서 발생한 곳인지를 파악할 수가 없다.
따라서 일괄적인 처리를 하고 싶어도 처리를 할 수가 없다.
// in SomeController.java
@Controller
public class SomeController {
// ...
@PostMapping("/some")
public ResponseEntity<Void> Some(@RequestBody SomeRequest request) {
Something something = someService.someMethod(request);
if (somevalidate(something)) {
throw new IllegalArgumentException();
}
SomeExternalLibrary.doSomething(something);
return ResponseEntity.ok().build();
}
// ...
}
Advice에서 ExpectedException을 처리하는 메서드로 API 에서 예측 가능한 예외가 그렇지 않은 예외를 구분할 수 있다.
// in GlobalExceptionHandler.java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<ErrorResponse> handleUnExpectedException(final RuntimeException error) {
// ...
}
@ExceptionHandler(ExpectedException.class)
public ResponseEntity<ErrorResponse> handleExpectedException(final ExpectedException error) {
// ...
}
// ...
}
인수로 건넨 수만큼의 카드를 뽑아 나눠주는 메서드를 제공한다고 해보자
이때, 남아 있는 카드 수보다 큰 값을 건네면 어떤 예외를 던져야 할까?
내 생각
사용자가 부적절한 값 자체를 건넨 건 아니고, 해당 객체가 더 이상 메서드를 수행하지 못 하는 것이기 때문에 IllegalStateException을 발생시켜야 할 것 같다.
하지만, 이 또한 관점에 따라서 다르다고 한다.
IllegalArgumentException 발생
IllegalStateException 발생
이런 상황에서는 인수 값이 무엇이었든 어차피 실패하였다면 IllegalStateException, 인수의 값에 따라 예외 발생 유무가 달라진다면 IlltelArgumentException을 발생시킨다.