자바/모던 자바 인 액션

[모던 자바 인 액션] 11장. null 대신 Optional 클래스

Rudtjs 2023. 1. 7. 15:16

자바로 개발을 하다 보면 NPE를 겪게 된다. 자바 8부터는 이 NPE를 어떻게 처리할까?

 

값이 없는 상황 처리 하기

public String getCarInsuranceName(Person person) {
    if (person != null) {
        Car car = person.getCar();
        if (car != null) {
            Insurance insurance = car.getInsurance();
            if (insurance != null) {
                return insurance.getName();
            }
        }
    }
    return "Unknown";
}
  • 위 예제 에서는 변수에 접근할 때마다 null 체크를 하기 때문에 코드가 지저분해진다. 
  • null 때문에 발생하는 문제가 많기 때문에 사용해서는 안된다.
    • 자바는 개발자로부터 모든 포인터를 숨겼다. 하지만 예외가 Null이다.
    • Null은 무형식이며 정보를 포함하지 않고 있기 때문에 모든 참조 형식에 Null을 할당할 수 있다.

Optional 클래스

  • Optional은 선택형 값을 캡슐화하는 클래스이다.
  • 값이 있으면 Optional 클래스는 값을 감싸고, 값이 없으면 Optional.empty 메서드로 Optional 객체를 반환한다.
public class Person {
    private Optional<Car> car;

    public Optional<Car> getCar() {
        return car;
    }
}

public class Car {
    private Optional<Insurance> insurance;

    public Optional<Insurance> getInsurance() {
        return insurance;
    }
}
  • Optional <car> 이런 식으로 명시하면 사람이 자동차를 소유했을 수도 아닐 수도 있다는 것을 나타낸다.

 

Optional 적용 패턴

Optional 객체 만들기

  • 빈 Optional: Optional.empty로 빈 Optional 객체를 얻을 수 있다.
  • null이 아닌 값으로 Optional 만들기: 정적 팩토리 메서드 of를 활용하여 null이 아닌 값을 포함하는 Optional을 만들 수 있다. Optional.of(car);
  • Null 값으로 Optional 만들기: Optional.ofNullable(car) 이런 식으로 null값을 저장할 수 있는 Optional을 만들 수 있다.

 

맵으로 Optional의 값을 추출하고 변환하기

Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);
  • Optional은 map 메서드를 지원해주기 때문에 이런 식으로 쉽게 값을 추출할 수 있다.

flatMap으로 Optional 객체 연결

Optional<Person> optionalPerson = Optional.of(person);
String name = optionalPerson.flatMap(Person::getCar)
                .flatMap(Car::getInsurance)
                .map(Insurance::getName)
                .orElse("UNKNOWN");

 

Optioanl 스트림 조작

Stream<Optional<String>> optionalStream = persons.stream()
                                                .map(Person::getCar)
                                                .map(optCar -> optCar.flatMap(Car::getInsurance))
                                                .map(optInsurance -> optInsurance.map(Insurance::getName));
Set<String> collect = optionalStream.filter(Optional::isPresent)
                                    .map(Optional::get)
                                    .collect(Collectors.toSet());
  • 자바 9에서는 스트림을 쉽게 처리할 수 있도록 Optional에 stream() 메서드를 추가했다.
  • 다음 코드 처럼 filter를 이용하여 결과를 얻을 수 있다.

Optional 클래스의 다양한 메서드

static <T> Optional<T> empty() 아무런 값도 가지지 않는 비어있는 Optional 객체를 반환함.
T get() Optional 객체에 저장된 값을 반환함.
boolean isPresent() 저장된 값이 존재하면 true를 반환하고, 값이 존재하지 않으면 false를 반환함.
static <T> Optional<T> of(T value) null이 아닌 명시된 값을 가지는 Optional 객체를 반환함.
static <T> Optional<T> ofNullable(T value) 명시된 값이 null이 아니면 명시된 값을 가지는 Optional 객체를 반환하며, 명시된 값이 null이면 비어있는 Optional 객체를 반환함.
T orElse(T other) 저장된 값이 존재하면 그 값을 반환하고, 값이 존재하지 않으면 인수로 전달된 값을 반환함.
T orElseGet(Supplier<? extends T> other) 저장된 값이 존재하면 그 값을 반환하고, 값이 존재하지 않으면 인수로 전달된 람다 표현식의 결괏값을 반환함.
<X extends Throwable> T
orElseThrow(Supplier<? extends X>  exceptionSupplier)
저장된 값이 존재하면 그 값을 반환하고, 값이 존재하지 않으면 인수로 전달된 예외를 발생시킴.

 

정리

  • null이 될 수 있을 경우 Optional.ofNullable를 이용하자.
  • Integer.parseInt와 같은 예를 보면 문자열을 정수로 반환할 수 없는 경우 NumberFormatException을 반환한다. 
public static Optional<Integer> stringToInt(String s) {
  try {
    return Optional.of(Integer.parseInt(s));
  } catch (NumberForamtException e) {
    return Optional.empty();
  }
}