본문 바로가기
Book/모던 자바 인 액션

[모던 자바 인 액션] Chapter 11. null 대신 Optional 클래스 (2)

by ghan2 2024. 11. 20.

 

Optional을 이용한 Person/Car/Insurance 참조 체인

앞에서 살펴본 예제에서는 Optional이 비어있을 때 기본값을 제공하는 orElse라는 메서드를 사용했다. 기본값을 제공하거나 언랩(unwrap)하는 다양한 메서드를 제공한다. 이제 다양한 기능을 자세히 알아보자

 

11.3.4 Optional 스트림 조작

stream()을 사용하여 유용하게 활용할 수 있다.

public Set<String> getCarInsuranceNames(List<Person> persons) {
    return persons.stream()
        .map(Person::getCar)
        .map(optCar -> optCar.flatMap(Car::getInsurance))
        .map(optIns -> optIns.map(Insurance::getName))
        .flatMap(Optional::stream)
        .collect(toSet());

 

11.3.5 디폴트 액션과 Optional 언랩
옵셔널 클래스는 옵셔널 인스턴스에 포함된 값을 읽는 다양한 방법을 제공한다.

  • get(): 값을 읽는 가장 간단하면서도 가장 안전하지 않은 메서드. 래핑된 값이 있으면 해당 값을 반환하고, 없으면 NoSuchElementException을 반환한다.
  • orElse(T other) 메서드는 Optional이 값을 포함하지 않을 때 기본 값을 제공할 수 있다.
  • orElseGet(Supplier< ? extends T> other)은 orElse 메서드에 대응하는 게으른 버전의 메서드다. 옵셔널에 값이 없을 때만 Supplier가 실행된다.
  • orElseThrow(Supplier< ? extends X> exceptionSupplier)는 옵셔널이 비어있을 때 지정한 예외를 발생시킨다.
  • ifPresent(Consumer< ? super T> consumer)는 값이 존재할 때 인수로 넘겨준 동작을 실행할 수 있다.
  • ifPresentOrElse(Consumer< ? super T> action, Runnable emptyAction)은 비어있을 때만 실행할 수 있는 Runnable을 인수로 받는다.

 

11.3.6 두 Optional 합치기

public Insurance findcheapestInsurance(Person person, Car car) {
...
    return cheapestCompany;
}

public Optional<Insurance> nullSafeFindCheapestInsurance(Optional<Person> person, Optional<Car> car) {
    if (person.isPresent() && car.isPresent()) {
        return Optional.of(findCheapestInsurance(person.get(), car.get()));
    } else {
        return Optional.empty();
    }
}

위 코드는 사람과 자동차의 옵셔널을 인수로 받아서 둘 중 하나라도 비어있으면 빈 옵셔널을 반환한다.

public Optional<Insurance> nullSafeFindCheapestInsurance(Optional<Person> person, Optional<Car> car) {
    return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c)));
}

위 코드로 메서드를 재구현 할 수 있다. 첫 번째 옵셔널에 flatMap 메서드로 옵셔널이 비어있다면 인수로 전달된 표현식이 실행되지 않고 빈 옵셔널을 반환한다.
두 번째 옵셔널도 존재한다면 map 메서드로 전달한 람다 표현식이 안전하게 함수를 호출한다.

 

11.3.7 필터로 특정값 거르기

Insurance insurance = ...;

if (insurance != null && "CambridgeInsurance".equals(insurance.getName())) {
    System.out.println("ok");
}

위 코드처럼 객체의 메서드를 호출해서 프로퍼티를 확인해야 하는 경우에 다음과 같이 재구성할 수 있다.

Optional<Insurance> optInsurance = ...;

optInsurance.filter(insurance -> "CambridgeInsurance".equals(insurance.getName()))
    .ifPresent(x -> System.out.println("ok"));

 

11.4 Optional을 사용한 실용 예제


11.4.1 잠재적으로 null이 될 수 있는 대상을 Optional로 감싸기

Object value = map.get("key");

// 위와 같이 널이 반환될 수 있는 코드를 옵셔널로 감싸 개선할 수 있다.
Optional<Object> value = Optional.ofNullable(map.get("key"));

 

11.4.2 예외와 Optional 클래스
null을 확인할 때는 if문을 사용하지만 예외를 발생시키는 메서드는 try/catch 블록을 사용해야 한다.

public static Optional<Integer> stringToInt(String s) {
    try {
        return Optional.of(Integer.parseInt(s));
    } catch (NumberFormatException e) {
        return Optional.empty();
    }
}

위 코드처럼 정수로 변환할 수 없는 문자열은 빈 옵셔널로 반환하도록 구현할 수 있다.

 

11.4.3 기본형 Optional을 사용하지 말아야 하는 이유
옵셔널에서도 기본형에 특화된 OptionalInt, OptionalLong, OptionalDouble등의 클래스를 제공한다.
그러나 옵셔널의 최대 요소 수는 한개이므로 기본형 특화 옵셔널로 성능을 향상시킬 수 없다.
또한 기본형 특화 옵셔널은 map, flatMap, filter 등을 제공하지 않고, 생성한 결과를 다른 일반 옵셔널과 혼용할 수 없다.

 

11.5 마치며

  • 역사적으로 프로그래밍 언어에서는 null 참조로 값이 없는 상황을 표현해왔다.
  • 자바 8에서는 값이 있거나 없음을 표현할 수 있는 클래스 java.util.Optional<T>를 제공한다.
  • 팩토리 메서드 Optional.empty, Optional.of, Optional.ofNullable 등을 이용해서 Optional 객체를 만들 수 있다. 
  • Optional 클래스는 스트림과 비슷한 연산을 수행하는 map, flatMap, filter등의 메서드를 제공한다.
  • 옵셔널로 값이 없는 상황을 적절하게 처리하도록 강제할 수 있다.
  • 옵셔널을 활용하면 더 좋은 API를 설계할 수 있다. 즉, 사용자는 메서드의 시그니처만 보고도 옵셔널 값이 사용되거나 반환되는지 예측할 수 있다.