JAVA/Effective Java

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

꾸준함. 2024. 3. 12. 09:21

자바 7 버전까지는 메서드가 특정 조건에서 값을 반환할 수 없을 때 취할 수 있는 선택지가 다음과 같이 두 가지였습니다.

  • 예외를 던지거나
  • null을 반환

 

1. 예외를 던질 때

 

  • 예외는 반드시 예외적인 상황에서만 던져야 함
  • e.printStackTrace()와 같이 스택 추적 전체를 캡처하는 것은 비싼 작업이므로 지양해야 함

 

2. null을 반환

 

  • null을 반환할 수 있는 메서드를 호출할 때는 별도의 null 처리 코드를 추가해야 함
  • null을 반환하는 경우 항상 NPE를 조심해야 함

 

자바 8+ 버전부터는 위 두 가지 선택지 외 Optional이라는 또 하나의 선택지가 추가되었습니다.

 

Optional

  • Optional<T>는 null이 아닌 T 타입 참조를 하나 담거나, 혹은 아무것도 담지 않을 수 있는 객체
    • 참조 타입의 객체를 한번 감싼 일종의 Wrapper 클래스
    • 아무 것도 담지 않은 Optional은 `비었다`라고 말함
    • 반대로 어떤 값을 담은 Optional은 `비지 않았다`라고 말함

 

  • Optional은 원소를 최대 1개 가질 수 있는 `불변` 컬렉션
  • Optional을 반환하는 메서드는 예외를 던지는 메서드보다 유연하고 사용하기 쉬움
  • 자바 8 이전의 코드보다 null-safe 한 로직을 처리할 수 있도록 지원

 

주어진 컬렉션에서 최댓값을 구하는 max 메서드를 예시로 Optional 적용 전과 후를 비교해 보겠습니다.

 

1. Optional 적용 전


 

코드 부연 설명

  • 빈 컬렉션을 건네면 IllegalArgumentException 예외 던짐

 

2. Optional 적용 후


 

코드 부연 설명

  • 위 코드에서는 두 가지 팩토리를 사용해 Optional을 생성
    • 빈 Optional은 Optional.empty()로 생성
    • 값이 든 Optional은 Optional.of(value)로 생성
      • Optional.of(value)에 null을 넣으면 NPE 발생하니 주의!

 

* 주의: Optional을 반환하는 메서드에서 null을 반환할 경우 Optional을 도입한 취지를 완전히 무시하는 행위이므로 절대 null을 반환하지 말아야 합니다!

 

3. Stream을 이용한 버전


 

코드 부연 설명

  • 스트림의 종단 연산 중 상당수가 Optional을 반환
  • 앞의 max 메서드를 스트림 버전으로 다시 작성한다면 Stream의 max 연산이 우리에게 필요한 Optional을 생성

 

Optional을 사용해야 하는 이유

Optional은 검사 예외와 취지가 비슷합니다.

  • 반환 값이 있을 수도 있고, 없을 수도 있다는 것을 API를 사용하는 클라이언트에게 명확히 고지함
  • Optional을 반환하는 메서드는 Checked Exception처럼 클라이언트가 값을 받지 못했을 때 대처 코드를 강제적으로 작성해야 함
    • 만약 비검사 예외를 던지거나 null을 반환할 경우 API 사용자가 그 사실을 컴파일 시점에 인지 못해 런타임에 예상치 못한 장애 발생 가능
    • 하지만 검사 예외를 던지면 클라이언트는 강제적으로 예외가 발생할 수 있음을 인지하고 try-catch 구문을 통해 예외를 처리하는 로직을 추가해야 함

 

  • 메서드가 Optional을 반환하는 경우, 클라이언트는 값이 없을 때 취할 대표적인 행동으로 다음 네 가지가 있음
    • 기본값을 정하기
    • 원하는 예외를 던지기
    • 항상 값이 채워져 있는 것이 보장되는 경우 그냥 받기
    • 기본값을 설정하는 비용이 부담되는 경우 orElseGet 사용

 

1. 기본값을 정하기

 

 

2. 원하는 예외를 던지기


 

코드 부연 설명

  • 실제 예외가 아니라 예외 팩토리를 던지는 것을 주목
  • 이렇게 처리하면 예외가 실제로 발생하지 않는 한 예외 생성 비용이 들지 않음

 

3. 항상 값이 채워져 있는 것이 보장되는 경우 그냥 받기


 

코드 부연 설명

  • 항상 값이 채워져 있다고 확신할 경우 get() 메서드를 통해 곧바로 값을 꺼내 사용해도 됨
  • 다만, 값이 채워져있지 않을 경우 NoSuchElementException 예외 발생

 

4. 기본값을 설정하는 비용이 부담되는 경우 orElseGet 사용


 

코드 부연 설명

  • 기본값을 설정하는 비용이 부담되는 경우 orElseGet 사용 시 값이 처음 필요할 때 Supplier를 통해 생성하므로 초기 생성비용을 낮출 수 있음
  • 만약 데이터 소스로부터의 연결 획득이 실패하면, orElseGet() 메서드가 실행되며 여기서 getLocalConnection() 메서드가 Supplier로 전달되어, 이를 통해 기본값으로 사용할 로컬 연결을 생성하며 해당 방식의 장점은 아래와 같음
    • 기본값 생성의 지연: orElseGet() 메서드는 값이 필요한 시점에 Supplier를 통해 기본값을 생성하므로, 기본값을 항상 생성하지 않고 필요할 때만 생성함, 이는 초기 생성 비용을 낮출 수 있어 기본값 설정의 비용이 부담되는 경우 효율적
    • Supplier를 통한 Lazy Evaluation: orElseGet()은 Supplier를 받아들이기 때문에, 값이 필요한 시점에서만 연결을 얻는 등 Lazy Evaluation을 구현할 수 있음

 

위에 설명한 대처법 외에도 더 특별한 쓰임에 대비한 메서드인 filter, map, flatMap, ifPresent가 있습니다.

또한 앞서의 기본 메서드로 처리하기 어려워 보인다면 API 문서를 참조해 고급 메서드들이 문제를 해결할 수 있는지 검토해 보는 것을 권장합니다.

 

Optional로 권장하지 않는 작업

 

1. isPresent() 메서드 사용

 

isPresent()는 Optional 객체 내부의 값이 있는 경우 true, 없는 경우 false를 반환하는 메서드입니다.

isPresent()를 쓴 코드 중 상당수는 앞서 언급한 메서드들로 대체할 수 있으며 앞서 언급한 메서드 사용 시 더 짧고 명확하고 용법에 맞는 코드가 됩니다.

부모 프로세스의 프로세스 ID를 출력하거나, 부모가 없다면 "N/A"를 출력하는 코드로 예시를 들겠습니다.


 

코드 부연 설명

  • isPresent()를 Optional의 map을 사용하여 다듬으면 훨씬 간결하고 가독성이 좋아짐

 

2. Collection, 배열, 그리고 Stream 같은 컨테이너 타입은 Optional로 감싸지 않는 것을 권장

 

  • Optional<List>를 반환하기보다는 빈 ArrayList를 반환하는 것이 좋으며 이유는 클라이언트 코드에서 Optional 처리 코드를 넣지 않아도 되기 때문
  • 빈 컨테이너를 그대로 반환하면 클라이언트에서 Optional 처리 코드를 넣지 않아도 됨
  • Stream을 사용한다면 Optional들은 Stream<Optional<T>>로 받아서, 그중 채워진 Optional들에서 값을 뽑아 Stream<T>에 건네 담아 처리하기 때문에 번거로움
    • 자바 9+ 버전을 사용하는 경우 Stream의 flatMap 메서드와 조합해 코드를 명료하게 작성하는 것을 권장


 

* Optional을 컬렉션의 키, 값, 원소나 배열의 원소로 사용하는 것이 적절한 상황은 극히 드묾

 

3. Optional을 Map의 key나 value로 사용하지 않는 것을 권장

 

  • 만약 Optional을 Map에서 사용한다면 다음과 같이 모호한 상황 발생하여 복잡도만 높아짐
    • Key 자체가 없음
    • Key는 있지만, 속이 빈 Optional
    • Key가 있고 Optional도 값이 있음

 

Optional을 권장하는 경우

메서드에서 Optional<T>를 반환하는 기본 규칙은 다음과 같습니다.

  • 결과가 없을 수 있으며
  • 클라이언트가 이 상황을 특별하게 처리해야 할 경우

 

위와 같은 상황에서도 성능에 민감한 애플리케이션에서는 Optional을 초기화하고 새로 할당한 후 안에서 값을 꺼내는 비용이 비싸기 때문에 권장하지 않습니다.

 

비고

  • 값을 반환하지 못할 가능성이 있고, 호출할 때마다 반환값이 없을 가능성을 염두에 둬야 하는 메서드라면 Optional을 반환해야 할 상황
  • Optional을 반환값 이외의 용도로 쓰는 경우는 극히 드물기 때문에 고려하지 말자
  • Wrapper 타입을 담는 Optional은 값을 두 겹이나 감싸기 때문에 Primitive 타입 자체보다 무거울 수밖에 없음
    • int, long, double을 사용할 때는 전용 Optional 클래스인 OptionalInt, OptionalLong, OptionalDouble을 사용하는 것을 권장
    • 이들은 Optional<T>가 제공하는 메서드를 거의 다 제공
    • 따라서 Wrapper 타입을 담은 Optional을 반환하는 일은 없어야 함
    • Boolean, Byte, Character, Short, Float은 예외

 

참고

이펙티브 자바

반응형