티스토리 뷰

728x90
반응형

옵셔널 반환은 신중히 하라


  • 자바 8이전에는 메서드에서 반환할 수 있는 값이 없는 경우 두 가지를 취할 수 있었습니다. 하지만 각각의 단점이 존재합니다.
  • 1. 예외를 던집니다.
    • 단점 1 - 예외는 반드시 예외적인 상황에서만 사용해야 합니다.
    • 단점 2 - 예외는 실행 스택을 추적을 캡처하기 때문에 비용이 비쌉니다.
  • 2. 반환 타입이 객체인 경우 null 반환
    • 단점 1 - 클라이언트 코드에서는 항상 null 체크 로직을 추가해야하며 그렇지 않은 경우 NPE가 발생합니다.

 

💡 Optional의 등장

  • Optional이란, 값이 있을 수도 있고 없을 수도 있는 객체입니다.
  • Optional은 원소 하나를 가지는 불변 컬렉션입니다.
  • 자바 8이전의 코드보다 null-safe한 로직을 처리할 수 있게끔 해줍니다.
  • Optional을 반환하여 조금 더 유연한 로직을 작성할 수 있게끔 해줍니다.

 

💡 Optional의 주요 메서드

 

📜 Optional.empty()

  • 내부 값이 비어있는 Optional 객체를 반환합니다.
public final class Optional<T> {
    
    private static final Optional<?> EMPTY = new Optional<>();
    
    public static<T> Optional<T> empty() {
        @SuppressWarnings("unchecked")
        Optional<T> t = (Optional<T>) EMPTY;
        return t;
    }
}

 

📜 Optional.of(T value)

  • 내부 값이 value인 Optional 객체를 반환합니다.
  • 만약 value가 null인 경우 NPE가 발생합니다.
public final class Optional<T> {

    private Optional(T value) {
        this.value = Objects.requireNonNull(value); // null인 경우 NPE 발생
    }
    
    public static <T> Optional<T> of(T value) {
        return new Optional<>(value);
    }
}

 

📜 Optional.ofNullable(T value)

  • value가 null이면 empty Optional 객체를 반환하고, null이 아닌 경우 Optional.of 메서드로 객체 생성 후 반환합니다.
public final class Optional<T> {

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

    private Optional(T value) {
        this.value = Objects.requireNonNull(value); 
    }
    
    public static <T> Optional<T> of(T value) {
        return new Optional<>(value);
    }
    
    public static <T> Optional<T> ofNullable(T value) {
        return value == null ? empty() : of(value);
    }
}

 

📜 T get()

  • Optional 내의 값을 반환합니다.
  • 만약 Optional 내부 값이 null인 경우 NoSuchElementException이 발생합니다.
public T get() {
    if (value == null) {
        throw new NoSuchElementException("No value present");
    }
    return value;
}

 

📜 boolean isPresent()

  • Optional 내부의 값이 있으면 true, null인 경우 false를 반환합니다.
public boolean isPresent() {
    return value != null;
}

 

📜 boolean isEmpty()

  • Optional 내부의 값이 null이라면 true, null이 아닌 경우 false를 반환합니다.
public boolean isEmpty() {
    return value == null;
}

 

📜 Optional.filter

  • Optional에 filter 조건을 걸어 조건에 맞을 때만 Optional 내부 값을 사용합니다.
  • 조건이 맞지 않으면 Optional.empty를 반환합니다.
public Optional<T> filter(Predicate<? super T> predicate) {
    Objects.requireNonNull(predicate);
    if (!isPresent()) {
        return this;
    } else {
        return predicate.test(value) ? this : empty();
    }
}

 

📜 Optional.map

  • Optional 내부의 값을 Function을 통해 가공합니다.
public <U> Optional<U> map(Function<? super T, ? extends U> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent()) {
        return empty();
    } else {
        return Optional.ofNullable(mapper.apply(value));
    }
}

 

💡 null 반환과 Optional<T> 반환 비교

  • 컬렉션에서 최댓값을 구합니다.(컬렉션이 비어있으면 예외를 던집니다.)
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<E>를 반환하도록 변경하였습니다. 그리고 컬렉션이 비어있다면 빈 Optional 객체를 생성 후 반환합니다.
  • Optional을 반환하는 메서드에서는 절대 null을 반환해서는 안됩니다.
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 활용 방법

 

1. 기본값을 정해둘 수 있습니다.

public static void main(String[] args) {

    Optional<String> word = Optional.ofNullable(null);
    String s = word.orElse("단어 없음");
    System.out.println(s);
}

 

2. 원하는 예외를 던질 수 있습니다.

  • 이렇게 하면 예외가 실제로 발생하지 않는한 예외 생성 비용은 들지 않습니다.
public static void main(String[] args) {

    Optional<String> word = Optional.ofNullable(null);
    String s = word.orElseThrow(NullPointerException::new);
    System.out.println(s);
}

 

3. 항상 값이 있다고 가정할 수 있습니다.

  • Optional에 항상 값이 채워져 있다고 확신한다면 곧바로 값을 꺼내 사용하는 선택지도 있습니다. 다만 잘못된 판단이라면 예외가 발생하게 됩니다.
public static void main(String[] args) {

    Optional<String> word = Optional.ofNullable("HELLO");
    String s = word.get();
    System.out.println(s);
}

 

4. 기본값을 설정하는 비용이 큰 경우

  • 기본값을 설정하는 비용이 아주 커서 부담되는 경우 orElseGet을 사용하면, 값이 처음 필요할 때 Supplier를 사용해 생성하므로 초기 생성 비용을 낮출 수 있습니다.
public static void main(String[] args) {

    Connection connection = getConnection(datasource).orElseGet(() -> getLocalConnection());
}

 

 

💡 반환타입을 Optional<T>를 사용하면 안되는 경우

  • 반환값으로 옵셔널을 사용한다고해서 무조건 득이 되는것은 아닙니다. 컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입은 옵셔널로 감싸면 안됩니다. 빈 Optional<List<T>>를 반환하기 보다는 List<T>를 반환하는게 좋습니다. 
  • 빈 List를 반환하는게 좋은 이유는 Optional로 반환하게 된다면 클라이언트에서 Optional 처리 코드를 넣어야 합니다. 아이템 54 >>
  • 박싱된 기본 타입을 담은 Optional은 기본 타입 자체보다 무거울 수밖에 없습니다. 값을 두 겹이나 감싸기 때문입니다. 그래서 자바 API 설계자들은 int, long, double 전용 Optional 클래스를 준비해놨습니다. OptionalInt, OptionalLong, OptionalDouble입니다. 이 Optional들도 Optional<T>가 제공하는 메서드를 거의 다 제공합니다. 이렇게 대체제까지 있으니 박싱된 기본 타입을 담은 
    Optional을 반환하는 일은 없도록 해야합니다. 다만 Boolean, Byte, Char 등은 예외일 수 있습니다.

 

💡 반환타입을 Optional<T>로 해야하는 경우

  • 결과가 없을 수 있으며, 클라이언트가 이 상황을 특별하게 처리해야 한다면 Optional<T>를 반환합니다. 하지만 이렇게 하더라도 Optional<T>를 반환하는 데는 댓가가 따릅니다. Optional도 엄연히 새로 할당하고 초기화해야하는 객체이고, 그 안에서 값을 꺼내려면 메서드를 호츨해야 하니, 한 단계를 더 거치는 셈입니다. 그래서 성능이 중요한 상황에서는 적절히 고려해봐야 합니다.

 

 

✔️ 정리

  • Optional을 사용하여 반환하는 경우 컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입은 옵셔널로 반환하기보다는 컨테이너 자체를 반환하는게 좋습니다.
  • 박싱된 기본 타입(int, long, double)을 담은 옵셔널을 반환하지 않도록 합시다. 다만 Boolean, Byte, Char등은 예외입니다. 

 

 

참고자료)

https://jaehun2841.github.io/2019/02/24/effective-java-item55/#isPresent-%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%A7%80-%EB%A7%90%EC%9E%90

https://escapefromcoding.tistory.com/241

 

 

 

 

 

 

728x90
반응형