Home [Effective Java] Item55. 옵셔널 반환은 신중히 하라!
Post
Cancel

[Effective Java] Item55. 옵셔널 반환은 신중히 하라!

Item55. 옵셔널 반환은 신중히 하라!

Intro

  • 자바 8 전에는 메서드가 특정 조건에서 값을 반환할 수 없는 경우 두가지 행동을 취했다.
    1. 예외를 던진다.
    2. null을 반환한다.
  • 두 방법 모두 허점이 있다.
    • 예외는 진짜 예외적인 상황에서만 사용해야 하고, 예외 생성 시 드는 비용도 만만치 않다.
    • null을 반환한다면 별도의 null 처리 코드가 필요하고, 이를 무시하면 언젠가 NullPointerException을 만나는 문제가 발생할 수 있다.

Optional<T>

  • 자바 버전이 8로 올라가면서 새로 생긴 선택지다.
  • Optional는 null이 아닌 T 타입 참조를 하나 담거나, 혹은 아무것도 담지 않을 수 있다.
  • 옵셔널은 원소를 최대 1개 가질수 있는 불변 컬렉션이다.

옵셔널을 사용하는 경우

  • 보통은 T를 반환해야 하지만 특정 조건에서는 아무것도 반환하지 않아야 할 때 T 대신 Optional를 반환하도록 선언한다.
    • 그러면 유효한 반환값이 없을 때, 빈 결과를 반환하는 메서드가 만들어진다.

옵셔널 사용의 장점

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

Optional 사용 예시

  • 예외를 던지는 예제

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    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;
    }
    
    • 이 메서드에 빈 컬렉션을 건네면, IllegalArgumentException을 던진다.
  • Optional을 반환하는 예제

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    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 사용 방법

Optional.empty()

  • 빈 옵셔널을 만든다.

Optional.of(value)

  • 값이 든 옵셔널을 생성한다.
  • null을 넣으면 NullPointerException을 던진다.

Optional.ofNullable(value)

  • null 값도 허용하는 옵셔널을 만들 때 사용한다.
  • 옵셔널의 반환 메서드에서는 절대 null을 반환해서는 안된다.
    • 옵셔널 도입의 취지를 완전히 무시하는 행위이므로

스트림과 옵셔널

  • 스트림의 종단 연산 중 상당수가 옵셔널을 반환한다.
1
2
3
4
public static <E extends Comparable<E>>
	Optional<E> max(Collection<E> c) {
    return c.stream().max(Comparator.naturalOrder());
}
  • Stream의 max 연산이 우리에게 필요한 옵셔널을 생성해줄 것이다.

옵셔널의 사용 기준

  • 옵셔널은 검사 예외와 취지가 비슷하다.
    • 검사 예외를 던지면 클라이언트에서는 반드시 이를 대처하는 코드를 넣어야 한다. (비검사 예외를 던지거나 null 반환 시, API 사용자기 이를 인식하지 못하면 끔찍한 결과로 이어질 것이다.)
    • 즉, 반환 값이 없을 수도 있음을 API 사용자에게 명확히 알려준다.
  • 검사 예외와 마찬가지로 옵셔널을 반환받은 클라이언트는 이에 대응하는 코드를 작성해야 할 것이다.

옵셔널 활용 1 - 기본값을 정해둘 수 있다.

1
String lastWordInLexicon = max(words).orElse("단어 없음...");
  • 옵셔널 메서드 반환 시 클라이언트가 값을 받지 못하는 상황에 취할 방법 중 하나인 기본값 사용이다.

옵셔널 활용 2 - 원하는 예외를 던질 수 있다.

1
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
  • 실제 예외가 아닌 예외 팩터리를 건네고 있다. 이렇게 하면 실제 예외 발생 전까지는 예외 생성 비용이 들지 않는다.

옵셔널 활용 3 - 항상 값이 채워져 있다고 가정한다.

1
Element lastNobleGas = max(Elements.NOBLE_GASES).get();
  • 항상 값이 채워져 있다고 확신한다면 곧바로 값을 꺼내 사용한다.
  • 하지만 빈 옵셔널 이었다면 NoSuchElementException이 발생할 것이다.

옵셔널 활용 4 - 기본값 설정 비용이 부담되는 경우

  • Supplier를 인수로 받는 orElseGet을 사용하면, 값이 처음 필요할 때 Supplier를 사용해 생성하므로 초기 설정 비용을 낮출 수 있다.

옵셔널 활용 5 - 안전밸브 역할을 하는 isPresent 메서드

  • 안전 밸브의 역할을 하는 메서드이다.
    • 옵셔널이 채워져 있으면 true, 비어 있으면 false를 반환한다.
  • 원하는 모든 작업을 수행할 수 있지만 신중히 사용하자.
    • 실제로 isPresent를 사용한 코드 중 상당수는 앞서 언급한 메서드로 대체 가능하다.
1
2
3
Optional<ProcessHandle> parentProcess = ph.parent();
System.out.println("Parent PID: " + (parentProcess.isPresent() ?
											String.valueOf(parentProcess.get().pid()) : "N/A"));
1
2
System.out.println("Parent PID: " +
                   ph.parent().map(h -> String.valueOf(h.pid())).orElse("N/A"));
  • isPresent를 사용한 코드를 map을 사용한 코드로 바꿨다.
    • 더 짧고, 의미가 명확해졌다.

스트림 사용

1
2
3
streamOfOptionals
    .filter(Optional::isPresent)
    .map(Optional::get)
  • 자바 8에서의 쓰임이다.
  • 값이 있는 옵셔널을 필터링 후, 값을 꺼내 스트림에 매핑하는 코드다.
1
2
streamOfOptionals
  	.flatMap(Optional::stream)
  • 자바 9에서의 쓰임이다.
  • Optional을 Stream으로 변환해주는 어댑터를 사용하고 있다.
    • 옵셔널에 값이 있다면 값을 담은 스트림으로, 없다면 빈 스트림으로 반환.

반환값으로서 사용되는 옵셔널의 득실

실이 되는 경우

  • 컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입은 옵셔널로 감싸면 안 된다.
    • 빈 Optional<List>를 반환하기보다 빈 List를 반환하는 것이 좋다.
    • 빈 컨테이너를 그대로 반환하면 클라이언트는 옵셔널 처리를 따로 해줘야 한다.
  • 박싱된 기본 타입을 담은 옵셔널을 반환하는 일은 없도록 하자.
    • 박싱된 기본 타입을 담는 옵셔널은 기본 타입 자체보다 무거울 수밖에 없다.
    • 그래서 자바 API 설계자는 int, long, double 전용 옵셔널인 OptionalInt, OptionalLong, OptionalDouble을 준비했다.
  • 옵셔널을 컬렉션의 키, 값, 원소나 배열의 원소로 사용하는 게 적절한 상황은 거의 없다.
    • 지금까지 옵셔널을 반환하고, 반환된 옵셔널을 처리하는 이야기만 한 이유는? 옵셔널의 다른 쓰임은 대부분 적절치 않기 때문.
    • 맵의 키를 옵셔널로 사용한다 가정해보자! 맵의 키가 없다는 사실을 두 가지 방법으로 나타내게 되는데, 바로 키 자체가 없는 경우와 키는 있지만 빈 옵셔널인 경우다. 쓸데없이 복잡해져 혼란이 오게 된다.

득이 되는 경우

  • **결과가 없을 수 있으며, 클라이언트가 이 상황을 특별하게 처리해야 하는 경우 Optional를 반환한다.**
    • 이렇게 하더라도 Optional를 반환하는 데는 대가가 따른다.
      • 엄연히 객체이므로 새로 할당 및 초기화하는 과정, 메서드 호출 과정이 필요해 성능이 중요시 되는 상황에서는 맞지 않을 수 있다.

핵심 정리

  • 값을 반환하지 못할 가능성이 있고, 호출할 때마다 반환값이 없을 가능성을 염두에 둬야 하는 메서드라면 옵셔널을 반환해야 할 상황일 수 있다.
  • 옵셔널 사용엔 성능 저하가 뒤따른다. 성능에 민감한 메서드라면 null 반환이나 예외를 던지는 것을 고려하자.
  • 옵셔널은 반환 이외의 용도로 사용되는 일이 거의 없다.
This post is licensed under younghwani by the author.

[Effective Java] Item54. null이 아닌, 빈 컬렉션이나 배열을 반환하라!

[Effective Java] Item56. 공개된 API 요소에는 항상 문서화 주석을 작성하라!

Comments powered by Disqus.