본문 바로가기

회고/이펙티브 자바 3판

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

728x90
반응형

반환값이 없을 때 과거의 방법

1. 예외를 던진다.

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

 

2. 반환 타입이 객체 참조라면 null을 반환한다.

-> null이 반환될 일이 절대 없다고 확신하지 않는 한) 별도의 null 처리 코드를 추가해야 한다. null 처리를 무시하고 반환된 null은 언젠가 NullPointerException이 발생할 수 있다. 

 

새로운 대안 : Optional<T>

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

 

 보통은 T를 반환하지만, 특정 조건에서는 아무것도 반환하지 않아야 할 때 T 대신Optional<T>를 반환하도록 선언한다. 그러면 유효한 반환값이 없을 때는 빈 결과를 반환하는 메서드가 만들어진다. 예외를 던지는 메서드보다 유연하고 사용하기 쉬우며, null을 반환하는 메서드보다 오류 가능성이 적다. 

 

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<E>로 반환한다.

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(vlaue)를 사용하면 된다. 옵셔널을 반환하는 메서드에서는 절대 null을 반환하지 말자

 

컬렉션에서 최댓값을 구해 Optional<E>로 반환한다 - 스트림 버젼

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

그렇다면 null을 반환하거나 예외를 던지는 대신 옵셔널 반환을 선택해야 하는 기준은 무엇인가? 옵셔널은 검사 예외와 취지가 비슷하다(아이템 71) 즉, 반환값이 없을 수도 있음을 API 사용자에게 명확하게 알려주어야 한다. 비검사 예외를 던지거나 null을 반환한다면 API 사용자가 그 사실을 인지하지 못해 끔찍한 결과가 나온다. 메서드가 옵셔널을 반환한다면 클라이언트는 값을 받지 못할 때 취할 행동을 선택한다.

 

Optional<T>에서 값을 받지 못했을 때 처리하는 행동 3가지

 

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

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

첫째는 기본값을 던지는 것이다.

 

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

Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);

또는 상황에 맞는 예외를 던질 수 있다. 실제 예외가 아니라 예외 팩터리를 건넨 것에 주목하자. 이렇게 하면 예외가 실제로 발생하지 않는한 예외 생성 비용은 들지 않는다.

 

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

Element lastNobleGas = max(Elements.NOBLE_GASES).get();

옵셔널에 항상 값이 채워져 있다고 확신한다면 그냥 곧바로 값을 꺼내 사용하는 선택지도 있다. 다만 잘못 판단한 것이라면 NoSuchElementException이 발생할 것이다.

 

이외에

 

 이따금 기본값을 설정하는 비용이 아주 커서 부담이 되면, Supplier<T>를 인수로 받는 orElseGet을 사용하면 값이 처음 필요할 때 Supplier<T>를 사용해 생성하므로 초기 설정 비용을 낮출 수 있다. 더 특별한 쓰임에 대한 메서드도 준비되어 있다. 바로 filter, map, flatMap, ifPresent다. 앞서의 기본 메서드로 처리하기 어려워 보인다면 API 문서를 참고해서 검토해본다.

 

 여전히 적합한 메서드를 찾지 못했다면 isPresent 메서드를 살펴보자. 안전 벨브 역할의 메서드로, 옵셔널이 채워져 있으면 true를, 비어 있으면 false를 반환한다. 신중히 사용해야 하는데, 실제로 isPresent를 쓴 코드 중 상당수는 앞서 언급한 메서드들로 대체할 수 있으며, 그렇게 하면 더 짧고 명확하고 용법에 맞는 코드가 되기 때문이다.

 

Optional<ProcessHandle> parentProcess = ph.parent();
System.out.println("부모 PID:" + (parentProcess.isPresent() ?
	String.valueOf(parentProcess.get().pid()) : "N/A"));

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

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

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

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

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

 

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

streamOfOptionals
	.flatMap(Optional::stream)

Optional<T>를 사용하면 안되는 경우

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

 

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

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

 

 박싱된 기본 타입을 담는 옵셔널은 기본 타입 자체보다 무거울 수밖에 없다. 값을 두 겹이나 감싸기 때문이다. 그래서 자바 API 설계자들은 int, long, double 전용 옵셔널 클래스들을 준비해놨다. 바로 OptionalInt, OptionalLong, OptionalDouble이다. 이 옵셔널들도 Optional<T>가 제공하는 메서드를 거의 다 제공한다. 이렇게 대체제까지 있으니 박싱된 기본 타입을 담은 옵셔널을 반환하는 일은 없도록 하자 상대적으로 덜 중요한 Boolean, Byte, Character, Short, Float은 예외일 수 있다.

 

하지 말아야 할 것

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

 

 결론

 값을 반환하지 못할 가능성이 있고, 호출할 때마다 반환값이 없을 가능성을 염두에 둬야 하는 메서드라면 옵셔널을 반환해야 하는 상화일 수 있다. 하짐나 옵셔널 반환에는 성능 저하가 뒤따르니, 성능에 민감한 메서드라면 null을 반환하거나 예외를 던지는 편이 나을 수 있다. 옵셔널을 반환값 이외ㅢ 용도로 쓰는 경우는 매우 드물다.

 

*느낀점

Optional<T>는 사용하는 경우가 거의 정해져 있다. 반환값에 대하여 사용해야하며, 그 값이 빈 값을 가질 가능성이 있는 경우이다. 따라서 빈 값을 가질 경우, 어떻게 처리해야 하는지 고민이 필요하다. 안전 장치를 잘 만들어두면, Optional을 통해 빈 값을 가질 수 있음을 보여주고 이전보다 편하게 대응할 수 있을 것이다. 

728x90
반응형