[이펙티브 자바 아이템 55] 옵셔녈 반환은 신중히 하라.

박상준·2024년 6월 29일
0

이펙티브 자바

목록 보기
15/19
post-custom-banner

배경

  • Java 8 이전의 특정 조건에서 값을 반환할 수 없는 경우
    1. 예외 던지기

    2. null 반환 (객체 참조 케이스의 경우)

      2가지가 존재했다.

  • 하지만 2가지 방식보다 문제가 있었고, 자바8 에서 Optional 이라는 새로운 선택지가 추가되었다.

문제점

  1. 예외
    1. 예외는 진짜 예외적인 상황에서만 사용해야함
    2. 스택 추적 전체를 캡처하므로 비용이 높다.
  2. null 반환
    1. 별도의 null 처리 코드가 필요하다
    2. NPE 발생의 가능성이 존재한다.

Java8 에서의 해결책: Optional

  • null 이 아닌 T 타입 참조를 담거나, 아무것도 담지 않을 수 있다.
  • 최대 1개의 원소를 가질 수 있는 불변 컬렉션이다.

Optional 사용 방법

  1. 메서드 반환 타입으로 Optional<T> 사용
    1. T를 반환해야 하지만 특정 조건에서 아무것도 반환하지 않아야 할 때 유용하다.
  2. Optional 생성 방법
    1. 빈 Optional : Optional.empty()
    2. 값이 있는 Optional
      • Optional.of(value)
    3. null 을 허용하는 Optional
      • Optional.ofNullable(value)

코드 예제

  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;
    }
  2. Optional 을 사용한 방식

    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 사용 이점

  • 예외를 던지는 메서드보다 유연하고 사용하기가 쉽다
  • null 을 반환하는 메서드보다 오류의 가능성이 적다.
  • 반환될 값이 없을 수 있다는 점을 API 의 사용자에게 명확하게 알려준다.

주의사항

  • Optional 을 반환하는 메서드에서 절대 null 을 반환하지 말아야 한다.
    • Optional 의 도입 취지에 반하는 행위임.
  • Optional.of(value) 에 null 을 넣으면 NPE 가 발생한다.
    • null 값을 허용하려면 Optional.ofNullable(value) 를 사용해야한다.

Optional 고급 사용법

스트림과 Optional

  • 스트림의 종단 연산 중 상당수가 Optional 을 반환한다.
  • 예를 들어 Streammax 연산을 사용하는 경우 Optional 을 쉽게 얻을 수 있다.
public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
    return c.stream().max(Comparator.naturalOrder());
}
Optional<String> maxed = collection.stream().max(String::compareToIgnoreCase);

Optional 사용기준

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

Optional 활용 방법

  1. 기본값의 설정

    String lastWordInLexicon = max(words).orElse("단어 없음...");
  2. 예외 던지기

    Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
  3. 항상 값이 존재한다고 가정하는 경우

    Element lastNobleGas = max(Elements.NOBLE_GASES).get();
  4. 지연 초기화

    • orElseGet 을 사용한다.
    Optional<ExpensiveObject> optionalExpensive = ...;
    ExpensiveObject result = optionalExpensive.orElseGet(() -> createExpensiveObject());
    • 보통 기본값 설정 비용이 큰 경우 orElseGet 을 사용하는데
      • 기본값 객체를 생성하는데, 많은 시간이 소요되는 경우

      • 메모리를 많이 사용하는 큰 객체를 생성해야하는 경우

      • 네트워크 호출이나 DB 조회 등 비용이 많이 드는 연산이 필요한 경우

        를 말한다

    • orElseGet 메서드의 경우 Supplier<T> 를 인수로 받는다.
    • 값이 처음 필요할 때 Supplier 를 사용하여 기본값을 생성하기에, 초기 설정 비용을 낮출 수 있다는 장점이 있다.
      • Supplier 는 함수형 인터페이스로서, orElseGet 에서 필요할때( 즉, Optional 이 비어있는 경우에만 ) 기본값을 계산한다.
      • orElse 의 경우에는 Optional 이 비어있든 말든 항상 메서드 호출시점에 이미 평가되는데, 이는 Java 메서드 호출 규칙에 따른 내용이다.
        • 메서드에서 인자를 전달시 호출전에 이미 평가가 되어 있다
      • 반면, orElseGet 은 인자로 함수형 인터페이스를 받기에, Optional 이 없다고 판단된 순간에 지연평가(Lazy evaluation) 을 통하여 필요한 경우에만 기본값 계산 로직이 실행된다.
      • 내부적으로
            public T orElseGet(Supplier<? extends T> supplier) {
                return value != null ? value : supplier.get();
            }
        
            // 비교를 위한 orElse 메서드
            public T orElse(T other) {
                return value != null ? value : other;
            }
        • 이런 형식으로 구성이 되어 있는데,
          • Optional 값이 null 이 아닌 경우에는 supplier 만 전달하지.. supplier.get() 을 기본값 설정 메서드를 실행하지 않는 것임.

Optional 고급 메서드

  • filter
  • map
  • flatMap
  • ifPresent 가 있다.

isPresent 메서드

  • 안전 밸브 역할을 하는 메서드
  • 옵셔널이 채워져 있으며 true, 비어 있으면 false 를 반환한다
  • 모든 작업 수행이 가능하지만 신중한 사용이 필요하다
  • 많은 경우 다른 메서드로 대체 가능하고, 코드가 더 간결하고 명확해진다.
    // 기존 코드
    Optional<ProcessHandle> parentProcess = ph.parent();
    System.out.println("부모 PID: " + (parentProcess.isPresent() ? 
        String.valueOf(parentProcess.get().pid()) : "N/A"));
    
    // 개선된 코드 (map 사용)
    System.out.println("부모 PID: " + 
        ph.parent().map(h -> String.valueOf(h.pid())).orElse("N/A"));

스트림에서의 Optional 처리

    public static void main(String[] args) {
        // 샘플 데이터 생성
        List<Optional<String>> streamOfOptionals = Arrays.asList(
                Optional.of("Hello"),
                Optional.empty(),
                Optional.of("World"),
                Optional.ofNullable(null),
                Optional.of("Java")
        );
        
        // Java 8 방식
        System.out.println("Java 8 방식:");
        Stream<String> resultJava8 = streamOfOptionals.stream()
                .filter(Optional::isPresent)
                .map(Optional::get);
        
        resultJava8.forEach(System.out::println);
        
        // Java 9 이후 방식
        System.out.println("\nJava 9 이후 방식:");
        Stream<String> resultJava9 = streamOfOptionals.stream()
                .flatMap(Optional::stream);
        
        resultJava9.forEach(System.out::println);
    }
  • 차이점
    1. 간결해짐
      1. Java9 방식이 더 코드적으로 간결
    2. 성능
      1. Java 9 의 경우 filter 와 map 두번의 순회대신에 한번의 flatMap 으로 처리됨
  • 사실 책에는 이런식으로 적혀 있지만,
    • Optional 값을 리스트로 들고있는 케이스를 한번도 보지 못해서,, 모르겠다.

Optional 사용시 주의

컨테이너 타입(컬렉션, 스트림, 배열, 옵셔널 등) 은 옵셔널로 감싸지 않는 것이 좋다.

  • Optional<List> 대신 빈 List 반환을 권장한다.
    • 리스트의 empty 와 Optional 의 empty 의 개념이 중복된다
      - 리스트가 비어 있어도 결국 컬렉션 객체는 존재하는데,
      - Optional 은 리스트가 비어 있더라도, 객체는 존재하기에, 존재함을 표현하게 된다.

      개인적으로는 LIst<Optional> 도 좋지 못한 케이스라고 생각한다.

    • List 가 비어있음을 나타내면 그만이지.. 내부 타입이 비어있다의 중첩된 사고를 해야하기에 상당히 코드 읽기가 피곤하다.

  • 클라이언트의 옵셔널 처리 코드를 최소화할 수 있다.
profile
이전 블로그 : https://oth3410.tistory.com/
post-custom-banner

0개의 댓글