Item55. Optional 반환은 신중히 하라

Optional API

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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을 반환하도록 변경하면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
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 반환을 지원한다.

1
2
3
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 사용자는 값이 비어있었을때 기본값을 아래와 같이 설정해둘 수 있다.

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

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

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

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

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

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

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

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

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

1
2
3
4
5
6
7
8
/**
* 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;
}
1
2
Optional\<ProcessHandle> parentProcess = ph.parent();
System.out.println("부모 PID: " + (parentProcess.isPresent() ? String.valueOf(parentProcesss.get().pid()) : "N/A" ));

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

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 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와 조합하면 앞의 코드를 다음처럼 변경할 수 있다.

1
2
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을 갖는 경우는 대부분 바람직하지 않으나, 선택적으로 필드값을 주입받을때는 유용할 수도 있다.

Comments