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

신명철·2022년 3월 21일
0

Effective Java

목록 보기
53/80

들어가며

자바 8 이전에는 메서드가 특정 조건에서 값을 반환할 수 없을 때 취할 수 있는 선택지가 두 가지 있었다. 예외를 던지거나 null을 반환하는 것이다. 두 가지 방법 모두 허점이 있다. 예외를 던지는 경우, 스택 추적 전체를 캡처하기 때문에 비용이 만만치 않다. null을 반환하는 경우에는 이런 문제가 없지만 null 처리 코드를 별도로 작성해야 한다. null 처리를 무시하고 저장해둔다면 언젠가 NPE가 발생할 수 있다. NPE가 발생한 근본적인 원인이 아닌 실제 원인과는 전혀 상관없는 코드에서 말이다.

Optioanl<T>

옵셔널은 최대 1개의 원소를 담을 수 있는 불변 컬렉션이다. 보통은 T를 반환해야 하지만 아무것도 반환하지 않아야 할 때는 T 대신 Optional<T>를 반환하도록 선언하면 된다. 옵셔널을 반환하는 메서드는 예외를 던지는 메서드보다 유연하고 사용하기 쉬우며, null 을 반환하는 메서드보다 오류 가능성이 작다.

컬렉션에서 최댓값을 구해 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을 넣으면 NPE을 던지니 주의하자. null 값도 허용하는 옵셔널을 만들기 위해서는 Optional.ofNullable(value)를 사용하면 된다. 옵셔널을 반환하는 메서드에서는 절대 null을 반환하지 말자.

Stream 의 종단 연산의 대부분이 Optional로 반환한다. 위의 코드를 Stream 버전으로 작성한다면 다음과 같다.

스트림 버전

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

옵셔널 반환을 선택하는 기준

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

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

String lastWordInLexicon = max(words).orElse("단어 없음...");
  • 옵셔널을 반환하면 클라이언트는 값을 받지 못했을 때 취할 행동을 미리 기본값으로 설정할 수 있다.

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

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

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

Element lastNobleGas = max(Elements.NOBLE_GASES).get();
  • 옵셔널에 항상 값이 채워져 있다고 확신할 수 있다면 그냥 곧바로 꺼내 사용하는 선택지가 있다. 다만 잘못 판단한다면 NoSuchElementException 이 발생할 수 있다.

ifPresent 메서드를 사용할 수도 있다. ifPresent 메서드는 밸브 역할의 메서드로, 옵셔널이 채워져 있으면 true, 비어 있으면 false를 반환한다. 이 메서드로는 원하는 모든 작업을 사용할 수도 있지만 신중히 사용해야 한다.

stream() 과 Optional

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

streamOfOptionals.flatMap(Optional::stream)

Optional 과 컨테이너

옵셔널로 반환하는 것이 무조건 득이 되는 것은 아니다. 컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입에는 옵셔널로 감싸면 안된다.Optional<List<T>>를 반환하는 것 보다 그냥 빈 List<T>를 반환하는게 좋다. 빈 컨테이너를 그대로 반환하면 옵셔널 처리 코드를 넣지 않아도 되기 때문이다.

Optional<T>로 반환 해야하는 경우

메서드의 반환 타입을 T 대신 Optional<T>로 선언해야 하는 경우는 결과가 없을 수 있으며, 클라이언트가 이 상황을 특별하게 처리해야 하는 경우 Optional<T>를 반환해야 한다.

그렇지만 이렇게 반환 하더라도 Optional<T>를 반환하는 데는 대가가 따른다. Optional도 엄연히 새로 할당하고 초기화해야 하는 객체이고, 그 안에서 값을 꺼내려먼 메서드를 호출해야 하니 한 단계를 더 거치는 셈이다. 그래서 성능이 중요한 상황에서는 Optional이 맞지 않을 수 있다.

기본 타입과 Optional

박싱된 기본 타입을 담는 옵셔널은 기본타입 자체보다 무거울 수밖에 없다. 값을 두 겹이나 감싸기 때문이다. 그래서 자바 API 설계자는 전용 옵셔널 클래스인 OptionalInt,OptionalLong,OptionalDouble를 준비했다. 이렇게 대체재가 있으니 박싱된 기본 타입을 담은 옵셔널을 반환하는 일은 없도록 하자 단, 덜 중요한 Boolean,Byte,Character,Short,Float는 예외일 수 있다.

옵셔널을 컬렉션의 키, 값, 원소나 배열의 원소로 사용하는게 적절한 상황은 거의 없다

지금까지 옵셔널을 반환하고 반환된 옵셔널을 처리하는 경우에 대해서 이야기 했다. 다른 쓰임에 관해서는 논하지 않았는데 대부분 적절치 않기 때문이다. 예컨대, 옵셔널을 맵의 값으로 사용하면 절대 안된다. 만약 그리 한다면 맵 안에 키가 없다는 사실을 나타내는 방법이 두 가지가 된다. 하나는 키 자체가 없는 경우고, 다른 하나는 키는 있지만 그 키가 속이 빈 옵셔널인 경우다. 쓸데없이 복잡성을 높여 혼란과 오류 가능성을 키운다.

더 일반화해 이야기한다면 옵셔널을 컬렉션의 키, 값, 원소나 배열의 원소로 사용하는게 적절한 상황은 거의 없다.

그렇다면 의문이 남는다. 옵셔널을 인스턴스 필드로 저장해두는 게 필요할 때가 있을까 ?
이런 상황의 대부분은 필수 필드를 갖는 클래스와, 이를 확장해 선택적 필드를 추가한 하위 클래스를 따로 만들어야 함을 암시하는 나쁜 냄새다. 하지만 가끔은 적절한 상황도 있다.

예를 들어 클래스의 필드들 중 상당수가 필수가 아니고, 그 필드들이 기본 타입이라 값이 없음을 나타낼 방법이 마땅치 않다. 이런 클래스라면 선택적 필드의 getter() 메서드들이 옵셔널을 반환하게 해주면 좋다. 따라서 이럴 때는 필드 자체를 옵셔널로 선언하는 것도 좋은 방법이다.

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

profile
내 머릿속 지우개

0개의 댓글