[Effective Java 3/E] ITEM 55. 옵셔널 반환은 신중히 하라

    Effective Java 표지. Source : https://blog.insightbook.co.kr/

    *알림 : *
    Effective Java 3판은 Java 9까지 도입된 언어적 기능을 중심으로 서술되어 있습니다. 10버젼 이후의 Java 개발을 하시는 분들은 우회적인 접근법 대신 Java 언어 내 새로 도입된 기능이 더 간결하고 좋을 수 있습니다.

    해당 포스팅은 SSAFY 내 책읽기 스터디의 활동을 통해 작성된 포스팅입니다.
    https://github.com/kjsu0209/JavaBook
    https://medium.com/javabook

     

    JavaBook – Medium

    Documentation space of our book study.

    medium.com

     

    kjsu0209/JavaBook

    책읽기 스터디. Contribute to kjsu0209/JavaBook development by creating an account on GitHub.

    github.com

    만약 어떤 메서드가 값을 반환할 수 없으면, null 혹은 예외를 던지는 선택지가 있었다. null을 반환하는 것의 문제점은 ITEM 54에서 다뤘고, 예외를 남발하면 예외가 예외적인 상황으로 받아들여지지 않으며 Stack tracing에 많은 오버헤드를 지불하여야 한다.

    Optional의 등장

    Java 8부터 Optional을 지원하는데, T타입의 참조를 담을 수 있거나('present') 혹은 아무것도 담지 않을 수 있다('empty'). 원소를 단 1개만 갖는 '불변' 유사 컬렉션이다.

    예제 1 : Optional를 이용한 최댓값 구하는 메서드

    public class Max {
        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);
        }
        public static void main(String[] args) {
            List<String> words = Arrays.asList(args);
    
            System.out.println(max(words));
    
            String lastWordInLexicon = max(words).orElse("단어 없음...");
            System.out.println(lastWordInLexicon);
        }
    }

    주의할 점은 Optional.of(null)은 NullPointerException을 던진다. ofNullable() 메서드를 사용해 null을 반환할 수 있지만 Optional을 반환할 때 null값을 반환하지 말자. 이렇게 작성할 코드였으면 그냥 null을 반환하는 것이 낫다.

    Optional을 반환하는 것 만으로 클라이언트가 반환 값이 없음을 인지할 수 있다. 클라이언트가 Optional을 사용하는 방법 3가지를 아래에 소개한다.

    예제 2 : 기본값 정하기 / 예외 던지기 / 항상 값이 채워져있다고 가정하기

    String lastWordInLexicon = max(words).orElse("No words...");
    
    Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
    
    Element lastNobleGas = max(Elements.NOBLE_GASES).get();
    //만약 'empty' Optional일 경우 NoSucheElementException 발생

    Optional.isPresent() 메서드는 empty / present 여부에 따라 각각 false, true를 반환한다. 그러나 null을 반환하는 메서드를 핸들링하는 로직과 크게 다르지 않으며 앞에 소개한 3개의 메서드로 대체할 수 있는 경우가 많다.

    예제 3: isPresent() 메서드를 이용한 Optional 핸들링

    Optional<ProcessHandle> parentProcess = ph.parent();
    System.out.println("Parent PID: " + (parentProcess.isPresent() ?
            String.valueOf(parentProcess.get().pid()) : "N/A"));
    // null을 처리하는 로직과 유사하다!
    
    System.out.println("Parent PID: " +
            ph.parent().map(h -> String.valueOf(h.pid())).orElse("N/A"));
    // Optional의 map() 메서드를 이용하여 다듬었다.
    
    streamOfOptionals
            .filter(Optional::isPresent)
            .map(Optional::get)
    // Stream으로 present의 value를 추출한다.

    주의 사항

    1. 컬렉션, 스트림, 배열, 옵셔널과 같은 컨테이너는 또 optional로 감싸지 말아야 한다.
    2. 앞의 아이템에서 다뤘듯이 컬렉션, 배열은 length/size가 0인 컨테이너를 반환하는 것이 좋다.
    3. 반환 결과가 없을 수 있을 때, 클라이언트가 특별하게 처리해야 한다면 Optional를 반환한다.
    4. 그러나 Optional로 wrap하고 다시 꺼내는 비용, 객체 생성 비용 등이 있으니 오버헤드가 발생한다. 유저가 실수할 여지를 줄이느냐, 성능을 중시하느냐 잘 고려해야 할 것이다.
    5. 박싱된 기본 타입을 옵셔널로 반환하지 말자.
    6. 기본 타입을 감싸서 Optional로 또 감싼다면 2중으로 감싸게 된다. 당연히 성능 문제가 있을 것이다. 친절하게 자바 API에는 int, double, long 전용 Optional 클래스를 만들었고 Optional의 메서드 대부분을 지원하므로 더더욱 2중으로 감쌀 필요가 없다.
    7. Optional을 컬렉션의 Key, Value, 혹은 배열의 Element로 사용하지 말자.
    8. Map의 Key로 Optional을 사용한다면, Key가 Map에 존재하지 않는 상태는 Key가 진짜 존재하지 않는 경우와 Key는 존재하지만 empty인 경우이다. 복잡성만 키우고 이득은 없으니 쓰지말자.
    9. 필드를 Optional로 선언하는 건 대부분의 상황에서 좋지 않다.NutritionFacts, 영양 정보는 대부분의 필드가 값이 없을 수 있으며 특별한 상속관계로 만들기도 마땅치 않으니 이 때는 Optional을 인스턴스 필드로 가질 수 있지만 일반적인 상황은 아닐 것이다.
    10. 대부분의 상황에서는 Optional을 사용하는 대신에 클래스를 나누어 필수 필드만을 담고 있는 클래스를 만들고 이를 확장해 선택 필드를 추가해야 한다는, 일종의 신호이기도 하다 (책에서는 'bad smell'로 표기했다).

    댓글