Language/Java

[effective java] 아이템 55. 옵셔널 반환은 신중히 하라.

JOYERIM 2023. 7. 17. 07:17

 

 

 

 

 

 

 

핵심 정리

 

값을 반환하지 못할 가능성이 있는 메소드라면 옵셔널을 반환하는 것을 고려해보자. 그러나 옵셔널 반환에는 성능 저하가 따를 수 있으니, 성능에 민감한 경우 null을 반환하거나 예외를 던지는 것이 나을 수 있다. 또한, 옵셔널을 반환값 이외의 용도로 쓰는 경우는 매우 드물다.

 

 

 

 

 

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

 

방법 1 : 예외 처리
👉
좋지 않은 방법

 

  • 예외는 정말 예외적인 상황에서만 사용해야 한다.(item 69)
  • 메소드의 정상적인 동작 흐름을 중단시킨다.
  • 스택 추적을 생성하므로 성능 측면에서 비용이 발생한다.

 

방법 2 : 반환 타입이 객체 참조라면, null 반환
👉
좋지 않은 방법

 

  • null 체크를 해야 하는 코드를 추가적으로 작성해야 한다.
  • 괴로운 NullPointerException이 발생할 수 있다.

 

예시 : Collection에서 최댓값 찾기 (예외 처리)

 

// 컬렉션에서 최댓값을 구한다(컬렉션이 비엇으면 예외를 던진다)
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이 발생한다.

 

 

 

 

 

메소드가 특정 조건에서 값을 반환할 수 없을 때
: 자바 8 이후 Optional<T> 등장

 

Optional<T>

 

  • null이 아닌 T 타입 참조를 하나 담거나 아무것도 담지 않는다.
  • 최대 원소를 1개 가질 수 있는 불변 컬렉션이다.
    • Optional<T>가 Collection<T>를 구현하진 않았지만, 원칙적으로 그렇다는 말이다.
  • 예외 처리 메소드보다 유연하고 사용하기 쉽다.
  • null을 반환하는 메소드보다 오류 가능성이 작다.

 

 

예시 : Collection에서 최댓값 찾기 (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()
    • 값이 든 옵셔널을 반환한다.
    • null을 넣으면 NulPointerException이 발생한다.
  • Optional.ofNullable()
    • null 값도 허용하는 옵셔널이다.

 

🚨 주의해야 할 점

옵셔널을 반환하는 메소드에서는 절대 null을 반환하지 말자.

Optional의 주된 취지는 메소드가 반환하는 값이 null일 가능성을 명확하게 표현하고, 그 결과로 발생할 수 있는 NPE를 미연에 방지하는 것이다.

 

예시 : Collection에서 최댓값 찾기 (스트림 사용 및 옵셔널 반환)
// 컬렉션에서 최댓값을 구해 Optional<E>로 반환한다. - 스트림 버전
public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
  return c.stream().max(Comparator.naturalOrder());

 

 

 

 

 

Optional의 메소드를 통해 값이 없을 경우의 처리를 명시적으로 정의할 수 있다.
: Optional.orElse(), Optional.orElseGet(), Optional.orElseThrow()

 

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

 

Optional.orElseThrow()
// 옵셔널 활용 2 - 원하는 예외를 던질 수 있다.
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
  • 위 예시는 예외 객체를 직접 전달하는 것이 아니라, 예외를 생성하는 함수를 전달하였다.
    • 따라서 실제로 예외가 필요한 상황이 될 때까지 예외 객체를 생성하는 비용을 피할 수 있다.
      • 👉 예외 생성 비용 절약

 

Optional.get()
// 옵셔널 활용 3 - 항상 값이 채워져 있다고 가정한다.
Element lastNobleGas = max(Elements.NOBLE_GASES).get();
  • Optional 객체가 감싸고 있는 값을 반환한다.
    • 위 예시는 Optional<Element>를 반환한다.
  • 만약 Optional이 값을 가지고 있지 않다면, NoSuchElementException이 발생한다.
  • 따라서 Optional이 반드시 값을 가지고 있음을 확신할 때만 사용해야 한다.
    • 그렇지 않다면 Optional.orElse(), Optional.orElseGet(), Optional.orElseThrow() 등의 메소드를 사용하여 값이 없을 때의 처리를 명시적으로 지정하는 것이 좋다.

 

Optional.orElseGet()
: 기본값을 설정하는 비용이 큰 경우 유용하다.

 

 

  • Optional에 값이 없을 때만 Supplier<T> 인터페이스의 get() 메소드를 호출한다.
  • Supplier<T> 인터페이스는 Java8에서 추가된 함수형 인터페이스로, 어떠한 인자를 받지 않지만 T 타입의 결과를 반환하는 get() 메소드를 정의하고 있다.
// orElseGet 예시
Optional<String> optionalValue = getOptionalValue(); // getOptionalValue() 메소드는 Optional<String>을 반환

// optionalValue가 값이 있으면 그 값을 사용하고, 없으면 "default"를 사용
String value = optionalValue.orElseGet(() -> "default");
  • 위 예시는 값이 없을 때만 비싼 연산을 수행하도록 할 수 있으므로, 불필요한 연산을 줄일 수 있다.
  • 단순한 기본값이 아니라 복잡한 연산을 수행해야 하는 경우, orElseGet 메소드를 사용하여 기본값을 생성하는 연산을 지연시키는 것이 좋다.

 

기타 메소드 - Optional.filter()

Optional 값을 검사하고, 특정 조건을 만족하는 값만을 유지하는 데 사용

 

기타 메소드 - Optional.map()

 

Optional 값을 변환하는 데 사용

 

기타 메소드 - Optional.flatMap()

Optional의 값을 변환하고, 이 결과를 Optional로 감싸는 연산을 하나의 메소드 호출로 합치는데 사용

 

기타 메소드 - Optional.ifPresent()

  • Optional 인스턴스가 값을 가지고 있으면 true, 아니면 false를 반환한다.
  • 대부분의 경우 isPresent를 사용하는 것보다 orElse, orElseGet 등의 메소드를 사용해 처리하는 것이 더 깔끔하고 효율적이다.

 

예제 - isPresent보다 orElse, orElseGet 등의 메소드를 사용하는 것이 더 깔끔하고 효율적
// isPresent 활용
Optional<ProcessHandle> parentProcess = ph.parent(); // 자바 9에 추가된 ProcessHandle 클래스
System.out.println("부모 PID: " + (parentProcess.isPresent() ?
  String.valueOf(parentProcess.get().pid()) : "N/A"));
  • 위 예시의 ph.parent() 메소드는 부모 프로세스의 ProcessHandle을 Optional로 감싸서 반환한다.
  • Optional이 값을 가지고 있으면, 해당 프로세스의 PID를 출력하고, 그렇지 않으면 "N/A"를 출력한다.
// map 활용
System.out.println("부모 PID: " +
            ph.parent().map(h -> String.valueOf(h.pid())).orElse("N/A"));
  • ph.parent().map(h -> String.valueOf(h.pid())
    • 부모 프로세스의 PID를 문자열로 변환한 결과를 담은 새로운 Optional을 생성한다.
  • orElse("N/A")
    • Optional이 값을 가지고 있으면 그 값을 반환하고, 그렇지 않으면 "N/A"를 반환한다.
  • isPresent()와 get()를 사용하는 것보다 map()과 orElse() 메소드를 함께 사용하면 더 간결하고 명확해진다.

 

 

 

 

 

 

Stream과 Optional을 함께 사용할 경우

 

예시 1 - Java 8 이후
// Stream<Optional<T>> 타입의 스트림이 있을 때, 이 스트림에서 채워진 Optional의 값을 추출
streamOfOptionals
  .filter(Optional::isPresent)  // 스트림에서 값이 존재하는 옵셔널만 선택
  .map(Optional::get);   // 선택된 옵셔널에서 값을 추출하여 새로운 스트림 생성

 

예시 2 - Java 9 이후(Optional.stream())
  • 자바 9 이후 이러한 작업을 더 간편하게 할 수 있는 Optional.stream() 메소드가 추가되었다.
  • 이 메소드는 옵셔널을 스트림으로 변환하는 어댑터로, 옵셔널에 값이 있으면 그 값을 원소로 담은 스트림을, 값이 없다면 빈 스트림을 반환한다.
// 예시1을 Optional.stream() 메소드를 이용해 단순화
streamOfOptionals
  .flatMap(Optional::stream);
  • flatMap(Optional::stream)
    • 스트림의 각 옵셔널을 스트림으로 변환 후, 이 스트림들을 합쳐 하나의 스트림으로 생성한다.
    • 값이 없는 옵셔널은 빈 스트림으로 변환되므로 결과 스트림에는 채워진 옵셔널의 값만이 포함된다.

 

 

 

 

 

 

Optional 주의사항
: 잘못 사용할 경우 코드 복잡성이 커지고 예상치 못한 문제가 발생할 수 있다.

 

컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입을 옵셔널로 감싸면 안 된다.(item 54)
  • 컨테이너 자체가 값이 없을 때를 표현하는 방법이 존재하기 때문이다.
  • 예를 들어, 컬렉션은 빈 컬렉션을, 배열은 길이가 0인 배열을 반환하여 값이 없음을 표현할 수 있다.
  • 이런 경우에 Optional을 사용하면 불필요하게 코드가 복잡해진다.

 

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

 

박싱된 기본 타입을 담은 옵셔널을 반환하지 말자.
  • 기본 타입을 박싱한 후에 이를 다시 Optional로 감싸면, 필요 이상의 메모리와 성능 손실이 발생할 수 있다.
  • 기본 타입을 옵셔널로 감싸야 할 경우에는 OptionalInt, OptionalLong, OptionalDouble 등의 전용 클래스를 사용하는 것이 좋다.
  • Boolean, Byte, Character, Short, Float는 예외이다.

 

옵셔널을 키, 값, 원소, 배열의 원소로 사용하면 안 된다.
  • Optional을 맵의 값으로 사용하면 키가 없다는 것을 표현하는 방법이 두 가지가 되어 복잡성이 커지며, 오류 가능성이 높아진다.
  • 마찬가지로, Optional을 컬렉션의 원소나 배열의 원소로 사용하는 것도 좋지 않다.

 

인스턴스 필드로 Optional을 사용하는 예외적인 경우
  • Optional은 반환 타입으로 주로 사용되며, 일반적으로 인스턴스 필드로 사용하는 것은 권장되지 않는다.
    • cuz 메모리 소비와 성능 이슈
  • 한 가지 예외적인 경우는 선택적 필드를 가진 클래스의 경우 Optional을 사용하는 것이 더 바람직할 수 있다.
import java.util.Optional;

public class NutritionFacts {
    private final int servingSize;  // (필수)
    private final int servings;     // (필수)
    private final Optional<Integer> calories;      // (선택)
    private final Optional<Integer> fat;           // (선택)
    private final Optional<Integer> sodium;        // (선택)

    public static class Builder {
        // 필수 매개변수
        private final int servingSize;
        private final int servings;

        // 선택 매개변수 - 기본값으로 초기화
        private Optional<Integer> calories = Optional.empty();
        private Optional<Integer> fat      = Optional.empty();
        private Optional<Integer> sodium   = Optional.empty();

        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }

        public Builder calories(int val) {
            calories = Optional.of(val);
            return this;
        }

        public Builder fat(int val) {
            fat = Optional.of(val);
            return this;
        }

        public Builder sodium(int val) {
            sodium = Optional.of(val);
            return this;
        }

        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }

    private NutritionFacts(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
    }

    // getter 메소드
    public Optional<Integer> getCalories() {
        return calories;
    }

    // 기타 getter 메소드
}