Effective Java를 읽고 정리한 정리본입니다.

📌 Item 55 : 옵셔널 반환은 신중히 하라

자바 8 전에는 메서드가 특정 조건에서 값을 반환할 수 없을 때 취할 수 있는 선택지가 두 가지였다.

  1. 예외 던지기
  2. null 반환 (반환 타입이 객체 참조라는 전제 하에)

그러나 예외는 진짜 예외적인 상황에서만 사용해야 하며, 예외를 생성할 때 스택 추적 전체를 캡쳐해야 하므로 비용도 만만치 않다.

✨ 예외 처리

예외 생성과 처리의 과정을 간단하게 알아보자.

자바로 프로젝트를 진행할 때, 보통 에러를 처리하기 위해 예외 처리를 자주 사용한다.

그러나 자바에서는 예외 처리 비용이 비싸다.

우선 Exception이 발생하면 JVM에서 Exception을 수행한다.

  1. 예외 발생 예외 발생 시 JVM은 예외 객체를 생성하고, 예외를 발생시킨 메서드의 호출 스택을 추적한다.
  2. 예외 객체 전파 JVM은 해당 예외를 발생시킨 메서드에서 예외 처리 코드를 찾는다. 만일 예외 처리 코드가 없다면 예외 객체를 호출하여 스택의 상위 메서드로 전파시킨다.
  3. 예외 처리 예외 객체가 상위 메서드로 전파되게 된다면, 해당 예외를 처리할 수 있는 catch 블록을 찾고, 없다면 다시 상위로 전파된다.
  4. 예외 처리 실패 예외 객체가 최상위 메서드까지 전파되어도, 예외를 처리할 수 있는 catch 블록이 없는 경우 JVM은 예외를 처리하지 못한 것으로 판단하여 해당 예외를 처리할 수 있는 DefaultExceptionHandler를 사용해 예외를 처리한다
  5. DefaultExceptionHandler 실행 예외 객체에 대한 정보를 출력하고 해당 예외를 처리하거나 스냅샷 정보를 수집하여 디버깅을 위한 정보로 제공한다.

예외가 발생한 메서드에서 바로 처리가 된다면 가장 좋지만, 바로 처리되지 못한다면 해당 예외를 처리할 수 있는 메서드를 찾을 때까지 계속 상위 메서드로 거슬러 올라가면서 메모리의 호출 스택을 탐색하게 된다.

이때 호출 스택을 탐색하는 비용 및 fillInStackTrace() 메서드가 호출 스택을 순회하며 클래스명, 메서드명, 코드 줄 번호 등의 정보를 모아 stacktrace로 만드는 과정을 거치며 비용이 더욱 비싸짐을 알 수 있다.

이 비용을 절감하는 방법으 다음과 같다.

  1. fillInStackTrace() 재정의
    • 보통 NPE나 OOM과 같이 자바에서 기본적으로 제공하는 예외를 제외한 CustomException은 에러의 추족보다는 유효하지 않은 값일 때 하위 비즈니스 로직을 수행하지 못하도록 하기 위한 용도로 주로 사용된다. 따라 보통 StackTrace가 필요 없다.
@Override 
public synchronized Throwable fillInStackTrace() { 
	return this; 
}

때문에 단순히 try-catch로 이후 flow를 제어하거나 Spring 환경에서 @ControllerAdvice로 예외 처리하는 경우 불필요한 성능 저하를 막기 위해 trace를 저장하지 않도록 오버라이딩하여 처리할 수 있다.

public class DuplicateLoginException extends RuntimeException {
	public DuplicateLoginException(String message) { 
		super(message); 
	} 
	
	@Override 
	public synchronized Throwable fillInStackTrace() {
		return this; 
    } 
}
  1. 예외 캐싱
    • static final로 선언하여 예외를 미리 캐싱하여 사용

일종의 상수 값 형태로 예외를 캐싱해 두고 쓰는 것이 매번 같은 종류의 예외로 new로 생성하는 방법보다 효율적이다.

public class CustomException extends RuntimeException {
	public static final CustomException INVALID_NICKNAME = new CustomException(ResponseType.INVALID_NICKNAME);     
	public static final CustomException INVALID_PARAMETER = new CustomException(ResponseType.INVALID_PARAMETER);     
	public static final CustomException INVALID_TOKEN = new CustomException(ResponseType.INVALID_TOKEN);     //생략 
}

위와 같이 Excepton 클래스에 예외 상황에 대한 적당한 응답 메시지나 코드를 담도록 한 뒤, 아래처럼 예외 발생 상황에서 new 키워드 없이 throw를 수행하도록 한다.

if (StringUtils.isBlank(parameter)) {
	throw WebtoonCoreException.INVALID_PARAMETER; 
}

만일 예외를 던지는 것이 아닌 null을 반환한다면 어떻게 될까?

별도의 null 처리 코드를 추가해야 하며, null 처리를 무시하고 반환된 null 값을 어딘가에 저장해두면 언젠가 NullPointerException이 발생할 수 있다.

🫧 Optional

자바 버전이 8로 올라가면서 Optional<\T>가 생김으로써 특정 조건에서 값을 반환할 수 없을 때 취할 수 있는 선택지가 하나 늘었다.

Optional<\T>는 null이 아닌 T 타입 참조를 하나 담거나, 혹은 아무것도 담지 않을 수 있다.

이때 아무것도 담지 않은 옵셔널은 ‘비었다’고 말한다.

✨ 옵셔널 특징

  1. 원소를 최대 1개 가질 수 있는 ‘불변’ 컬렉션
  2. 예외를 던지는 메서드보다 유연하고 사용하기 쉬움
  3. null을 반환하는 메서드보다 오류 가능성이 적음

✨ 옵셔널 구현

적절한 정적 팩터리를 사용해 옵셔널 구현이 가능하다.

ex) 빈 옵셔널은 Optiona.empty()로 만들고, 값이 든 옵셔널은 Optional.of(value)로 생성

해당 예시에서는 Optional.of(value)에 null을 넣으면 NullPointerException을 반환한다.

만일 null도 포함되는 옵셔널을 만들고자 하면 Optional.ofNullable(value)를 사용하자.

✨ 옵셔널 처리

만일 메서드가 옵셔널을 반환한다면 클라이언트는 값을 받지 못했을 때 취할 행동을 선택해야 한다.

  1. 기본값을 설정하는 방법
String lastWordInLexicon = max(words).orElse("단어 없음...");
  1. 상황에 맞는 예외 던지기
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
  1. 항상 값이 채워져 있다고 가정
Element lastNobleGas = max(Elements.NOBLE_GASES).get();

✨ 옵셔널 사용 시 주의 사항

  1. 옵셔널을 반환하는 메서드에서는 null을 반환해서는 안 된다.
    -> 옵셔널의 취지 무시

  2. 컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입은 옵셔널로 감싸면 안 된다. (item 54)

  3. 성능이 중요한 상황에서는 옵셔널을 사용하지 마라.
    -> 객체 생성 비용이 들며, 그 안에서 값을 꺼내려 메서드를 호출함으로써 한 단계를 더 거치게 된다.

  4. 박싱된 기본 타입을 담은 옵셔널을 반환하지 않도록 하자.
    -> 이미 int, long, double 전용 옵셔널 클래스가 존재한다.

  5. 옵셔널을 맵의 값으로 사용하면 안 된다.
    -> 만약 그리 한다면 맵 안에 키가 없다는 사실을 나타내는 방법이 두 가지 (키가 없음, 키가 속이 빈 옵셔널임)가 되므로 혼란을 야기할 것이다.

  6. 만일 인스턴스 필드 중 상당수가 필수가 아니고, 기본 타입 등의 이유로 값이 없음을 나타낼 방법이 마땅치 않다면 옵셔널을 인스턴스 필드에 저장하는 것이 좋다.

🫧 참고 자료

  • [Java의 예외 생성 비용은 비싸다! Tistory 김희망](https://velog.io/@hope0206/Java%EC%9D%98-%EC%98%88%EC%99%B8-%EC%83%9D%EC%84%B1-%EB%B9%84%EC%9A%A9-%EB%B9%84%EC%9A%A9-%EC%A0%88%EA%B0%90-%EB%B0%A9%EB%B2%95#:~:text=%EC%98%88%EC%99%B8%20%EB%B0%9C%EC%83%9D:%20%EC%98%88%EC%99%B8%EA%B0%80%20%EB%B0%9C%EC%83%9D%ED%95%98%EB%A9%B4%20JVM%EC%9D%80%20%EC%98%88%EC%99%B8%20%EA%B0%9D%EC%B2%B4%EB%A5%BC,%EC%98%88%EC%99%B8%20%EA%B0%9D%EC%B2%B4%EB%A5%BC%20%ED%98%B8%EC%B6%9C%ED%95%98%EC%97%AC%20%EC%8A%A4%ED%83%9D%EC%9D%98%20%EC%83%81%EC%9C%84%20%EB%A9%94%EC%84%9C%EB%93%9C%EB%A1%9C%20%EC%A0%84%ED%8C%8C%EC%8B%9C%ED%82%A8%EB%8B%A4.)