티스토리 뷰

자바 8 전에는 메서드가 특정 조건에서 값을 반환할 수 없을 때 취할 수 있는 선택지가 두 가지 있었습니다.

예외를 던지거나, (반환 타입이 객체 참조라면) null을 반환하는 것입니다.

두 방법 모두 허점이 존재합니다.

예외

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

null 반환

null을 반환하면 위같은 문제는 생기지 않지만, 그 나름의 문제가 있습니다.

null을 반환할 수 있는 메서드를 호출할 때는, (null이 반환될 일이 절대 없다고 확신하지 않는 한) 별도의 null 처리 코드를 추가해야 합니다.

null 처리를 무시하고 반환된 null 값을 어딘가에 저장해두면 언젠가 NullPointerException이 발생할 수 있습니다.

그것도 근본적인 원인, 즉, null을 반환하게 한 실제 원인과는 전혀 상관없는 코드에서 말입니다.

자바 버전 8로 올라가면서 또 하나의 선택지가 생겼습니다.

그 주인공인 Optional<T>는 null이 아닌 T 타입 참조를 하나 담거나, 혹은 아무것도 담지 않을 수 있습니다.

아무것도 담지 않은 옵셔널은 '비었다'고 말합니다.

옵셔널은 원소를 최대 1개 가질 수 있는 '불변' 컬렉션입니다.

Optional<T>가 Collection<T>를 구현하지는 않았지만, 원칙적으로는 그렇다는 말입니다.

보통은 T를 반환해야 하지만 특정 조건에서는 아무것도 반환하지 않아야 할 때 T 대신 Optional<T>를 반환하도록 선언하면 됩니다.

그러면 유효한 반환값이 없을 때는 빈 결과를 반환하는 메서드가 만들어집니다.

옵셔널을 반환하는 메서드는 예외를 던지는 메서드보다 유연하고 사용하기 쉬우며, null을 반환하는 메서드보다 오류 가능성이 작습니다.

아이템 30에서도 등장했던 다음 코드는 주어진 컬렉션에서 최댓값을 뽑아주는 메서드입니다.

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.requireNonull(e);
        }
    return result;
}

이 메서드에 빈 컬렉션을 건네면 IllegalArgumentException을 던집니다.

아이템 30에서도 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.requireNonull(e);
        }
    return Optional.of(result);
}

보다시피 옵셔널을 반환하도록 구현하기는 어렵지 않습니다.

적절한 정적 팩터리를 사용해 옵셔널을 생성해주기만 하면 됩니다.

이 코드에서는 두 가지 팩터리를 사용했습니다.

빈 옵셔널은 Optional.empty()로 만들고, 값이 든 옵셔널은 Optional.of(value)로 생성했습니다.

Optional.of(value)에 null을 넣으면 NullPointerException을 던지니 주의합시다.

null 값도 허용하는 옵셔널을 만들려면 Optional.ofNullable(value)을 사용하면 됩니다.

옵셔널을 반환하는 메서드에서는 절대 null을 반환하지 말아야 합니다.

옵셔널을 도입한 취지를 완전히 무시하는 행위이기 때문입니다.

스트림의 종단 연산 중 상당수가 옵셔널을 반환합니다.

앞의 max 메서드를 스트림 버전으로 다시 작성한다면 Stream의 max 연산이 우리에게 필요한 옵셔널을 생성해 줄 것입니다. (비교자를 명시적으로 전달해야 하지만)

public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
    return c.stream().max(Comparator.naturalOrder());
}
🤔
null을 반환하거나 예외를 던지는 대신 옵셔널 반환을 선택해야 하는 기준은 무엇일까요?

 

옵셔널은 검사 예외와 취지가 비슷합니다. (아이템 71)

즉, 반환값이 없을 수도 있음을 API 사용자에게 명확히 알려줍니다.

비검사 예외를 던지거나 null을 반환한다면 API 사용자가 그 사실을 인지하지 못해 끔찍한 결과로 이어질 수 있습니다.

하지만 검사 예외를 던지면 클라이언트에서는 반드시 이에 대처하는 코드를 작성해넣어야 합니다.

비슷하게, 메서드가 옵셔널을 반환한다면 클라이언트는 값을 받지 못했을 때 취할 행동을 선택해야 합니다.

그 중 하나는 기본값을 설정하는 방법입니다.

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

또는 상황에 맞는 예외를 던질 수 있습니다.

다음 코드에서 실제 예외가 아니라 예외 팩터리를 건넨 것에 주목합시다.

이렇게 하면 예외가 실제로 발생하지 않는 한 예외 생성 비용은 들지 않습니다.

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

옵셔널에 항상 값이 채워져 있다고 확신한다면 그냥 곧바로 값을 꺼내 사용하는 선택지도 있습니다.

다만 잘못 판단한 것이라면 NoSuchElementException이 발생할 것입니다.

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

이따금 기본값을 설정하는 비용이 아주 커서 부담이 될 때가 있습니다.

그럴 때에는 Suplier<T>를 인수로 받는 orElseGet을 사용하면, 값이 처음 필요할 때 Supplier<T>를 사용해 생성하므로 초기 설정 비용을 낮출 수 있습니다.

더 특별한 쓰임에 대비한 메서드도 준비되어 있습니다.

바로 filter, map, flatMap, ifPresent입니다.

앞서의 기본 메서드로 처리하기 어려워 보인다면 API 문서를 참조해 이 고급 메서드들이 문제를 해결해줄 수 있을지 검토해봅시다.

여전히 적합한 메서드를 찾지 못했다면 isPresent 메서드를 살펴봅시다.

안전밸브 역할의 메서드로, 옵셔널이 채워져 있으면 true, 비어 있으면 false를 반환합니다.

이 메서드로는 원하는 모든 작업을 수행할 수 있지만 신중히 사용해야 합니다.

실제로 isPresent를 쓴 코드 중 상당수는 앞서 언급한 메서드들로 대체할 수 있으며, 그렇게 하면 더 짧고 명확하며 용법에 맞는 코드가 됩니다.

다음 코드를 예로 생각해봅시다.

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

자바 9에서 소개된 ProcessHandle 클래스를 사용했습니다.

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>에 건네 담아 처리하는 경우가 드물지 않습니다.

자바 8에서는 다음과 같이 구현할 수 있습니다.

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

보다시피 옵셔널에 값이 있다면(Optional::isPresent) 그 값을 꺼내 (Optional::get) 스트림에 매핑합니다.

자바 9에서는 Optional에 stream() 메서드가 추가되었습니다.

이 메서드는 Optional을 Stream을 변환해주는 어댑터입니다.

옵셔널에 값이 있으면 그 값을 원소로 담은 스트림으로, 값이 없다면 빈 스트림으로 변환합니다.

이를 Stream의 flatMap 메서드(아이템 45)와 조합하면 앞의 코드를 다음처럼 명료하게 바꿀 수 있습니다.

streamOfOptionals
    .flatMap(Optional::stream)

반환값으로 옵셔널을 사용한다고 해서 무조건 득이 되는 건 아닙니다.

컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입은 옵셔널로 감싸면 안 됩니다.

빈 Optional<List<T>>를 반환하기보다는 빈 List<T>를 반환하는 게 좋습니다. (아이템 54)

빈 컨테이너를 그대로 반환하면 클라이언트에 옵셔널 처리 코드를 넣지 않아도 됩니다.

참고로 ProcessHandle.Info 인터페이스의 arguments 메서드는 Optional<String[]>을 반환하는데, 이는 예외적인 경우이니 따라하지 맙시다.

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

 

기본 규칙은 이렇습니다.

결과가 없을 수 있으며, 클라이언트가 이 상황을 특별하게 처리해야 한다면 Optional<T>를 반환합니다.

그런데 이렇게 하더라도 Optional<T>를 반환하는 데는 대가가 따릅니다.

Optional도 엄연히 새로 할당하고 초기화해야 하는 객체이고, 그 안에서 값을 꺼내려면 메서드를 호출해야 하니 한 단계를 더 거치는 셈입니다.

그래서 성능이 중요한 상황에서는 옵셔널이 맞지 않을 수 있습니다.

어떤 메서드가 이 상황에 처하는지 알아내려면 세심히 측정해보는 수밖에 없습니다. (아이템 67)

박싱된 기본 타입을 담는 옵셔널은 기본 타입 자체보다 무거울 수밖에 없습니다.

그래서 자바 API 설계자는 int, long, double 전용 옵셔널 클래스들을 준비해놨습니다.

바로 OptionalInt, OptionalLong, OptionalDouble입니다.

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

이렇게 대체재까지 있으니 박싱된 기본 타입을 담은 옵셔널을 반환하는 일은 없도록 합시다.

단, '덜 중요한 기본 타입'용인 Boolean, Byte, Character, Short, Float은 예외일 수 있습니다.

지금까지 옵셔널을 반환하고 반환된 옵셔널을 처리하는 이야기를 나눴습니다.

다른 쓰임에 관해서는 논하지 않았는데, 대부분 적절하지 않기 때문입니다.

예컨대 옵셔널을 맵의 값으로 사용하면 절대 안 됩니다.

만약 그리 한다면 맵 안에 키가 없다는 사실을 나타내는 방법이 두 가지가 됩니다.

하나는 키 자체가 없는 경우고, 다른 하나는 키는 있지만 그 키가 속이 빈 옵셔널인 경우입니다.

쓸데없이 복잡성만 높여서 혼란과 오류 가능성을 키울 뿐입니다.

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

그렇다면 커다란 의문이 하나 남습니다.

🤔
옵셔널을 인스턴스 필드에 저장해두는 게 필요할 때가 있을까?

 

이런 상황 대부분은 필수 필드를 갖는 클래스와, 이를 확장해 선택적 필드를 추가한 하위 클래스를 따로 만들어야 함을 암시하는 '나쁜 냄새'입니다.

하지만 가끔은 적절한 상황도 있습니다.

예를 들어 아이템 2의 NutritionFacts 클래스를 다시 봅시다.

public class NutritionFacts {
    private final int servingSize;  // 필수
    private final int servings;     // 필수
    private final int calories;     // 선택
    private final int fat;          // 선택
    private final int sodium;       // 선택
    private final int carbohydrate; // 선택

    public NutritionFacts(int servingSize, int servings) {
        this(servingSize,servings,0);
    }

    public NutritionFacts(int servingSize, int servings, int calories) {
        this(servingSize,servings,calories,0);
    }

    public NutritionFacts(int servingSize, int servings, int calories, int fat) {
        this(servingSize,servings,calories,fat,0);
    }

    public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
        this(servingSize,servings,calories,fat,sodium,0);
    }

    public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
        this.servingSize  = servingSize;
        this.servings     = servings;
        this.calories     = calories;
        this.fat          = fat;
        this.sodium       = sodium;
        this.carbohydrate = carbohydrate;
    }

}

NutritionFacts 인스턴스의 필드 중 상당수는 필수가 아닙니다.

또한 그 필드들은 기본 타입이라 값이 없음을 나타낼 방법이 마땅치 않습니다.

이런 NutritionFacts라면 선택적 필드의 getter 메서드들이 옵셔널을 반환하게 해주면 좋았을 것입니다.

따라서 이럴 때는 필드 자체를 옵셔널로 선언하는 것도 좋은 방법입니다.

정리

  • 값을 반환하지 못할 가능성이 있고, 호출할 때마다 반환값이 없을 가능성을 염두에 둬야 하는 메서드라면 옵셔널을 반환해야 할 상황일 수 있다.
  • 하지만 옵셔널 반환에는 성능 저하가 뒤따르니, 성능에 민감한 메서드라면 null을 반환하거나 예외를 던지는 편이 나을 수 있다.
  • 그리고 옵셔널을 반환값 이외의 용도로 쓰는 경우는 매우 드물다.
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
글 보관함