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

oyeon·2022년 2월 13일
0

Effective Java

목록 보기
4/8

반환값이 없을 때 과거의 방법 (JAVA 8 전)

1. 예외 던지기

문제점

  • 예외는 진짜 예외적인 상황에서만 사용해야 한다.
  • 예외를 생성할 때 스택 추적 전체를 캡처하므로 비용이 만만치 않다.

2. null을 반환(반환 타입이 객체 참조일 경우)

문제점

  • 별도의 null 처리 코드를 추가해야 한다.
  • null 처리를 무시하고 반환된 null은 언젠가 NullPointerException이 발생할 수 있다.

Optional (JAVA 8 이후)

  • null이 아닌 T 타입 참조를 하나 담거나, 혹은 아무것도 담지 않을 수 있다.
  • 아무것도 담지 않은 옵셔널 - '비었다', 어떤 값을 담은 옵셔널 - '비지 않았다'고 한다.
  • 옵셔널은 원소를 최대 1개 가질 수 있는 '불변' 컬렉션이다.
  • 예외를 던지는 메서드보다 유연하고 사용하기 쉬우며, null을 반환하는 메서드보다 오류 가능성이 적다.

예제

코드 55-1 컬렉션에서 최댓값을 구한다(컬렉션이 비어있으면 예외를 던진다)

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;
}

코드 55-2 컬렉션에서 최댓값을 구해 Optional로 반환한다.

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.Empty()로 만들고, 값이 든 옵셔널은 Optional.of(value)로 생성했다.
  • Optional.of(value)에 null을 넣으면 NullPointerException을 던지니 주의하자.
  • null 값도 허용하는 옵셔널을 만들려면 Optional.ofNullable(value)를 사용하면 된다. 옵셔널을 반환하는 메서드에서는 절대 null을 반환하지 말자.

코드 55-3 컬렉션에서 최댓값을 구해 Optional로 반환한다 - 스트림 버젼

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

그렇다면 null을 반환하거나 예외를 던지는 대신 옵셔널 반환을 선택해야 하는 기준은 무엇인가?
옵셔널은 검사 예외와 취지가 비슷하다(아이템 71) 즉, 반환값이 없을 수도 있음을 API 사용자에게 명확하게 알려주어야 한다.

  • 비검사 예외를 던지거나 null을 반환한다면 API 사용자가 그 사실을 인지하지 못해 끔찍한 결과가 나온다.

메서드가 옵셔널을 반환한다면 클라이언트는 값을 받지 못했을 때 취할 행동을 선택해야 한다.

코드 55-4 옵셔널 활용1 - orElse : 기본값을 정해둘 수 있다

String lastWordInLexicon = max(words).orElse("단어 없음...");

코드 55-5 옵셔널 활용2 - orElseThrow : 원하는 예외를 던질 수 있다

Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
  • 위 코드처럼 상황에 맞는 예외를 던질 수 있다.
  • 실제 예외가 아니라 예외 팩터리를 건넨 것에 주목하자. 이렇게 하면 예외가 실제로 발생하지 않는한 예외 생성 비용은 들지 않는다.

코드 55-6 옵셔널 활용 3 - get : 항상 값이 채워져 있다고 가정한다

Element lastNobleGas = max(Elements.NOBLE_GASES).get();
  • 옵셔널에 항상 값이 없다면 NoSuchElementException이 발생할 것이다.

이외에 이따금 기본값을 설정하는 비용이 아주 커서 부담이 되면, Supplier를 인수로 받는 orElseGet을 사용하면 값이 처음 필요할 때 Supplier를 사용해 생성하므로 초기 설정 비용을 낮출 수 있다.

이외에 filter, map, flatMap, ifPresent 메서드가 있다.

isPresent

  • 안전 벨브 역할의 메서드로, 옵셔널이 채워져 있으면 true를, 비어 있으면 false를 반환한다.
  • 신중히 사용해야 하는데, 실제로 isPresent를 쓴 코드 중 상당수는 앞서 언급한 메서드들로 대체할 수 있으며, 그렇게 하면 더 짧고 명확하고 용법에 맞는 코드가 되기 때문이다.
Optional<ProcessHandle> parentProcess = ph.parent();
System.out.println("부모 PID:" + (parentProcess.isPresent() ?
	String.valueOf(parentProcess.get().pid()) : "N/A"));

이 코드는 Optional의 map을 사용하여 다음처럼 다듬을 수 있다.

map

System.out.println("부모 PID:" +
	ph.parent().map(h-> String.valueOf(h.pid))).orElse("N/A");

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

filter

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

Optional에 값이 있다면, 그 값을 꺼내서 스트림에 매핑한다.

flatMap

자바 9에서는 Optional에 stream() 메서드가 추가되었다. 이 메서드는 Optional을 Stream으로 변환해주는 어댑터다. 옵셔널에 값이 있으면 그 값을 원소로 담은 스트림으로, 값이 없다면 빈 스트림으로 변환한다.

streamOfOptionals
	.flatMap(Optional::stream)

Optional를 사용하면 안되는 경우

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

언제 Optional를 사용해야 하는가?

  • 결과가 없을 수 있으며, 클라이언트가 이 상황을 특별하게 처리해야 한다면 Optional를 반환한다.

성능 이슈

  • Optional도 엄연히 새로 할당하고 초기화해야 하는 객체이고, 그 안에서 값을 꺼내려면 메서드를 호출해야 하니 한 단계를 더 거치는 셈이다. 그래서 성능이 중요한 상황에서는 옵셔널이 맞지 않을 수 있다.
  • 박싱된 기본 타입을 담는 옵셔널은 기본 타입 자체보다 무거울 수밖에 없다. 값을 두 겹이나 감싸기 때문이다.
    그래서 자바 API 설계자들은 int, long, double 전용 옵셔널 클래스들을 준비해놨다. 바로 OptionalInt, OptionalLong, OptionalDouble이다.
    이 옵셔널들도 Optional가 제공하는 메서드를 거의 다 제공한다.
    이렇게 대체제까지 있으니 박싱된 기본 타입을 담은 옵셔널을 반환하는 일은 없도록 하자. 상대적으로 덜 중요한 Boolean, Byte, Character, Short, Float은 예외일 수 있다.

하지 말아야 할 것

  • 옵셔널을 맵의 값으로 사용하면 절대 안 된다. 그리 한다면 맵 안에 키가 없다는 사실을 나타내는 방법이 두 가지가 된다.
    하나는 키 자체가 없는 경우고, 다른 하나는 키는 있지만 그 키가 속이 빈 옵셔널인 경우다. 쓸데없이 복잡성만 높여서 혼란과 오류 가능성을 키울 뿐이다.
    옵셔널을 컬렉션의 키, 값, 원소나 배열의 원소로 사용하는 게 적절한 상황은 거의 없다.

핵심정리

  • 값을 반환하지 못할 가능성이 있는 메서드라면 옵셔널을 반환해야 하는 상황일 수 있다.
  • 하지만 옵셔널 반환에는 성능 저하가 뒤따르니, 성능에 민감한 메서드라면 null을 반환하거나 예외를 던지는 편이 나을 수 있다.
  • 옵셔널을 반환값 이외의 용도로 쓰는 경우는 매우 드물다.
profile
Enjoy to study

0개의 댓글