Effective Java : 아이템 55. 옵셔널 반환은 신중히 하라.

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

    • 반환할 값이 없을 때는 세 가지 방법이 있음.
      • 예외 던지기 → 예외는 진짜 예외일 때 던져야 하는데, 이게 예외인지?
      • null 반환하기 → null 반환 시 null 대처해야하며, 그러기 위해서 사용자는 메서드의 내부 구조도 알아야함. 
      • Optional 반환하기
    • Optional은 체크 예외처럼 메서드 사용자에게 빈 값이 반환될 수 있음을 알리고 적절히 대체할 것을 요구하기 위한 용도로 사용됨. 
    • Optional의 기본 처리 방법은 다음과 같음.
      • 값을 바로 얻기
      • 없는 경우 기본값 설정하기.  (비용이 클 경우, orElseGet으로 Supplier를 이용해서 비용 최적화 가능)
      • 없는 경우 예외 던지기. 
    • Optional을 잘 사용하는 방법
      • isPresent()보다 orElseThrow() 같은 것을 적극 사용하라.
      • ofNullable() 절대 사용하지 마라. Optional을 무의미하게 만듬.
    • Optional을 사용하면 안되는 경우
      • 컬렉션, 컨테이너를 감싸는 경우.  Optional<Optional<String>>이 무슨 의미가 있는지? 
      • 컬렉션, 컨테이너의 원소로 사용되는 경우. 컬렉션을 만들 때, 그냥 원소를 넣어주는게 맞지 않는지?
    • Optional은 객체이기 때문에 성능에 민감한 메서드에서는 성능 저하를 유발할 수 있음.
      • 기본형의 경우 OptionalInt 등을 사용해서 성능 개선 도모 할 수 있음.
    • Optional을 인스턴스 필드로 사용하는 것은 나쁜 냄새(리팩토링 대상)에 해당된다. 따라서 사용하지 않도록 해야 함. 

     

     


    값을 반환할 수 없을 때 선택지는?

    메서드가 호출되었는데 값을 반환할 수 없을 때 선택지는 아래 세 가지가 있다.

    • 예외를 던진다
    • Null을 반환한다.
    • Optional을 반환한다.

    예외를 던지는 것과 Null을 반환하는 것은 그다지 좋은 선택이 아니다. 

    • 예외를 던지는 것은 진짜 예외적인 상황에만 사용해야 하고, 예외를 던졌을 때 캡쳐하는 비용이 만만치 않다. 
    • Null을 반환하면, 받는 쪽에서 Null 처리 코드를 작성해야한다. 상대방은 Null을 받는지도 모를 수 있다. 

    이럴 때 예외를 던지면 예외의 의미가 퇴색되며, Null을 던지면 Null을 던지는지 알아내기 위해서 메서드를 사용하는 쪽에서 내부 코드를 알아야 한다는 단점이 있다. 이런 선택지들의 대안으로 Optional 컨테이너를 던지는 방법이 추가되었다. 

    만약 반환 값이 없을 수도 있고, 그것을 사용하는 쪽에 명확히 알려주고 싶다면 이 때 Optional 컨테이너를 반환값으로 사용하는 것은 적절할 것이다. 아래 코드에서는 예외를 던지는 경우 / Optional을 던지는 경우를 각각 볼 수 있다. 

    // 반환할 값이 없을 때 예외를 던지는 경우 → BadCase
    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;
    }
    
    // 반환할 값이 없을 때 Optional을 던지는 경우 → Better Case
    public static <E extends Comparable<E>> Optional<E> maxOptional(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의 목적

    Optional은 체크 예외와 비슷한 취지로 도입된 컨테이너다. 특정 메서드가 체크 예외를 던진다면, 이 메서드를 사용하는 클라이언트는 반드시 해당 예외를 처리해야한다. 마찬가지로 Optional을 반환하는 메서드를 클라이언트가 사용한다면, 클라이언트는 Optional에 있는 값을 사용하기 위한 작업을 해야한다.

    public static void main(String[] args) {
        Optional<Integer> integer = maxOptional(List.of(1, 2, 3, 4));
        Integer integer1 = integer.orElseGet(() -> 1);
    }

    위 코드처럼 Optional을 던져주면, 사용자는 다음 의도를 이해할 것이다.

    Optional()에는 값이 있을 수도, 없을 수도 있다. 

    이 경우, 사용자는 Optional 객체에서 값을 꺼낼 때 값이 없으면 기본값을 설정해서 받던지, 혹은 예외를 던지는 작업들을 후속으로 처리할 수 있게 된다. 

     


    Optional의 기본 처리 방법

    기본적으로 Optional은 다음 형태로 사용할 수 있을 것이다.

    • 값이 있다고 확신하고 바로 얻기
    • 값이 없는 경우 Default 값을 설정하고, 그 값을 얻기
    • 값이 없는 경우 예외를 던지기

    아래 코드에서 각각을 살펴볼 수 있다.

    // 있다고 확신하는 경우
    Integer integer3 = integer.get();
    
    // 기본값 설정
    Integer integer1 = integer.orElseGet(() -> 1);
    Integer integer2 = integer.orElse(1);
    
    // 없는 경우 예외를 던짐.
    Integer integer4 = integer.orElseThrow();

    가끔씩 기본값을 설정하는 비용이 아주 큰 경우가 있다. 예를 들면 원소가 백만개쯤은 있는 컬렉션이 기본값이라고 가정을 해볼 수 있는데, 이럴 때 기본값을 미리 생성해두는 것은 부담스러울 수 있다. 이럴 때는 Supplier<T>를 인자로 받는 orElseGet()를 사용하면 된다. 이 메서드를 사용하면 값이 처음으로 필요한 시점에 객체를 생성해서 사용하기 때문에 초기 설정 비용을 낮출 수 있다.

    public T orElseGet(Supplier<? extends T> supplier) {
        return value != null ? value : supplier.get();
    }

     


    Optional을 잘 사용하는 방법

    Optional을 잘 사용하는 기초적인 방법을 살펴보려고 한다. 중점적으로 볼 부분은 아래 두 가지다. 

    • isPresent() 같은거 쓰지 마라.  어쩔 수 없을 때만 써야함. 
    • Optional.ofNullable() 절대 사용 하지마라.

     

    isPresent() 사용 최소화.

    isPresent()는 Optional()에 객체가 있는지 없는지를 판단하는 메서드다. 메서드 자체는 나쁘지 않으나, 이미 이 메서드의 기능을 포함하면서 다른 기능까지 함께 추가된 메서드가 많이 있다. 예를 들면 orElseThrow() 같은 메서드들인데 isPresent()를 이용해서 조건문으로 필터링하고 예외를 던지는 것보다 orElseThrow()로 한줄에 처리하는게 더 읽기 좋다.

    // 굳이 이렇게?
    System.out.println("integer의 값은 얼마인가요 : " + (integer.isPresent() ? integer.get() : -999999));
    
    // orElse()로 한줄 처리가 나음. 
    System.out.println("integer의 값은 얼마인가요 : " + integer.orElse(-999999));

     

    Optional.ofNullable() 절대 쓰지마라

    Optional()은 반환값이 얻을 수 있다는 것을 사용자에게 알려주기 위해 사용한다. 그런데 Optional에 들어간 객체가 null일 수 있다는 것은 Optional에서 꺼내온 객체를 다시 한번 null 체크를 해야한다는 것을 의미한다. Optional을 도입한 취지를 완전히 무시한 코드이기 때문에 절대로 사용하지 않도록 한다. 

    Optional.ofNullable()을 사용하는 대신 아래의 empty() 메서드를 사용하는 것이 더 좋다.

    Optional.empty();

     


    Optional의 UseCase

    앞서서 Optional의 메서드 중 isPresent()는 거의 사용하지 않는다고 했다. isPresent()를 유용하게 사용하는 경우가 하나 있는데, 주로 Stream에서 Optional의 값을 뽑아오는데 사용한다. 예를 들면 아래와 같이 사용해 볼 수 있다.

    List<Integer> integerList = list.stream()
            .filter(Optional::isPresent)
            .map(Optional::get)
            .toList();

     

     


    Optional을 사용하면 안되는 경우

    그렇다면 어떤 경우에 Optional을 사용하면 안될까? 다음 두 가지 경우에는 절대로 사용하면 안된다. 아래처럼 사용할 경우, Optional을 오히려 애물단지로 만들어 버리기 때문이다.

    • 컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입을 옵셔널로 감싸는 경우. 
    • 컬렉션(맵, 리스트 등)의 키, 값, 원소나 배열의 원소로 사용하는 경우.

     

     

    컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입을 옵셔널로 감싸는 경우. 

    일반적으로 컬렉션에 있는 원소들은 반드시 실제하는 값으로 생각한다. 그런데 옵셔널 타입을 원소로 가지는 컬렉션이 있다면, 매 원소마다 Optional에 값이 있는지를 체크한 후 다음 작업을 진행해야한다. 예를 들어 아래 Optional 리스트에서는 아래처럼 매번 체크를 해야하는데, 이렇게 하는 것보다는 Optional에서 유효한 값만 있는 컬렉션을 만들어서 사용하는 것이 낫다.

    List<Optional<Integer>> isGood = new ArrayList<>();
    for (Optional<Integer> i : isGood) {
        Integer integer5 = i.orElse(1);
        System.out.println(integer5);
    }

     

     

    컬렉션(맵, 리스트 등)의 키, 값, 원소나 배열의 원소로 사용하는 경우.

    컬렉션의 키 원소등으로 Optional을 절대로 사용하면 안된다. 예를 들어 HashMap의 Value로 Optional 타입이 들어간다고 가정해보자. A라는 키에 Optional 객체가 들어있으면, 사용하는 쪽에서 A라는 키에는 값이 있는 것일지 없는 것일지 알 수 없다.  Map에서 Value에 대응되는 Optional 값을 꺼낸 후 다시 한번 값이 있는지 체크를 해야지 비로소 사용할 수 있게 된다. 굉장히 비효율적이고 잘못된 코딩이 될 수 있으므로 컬렉션의 키, 값, 원소등으로 절대로 사용하지 않는다.

    // 이게 깔끔한가? 
    Map<String, Optional<String>> badMap = new HashMap<>();
    badMap.get("hello").orElseGet(() -> "ABC");

     

     


    Optional의 성능 문제

    Optional은 기본적으로 객체를 한번 감싼 객체이기 때문에 성능에 민감한 메서드에서 사용할 경우 부담이 될 수 있다. 만약 어플리케이션에서 성능 문제가 발생하고, 그 성능 문제가 Optional을 사용하는 메서드에서 발생하는 것이 명확하다면 Optional을 사용하지 않는 것이 맞다.

    한 가지 최적화 할 수 있는 방법은 기본형을 Optional로 사용하는 경우다. Optional<Integer> 같은 객체들은 int 같은 기본형을 Optional, Integer 객체로 두 번 감싸는 것이기 때문에 성능 낭비가 발생할 수 있다. 이런 부분을 개선하기 위해 OptionalInt, OptionalLong 같은 클래스가 있다. 이것을 사용해서 성능 개선을 도모해 볼 수 있다.

    OptionalInt optionalInt = OptionalInt.of(1);

     

     


    Optional은 인스턴스의 필드로 사용할 수 있을까? 

    Optional을 인스턴스 필드로 사용하는 것은 대부분의 경우 나쁜 경우다. 예를 들어 아래 코드를 감안해보자.

    public class OptionalFieldClass {
    
        private final String name;
        private final Optional<String> middleName;
    
        public OptionalFieldClass(String name, Optional<String> middleName) {
            this.name = name;
            this.middleName = middleName;
        }
        
        public String getFullName() {
    
            if (middleName.isPresent()) {
                return name + '-' + middleName.get();
            } else {
                return name;
            } 
        }
    }

    아래 코드에서는 인스턴스 필드에 middleName에 Optional<String>을 가진다. 만약 인스턴스 필드가 클래스 메서드에서 활발하게 사용된다면, Optional의 값 유무에 따라서 분기처리를 해야하는 로직이 많이 들어가게 된다. 즉, '나쁜 냄새'를 유발한다. 

    이런 냄새는 Optional 필드를 필수값으로 가지는 새로운 클래스로 분리해야하는 '리팩토링'이 필요한 것으로 이해를 할 수 있다. 따라서 대부분 인스턴스 필드로 Optional을 가지는 것은 적절하지 않다. 

    댓글

    Designed by JB FACTORY