DEV ℧ Developer Diary

[EffectiveJava] item55 - 옵셔널 반환은 신중히 하라

자바 8 전에는 메서드가 특정 조건에서 값을 반환할 수 없을때 null을 던지거나 예외를 던지는 방식으로 처리를 진행했다. 하지만 각각의 문제점이 존재했다.

  • null로 처리할 경우 : null을 반환하는 메서드를 처리할 때, null을 처리하는 로직이 필수적으로 들어가야 하며, 이를 놓칠경우 NullPointerException 예외가 발생
  • 예외로 처리할 경우 : 예외는 진짜 예외적인 상황에서 사용해야 하며, 예외를 생성할 때 스택 추적 전체를 캡처하므로 비용이 만만치 않다.

옵셔널 (Optional)

옵셔널 (Optional)
자바8에서 도입된 API로 NullPointerException 예외의 문제를 해결하기 위해 Nullable한 객체를 만들때 사용한다.

자바 8이후 부터 또 하나의 선택지가 생겼다. Optional<T>는 null이 아닌 T타입의 참조를 하나 담거나, 혹은 아무것도 담지 않을 수 있다.
옵셔널은 원소를 최대 1개 가질 수 있는 ‘불변’ 컬렉션이다. Optional<T>Collection<T>를 구현하지는 않았지만, 원칙적으로는 그렇다는 말이다.

옵셔널의 사용법

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

그러면 유효한 반환값이 없을 때는 빈 결과를 반환하는 메서드가 만들어진다. 옵셔널을 반환하는 메서드는 예외를 던지는 메서드보다 유연하고 사용하기 쉬우며, null을 반환하는 메서드보다 오류 가능성이 작다.

아래는 CollectionOptional로 반환하는 예제를 나타낸다.

컬렉션에서 최댓값을 구한다(컬렉션이 비었으면 예외를 던진다).

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을 사용한 예시코드는 아래와 같다.

컬렉션에서 최댓값을 구해 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.empty(), 값이 든 옵셔널은 Optional.of(value)

하지만 Optional.of(value)에 null을 넣으면 NullPointerException을 던지니 주의하자. null 값도 허용 하는 옵셔널을 만드려면 Optional.ofNullable(value)를 사용하면 된다.

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

Stream API 에서의 옵셔널

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

앞의 max 메서드를 스트림 버전으로 다시 작성한다면 Stream이 max 연산에 필요한 옵셔널을 생성해줄것이다(비교자를 명시적으로 전달해야 한다).

컬렉션에서 최댓값을 구해 Optional<E>로 반환한다. - 스트림버전

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

옵셔널 반환 선택의 기준은

null을 반환하거나 예외를 던지는 대신 옵셔널 반환을 선택해야 하는 기준은 무엇일까? 옵셔널은 검사 예외와 취지가 비슷하다.

즉, 반환값이 없을 수도 있음을 API 사용자에게 명확히 알려준다. 메서드가 옵셔널을 반환한다면 클라이언트는 값을 받지 못했을 때 취할 행동을 선택해야 한다.

옵셔널의 활용

옵셔널을 반환받아 사용할때 옵셔널의 활용법은 여러가지가 있는데, 앞서 max메서드를 사용했을 때를 가정하고 간략하게 살펴보도록 하자.

기본값을 정해둘 수 있다.

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

원하는 예외를 던질 수 있다.

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

예외 팩터리를 건넴으로써, 예외가 실제로 발생하지 않는 한 예외 생성 비용은 들지 않는다.

항상 값이 채워져 있다고 가정한다.

Element lastNobleGas = max(Elements.NOBLE_GLASS).get();

항상 값이 채워져 있을경우 곧바로 값을 꺼내 사용할 수 있다. 다만 내부에 값이 없을 경우 NoSuchElementException이 발생한다.

값이 채워져있는 여부를 찾을때

Optional<ProcessHandle> parentProcess = ph.parent();
System.out.println("부모 PID: " + (parentProcess.isPresent() ?
    String.valueOf(parentProcess.get().pid() : "N/A")))

옵셔널 내부에 값이 있는지 여부를 판별할 수 있다.

이따금 기본값을 설정하는 비용이 아주커서 부담이 될 경우 Supplier<T>를 인수로 받는 Optional.orElseGet()을 사용하면, 값이 처음 필요할 때 Supplier<T>를 사용해 생성하므로 초기 설정 비용을 낮출 수 있다.

그외 filter, map, flatMap, ifPresent 등 고급메서드를 통해서 앞선 옵셔널 객체를 처리할 수 있다.

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)

flatMap을 활용한 방법

자바 9에서는 Optionalstream() 메서드가 추가 되었다. 옵셔널을 스트림을 변환해주는 어댑터이다. flatMap을 사용하면 옵셔널에 값이 있으면 그 값을 담은 원소로 담은 스트림으로, 값이 없다면 빈스트림을 반환하는 로직을 작성할 수 있다.

streamOfOptionals
    .flatMap(Optional::stream)

옵셔널의 주의할 점

반환값으로 옵셔널을 사용한다고 해서 무조건 득이 되는 건 아니다. 컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입은 옵셔널로 감싸면 안 된다.

Optional<List<T>>를 반환하기 보다는 빈 List<T>를 반환하는게 좋다. (Item54)

또한 다른 쓰임에서는 대부분 적절하지 않으므로 자제하는 것이 좋다.

예를 들면, 옵셔널을 맵의 값으로 사용하면 절대 안된다. 만약 그리 한다면 맵안에 키가 없다는 사실을 나타내는 방법이 두가지가 된다.

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

언제 옵셔널을 사용하면 좋을까

그렇다면 어느 경우에 메서드 반환 타입을 T 대신 Optional<T>로 선언해야 할까?

옵셔널의 기본 규칙 : **결과가 없을 수 있으며, 클라이언트가 이 상황을 특별하게 처리해야 한다면 Optional를 반환한다.**

결국 Optional도 엄연히 새로 할당하고 초기화해야 하는 객체고, 그 안에서 값을 꺼내려면 메서드를 호출해야 하니 한 단계를 더 거치는 셈이다. 그래서 성능을 세심히 측정하고, 옵셔널이 맞는지 유무를 판단하는 것이 좋다.

박싱된 옵셔널의 사용

박싱된 기본타입을 담는 옵셔널은 기본 타입 자체보다 무거울 수밖에 없다. 그래서 아래와 같은 전용 옵셔널을 사용한다.

  • int : OptionalInt
  • long : OptionalLong
  • double : OptionalDouble1

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

단 ‘덜 중요한’ 그외 기본타입인 Boolean, Byte, Character, Short, Float은 예외일 수 있다.

필드로 사용하는 옵셔널?

옵셔널을 인스턴스 필드에 저장해두는 게 필요할 때가 있을까? 이런 상황 대부분은 필수 필드를 갖는 클래스와. 이를 확장해 선택적 필드를 추가한 하위 클래스를 따로 만들어야 함을 암시하는 “나쁜냄새”이다.