[Item 55] 옵셔널 반환은 신중히 하라

2022. 7. 30. 18:10JAVA/Effective Java

메서드가 특정 조건에서 값을 반환할 수 없을 때

자바 8 이전 

  • 예외를 던진다.
    • 예외는 진짜 예외적인 상황에서만 사용해야 하고, 예외 생성 시 스택 추적 전체를 캡처하므로 비용도 만만치 않다.
  • (반환 타입이 객체 참조라면) null을 반환한다.
    • 반환된 null 값을 어딘가에 저장해두면 언젠가 NullPointerException이 발생할 가능성이 있다. 

자바 8 이후

 Optional<T>

  • null이 아닌 T타입 참조를 하나 담거나, 혹은 아무것도 담지 않을 수 있다.
  • Optional은 원소를 최대 1개 가질 수 있는 '불변' 컬렉션이다.
  • 옵셔널을 반환하는 메서드는 예외를 던지는 메서드보다 유연하고 사용하기 쉬우며, null을 반환하는 메서드보다 오류  가능성이 적다.
// 코드 55-1 컬렉션에서 최댓값을 구한다. - 컬렉션이 비었으면 예외를 던진다. (327쪽)
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;
}

위 코드는 빈 컬렉션을 건네면 IllegalArgumentException을 던진다.

 

Optional <E>를 반환하면 어떻게 될까?

다음 코드를 보자.

// 코드 55-2 컬렉션에서 최댓값을 구해 Optional<E>로 반환한다. (327쪽)
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()로 만들고, 값이 든 옵셔널은 Optional.of(value)로 생성했다.

Optional.of(value)에 null을 넣으면 NullPointerException을 던진다.

null 값도 허용하는 Optional을 만들려면 Optional.ofNullable(value)를 사용하면 된다.

 

Optional을 반환하는 메서드에서는 null을 반환하지 말자. 도입한 취지를 무시하는 행위이다. 

 

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

앞의 max 메서드를 스트림 버전으로 다시 작성한다면 Stream의 max 연산이 Optional을 생성해 준다. 

예시는 다음과 같다.

// 코드 55-3 컬렉션에서 최댓값을 구해 Optional<E>로 반환한다. - 스트림 버전 (328쪽)
public static <E extends Comparable<E>>
Optional<E> max(Collection<E> c) {
    return c.stream().max(Comparator.naturalOrder());
}

Optional은 검사 예외와 취지가 비슷하다. 반환 값이 없을 수도 있음을 API 사용자에게 명확히 알려준다.

검사 예외를 던지면 클라이언트에서는 반드시 이에 대처하는 코드를 작성해야 한다.

 

그렇다면 클라이언트에서는 어떻게 대처해야 될까?

 

  • 방법 1. 기본 갑을 정해둘 수 있다.
String lastWordinLexion = max(words).orElse("단어없음x");
  • 방법 2. 원하는 예외를 던질 수 있다.
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);

실제 예외가 아닌 예외 팩토리를 건넸다. 이렇게 하면 예외가 실제로 발생하지 않는 한 예외 생성 비용은 들지 않는다.

  • 방법 3. 항상 값이 채워져 있다고 가정한다.
Element lastNobleGas = max(Elements.NOBLE_GASES).get();

Optional에 항상 값이 채워져 있다고 확신한다면 곧바로 값을 꺼내 사용하는 선택지도 있다.

다만 잘못 판단한 거라면, NosuchElementException이 발생할 것이다.

 

기본값 설정 비용이 커서 부담스럽다면?

Supplier <T>를 인수로 받는 orElseGet를 사용하면 된다.

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

 

isPresent()

안전 밸브 역할의 메서드이다. (Optional이 채워져 있다면 true, 비어 있다면 false 리턴)

이 메서드로는 원하는 모든 작업을 수행할 수 있지만 사용에 있어 신중해야 한다.

 

실제 isPresent()를 쓴 코드 중 상당수는 앞서 언급한 메서드들로 대체할 수 있으며, 그렇게 하면 더 짧고 명확하고 용법에 맞는 코드가 된다. 

 

  • 예시 :: 부모 프로세스의 프로세스 ID를 출력하거나, 부모가 없다면 'N/A'를 출력하는 코드
package effectivejava.chapter8.item55;

import java.util.Optional;

// 불필요하게 사용한 Optional의 isPresent 메서드를 제거하자. (329쪽)
public class ParentPid {
    public static void main(String[] args) {
        ProcessHandle ph = ProcessHandle.current();

        // 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들을 Stream <Optional <T>>로 받아서, 그중 채워진 Optional들에 값을 뽑아 Stream <T>에 건네 담아 처리할 수 있다.

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

 

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

Optional을 Stream으로 반환해주는 어댑터이다. (Optional에 값이 있으면 그 값을 원소로 담은 스트림, 값이 없다면 빈 스트림) Stream의 flatMap 메서드와 조합하면 앞의 코드를 명료하게 바꿀 수 있다.

streamOfOptionals.flatMap(Optional::stream)

 

반환 값으로 Optional을 사용한다고 해서 무조건 득은 아니다!

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

참고로, ProcessHandle.Info 인터페이스의 arguments 메서드는 Optional <String []>을 반환하는데 예외적인 사항이니 따라 하면 안 된다.

 

T 대신 Optional <T>로 선언해야 될 때는 언제일까?

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

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

 

그래서,  int, long, double 전용 옵셔널 클래스들을 준비했다. 바로 OptionalInt, OptionalLong, OptionalDouble이다. 

이 옵셔널들도 Optional <T>가 제공하는 메서드를 거의 다 제공한다. 

 

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

물론, 덜 중요한 기본 타입인 Boolean, Byte, Character, Short, Float은 예외일 수 있다.

 

Optional의 쓰임

  • Optional을 Map의 값으로 사용하면 안 된다.
    • Map안에 key가 없다는 방식이 2가지가 되어서 쓸데없이 복잡성만 높고 혼란과 오류 가능성만 높다.
  • Optional을 Collection의 키, 값, 원소나 배열의 원소로 사용하는 게 적절한 상황은 거의 없다.

핵심정리

  • 값을 반환하지 못할 가능성이 있고, 호출할 때마다 반환 값이 없을 가능성을 염두해둬야 하는 메서드라면 Optional을 반환하면 된다.
  • Optional반환은 성능 저하가 뒤따르니, 성능에 민감한 메서드라면 Null을 반환하거나 예외를 던지는 편이 낫다.
  • Optional을 반환값 이외의 용도로 쓰는 경우는 매우 드물다.

참고자료

www.kyobobook.co.kr/product/detailViewKor.laf?ejkGb=KOR&mallGb=KOR&barcode=9788966262281&orderClick=LEa&Kc=

 

이펙티브 자바 3/E - 교보문고

프로그래밍인사이트 | 자바 6 출시 직후 출간된 『이펙티브 자바 2판』 이후로 자바는 커다란 변화를 겪었다. 그래서 졸트상에 빛나는 이 책도 자바 언어와 라이브러리의 최신 기능을 십분 활용

www.kyobobook.co.kr