Item55. Optional 반환은 신중히 하라

2022-02-13 17:48:55

#Java#Effective Java 3/E

Optional API

jdk 8 이전에는 method가 특정 조건에서 값을 반환할 수 없을 때 취할 수 있는 선택지는 null을 반환하거나 , 예외를 던지는 방법이 있었다.

예외를 던지는 경우에는 진짜 예외적인 상황에만 사용해야 하며, null을 반환하는 경우에는 null처리로직이 client코드에 들어가야한다는 단점이 있었다.

jdk 8 이후부터는 Optional API 가 추가되었다.

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

보통은 T를 반환해야 하지만 특정 조건에서는 아무것도 반환하지 않아야 할때, T 대신 Optional<T>를 반환하도록 선언하면 된다.

// collection에서 최댓값을 구하는 method로 collection이 비어있으면 IllegalArgumentException을 던진다. 
public static <E extends Comparable<E>> E max(Collection<E> c){
    if(c.isEmpty()){
        throw new IllegalArgumentException("빈 collection");
    }
    E result = null;
    for (E e : c) {
        if(result == null || e.compareTo(result)>0){
            result = Objects.requireNonNull(e);
        }
    }
    return result;
}

위 method를 Optional을 반환하도록 변경하면 다음과 같다.

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은 Optional.empty()
  • null이 아닌 값이 들어있는 optional은 Optional.of(value)
  • null일 수 있는 값이 들어있는 optional은 Optional.ofNullable(value)를 사용하면 된다.

주의사항은 optional을 반환하는 method에는 당연히 null을 반환하면 안된다. 이는 API 도입 취지를 완전히 무시하는 행동이다.

추가로 Stream의 max연산을 비롯한 상당수의 종단 연산이 Optional 반환을 지원한다.

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

Optional 반환 기준

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

Optional을 반환할때 의도는 Optional안에 든 값이 비어있을수도 있음을 API 사용자에게 명확하게 알려준다.

따라서 결과가 없을 수 있으며, client가 결과가 없을때 상황을 특별하게 처리해야 한다면 Optional을 반환하는게 좋다.

예를 들면 Optional 사용자는 값이 비어있었을때 기본값을 아래와 같이 설정해둘 수 있다.

String result = max(words).orElse("word not exist"); 

또는 상황에 맞게 예외를 던질 수 있다.

max(words).orElseThrow(NoMaxValException::new);

항상 값이 채워져 있다고 보장되는 상황이라면 바로 꺼내도 무관하나, 값이 없다면 NoSuchElementException이 발생할 것이다.

max(Elements.NOBLE_GASES).get(); // 값이 없는 경우 NoSuchElementException 

이따금 기본값을 설정하는 비용이 아주 클 경우에는 Supplier<T> 를 인수로 받는 orElseGet을 사용하면

값이 필요할 때, Supplier<T>를 사용해 생성하므로 초기 설정 비용을 낮출 수 있다.

optional 이 제공하는 여러 method중에 활용할 수 있는게 없다면 isPresent method를 활용하면 된다.

isPresent method는 optional이 채워져 있으면 true를, optional이 비어 있으면 false를 반환한다.

/**
 * If a value is present, returns {@code true}, otherwise {@code false}.
 *
 * @return {@code true} if a value is present, otherwise {@code false}
 */
public boolean isPresent() {
    return value != null;
}
Optional\<ProcessHandle> parentProcess = ph.parent();
System.out.println("부모 PID: " + (parentProcess.isPresent() ? String.valueOf(parentProcesss.get().pid()) : "N/A" ));

isPresent method는 다음과 같이 다듬을 수 있다.

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

Stream을 사용한다면 optional들을 Stream<Optional<T>> 로 받아서, 그 중 채워진 Optional들에서 값을 뽑아, Stream<T> 에 건너 담아 처리하는 경우가 많다.

streamOfOptionals
	.filter(Optional::isPresent) // optional에 값이 있다면
	.map(Optional::get)  // 꺼내서 스트림에 매핑 

jdk 9에서는 Optional을 stream으로 변환해주는 stream() method가 추가되었다.

optional에 값이 있으면 그 원소를 담은 stream 으로 , 값이 없다면 빈 stream으로 변환한다.

/**
 * If a value is present, returns a sequential {@link Stream} containing
 * only that value, otherwise returns an empty {@code Stream}.
 *
 * @apiNote
 * This method can be used to transform a {@code Stream} of optional
 * elements to a {@code Stream} of present value elements:
 * <pre>{@code
 *     Stream<Optional<T>> os = ..
 *     Stream<T> s = os.flatMap(Optional::stream)
 * }</pre>
 *
 * @return the optional value as a {@code Stream}
 * @since 9
 */
public Stream<T> stream() {
    if (!isPresent()) {
        return Stream.empty();
    } else {
        return Stream.of(value);
    }
}

이를 Stream의 flatMap method와 조합하면 앞의 코드를 다음처럼 변경할 수 있다.

streamOfOptionals
	.flatMap(Optional::stream)

collection과 같은 container 타입은 optional로 감싸면 안된다. 빈 optional을 반환하기 보다 빈 List를 반환하는게 client가 optional 처리 코드를 넣지 않아도 되기 떄문이다.

추가적으로 박싱된 기본 타입을 담는 optional은 값을 2번 담기 떄문에 기본 타입보다 당연히 무거울 수 밖에 없는데, Optional API 는 기본적으로 int,long,double 전용 Optional class들인 OptionalInt, OptionalLong, OptionalDouble을 제공한다.

예외

  • Optional은 map 의 key로 사용하면 안된다. key가 없을때의 처리로직이 복잡해진다.

  • 인스턴스 필드로 Optional을 갖는 경우는 대부분 바람직하지 않으나, 선택적으로 필드값을 주입받을때는 유용할 수도 있다.

프로필 이미지
@chani
바둑 좋아하는 개발자의 의미있는 학습 기록을 위한 공간입니다.

댓글

이 게시글에 대한 의견을 공유해주세요!

댓글