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

wisdom·2022년 10월 6일
0

Effetctive Java

목록 보기
55/80
post-thumbnail

값을 반환할 수 없는 경우

메서드가 특정 조건에서 값을 반환할 수 없을 때 취할 수 있는 선택지는 3가지다.

  1. 예외 를 던진다.

  2. null 을 반환한다.

  3. Optional<T> 를 사용한다.

자바 8 이전에는 1,2만 지원했다.

1번 예외는 정말 예외적인 상황에서만 사용해야 하며, 예외를 생성할 때 스택 추적 전체를 캡처하므로 비용이 만만치 않다.

2번 null 반환은 별도의 null 처리 코드를 추가해야 한다.
null 처리를 무시할 경우, 언젠가 NullPointerException 이 발생할 수 있다.


Optional

자바 8부터 Optional<T> 을 지원하기 시작했으며, 특정 조건에서 아무것도 반환하지 않아야 할 때 사용할 수 있다.

1. Optioanl<T> 이란?

null이 아닌 T 타입 참조를 하나 담거나, 아무것도 담지 않는 불변 컬렉션이다.
Collection<T>를 구현하지는 않았지만 원칙적으로 그렇다.

Q. 어떤 경우에 메서드 반환 타입을 T 대신 Optional<T>로 선언해야 할까?

결과가 없을 수 있고, 클라이언트가 이 상황을 특별하게 처리해야 하는 경우 Optional<T>를 반환하자.

단, Optional을 반환할 때는 대가가 있다.
Optional도 엄연히 새로 할당하고 초기화해야 하는 객체이므로, 그 안에서 값을 꺼내려면 메서드를 호출하는 단계를 하나 더 거쳐야한다. 따라서 성능이 중요한 상황에서는 다른 선택지(예외 던지기, null 반환)가 더 나을 수 있다.


2. 정적 팩터리

옵셔널을 반환하도록 구현할 때는 적절한 정적 팩터리를 사용하면 된다.

  • Optional.empty() : 빈 옵셔널을 만든다.
  • Optional.of(value) : 값이 든 옵셔널을 만든다. (단, null을 넣으면 NullPointerException 을 던진다.)
  • Optional.ofNullable(value) : null 값을 허용하는 옵셔널을 만든다.

3. 메서드

1) 값이 없는 경우 처리를 위한 기본 메서드

  • orElse(value) : 기본값을 설정한다.
  • orElseGet(Supplier<T>) : 값이 처음 필요할 때 Supplier<T>를 사용해 생성한다. 기본값을 설정하는 비용이 커서 부담이 될 경우, 초기 설정 비용을 낮출 수 있다.
  • orElseThrow(exception) : 예외를 던진다.
  • get() : 항상 값이 있다고 가정하여 곧바로 값을 꺼낸다. (단 잘못 판단한 경우, NoSuchElementException 이 발생할 수 있다)

2) 기본 메서드 외 특별한 쓰임을 위한 메서드

  • filter
  • map
  • flatMap
  • ifPresent

3) isPresent 메서드

위의 메서드들 중 적합한 메서드를 찾지 못했다면 isPresent 메서드를 사용할 수 있다. 이 메서드는 옵셔널이 채워져 있으면 true를, 비어 있으면 false를 반환한다.
이 메서드로는 원하는 모든 작업을 수행할 수 있지만, 신중히 사용해야 한다.
실제로 isPresent를 쓴 코드 중 상당수는 앞서 언급한 메서드들로 대체할 수 있으며, 그렇게 하는 것이 더 짧고 명확하고 용법에 맞는 코드가 되기 때문이다.

Optional<ProcessHandle> parentProcess = ph.parent();

// isPresent 를 사용한 코드 - 적절하지 않다.
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"));

4) stream 메서드

스트림을 사용한다면 Stream<Optional<T>>을 받아 그 중 값이 있는 옵셔널들에서 값을 뽑아 Stream<T>에 건네 담아 처리하는 일은 드물지 않을 것이다.

이는 다음과 같이 구현할 수 있다.

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

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

stream 메서드를 사용하면 앞의 코드보다 더 명료하게 구현할 수 있다.

streamOfOptionals
	.flatMap(Optional::stream)

4. 기본 타입을 위한 Optional 클래스

박싱된 기본 타입을 담는 옵셔널은 기본 타입 자체보다 무거울 수 밖에 없다.
그래서 자바는 int, long, double 전용 옵셔널 클래스를 제공한다.

  • OptionalInt
  • OptionalLong
  • OptionalDouble

이 옵셔널들도 Optional<T>가 제공하는 메서드를 거의 다 제공한다.

따라서 박싱된 기본 타입을 담은 옵셔널을 반환하는 일은 없도록 하자. (단, 덜 중요한 기본 타입용인 Boolean, Byte, Character, Short, Float은 예외일 수 있다.)


5. 반환값 외의 쓰임

옵셔널을 반환값 처리 외의 다른 용도로 쓰는 것은 대부분 적절하지 않다.

예를 들어, 옵션을 맵의 값으로 사용하는 것은 절대 안 된다.
맵 안에 키가 없다는 사실을 나타내는 방법이 2가지(키 자체가 없는 경우, 키는 있지만 빈 옵셔널인 경우)가 되면서, 쓸데없이 복잡성만 높여 혼란과 오류 가능성을 키우게 되기 때문이다.

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

옵셔널을 인스턴스 필드에 사용하는 상황은 어떨까?
이런 상황은 대부분 필수 필드를 갖는 클래스와 이를 확장해 선택적 필드를 추가한 하위 클래스를 따로 만들어야 한다.
물론 적절한 상황도 있다. 인스턴스의 상당수는 필수가 아니고, 필드들이 기본 타입이라 값이 없음을 나타낼 방법이 마땅하지 않는 경우 필드 자체를 옵셔널로 선언하는 것도 좋은 방법이다.


📌 핵심 정리

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

profile
백엔드 개발자

0개의 댓글