null 대신 Optional

Java에서 NullPointerException(NPE)을 겪지 않은 개발자가 몇명이나 될까요? 그런데 NPE는 모든 자바 개발자를 괴롭히는 예외긴 하지만 null이라는 표현을 사용하면서 치러야할 댓가 일까요? 거시적인 프로그래밍 관점에서 조금 다르게 null 문제에 접근해보도록 하겠습니다.

토니 호어(Tony Hoare)라는 영국 컴퓨터과학자가 힙에 할당되는 레코드를 사용하며 형식을 갖는 최초의 프로그래밍 언어 중 하나인 ALGOL W를 설계하면서 null이 등장합니다. 여러 해가 지난 후에 호어는 null 및 예외를 만든 결정을 '억만 달러짜리 실수'라고 표현했습니다.

그런데 값이 없는 상황을 어떻게 처리해야 할까요?

값이 없는 상황

아래 코드를 기반으로 getCarInsuranceName()person 객체가 호출하는 getter의 반환값이 null일 경우 NPE가 발생합니다. 대부분의 개발자는 이런 코드를 작성하는 경우는 매우 드뭅니다.


public String getCarInsuranceName(Person person) {
  return person.getCar().getInsurance().getName();
}

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; }
}

if문을 사용해서 방어코드를 작성하게 되면 아래와 같은 코드가 작성됩니다. 흔히 deep doubt 코드라 불리는 형태로 작성되며, 다른 형태로 작성된다고 해도 중첩 if 블록을 사용하는 것엔 변함이 없습니다.


public String getCarInsuranceNameofChain(Person person) {
    if (person != null) {
        Car car = person.getCar();
        if (car != null) {
            Insurance insurance = car.getInsurance();
            if (insurance != null) {
                return insurance.getName();
            }
        }
    }
    return "Unknown";
}

public String getCarInsuranceNameofChain2(Person person) {
    if (person == null) {
        return "Unknown";
    }
    Car car = person.getCar();
    if (car == null) {
        return "Unknown";
    }
    Insurance insurance = car.getInsurance();
    if (insurance == null) {
        return "Unknown";
    }
    return insurance.getName();
}

null로 인해서 NPE가 발생하는건 경험적으로 알고 있으며, NPE를 방어하기 위해서 코드가 장황해집니다. 무엇보다 null은 아무런 의미가 없기 때문에 정적 언어에서 값을 표현하는 방법으로는 적절하지 않습니다. 그리고 Java가 모든 포인터를 숨겼던 이유는 개발자가 포인터를 직접적으로 처리하지 않도록 하기 위해서임에도 불구하고, null 포인터는 예외로 처리됩니다. 제일 큰 문제는 형식 시스템에 구멍을 만듭니다. null은 형식이 없기 때문에 정보를 포함하고 있지 않으므로 모든 레퍼런스 형식에 null이 할당될 수 있으며, null을 적용했을 때 null이 가진 의미를 알 수 없다는 점은 형식 시스템에 적합하지 않습니다.

Optional 클래스

Java 8은 하스켈과 스칼라의 영향을 받아 java.util.Optional<T>라는 새로운 클래스 제공합니다. Optional은 선택형값을 캡슐화 하는 클래스입니다. 값이 있으면 Optional 클래스는 값을 감싸며 값이 없으면 Optional.empty 메서드로 Optional을 반환하빈다. Optional.empty는 Optional의 특별한 싱글턴 인스턴스 반환 정적 팩토리 메서드입니다.

nullOptional.empty() 의미상 비슷하지만, null을 참조하려면 NullPointerException이 발생, Optional.empty()Optional객체이므로 다양한 방식으로 처리할 수 있습니다. Optional 클래스를 사용하면서 모델의 의미가 더 명확해집니다. Optional을 이용하면 값이 없는 상황이 우리 데이터에 문제가 있는 것인지 아니면 알고리즘의 버그인지 명확하게 구분 가능합니다. 모든 null 레퍼런스를 Optional로 대치하는 것은 바람직하지 않지만 Optional을 고려해보는 것은 필요합니다.

Optional 적용 패턴

Optional.empty로 빈 Optional 객체를 얻을 수 있습니다.


Optional<Car> optCar = Optional.empty();

Optional.ofnull이 아닌 객체를 담고 있는 Optional 객체를 생성합니다.


Optional<Car> optCar = Optional.of(car);

Optional.ofNullablenull인지 아닌지 확신할 수 없는 객체를 담고 있는 Optional 객체를 생성합니다.


Optional<Car> optCar = Optional.ofNullable(car);

Map으로 Optional 값을 추출하고 변환할 때는 map을 사용하면 됩니다. 아래와 같이 NPE를 방지하기 위해서 작성하던 코드형태의 경우 Optionalmap을 사용해서 단순하게 해결할 수 있습니다.


String name = null;
if(insurance != null) {
  name = insurance.getName();
}

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

map을 사용하면 처음 봤던 예제를 다음과 같이 변경할 수 있습니다.


Optional<Person> optPerson = Optional.of(person);
Optional<String> name = optPerson.map(Person::getCar)
                                 .map(Car::getInsurance)
                                 .map(Insurance::getName);

하지만 위의 코드는 작동하지 않습니다. Stream에서 설명했듯이 optPerson의 형식이 Optional<Person>이기 때문에 map 형식으로 호출할 순 있지만, getCar의 반환형이 Optional<Car>를 반환하기 때문에 타입 오류가 발생합니다. 이런 문제를 해결하기 위해선 flatMap을 사용하면 됩니다. 기본적으로 flatMap은 함수를 인수로 받아서 다른 스트림을 반환하는 메서드로, 인수로 받은 함수를 적용해서 생성된 각각의 스트림에서 값만 남깁니다. 함수를 적용해서 생성된 모든 스트림이 하나의 스트림으로 병합됩니다.

따라서 아래와 같이 flatMap을 적용하여 코드를 작성하면 됩니다.

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

Optional의 값을 읽고, 기본설정값을

get()Optional 값을 읽는 가장 간단한 메서드입니다. 동시에 가장 불안정한 메서드입니다. 값이 있으면 해당 값을 반환하지만, 값이 없으면 NoSuchElementException을 발생시킵니다.

orElse()를 이용하면 Optional이 값을 포함하지 않을 때 기본값을 제공할 수 있습니다. orElseGet()orElse() 메서드에 대응하는 게으른 버전의 메서드입니다. Optional에 값이 없을 때만 Supplier가 실행되기 때문에 Optional이 비어있을 때만 디폴트값을 생성하고 싶다면 orElseGet()을 사용하면 됩니다. 또한 orElseThrow()는 비어있을 경우 예외를 발생시킬 수 있습니다.

ifPresent()를 이용하면 값이 존재할 때 인수로 넘겨준 동작을 실행하 수 있습니다. isPresent()는 값의 존재유무를 반환합니다.

Optional의 특정 값을 거르고 싶다면, filter()를 사용하면 됩니다.

참고

Optional 참고