Java/Effective Java 3E

[이펙티브자바 3판] ITEM55. 옵셔널 반환은 신중히 하라

잭피 2021. 1. 16. 12:56

이번장의 핵심은...

값을 반환하지 못할 가능성이 있고, 호출할 때마다 반환 값이 없을 가능성이 있는 메서드면 옵셔널을 반환해야 할 상황일 수 있습니다

하지만, 옵셔널 반환에는 성능 저하가 뒤따르니,

성능에 민감한 메서드라면 null을 반환하거나 예외를 던지는 편이 나을 수 있습니다

그리고 옵셔널을 반환값 이외의 용도로 쓰는 경우는 매우 드물다


자바 8 이전) 메서드가 특정 조건에서 값을 반환할 수 없을 때

1. 예외를 던집니다

→ 예외는 진짜 예외적인 상황에서만 사용해야 하고, 예외 생성 시, 스택 추적 전체를 캡쳐하므로 비용도 만만치 않습니다

 

2. (반환 타입이 객체 참조라면) null을 반환합니다 

→ 반환된 null 값을 어딘가에 저장해두면 언젠가 NullPointerException이 발생할 가능성이 있습니다

 

자바 8) 1가지 선택지 추가

Optional<T>

null이 아닌 T타입 참조를 하나 담거나, 혹은 아무것도 담지 않을 수 있습니다

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

 

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

 

예제)

// 코드 55-1 컬렉션에서 최댓값을 구한다. - 컬렉션이 비었으면 예외를 던진다
    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>로 반환한다
    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.of(value)에 null을 넣으면 NullPointerException을 던지니 주의합시다

null 값도 허용하는 옵셔널을 만들려면 Optional.ofNullable(value)를 사용합시다

→ 옵셔널을 반환하는 메서드에서는 절대 null을 반환하지 말아야 합니다

이건 옵셔널을 도입한 취지를 완전히 무시하는 행위입니다

 

스트림의 종단 연산 중 상당수가 옵셔널을 반환합니다

앞의 max 메서드를 스트림 버전으로 다시 작성한다면 Stream의 max 연산이 옵셔널을 생성해 줄 것입니다

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

클라이언트에서 어떻게 Optional을 활용하나?

기본값을 정해둘 수 있습니다

String lastWordInLexicon = max(words).orElse("단어없다");

원하는 예외를 던질 수 있습니다

예외가 실제로 발생하지 않는한 예외 생성 비용은 들지 않습니다

Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);

옵셔널에 항상 값이 채워져 있다고 확신한다면 바로 값을 꺼내 사용할 수 있습니다

(잘못 판단한 것이라면 NoSuchElementEeception 발생시킵니다)

Element lastNobelGas = max(Elements.NOBLE_GASES).get();

 

기본값 설정 비용이 커서 부담스러우면?

Supplier<T>를 인수로 받는 orElseGet을 사용합시다

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

 

 

isPresent()

안전 밸브 역할의 메서드입니다 -> 옵셔널이 채워져 있으면 true, 비어 있으면 false

이 메서드로는 원하는 모든 작업을 수행할 수 있지만 신중히 사용해야 합니다

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

 

ex) 부모 프로세스의 프로세스 ID를 출력하거나, 부모가 없다면 "N/A"를 출력하는 코드

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

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

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

 

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

Optional을 Stream으로 변환해주는 어댑터입니다 (옵셔널에 값이 있으면 그 값을 원소로 담은 스트림, 값이 없다면 빈 스트림)

Stream의 flatMap 메서드와 조합하면 앞의 코드를 명료하게 바꿀 수 있습니다

streamOfOptionals.flatMap(Optional::stream)

 

반환값으로 옵셔널을 사용한다고 무조건 득이 아님

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

→ 빈 Optional<List<T>>를 반환하기보다는 빈 List<T>를 반환하는 게 좋습니다

빈 컨테이너를 그대로 반환하면 클라이언트에 옵셔널 처리 코드를 넣지 않아도 됩니다

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

이렇게 하더라도 Optional<T>를 반환하는 데는 대가가 따릅니다

옵셔널도 엄연히 새로 할당하고 초기화해야 하는 객체이고, 그 안에서 값을 꺼내려면 메서드를 호출해야 하니 한 단계를 더 거치는 셈입니다

그래서 성능이 중요한 상황에서는 옵셔널이 맞지 않을 수 있습니다

 

박싱된 기본 타입을 담는 옵셔널은 기본 타입 자체보다 무거울 수 밖에 없다

값을 두 겹이나 감싸기 때문입니다

그래서 자바 API 설계자는 int, long, double 전용 옵셔널 클래스를 준비해둡니다

 

OptionalInt, OptionalLong, OptionalDouble

→ 박싱된 기본 타입을 옵셔널로 담지 말고 위의 클래스를 사용합시다

 

단, '덜 중요한 기본 타입' 용인 Boolean, Byte, Character, Short, Float는 예외입니다

 

옵셔널의 쓰임

옵셔널을 맵의 값으로 사용하면 절대 안 됩니다

→ 맵 안에 키가 없다는 방식이 2가지가 되어서 쓸데없이 복잡성만 높고 혼란과 오류 가능성만 높습니다

 

옵셔널을 컬렉션의 키, 값, 원소나 배열의 원소로 사용하는게 적절한 상황은 거의 없습니다

 

옵셔널을 인스턴스 필드에 저장해두는게 필요할 때가 있을까?

이런 상황은 대부분 필수 필드를 갖는 클래스와 이를 확장해 선택적 필드를 추가한 하위 클래스를 따로 만들어야 함을 암시하는 '나쁜 냄새'입니다

 

가끔 적절한 상황도 있습니다

인스턴스 필드 중 상당수는 필수가 아닙니다

또한 그 필드들은 기본 타입이라 값이 없음을 나타낼 방법이 마땅치 않습니다

→ 이러면 선택적 필드의 getter 메서드들이 옵셔널을 반환하게 해주면 좋습니다