[Effective Java] 아이템55 - 옵셔널 반환은 신중히 하라
자바 8 전의 경우 메서드가 특정 조건에서 값을 반환할 수 없을 때,
- 예외를 던지거나
- (반환 타입이 객체 참조라면) null을 반환했다.
하지만 예외는 진짜 예외적인 상황에서만 사용해야 하며 예외를 생성할 때 스택 추적 전체를 캡처하므로 비용도 만만치 않다.
null을 반환하면 위 문제는 생기지 않지만, 별도의 null 처리 코드를 추가해야 한다.
자바 버전이 8로 올라가면서 또 하나의 선택지가 생겼다.
Optional<T>는 null이 아닌 T 타입 참조를 하나 담거나, 혹은 아무것도 담지 않을 수 있다.
옵셔널은 최대 1개 가질 수 있는 불변 컬렉션이다.
T를 반환해야 하지만 특정 조건에서는 아무것도 반환하지 않아야 할 때 T 대신 Optional<T>를 반환하도록 선언하면 된다.
그러면 유효한 반환값이 없을 대는 빈 결과를 반환하는 메서드가 만들어진다.
옵셔널을 반환하는 메서드는 예외를 던지는 메서드보다 유연하고 사용하기 쉬우며, null을 반환하는 메서드보다 오류 가능성이 작다.
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 11 e.compareTo(result) > 0)
result = Objects.requireNonNull(e);
return Optional.of(result);
}
→ 적절한 정적 팩터리를 사용해 옵셔널을 생성해주기만 하면 된다.
위 코드에서는 두 가지 팩터리를 사용했다.
- 빈 옵셔널은 Optional.empty()로 만들고
- 값이 든 옵셔널은 Optional.of(value)로 생성했다.
Optional.ofNullable(value)를 통해 null 값도 허용하는 옵셔널을 만들 수 있지만
옵셔널을 도입한 취지를 완전히 무시해버리기 때문에,
옵셔널을 반환하는 메서드에서는 절대 null을 반환하지 말자!
그렇다면 null을 반환하거나 예외를 던지는 대신 옵셔널 반환을 선택해야 하는 기준은 무엇인가?
옵셔널은 검사 예외와 취지가 비슷하다.
즉, 반환값이 없을 수도 있음을 API 사용자에게 명확히 알려준다.
(검사 예외를 던지면 클라이언트에서는 반드시 이에 대처하는 코드를 작성해야 한다.)
메서드가 옵셔널을 반환한다면 클라이언트는 값을 받지 못했을 때 취할 행동을 선택해야 한다.
#1. 기본값을 설정한다.
String lastWordInLexicon = max(words).orElse("단어 없음...");
#2. 상황에 맞는 예외를 던질 수 있다.
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
→ 실제 예외가 아니라 예외 팩터리를 건네므로 예외가 실제 발생하지 않는한 예외 생성 비용은 들지 않는다.
#3. (옵셔널에 항상 값이 채워져 있을 경우) 바로 값을 꺼내 사용한다.
Element lastNobleGas = max(Elements.NOBLE_GASES).get();
옵셔널에는 득별한 쓰임에 대한 메서드인 filter, map, flatMap, ifPresent가 있다.
특히 isPresent는 옵셔널이 채워져 있으면 true를, 비어 있으면 false를 반환한다.
이 메서드로는 원하는 모든 작업을 수행할 수 있지만 신중히 사용해야 하며,
isPresent를 쓴 코드 중 상당수는 앞서 언급한 메서드들로 대체할 수 있다.
Optional<ProcessHandle> parentprocess = ph.parent();
System.out.println("부모 PID: " + (parentProcess.isPresent() ?
String.valueOf(pa rentProcess. get().pid()) : "N/A"));
→ isPresent()를 사용한 경우
System, out. printIn("부모 PID: " +
ph.parent().map(h -> String.valueOf(h.pid())).orElse("N/A"));
→ 옵셔널의 map을 사용한 경우
반환값으로 옵셔널을 사용한다고 해서 무조건 득이 되는 건 아니다!
컬렉션, 스트림, 배열, 옵셔널 깥은 컨테이너 타입은 옵셔널로 감싸면 안된다.
빈 Optional<List<T>>를 반환하기 보다는 빈 List<T>를 반환하는 것이 좋다.
→ 빈 컨테이너를 그대로 반환하면 클라이언트에 옵셔널 처리 코드를 작성하지 않아도 된다.
어떤 경우에 메서드 반환 타입을 T 대신 Optional<T>로 선언해야 할까?
바로 결과가 없을 수 있으며, 클라이언트가 이 상황을 특별하게 처리해야 한다면 Optional<T>를 반환한다.
그런데 Optional도 새로 할당하고 초기화해야 하는 객체이고, 그 안에서 값을 꺼내려면 메서드를 호출해야 하니 한 단계를 더 거쳐야 한다.
이러한 이유로 성능이 중요한 상황에서는 옵셔널이 맞지 않을 수 있다.
박싱된 기본 타입을 담는 옵셔널은 기본 타입 자체보다 무거울 수밖에 없다.
그래서 int, long, double 전용 옵셔널 클래스인 OptionalInt, OptionalLong, OptionalDouble가 있다.
이렇게 대체재가 있으므로 박싱된 기본 타입을 담은 옵셔널을 반환하는 일은 없도록 하자!
마지막으로 옵셔널을 맵의 값으로 사용하면 절대 안 된다.
- 키 자체가 없는 경우
- 키는 있지만 그 키가 속이 빈 옵셔널인 경우
위의 두 가지 경우로 인해 오류 가능성을 키울 뿐이다.
옵셔널을 컬렉션의 키, 값, 원소나 배열의 원소로 사용하는 게 적절한 상황은 거의 없다.
값을 반환하지 못할 가능성이 있고, 호출할 때마다 반환값이 없을 가능성을 염두에 둬야 하는 메서드라면 옵셔널을 반환해야 할 상황일 수 있다. 하지만 옵셔널 반환에는 성능 저하가 뒤따르니, 성능에 민감한 메서드라면 null을 반환하거나 예외를 던지는 편이 나을 수 있다. 그리고 옵셔널을 반환값 이외의 용도로 쓰는 경우는 매우 드물다.