옵셔널 반환은 신중히 하라

이진호·2022년 9월 5일
0

Effective Java

목록 보기
5/11
post-thumbnail

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

자바 8 이전) 메서드가 특정 값을 반환할 수 없는 경우 처리방식과 문제점

1. 예외를 던져서 처리.

예외는 진짜 예외적인 상황에서만 사용해야 합니다.(Item 69)

2. (반환 타입이 객체 참조일 경우) null을 반환

별도의 null 처리 코드가 필요하고, 이를 무시할 경우 NullPointerException 발생 위험이 있습니다.

자바 8 이후) Optional을 반환

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

옵셔널은 원소를 최대 1개 가질 수 있는 '불변' 컬랙션입니다.

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

옵셔널을 반환하는 메서드는 예외를 던지는 메서드보다 유연하고 사용하기 쉬우며, null 을 반환하는 메서드보다 오류 가능성이 적습니다.

AS-IS

// 컬렉션에서 최댓값을 구한다. - 컬렉션이 비었으면 예외를 던진다.
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;
}

TO-BE

// 컬렉션에서 최댓값을 구해 Optional<E>로 반환한다.
public static <E extends Comparable<E>>
        Optional<E> max(Collection<E> c) {
    if (c.isEmpty())
        return Optional.empty(); // 정적 팩터리 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.of(value) 사용
}

Optional.of(value)에 null을 넣으면 NullPointerException을 반환하게 됩니다.

옵셔널을 반환하는 메서드에서는 절대 null을 반환하지 말자. 이는 옵셔널을 도입한 취지를 완전히 무시하는 행위입니다.

스트림의 종단 연산 중 상당수가 옵셔널을 반환하는데. 위의 예제를 아래와 같이 개선 가능합니다.

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

옵셔널 반환을 선택해야 하는 기준

옵셔널은 검사 예외와 취지가 비슷합니다. (Item 71)

즉, 반환값이 없을 수도 있음을 API 사용자에게 명확히 알려줘야 하는 경우 사용하는 것이 좋습니다.

옵셔널에 대한 클라이언트의 활용

기본값을 설정

// 옵셔널 활용 1 - 기본값을 정해둘 수 있다.
String lastWordInLexicon = max(words).orElse("단어 없음...");

이따금 기본값을 설정하는 비용이 커서 부담이 될 수 있는데, 이 경우 Supplier<T>를 인수로 받는 orElseGet을 사용하면, 초기 설정 비용을 낮출 수 있습니다.

상황에 맞는 예외를 던지기

// 옵셔널 활용 2 - 원하는 예외를 던질 수 있다.
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);

항상 값이 채워져 있다고 확신하고 바로 사용하기

// 옵셔널 활용 3 - 항상 값이 채워져 있다고 가정한다.
Element lastNoBleGas = max(Elements.NOBLE_GASES).get();

잘못 판단했을 경우 NoSuchElementException이 발생하게 됩니다.

이외에도 filter, map, flatMap, ifPresent 등의 메서드가 존재

적합한 메서드를 찾지 못했다면 isPresent 메서드를 활용. 그러나 isPresent를 쓴 코드 중 상당수는 앞서 언급한 메서드들로 대체할 수 있습니다.

// 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"));

스트림과 함께 활용

스트림을 사용한다면 옵셔널들을 Stream<Optional<T>>로 받아서, 그중 채워진 옵셔널들에서 값을 뽑아 Stream<T>에 건너 담아 처리할 수 있습니다.

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

자바 9에서는 Optional에 stream() 메서드가 추가되었습니다. 이 메서드는 Optional을 Stream으로 변환해주는 어댑터로, 옵셔널에 값이 있으면 그 값을 원소로 담는 스트림으로, 값이 없다면 빈 스트림으로 변환합니다.

flatMap 메서드(Item 45)와 조합하면 앞의 코드를 다음처럼 명료하게 바꿀 수 있습니다.

streamOfOptionals
    .flatMap(Optional::stream)

반환값으로 옵셔널을 사용하면 안되는 경우

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

Optional<List<T>>를 반환하기보다는 빈 List<T>를 반환하는 게 좋습니다. (Item 54).

(예외로 ProcessHandle.Info 인터페이스의 arguments 메서드는 Optional<String[]>를 반환합니다.)

옵셔널 사용의 기본 규칙

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

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

박싱된 기본 타입을 담는 옵셔널은 값을 두 겹이나 감싸기 때문에 기본 타입 자체보다 무거울 수밖에 없습니다. 그래서 자바 API 설계자는 int, long, double 전용 옵셔널 클래스인 OptionalInt, OptionalLong, OptionalDouble을 준비해두었습니다. 따라서 박싱된 기본 타입을 담은 옵셔널을 반환하는 일은 없도록 하는 것이 좋습니다. (덜 중요한 기본 타입인 Boolean, Byte, Character, Short, Float은 예외)

옵셔널을 반환 이외의 쓰임으로 사용하는 것은 대부분 적절하지 않습니다. 옵셔널을 컬렉션의 키, 값, 원소나 배열의 원소로 사용하는 게 적절한 상황은 거의 없습니다.

옵셔널을 인스턴스 필드에 저장해두는 경우도 대부분 필수 필드를 갖는 클래스와, 이를 확장해 선택적 필드를 추가한 하위 클래스를 따로 만들어야 함을 암시하는 '나쁜 냄새'에 해당합니다.

핵심 정리

값을 반환하지 못할 가능성이 있고, 호출할 때마다 반환값이 없을 가능성을 염두에 둬야하는 메서드라면 옵셔널을 반환해야 할 상황일 수 있다. 하지만 옵셔널 반환에는 성능 저하가 뒤따르니, 성능에 민감한 메서드라면 null을 반환하거나 예외를 던지는 편이 나을수 있다. 그리고 옵셔널을 반환값 이외의 용도로 쓰는 경우는 매우 드물다.

출처

0개의 댓글