[교재 EffectiveJava] 아이템 55. 옵셔널 반환은 신중히 하라

반응형
728x90
반응형

빈 값 처리방법 Optioanl

자바 8 전에는 메서드가 특정 조건에서 값을 반환할 수 없을때 취할 수 있는 선택지는 두가지였다.

1) 예외를 던진다.
2) null 을 반환한다. (반환 타입이 객체 참조일 경우)

두 가지 방법 모두 허점이 있다. 예외는 진짜 예외적인 상황에서 사용해야한다. null을 반환하면 클라이언트에서 별도의 null 처리 코드를 추가해야한다. 

 

자바 8 후에 한가지 방법이 생겼다.

3) Optional<T> 로 처리한다.

3)번의 방법은 null 이 아닌 T 타입 참조를 하나 담거나, 혹은 아무것도 담지 않을 수 있다. 아무것도 담지 않은 옵셔널은 '비었다'라고 말한다. Optional은 원소를 최대 1개 가질 수 있는 '불변' 컬렉션이다. Optional<T> 가 Collection<T>를 구현하지는 않았지만, 원칙적으로는 그렇다. 

 

Optional을 반환하는 메서드는 예외를 던지는 메서드보다 유연하고 사용하기 쉬우며, null을 반환하는 메서드보다 오류 가능성이 작다.

 

Optional 포스팅 바로가기

https://devfunny.tistory.com/330

 

자바 8의 Optional 등장

NullPointerException NullPointerException은 개발자가 한번이라도 만나봤을 에러이다. 그정도로 흔하게 일어나는 에러로, 이는 “자바의 모든 객체는 NULL일 수 있다.”” 라는 말을 확인시켜준다. NullPoint

devfunny.tistory.com

 

 

예제로 이해하기

1) 예외를 던진다.
package com.java.effective.item55;

import java.util.Collection;
import java.util.Objects;
import java.util.Optional;

public class Main {
    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 을 반환한다.
package com.java.effective.item55;

import java.util.Collection;
import java.util.Objects;
import java.util.Optional;

public class Main {
    public static <E extends Comparable<E>> Optional<E> maxOptional(Collection<E> c) {
        /* 빈 컬렉션일 경우 빈 Optional 을 반환한다. */
        if (c.isEmpty()) {
            return Optional.empty();
        }

        E result = null;

        for (E e : c) {
            if (result == null || e.compareTo(result) > 0) {
                result = Objects.requireNonNull(e);
            }
        }

        /* 값이 든 Optional 을 반환한다. */
        return Optional.of(result);

        /* Optional 을 사용하는 경우에는 null을 절대 반환하지 말자.
           null 을 허용하는 Optional 을 반환하자. */
        // return Optional.ofNullable(result);
    }

}

 

3) 스트림의 종단 연산 중 상당수가 옵셔널을 반환한다. 스트림 버전을 보자.
package com.java.effective.item55;

import java.util.Collection;
import java.util.Comparator;
import java.util.Objects;
import java.util.Optional;

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

▶ Stream.max()

Optional<T> max(Comparator<? super T> comparator);

 

 

Optional 반환 선택의 기준

옵셔널은 검사 예외와 취지가 비슷하다. 반환값이 없을 수도 있음을 클라이언트에게 명확히 알려준다. 비검사 예외를 던지거나 null을 반환한다면 API 사용자가 그 사실을 인지하지 못해서 예상치못한 에러가 발생할 수도 있다. 하지만 검사 예외를 던지면 클라이언트에서 반드시 이에 대처하는 코드를 작성해넣어야 한다. 

 

클라이언트는 Optional 을 반환받고, 그 값의 여부에 따라 취할 행동을 선택해야한다.

 

1) 기본값 설정
package com.java.effective.item55;

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<String> words = new ArrayList<>();
        words.add("A");
        words.add("B");
        words.add("C");
        
        String lastWordInLexicon = maxOptional(words).orElse("단어 없음");
    }

    public static <E extends Comparable<E>> Optional<E> maxOptional(Collection<E> c) {
        /* 빈 컬렉션일 경우 빈 Optional 을 반환한다. */
        if (c.isEmpty()) {
            return Optional.empty();
        }

        E result = null;

        for (E e : c) {
            if (result == null || e.compareTo(result) > 0) {
                result = Objects.requireNonNull(e);
            }
        }

        /* 값이 든 Optional 을 반환한다. */
        return Optional.of(result);

        /* Optional 을 사용하는 경우에는 null을 절대 반환하지 말자.
           null 을 허용하는 Optional 을 반환하자. */
        // return Optional.ofNullable(result);
    }
}

 

2) 원하는 예외 던지기
package com.java.effective.item55;

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<String> words = new ArrayList<>();
        words.add("A");
        words.add("B");
        words.add("C");
        
        String lastWordInLexicon = maxOptional(words).orElseThrow(RuntimeException::new);
    }

    public static <E extends Comparable<E>> Optional<E> maxOptional(Collection<E> c) {
        /* 빈 컬렉션일 경우 빈 Optional 을 반환한다. */
        if (c.isEmpty()) {
            return Optional.empty();
        }

        E result = null;

        for (E e : c) {
            if (result == null || e.compareTo(result) > 0) {
                result = Objects.requireNonNull(e);
            }
        }

        /* 값이 든 Optional 을 반환한다. */
        return Optional.of(result);

        /* Optional 을 사용하는 경우에는 null을 절대 반환하지 말자.
           null 을 허용하는 Optional 을 반환하자. */
        // return Optional.ofNullable(result);
    }
}

 

 Optional.of()

public static <T> Optional<T> of(T value) {
    return new Optional<>(value);
}

 

▶ Optional.ofNullable()

public static <T> Optional<T> ofNullable(T value) {
    return value == null ? empty() : of(value);
}

-- empty()
public static<T> Optional<T> empty() {
    @SuppressWarnings("unchecked")
    Optional<T> t = (Optional<T>) EMPTY;
    return t;
}

 

3) 값이 있다고 가정하고 꺼내기 

만약 꺼냈는데 값이 존재하지 않다면, NoSuchElementException 이 발생한다.

package com.java.effective.item55;

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<String> words = new ArrayList<>();
        words.add("A");
        words.add("B");
        words.add("C");
        
        String lastWordInLexicon = maxOptional(words).get();
    }

    ...
}

▶ Optional.get()

public T get() {
    if (value == null) {
        throw new NoSuchElementException("No value present");
    }
    return value;
}

 

4) Supplier<T>를 인수로 받는 orElseGet을 사용하여 값 초기 설정 비용을 낮출 수 있다.

 Optional.orElseGet()

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

 

5) isPresent() 메서드 사용하기
Optional<ProcessHandle> parentProcess = ph.parent();
System.out.println("부모 PID: " + (parentProcess.isPresent() ? 
                       String.valueOf(parentProcess.get().pid()) : "N/A"));
                       
/* 다듬은 코드 */
System.out.println("부모 PID: " + 
		ph.parent().map(h -> String.valueOf(h.pid()).orElse("N/A"));

- isPresent() : Optional이 채워져있을 경우 true, 비어있을 경우 false 를 반환한다.

 

- 스트림을 사용한다면 옵셔널들을 Stream<Optional<T>>로 받아서, 그 중 채워진 옵셔널들에서 값을 뽑아 Stream<T>에 건네 담아 처리하는 경우도 있다.

streamOfOptionals
    .filter(Optional::isPresent)
    .map(Optional::get)

자바 9에서 Optional에 stream() 메서드가 추가되었다. 이는 Optional 을 Stream으로 변환해주는 어댑터다. 옵셔널에 값이 있으면 그 값을 원소로 담은 스트림으로, 값이 없다면 빈 스트림으로 변환한다. 이를 Stream의 flatmap 메서드와 조합하면 앞의 코드를 다음처럼 명료하게 바꿀 수 있다.

streamOfOptionals.flatMap(Optional::stream)

 

flatMap 관련 포스팅 바로가기

https://devfunny.tistory.com/458

 

[Java] List<String>의 각 원소 고유문자 리턴하기 - Stream 평면화 과정 (flatMap 사용)

평면화 FlatMap List 의 요소에 속하는 모든 고유문자를 리스트로 리턴받고 싶다. 아래의 과정을 걸쳐서 스트림 평면화를 진행하는 flatMap을 이해해보자. 우선, 우리가 하고싶은 상황은 아래와 같다.

devfunny.tistory.com

 

 

Optional 선택 상황 

컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입은 옵셔널로 감싸면 안된다. 빈 Optional<List<T>>를 반환하기보다는 빈 List<T>를 반환하는게 좋다. 빈 컨테이너를 그대로 반환하면 클라이언트에서 옵셔널 처리 코드를 넣지 않아도 된다. 

즉, Optional<T>를 사용해야하는 상황은 아래와 같다.

1) 결과가 없을 수 있다.
2) 클라이언트가 이 상황을 특별하게 처리를 해야한다.

박싱된 기본 타입을 담는 옵셔널은 기본 타입 자체보다 무거울 수 밖에 없다. 자바 API는 int, long, double 전용 옵셔널 클래스도 제공한다. OptionalInt, OptionalLong, OptionalDouble이다. 이 옵셔널들도 Optional<T>가 제공하는 메서드를 거의 다 제공한다. 이렇게 대체재가 있으므로 박싱된 기본타입을 담은 옵셔널을 반환하면 안된다.

 

 

반응형

Designed by JB FACTORY