본문 바로가기
JAVA/Effective Java

item 55. 옵셔널 반환은 신중하게 해라

by Garonguri 2022. 7. 14.
728x90

메서드가 특정 조건에서 값을 반환할 수 없을 때 취할 수 있는 선택지는 무엇이 있을까?

 

[ 자바 8 이전 ]

  • 예외 던지기
    • 진짜 예외 사항에서 사용하고 있는지 생각해라.
    • 예외를 생성할 때 스택 추적 전체를 캡쳐해야 하므로 비용이 많이 든다.
  • null 반환하기
    • null처리 코드를 별도로 추가해야 한다.
    • null처리를 무시하고 반환된 null값을 어딘가에 저장하면 언젠가 NullPointerException이 발생할 수 있다.

[ 자바 8 이후 ]

  • Optional<T>
    • 원소를 최대 1개 가질 수 있는 '불변' 컬렉션 
    • null이 아닌 T 타입 참조를 하나 담거나, 혹은 아무것도 담지 않을 수 있다.
    • 아무것도 담지 않은 optional은 '비었다'고 하고, 무엇인가 담은 옵셔널은 '비지 않았다'라고 한다.
    • 보통은 T를 반환하지만, 특정 조건에서는 아무 것도 반환해야하지 않을 경우에 사용.

Optional를 반환하도록 선언하면 된다.

유효한 반환값이 없을 때는 빈 결과를 반환하는 메소드가 만들어진다.

옵셔널을 반환하는 메서드는 예외를 던지는 메서드보다 유연하고 사용하기 쉬우며, null을 반환하지 않으므로 오류가 날 가능성도 적어진다.

 

그런 사람이 있으면 안되겠지만 Optional에 null을 반환하는 이상한 행동은 절대 하지 말자.


hoxy 자바 8 이상을 쓰고 있는가? 그럼 optional을 써라.

왜? 사용자는 null 이 반환되는지, 예외가 발생하는지 알 수 없다. 반환 값이 없을 수도 있다는 것 또한  알 수 있기 때문이다.

 

Optional은 검사 예외와 취지가 비슷하다. 즉 반환 값이 없을 수 도 있다는것을 사용자에게 명확히 알려준다.

이는 비검사 예외 또는 null을 반환하는 것과 달라, 사용자가 그 사실을 인지하지 못해 이상한 결과가 나올 수 있는 것을 막아준다.


그럼 메서드가 옵셔널을 반환했다고 치자. 클라이언트는 값을 받지 못했을 때 어떻게 해야할까?

1. 상황에 맞는 예외를 던질 수 있다.

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

특히 이렇게 하면 실제 예외가 아니라 예외 팩터리를 건냈으므로, 예외 생성 비용이 들지 않는다.


2. 원하는 예외를 던질 수 있다.

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

옵셔널에 항상 값이 채워져 있다 확신하면 곧바로 값을 꺼내 선택하면 된다.


3. 항상 값이 채워져 있다고 가정한다. 

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

4. 기본값 설정 비용이 클 경우 Supplier<T>를 인수로 받는 orElseGet을 사용한다.


5. 적합한 메서드를 찾지 못했을 경우 isPresent 메서드를 살펴보자.

안전 밸브 역할로, optional이 채워져 있으면 true, 비어있으면 false를 반환한다.

public class ParentPid {
    public static void main(String[] args) {
        ProcessHandle ph = ProcessHandle.current();

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

        // Equivalent (and superior) code using orElse
        System.out.println("Parent PID: " +
            ph.parent().map(h -> String.valueOf(h.pid())).orElse("N/A"));
    }
}

원하는 모든 메서드를 수행할 수 있으나 신중히 사용하자. isPresent를 사용하기 전에 앞의 옵셔널 활용 1~4로 표현할 수 있는지 면밀히 검토하자. 그 편이 더 짧고 명확하며 용법에 맞는 코드이다.


[ 자바 9 이후 ]

stream() 메서드가 추가되었다. 이는 optional을 Stream으로 변환해주는 어댑터다.

값이 있으면 값을 담은 스트림을, 없으면 빈 스트림으로 변환한다.

 


옵셔널을 사용하면 안되는 경우

  • 컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입
    • 빈 Optional<List<T>>를 반환하기보다는 빈 List<T>를 반환하는 게 좋다.
    • 빈 컨테이너를 그대로 반환하면 클라이언트에 옵셔널 처리 코드를 넣지 않아도 된다.
    • 박싱된 기본 타입을 옵셔널에 담는 것은 값을 두 겹 감싸는 것으로 무거울 수 있다.
      • 따라서 int, long, double 전용 옵셔널 클래스가 존재한다. 
      • Optional<T>가 박싱된 기본 타입을 담은 옵셔널을 반환하는 일은 절대 없게 하자.
    • 옵셔널을 맵의 값으로 사용하면 절대 안된다.
      • 맵 안에 키가 없다는 사실을 나타내는 방법이 두가지가 된다. 
      • 쓸데없이 복잡하고 오류 가능성이 높아진다.
    • 옵셔널을 컬렉션의 키, 값, 원소나 배열의 원소로 사용하는 게 적절한 상황은 거의 없다.

옵셔널을 사용하면 좋은 경우

  • 결과를 알 수 없으며, 클라이언트가 이 상황을 특별하게 처리해야 하는 경우
  • 단, Optional을 사용하면 새로 할당하고 객체를 초기화하며 값을 꺼내기 위해 메서드를 호출하는 한 단계를 더 거치기 때문에 성능이 저하될 수 있다. (따라서 성능이 중요한 상황에는 안맞을 수도 있다.)

핵심 정리

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


728x90

댓글