Chapter 11. null 대신 Optional 클래스

11.0.

기본적으로 null체크는 자바 프로그래밍을 하면서 꼭 필요한 체크이다. 테스트 시에 항상 있던 값이 실제 서비스 운영 중에는 의도치 않게 않았던 null로 들어오게 되는 경우가 있다. 이런 경우는 컴파일 에러로 잡지 못하기 때문에 이를 대비해서 해당 값에 default값을 설정하거나 null 체크는 필수적이다.

하지만 이러한 null 체크는 누락되기 쉽고 코드의 가독성을 해치는 요인이 된다. 이 장에서는 이를 대체할 수 있는 Optional 클래스에 대해서 이야기한다.


11.1. 값이 없는 상황 처리

public class Person{
    private Car car;
    public Car getCar() { return car; }
}

public class Car{
    private Insurance insurance;
    public Insurance getInsurance() { return insurance; }
}

public class Insurance{
    private String name;
  public String getName(){ return name; }
}

person.getCar().getInsurance().getName();

위와 같은 상황에서 NPE를 경험할 가능성이 높다.

Person객체 자체가 null일수도 있고 Person객체가 Car 객체를 가지고 있지 않을 수 있다. 최종적으로 getName()에서 참조하는 Insuranced의 name필드가 null 일수도 있다. 이를 보통 어떻게 처리하는가?


11.1.2 보수적인 자세로 NPE 줄이기

  • 보통은 null 체크 코드를 통해서 이를 확인한다. 하지만 다음과 같이 체크 했을 때 코드의 복잡도나 들여쓰기 수준이 증가한다.
if(person != null){
    Car car = person.getCar();
    if(car != null){
        .....중략.....
    }
}

다음과 같이 getter메서드에서 이를 분기처리하는 것도 결국 하나의 메서드에 네 개의 출구가 생겼기 때문에 이를 유지보수하기 어려워진다.

public String getCarInsuranceName(Person person){
    if(person == null){
      //처리
    }
    Car car = person.getCar();
    if(car == null){
        //처리
    }
    .....중략.....
}



11.1.2 - 3 null 때문에 발생하는 문제 및 다른 언어에서의 대처법

  1. 다수 에러의 근원
  2. 코드 가독성을 해친다.
  3. 아무 의미가 없는 값이다. 그래서 모든 참조 형식에 null을 할당할 수 있는데, 이때 이 null이 할당된 의미는 코드를 작성한 사람 이외에는 알 수 없다.
  4. 자바 철학에 위배된다. 자바는 개발자에 포인터를 숨겼지만 null 포인터는 예외이다.

그루비 언어 등에서는 safe navigation operator (?.) 를 도입해서 null문제를 해결했다.

def carInsuranceName = person?.car?.insurance?.name 



11.2. Optional 클래스

자바 8은 하스켈, 스칼라와 같은 함수형 프로그래밍 언어의 영향을 받아서 java.util.Optional라는 새로운 클래스를 제공한다. 이는 선택형 값을 캡슐화하는 클래스이다. 위의 Person, Car예제처럼 모든 Person의 인스턴스가 차를 가지고 있지 않을 수 있다. 그런 경우 Car 인스턴스에 null을 할당하는 것이 아니라, Optional클래스로 이 값을 감싸면 된다.

Optional<Car> car에서 car값이 있다면 이를 Optional클래스로 감싸고, 값이 없다면 Optional.empty 메서드를 통해서 Optional을 반환한다.

Optional을 사용함으로써 NPE를 줄일 수 있고 추가적으로 도메인 모델에 대한 의미를 좀 더 명확하게 만들 수 있다. (클래스 내의 변수가 null일 수도 있는 케이스, 절대 null이면 안되는 케이스를 Optional클래스 사용 여부에 따라서 구분할 수 있다.)

11.3. Optional 적용 패턴

11.3.1. Optional 객체 만들기

필요에 따라서 빈 Optional 객체, 혹은 값을 가지고 있는 Optional 객체를 만들 수 있다

Optional<Car> optCar = Optional.empty(); // 팩토리 메서드 
Optional<Car> optCar = Optional.of(car); // null이 아닌 값
Optional<Car> optCar = Optional.ofNullable(car); // null값을 저장할 수 있는 Optional 생성



11.3.2. Optional 값 가져오기

Optional의 get()메서드를 통해서 값을 가져올 때, Optional이 비어있으면 익셉션이 발생한다. 이는 결국 Optional을 잘못 사용한다면 결국 null 사용과 동일한 문제를 겪을 수 있다.

Optional 클래스는 위와 같은 문제를 겪지 않게 내부적으로 map 메서드를 사용하도록 한다. Optional이 값을 가지고 있으면 map의 인수로 제공되는 함수에 의해서 값을 반환하고 Optional이 비어있으면 아무 일도 일어나지 않는 형태가 된다. (Stream 연산과 비슷함)

Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);

실제 위에서 봤었던 예제에서는 person.getCar().getInsurance().getName(); 이렇게 참조를 중첩적으로 하는 구조를 가진다. Map()의 경우 Optional(결과값) 타입을 반환하기 때문에 Optional 중첩을 없애기 위해서는 flatMap() 메서드를 이용해서 Optional의 중첩이 일어나지 않도록 한다.

결과적으로 Person클래스의 Car 멤버변수를 Optional로 감싸고 Car클래스 Insurance 멤버변수를 Optional 로 감싼 상태에서는 아래와 같이 보험명을 가져올 수 있다.

public String getCarInsuranceName(Optional<Person> person){
    return person.flatMap(Person::getCar)
            .flatMap(Car::getInsurance)
            .map(Insurance::getName)
            .orElse("Unknown"); // 결과 Optional 이 비었으면 기본값 사용
}



11.3.4. Optional 스트림 조작

자바9 에서는 Optional 을 포함하는 스트림 처리를 지원한다. 위의 하나 Person 객체의 보험사 이름조회를 Person객체 리스트에 대해 가입되어있는 보험사 Set으로 조회할 수 있다.

Optional 클래스의 stream() 메서드는 각 Optional의 값이 비어있는지 여부에 따라서 내부 값을 0~1개 포함하는 스트림을 반환하게 되고 이를 flatMap으로 하나의 스트림으로 합쳐서 Optional을 언랩 할 수 있다.

public Set<String> getCarInsuranceNameSet(List<Person> personList){
        return personList.stream()
            .map(Person::getCar)
            .map(optCar -> optCar.flatMap(Car::getInsurance))
            .map(optIns -> optIns.map(Insurance::getName))
            .flatMap(Optional::stream) // 이전단계까지의 결과물인 Stream<Optional<String>>에서 빈 Optional은 제외하고 실제 값이 있는 Optional만 가져오기 위해서 Optional::stream 메서드 활용
            .collect(toSet());
}

// 위에서 Optional의 stream() 메서드 사용부분은 아래와 같이 변환 가능
// Stream<Optional<String>> 
Set<String> result = stream.filter(Optional::isPresent)
                                                    .map(Optional::get)
                                                    .collect(toSet());



11.3.5 디폴트 액션과 Optional 언랩

이전에 orElse 를 통해서 Optioanl 에 대한 디폴트 값을 처리했었고 더 다양한 방식이 있다.

  • get()
    • Optional 값이 있으면 값을 반환하고 없으면 NoSuchElementException 발생. 내부적으로 unchecked exception이 발생
  • orElse()
    • 11.3.3 절 예제에 있듯이 값이 없으면 default 값으로 내려줄 수 있음
  • orElseGet()
    • orElse() 의 lazy 버전으로 Optional에 값이 없을 때만 default 값 생성
  • orElseThrow()
    • Optional이 비어있을때 예외를 발생시키는 부분은 get()과 동일하지만 원하는 예외를 던질 수 있음
  • ifPresentOrElse()
    • 자바9에서 추가되었으며 ifPresent와 동일하지만 Optional이 비었을때 실행 가능한 Runnable을 인수로 받음



11.3.6. 두 Optional 합치기

위의 예제의 연장선으로 Person과 Car의 정보를 기준으로 가장 싼값의 Insurance를 구한다고 했을 때 isPresent() 를 사용해서 일일히 Optional값의 존재여부를 체크하는건 기존의 null 체크와 다를바 없다. Optional의 map과 flatMap을 이용해서 다음과 같이 표현할 수 있다

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

위와 같이 표현함으로써 메서드 시그니처만 보고도 person과 car가 null일 수 있음을 인지할 수 있고 Optional 클래스의 flatMap, map 메서드 연산 시, Optional이 비여있는지 여부에 따라서 findCheapestInsurance() 메서드가 실행되므로 기존 null체크에 비해서 훨씬 코드가 간결하다.


11.4. Optional 실용 예제

11.4.1 잠재적 null 값을 Optional로 감싸기

기존 자바 API 자체에서 특정 액션에 대한 반환값으로 null을 반환하는 경우가 많은데 이런 케이스는 반환값을 Optional.ofNullable 로 감싸서 처리

Optional<Object> value = Optional.ofNullable(map.get("key"));



11.4.2. 예외와 Optional 클래스

자바 API 에서 값을 제공할 수 없을때 반환값으로 null이 아닌 익셉션을 내리는 경우가 있는데 이런 경우, 자바 API를 한 단계 감싸서 Optional로 반환하도록 유틸리트 클래스를 따로 만들어서 사용할 수 있음

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

+ Recent posts