아이템 55. 옵셔널 반환은 신중히 하라. - ksw6169/effective-java GitHub Wiki

자바 8 전에 특정 조건에서 값을 반환할 수 없는 경우의 선택지

  • 자바 8 전에는 특정 조건에서 값을 반환할 수 없는 경우의 선택지가 두 가지가 있었다.
  • 하지만 두 방식 모두 허점이 있다.

1. 예외를 던진다.

  • 예외는 진짜 예외적인 상황에서만 사용해야 한다는 원칙에 위배된다.
  • 예외를 생성할 때 스택 추적 전체를 캡처하므로 비용이 만만치 않다.

2. null을 반환한다.

  • 클라이언트에서 별도의 null 처리 코드를 추가해야 한다.
  • null 처리를 하지 않으면 다른 어딘가에서 NPE가 발생할 위험이 있다.

자바 8부터 특정 조건에서 값을 반환할 수 없는 경우의 선택지

  • 자바 8부터는 또 다른 선택지가 생겼다. 바로 Optional<T> 을 반환하는 것이다.
  • Optional<T> 는 null이 아닌 T 타입 참조를 하나 담거나 혹은 아무것도 담지 않을 수 있다.

옵셔널은 언제 반환해야 하는가?

  • 결과가 없을 수 있으며, 클라이언트가 이 상황을 특별하게 처리해야 한다면 옵셔널을 반환한다.
  • 옵셔널을 반환하는 메소드는 예외를 던지는 메소드보다 유연하고 사용하기 쉬우며 null을 반환하는 메소드보다 오류 가능성이 적다.

예제 - 컬렉션에서 최댓값을 추출하는 메소드

다음 메소드에 빈 컬렉션을 보내면 IllegalArgumentException 을 던진다.

public static <E extends Comparable<E>> E max(Collection<E> c) {
    if (c.isEmpty())
        throw new IllegalArgumentException("빈 컬렉션");

    E result = null;

    for (E e : c)
        if (result == null || e.compareTo(result) > 0)
            result = Objects.requireNonNull(e);

    return result;
}

위 메소드에서 예외 대신에 옵셔널을 반환하도록 수정하면 다음과 같다.

public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
    if (c.isEmpty())
        return Optional.empty();    // new Optional<>() 반환

    E result = null;

    for (E e : c)
        if (result == null || e.compareTo(result) > 0)
            result = Objects.requireNonNull(e);

    return Optional.of(result);     // new Optional<>(value) 반환
}

null 이나 예외 대신 옵셔널을 반환해야 되는 이유

  • 검사 예외의 의도와 마찬가지로 옵셔널을 반환하면 반환 값이 없을 수도 있음을 API 사용자에게 명확하게 알려준다.
  • 따라서 클라이언트가 이에 대비한 코드를 작성하도록 강제할 수 있다.

옵셔널을 반환하면 클라이언트가 취할 수 있는 행동

옵셔널을 반환하면 클라이언트는 값을 받지 못했을 때 취할 행동을 선택할 수 있다.


1. 기본값을 지정한다.

String lastWordInLexicon = max(words).orElse("단어 없음...");

만약 기본값을 설정하는 비용이 아주 큰 경우 Supplier<T> 를 인수로 받는 orElseGet을 사용하면 값이 처음 필요할 때 Supplier<T> 를 사용해 생성하므로 초기 설정 비용을 낮출 수 있다.


2. 원하는 예외를 던진다.

Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);

3. 항상 값이 채워져 있다고 가정하고 이를 꺼내 사용한다.

이 때 값이 만약 없는 경우 NoSuchElementException 이 발생할 것이다.

Element lastNobleGas = max(Elements.NOBLE_GASES).get();

4. 고급 메소드를 사용해 별도의 처리를 한다.

앞에서 언급한 기본 메소드 외에 filter, map, flatMap, ifPresent를 사용하면 다양한 처리를 할 수 있다.


isPresent는 다른 메소드로 대체될 수 있으니 신중히 사용하라.

  • isPresent는 옵셔널이 채워져 있으면 true를, 비어 있으면 false를 반환하는 메소드다.
  • 이 메소드를 쓴 상당수의 코드는 앞서 언급한 대부분의 메소드로 대체될 수 있다.
  • 그렇게 하면 더 짧고 명확하고 용법에 맞는 코드를 쓸 수 있으니 isPresent는 신중히 사용해야 한다.
Optional<ProcessHandle> parentProcess = ph.parent();
System.out.println("부모 PID: " + (parentProcess.isPresent() ?
    String.valueOf(parentProcess.get().pid()) : "N/A"));

위 코드는 Optional의 map을 사용하여 다음처럼 다듬을 수 있다.

System.out.println("부모 PID: " + 
    ph.parent().map(h -> String.valueOf(h.pid())).orElse("N/A"));

컨테이너 타입은 옵셔널로 감싸면 안된다.

  • 컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입은 옵셔널로 감싸면 안된다.
  • Optional<List<T>> 를 반환하기보다는 빈 List<T> 를 반환하는 게 좋다.
  • 빈 컨테이너를 그대로 반환하면 클라이언트에 옵셔널 처리 코드를 넣지 않아도 되기 때문이다.

옵셔널 반환 시 주의사항

1. 성능이 중요한 상황에서는 옵셔널이 맞지 않을 수 있다.

  • 옵셔널도 엄연히 새로 할당하고 초기화해야 하는 객체이기 때문이다.

2. 박싱된 기본 타입을 담은 옵셔널을 반환하지 마라.

  • 박싱된 기본 타입을 담는 옵셔널은 값을 두 겹이나 감싸기 때문에 무거울 수 밖에 없다. 대신에 기본 타입용 Optional인 OptionalInt, OptionalLong, OptionalDouble을 반환하자. (단, 덜 중요한 기본 타입인 Boolean, Byte, Character, Short, Float은 예외일 수 있다.)

3. null을 허용하는 옵셔널은 Optional.ofNullable(value)로 만들어라.

  • null을 허용하는 옵셔널을 만들려면 Optional.of(value) 대신에 Optional.ofNullable(value) 를 사용하면 된다.

4. 옵셔널을 반환하는 메소드에서는 절대 null을 반환하지 말자.

  • 옵셔널의 도입 취지를 무시하는 행위다.

옵셔널 사용 시 주의사항

  • 옵셔널을 컬렉션의 키, 값, 원소나 배열의 원소로 사용하는 게 적절한 상황은 거의 없으니 절대 사용해서는 안된다. 쓸데없이 복잡성만 높여서 혼란과 오류 가능성을 키울 뿐이다.
  • 예를 들어 옵셔널을 맵의 키로 사용할 경우 컬렉션에서 키가 없음을 알아내는 방법이 두 가지가 된다. 하나는 진짜 키가 없는 경우이고, 다른 하나는 키가 속이 빈 옵셔널인 경우다.

핵심 정리

  • 값을 반환하지 못할 가능성이 있고, 호출할 때마다 반환값이 없을 가능성을 염두에 둬야 하는 메소드라면 옵셔널을 반환해야 할 상황일 수 있다.
  • 하지만 옵셔널 반환에는 성능 저하가 뒤따르니 성능에 민감한 메소드라면 null을 반환하거나 예외를 던지는 편이 나을 수 있다.
  • 참고로 옵셔널을 반환값 이외의 용도로 쓰는 경우는 매우 드물다.

참고 자료

  • Effective Java 3/E
⚠️ **GitHub.com Fallback** ⚠️