5 분 소요

자바 8부터 제공되기 시작한 Optional은 null을 처리하는데 도움을 주는 새로운 클래스입니다.
자바는 왜 null을 처리하기 위해 Optional을 도입했을까요?

null이 가지는 문제

null은 무엇일까요? 바로 참조가 없음을 나타내는 키워드입니다.
우리는 명시적으로 null을 할당하여 변수가 아직 초기화되지 않았다는 것을 표현할 수 있지만, 이로 인해 발생할 수 있는 많은 문제점이 있습니다.

public class Owner {
    private Item item;

    public Item getItem() {
        return item;
    }
}

public class Item {
    private String name;

    public String getName() {
        return name;
    }
}

위와 같이 주인과 아이템의 중첩 구조로 구현된 코드를 예제로 들어보겠습니다.
아래와 같은 코드에서는 어떤 문제가 발생할 수 있을까요?

public String getItemName(Owner owner) {
    return owner.getItem().getName();
}
  1. Owner 객체가 null일 수 있다
  2. Owner 객체의 Item이 null일 수 있다

크게 두 가지 경우가 발생할 수 있을 것 같습니다.
이때 자바는 우리에게 친숙한 NullPointerException을 반환하게 됩니다.

image

그럼 이 문제를 해결하기 위해 어떻게 할 수 있을까요?

public String getItemNameV1(Owner owner) {
    if (owner == null) {
        return null;
    }

    Item item = owner.getItem();
    if (item == null) {
        return null;
    }

    return item.getName();
}

public String getItemNameV2(Owner owner) {
    if (owner == null) {
        return "No owner";
    }

    Item item = owner.getItem();
    if (item == null) {
        return "No item";
    }

    return owner.getItem().getName();
}

위의 코드들처럼 보수적으로 계속 null 체크를 하며 다시 null을 반환하던지, 다른 값을 반환할 수 있을 것 같습니다.
1번처럼 코드를 작성한다면 메서드를 호출한 클라이언트에서도 null을 체크해야 하고, 2번처럼 코드를 작성한다면 각각의 값에 대한 처리를 해줘야 하겠죠.

자바에서 null 참조를 허용하게 되면서 가진 문제들은 코드의 복잡도를 높이고, 중복이 많아지게 되며 유지보수가 어려워지게 합니다.

이러한 문제를 해결하기 위해 자바는 Optional을 도입하게 되었습니다.
위의 Owner와 Item의 예제를 Optional을 사용하여 다시 작성해보겠습니다.

public String getOptionalItemName(Optional<Owner> owner) {
    return owner.flatMap(Owner::getItem)
            .map(Item::getName)
            .orElse("No item");
}

위처럼 사용하게 되면 null 체크를 하지 않아도 되고, 코드의 복잡도도 줄어들게 됩니다.
물론 Owner를 null이 아닌 Optional로 감싸서 보내줘야 하는데 이는 아래에서 자세히 살펴보겠습니다.

Optional이란?

Optional은 자바 8에 추가된 기능으로 값이 있거나 없음을 표현하는 불변 클래스입니다.
이제 우리는 참조가 되지 않은 객체를 표현하기 위해 null 키워드 대신 Optional을 사용할 수 있게 되었습니다.
위에서 Owner를 null로 보내면 안된다고 했는데, Optional로 감싸서 null일 수 있는 객체를 표현할 수 있기 때문입니다. (null로 보내면 NPE는 여전히 발생합니다.) Optional 객체는 empty, of, ofNullable의 정적 팩토리 메서드를 호출하여 생성할 수 있으며, 스트림처럼 map, flatMap, filter 등의 메서드를 제공합니다.

Optional은 값이 없는 상황을 적절하게 처리할 수 있어서 NullPointerException을 방지하고, Optional을 유틸리티 클래스로 만들어서 try/catch 로직을 최소화할 수 있다는 특징이 있습니다.

다만 Optional은 null 일 수 있는 값을 포장하는 Wrapper 클래스이기 때문에 무조건적인 사용은 오버헤드로 인한 성능 저하가 일어날 수 있습니다.
그럼 Optional을 어떻게 사용하는 것이 좋을까요?

생성자를 적절하게

public static<T> Optional<T> empty() {
    @SuppressWarnings("unchecked")
    Optional<T> t = (Optional<T>) EMPTY;
    return t;
}

public static <T> Optional<T> of(T value) {
    return new Optional<>(value);
}

public static <T> Optional<T> ofNullable(T value) {
    return value == null ? empty() : of(value);
}

empty를 제외한 of와 ofNullable 메서드는 제네릭 타입의 값을 받아 Optional 객체를 생성합니다.

  • of : 인자가 null 이라면 NPE 발생
  • ofNullable : 인자가 null 이라면 비어있는 Optional 객체 반환

위와 같은 차이가 있으니 Optional을 생성할 땐 적절한 생성자 메서드를 고려해서 사용해야 합니다.

isPresent, get 대신 orElse, orElseGet, orElseThrow를 사용하자

// Bad
Optional<Member> member = memberRepository.findById(1L);
if (member.isPresent()) {
    return member.get();
} else {
    return new Member();
}

// Good
Optional<Member> member = memberRepository.findById(1L);
return member.orElseThrow(()-> new NotFoundMemberException);

isPresent, get 메서드를 사용하면 안전하지만 가독성이 떨어지는 코드가 될 수 있습니다.
기존의 null 체크와 같은 방식이기 때문에 Optional을 현명하게 쓰고 있다고 보기 어렵기도 하고요.

orElse, orElseGet, orElseThrow 메서드는 Optional 객체가 비어있을 때 값을 반환하거나 예외를 발생시킵니다.
아래쪽의 코드가 더 잘 읽히지 않나요?

새로운 객체를 반환해야 할 때는 orElse 대신 orElseGet

/**
	* If a value is present, returns the value, otherwise returns
	* {@code other}.
	*
	* @param other the value to be returned, if no value is present.
	*        May be {@code null}.
	* @return the value, if present, otherwise {@code other}
	*/
public T orElse(T other) {
    return value != null ? value : other;
}

/**
 * If a value is present, returns the value, otherwise returns the result
 * produced by the supplying function.
 *
 * @param supplier the supplying function that produces a value to be returned
 * @return the value, if present, otherwise the result produced by the
 *         supplying function
 * @throws NullPointerException if no value is present and the supplying
 *         function is {@code null}
 */
public T orElseGet(Supplier<? extends T> supplier) {
    return value != null ? value : supplier.get();
}

orElse와 orElseGet은 같은 역할을 하지만 다른 기능으로 구현되어 있습니다.

Api docs를 번역해보면 orElse는 Optional 값이 있든 없든 무조건 실행되기 때문에 값이 있는 경우 불필요한 연산을 하게 됩니다. orElse의 인자로 실행된 값이 무시되고 버려지기 때문에 미리 생성된 객체나, 연산된 값일 때 사용하는 것이 좋습니다.

orElseGet은 인자로 Supplier 타입의 함수형 인터페이스를 받으며 Optional 값이 없을 때만 실행됩니다. 따라서 불필요한 오버헤드가 없어 orElse와 비교하여 쉽게 쓸 수 있고, 성능 상 이점을 가질 수 있습니다.

불필요한 Optional

return Optional.ofNullable(status).orElse(READY); // Bad
return status != null ? status : READY; // Good

List<Member> members = memberRepository.findAll();
return Optional.ofNullable(members); // Bad

return memberRepository.findAll(); // Good

Map<String, Optional<Member>> map = new HashMap<>(); // Bad

위와 같이 억지로 Optional을 사용하는 것은 지양해야 합니다. Optional이 아닌 특정 값을 얻기위해 Optional을 사용하게 되면 불필요한 객체를 생성해야 합니다.

컬렉션의 경우 Optional로 감싸는 것보다 size가 0인 빈 컬렉션으로 반환하는 것이 좋습니다.

Map의 값으로 Optional을 사용하게 되면 복잡도가 늘어납니다. 키 자체가 없는 경우와 키는 있지만 값이 빈 Optional인 경우를 체크해야 하며, 오류 가능성을 키우게 됩니다. 특히 Map의 경우 자바 8에서 추가된 putIfAbsent, getOrDefault, compute 등의 메서드가 추가되었으니 이를 사용하는 것이 좋습니다.

Optional을 필드로 사용하지 말자

// Bad
public class Person {

  private Optional<Car> car;

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

// Good
public class Person {

  private Car car;

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

애초에 Optional은 비즈니스 로직을 작성하며 메서드 반환 타입을 제한하고 NullPointerException이 발생할 수 있는 상황을 회피할 수 있도록 만들어졌습니다.
필드로 Optional을 사용하게 되면 직렬화가 불가능합니다. 애초에 필드 형식으로 사용할 것을 가정하지 않았기 때문에 Serializable 인터페이스를 구현하지 않았기 때문입니다.
또한 필드 값의 초기화는 생성자의 책임이며, 생성자를 호출할 때 매개변수에 null을 넘기는 대신 적절한 생성자 오버로딩 또는 빌더 패턴이나 정적 팩토리 메서드 패턴을 사용하는 것이 더 직관적이며 바람직합니다.

Optional에 대해 알아봤는데 더 자세한 내용은 아래 참고에서 확인하실 수 있습니다.

참고
이펙티브 자바 3판
모던 자바 인 액션
26 Reasons Why Using Optional Correctly Is Not Optional

댓글남기기