본문 바로가기

독서찰기(讀書札記)/이펙티브 자바

[아이템 55] 옵셔널 반환은 신중히 하라

[배경]

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

  • 첫 번째는, 예외를 던지는 것이었다.
    • 하지만 예외는 진짜 예외적인 상황에서만 사용해야 한다.(아이템69
    • 또한 예외를 생성할 때 스택 추적 전체를 캡처하므로 비용도 만만치 않다.
  • 두 번째는, null을 반환하는 것이었다.
    • 하지만 null을 반환할 수 있는 메서드를 호출할 때는 별도의 null 처리 코드를 추가해야 한다.

자바 8로 올라가면서 또 하나의 선택지가 생겼다. 

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

  • 아무것도 담지 않은 옵셔널은 '비었다'고 말한다.
  • 반대로 어떤 값을 담은 옵셔널은 '비지 않았다'고 한다.
  • 옵셔널은 원소를 최대 1개 가질 수 있는 '불변' 컬렉션이다.
    Optional<T>가 Collection<T>를 구현하지는 않았지만, 원칙적으로 그렇다는 말이다.
  • 옵셔널을 반환하는 메서드는 예외를 던지는 메서드보다 유연하고 사용하기 쉬우며, null을 반환하는 메서드보다 오류 가능성이 작다.

 

아이템30에서도 등장했던, 주어진 컬렉션에서 최댓값을 뽑아주는 메서드다.

public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
    if (c.isEmpty()) {
        return Optional.empty();
    }
    
    E result = null;
    for (E e : c) {
        if (result == null || e.compareTo(result) > 0) {
            result = Objects.requireNonNull(e);
        }
    }
    return Optional.of(result);
}
  • 보다시피 옵셔널을 반화하도록 구현하기는 어렵지 않다.
  • 적절한 정적 팩터리를 사용해 옵셔널을 생성해주기만 하면 된다.
  • 위 코드에서는 두 가지 팩터리를 사용했다.
  • 빈 옵셔널은 Optional.empty()로 만들고, 값이 든 옵셔널은 Optional.of(value)로 생성했다.

 

[Why]

Optional.of(value)에 null을 넣으면 NullPointerException을 던지니 주의하자.

  • null 값도 허용되는 옵셔널을 만들려면 Optional.ofNullable(value)를 사용하면 된다.
  • 옵셔널을 반환하는 메서드에서는 절대 null을 반환하지 말자.
    옵셔널을 도입한 취지를 완전히 무시하는 행위다.

Optional<T>를 반환하는 데는 대가가 따른다.

  • Optional도 엄연히 새로 할당하고 초기화해야 하는 객체이고,
    그 안에서 값을 꺼내려면 메서드를 호출해야 하니 한 단계를 더 거치는 셈이다.
  • 그래서 성능이 중요한 상황에서는 옵셔널이 맞지 않을 수 있다.

박싱된 기본 타입을 담은 옵셔널을 반환하는 일은 없도록 하자.

  • 박싱된 기본 타입을 담는 옵셔널은 기본 타입 자체보다 무거울 수밖에 없다. 값을 두 겹이나 감싸기 때문이다.
  • 그래서 OptionalInt, OptionalLong, OptionalDouble이 따로 있다.

 

[When]

null을 반환하거나 예외를 던지는 대신 옵셔널 반환을 선택해야 하는 기준은 무엇인가?

  • 옵셔널은 검사 예외와 취지가 비슷하다.(아이템71)
  • 즉, 반환값이 없을 수도 있음을 API 사용자에게 명확히 알려준다.

스트림의 종단 연산 중 상당수가 옵셔널을 반환한다.

public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
    return c.stream().max(Comparator.naturalOrder());
}

 

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

  • 빈 Optional<List<T>>를 반환하기보다는 빈 List<T>를 반환하는 게 좋다. (아이템54)

어떤 경우에 메서드 반환 타입을 T 대신 Optional<T>로 선언해야할까?

  • 결과가 없을 수 있으며, 클라이언트가 이 상황을 특별하게 처리해야 한다면 Optional<T>를 반환한다.

옵셔널을 컬렉션의 키, 값, 원소나 배열의 원소로 사용하는 게 적절한 상황은 거의 없다.

  • 쓸데없이 복잡성만 높여서 혼란과 오류 가능성을 키울 뿐이다.

 

[How]

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

String lastWordInLexicon = max(words).orElse("단어 없음...");
  • 기본값을 정해둘 수 있다.
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
  • 상황에 맞는 예외를 던질 수 있다.
Element lastNobleGas = max(Elements.NOBLE_GASES).get();
  • 항상 값이 채워져 있다고 가정한다.
  • 다만 잘못 판단한 것이라면 NoSuchElementException이 발생한다.

기본값을 설정하는 비용이 아주 커서 부담이 될 때가 있다.

  • 그럴 때는 Supplier<T>를 인수로 받는 orElseGet을 사용하면,
    값이 처음 필요할 때 Supplier<T>를 사용해 생성하므로 초기 설정 비용을 낮출 수 있다.
  • 더 특별한 쓰임에 대비한 메서드도 준비되어 있다. 바로 filter, map, flatMap, ifPresent다.
    • ifPresent는 안전밸브 역할의 메서드로, 옵셔널이 채워져 있으면 true를, 비어 있으면 false를 반환한다.
    • ifPresent를 쓴 코드 중 상당수는 앞서 언급한 메서드들로 대체할 수 있으며, 그렇게 하면 더 짧고 명확하고 용법에 맞게 된다.

Optional의 map을 사용하여 코드를 다듬는 예제

Optional<ProcessHandle> parentProcess = ph.parent();
System.out.println("부모 PID: " + (parentProcess.isPresent() ?
    String.valueOf(parentProcess.get().pid()) : "N/A"));
System.out.println("부모 PID: " +
    ph.parent().map(h -> String.valueOf(h.pid())).orElse("N/A"));

Stream<Optional<T>>로 받아서, 그중 채워진 옵셔널들에서 값을 뽑아 Stream<T>에 건네 처리하는 예제

streamOfOptionals
    .filter(Optional::isPresent)
    .map(Optional::get)

자바 9에 추가된 Optional의 stream() 메서드

streamOfOptionals
    .flatMap(Optional::stream)
  • Optional을 Stream으로 변환해주는 어댑터다.
  • 옵셔널에 값이 있으면 그 값을 원소로 담은 스트림으로, 값이 없다면 빈 스트림으로 변환한다.
  • 이를 Stream의 flaMap 메서드(아이템45)와 조합하면 위 코드처럼 명료하게 바꿀 수 있다.