JAVA/Optional

JAVA Optional 클래스를 통한 null 처리

수달하나 2023. 9. 24. 21:35

자바로 프로그램을 개발하면서 NullPointerException 을 한번쯤은 무조건 겪는다. 모든 상황에서 null 이라는 표현을 사용하면서 치뤄야 할 당연한 대가이면서 어떻게 해결 할 수 없는 것이라고 생각 할 수도 있다. 하지만 명령형 프로그래밍을 통해서 null을 처리하고 싶다면 관점을 조금 다르게 접근하는 방법이 필요하다.


이 없다.

기존의 프로그래밍은 값이 없는 상태를 null 로 표현해서 처리 했다. 하지만 null 처리는 NullPointExceptions을 모든상황에서 완벽하게 대응할 수 있는 방법이 아니다. 개발자가 실수로 null 처리를 하지 않았고 개발을 진행하는 과정에서 운이 좋게 NullPointException 이 터지지 않았다고 해서 실제 서비스를 제공 할 때도 NullPointException 이 터지지 않을 것이라는 것을 보장 할 수 없기 때문이다.


null 때문에 발생하는 문제

  • 에러의 근원 : NullPointException 은 자바에서 가장 흔히 발생하는 에러다.
  • 코드를 어지럽힘 : 중첩된 null 확인 코드를 추가해야 할 경우 null 때문에 코드의 가독성이 떨어 질 수 있다.
  • 아무 의미가 없음 : null 은 아무 값도 표현하지 않는데 정적 형식 언어에서 값이 없음을 표현하는 방법은 모순이다.
  • 자바 철학에 위배 : 포인터 참조를 숨긴 자바에서 null 포인터는 유일한 포인터 참조이다.
  • 형식 시스템에 구멍을 만듬 : 형식 정보를 포함하지 않기 때문에 모든 참조 형식에 null 을 할당할 수 있고 이런 식으로 null을 할당하게 된다면 특정 부분에서 null 이 어떤 의미로 사용되었는지 파악 할 수 없다.

Optional 클래스

자바 8 에서부터는 java.util.Optional<T>라는 클래스를 통해 선택형 값을 캡슐화 하는 기술을 제공한다.

Car 라는 클래스가 존재 할 때  Car 클래스를 Optional 클래스를 통해 캡슐화 하여 Optional<Car> 로 사용 할 수 있다.

Optional 은 캡슐화라는 말 그대로 선택형 클래스를 감싸고 감쌀 클래스가 없다면 비워둔다.

 

위와 같은 방식으로 Optional을 이용하면 값이 없는 상황이 특정 데이터에 문제가 있는 것인지 아니면 알고리즘의 버그인지 명확하게 구분할 수 있다. 물론 모든 null 참조를 Optional로 대치하는것은 바람직하지 않다. Optional의 역할은 더 이해하기 쉬운 API를 설계하도록 돕는 것이며, 값이 없을 수 있는 상황에 적절하게 대응하도록 강제할 수 있는 역할에서만 제대로 된 힘을 발휘하기 때문이다.  


Optional 적용 패턴

Car 라는 객체를 Optional 로 적용한다고 했을 때 빈 Optional 객체, 즉 null 인 값은 아래와 같이 적용할 수 있다.

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

혹은 null이 아닌 값을 포함하는 아래와 같은 방식으로 사용 할 수 있다.

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

위와 같은 방식은 car가 null이라면 NullPointException을 발생시켜서 코드상의 오류가 발생했다는 것을 확인 할 수 있다.

 

마지막으로 null 객체인지 아닌지 알 수 없을  경우에는 ofNullable 메서드를 통해서 선언 할 수 있다.

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

Optional 값 추출

Optional을 이용하여 값을 추출할 경우에는 캡슐화로 감싸져 있기 때문에 일반적인 방법과는 다르게 추출해야 한다. 

isPresent 메서드를 통해 값이 들어간 Optional 인지 확인하고 이후에 해당 객체를 get 메서드를 통해 뽑아와서 처리해야 한다. 

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

if(car.isPresent())
	Car carInstance = car.get();

 

carInstance의 특정 부분을 받아오거나 혹은 다른 작업을 하고 싶다면  map 메서드를 통해서 로직을 처리 할 수 도 있다.

public final class Optional<T> {

    ....
    
    public <U> Optional<U> map(Function<? super T, ? extends U> mapper) {
        Objects.requireNonNull(mapper);
        if (!isPresent()) {
            return empty();
        } else {
            return Optional.ofNullable(mapper.apply(value));
        }
    }
    
    ....
}

Optional 클래스 안에 선언된 map 메서드는 값이 없으면 Optional 빈 객체를 반환하고 값이 있으면 Opional 에 값을 담아서반환하게 된다. 이때 전달인자로 함수형인터페이스를 전달받아 반환값까지 정의하여 사용 할 수도 있는데 예를 들어 carInstance 의 carName 을 추출하고 싶을 때 코드를 아래와 같이 정의 할 수 있다.

Optional<String> carName = car.map(Car::getCarName);

세 내 줄이 되어야 할 코드가 한줄로 처리될 수 있는 것을 확인했다. 


Optional 의 flatMap 메서드

Optional 은  map 메서드와 함께 flatMap 메서드도 제공하고 있다.

Person 이 Car를 가지고 있고 Optional 로 캡슐화를 진행했다고 할 때 Car 의 carName 을 가져오고 싶다면 아래와 같은 코드로 작성 할 수 있다.

Optional<Person> person = Optional.of(person);

String name = person
    .flatMap(Person::getCar)
    .map(Car::getCarName)
    .orElse("unKnown");

Optional 이 제공하는 다양한 메서드

  • filter : 값이 존재하면 Predicate 와 일치하는 값을 Optional로 반환하고 결과 값이 없으면 빈 Optional을 반환
  • of : 값이 존재하면 값을 감싸는 Optional 을 반환하고, 값이 null 이면 NullPointerException을 발생
  • ofNullable : 값이 존재하면 같은 Optional 을 반환하고, 값이 null 이면 빈 Optional을 반환
  • or : 값이 존재하면 같은 Optional 을 반환하고, 값이 없으면 Supplier에서 생성하는 값을 Optional로 반환
  • orElse : 값이 존재하면 값을 반환하고 값이 없으면 기본값을 반환
  • orElseGet : 값이 존재하면 값을 반환하고 값이 없으면 Supplier에서 생성하는 값을 반환
  • orElseThrow : 값이 존재하면 값을 반환하고 값이 없으면 Supplier에서 생성한 예외를 발생
  • stream : 값이 존재하면 존재하는 값만 포함하는 스트림을 반환하고 값이 없으면 빈 스트림을 반환

Optional 주의 사항

Optional 을 사용하면 null 처리에 대한 부담감이 줄어들고 null 포인터에 대한 호출을 줄임으로서 좀 더 객체지향적인 프로그래밍 설계가 가능하다. 하지만 Optional 사용이 언제나 모든 상황에서 최선의 선택은 아님을 말 했듯이 도메인 모델에 Optional을 사용했을 때는 직렬화를 할 수 없다는 단점이 있다.

 

Optional의 정확한 사용법은 필드 형식의 값의 선언이 아닌, 선택형 반환값을 지원하는 기술 이다.

Optional을 필드 형식으로 사용한다면 Serializable 인터페이스를 구현하지 못하기 때문에 직렬화 모델을 사용하는 도구나 프레임워크에서 문제가 발생 할 수 있기 때문이다.

 

위와 같은 단점에도 불구하고 여전히 Otional을 사용해서 도메인 모델을 구성하는 것이 바람직하다고 생각 할 수 있다. 일부가 null 이거나 혹은 전체 값이 null일 수가 있는 상황이 발생할 수 있기 때문이다. 따라서 Optional을 필드로 지정하여 사용을 원한다면 직렬화 기능을 수행할 수 있도록 Optional 값을 반환받을 수 있는 메서드를 추가하는 방식을 통해서 해결하는 것도 한가지 방법이 될 수 있다.