[Java] Optional

Minjun Kang·2023년 1월 16일
0

basic-java

목록 보기
5/5

Optional 이란?

Null값을 가질 수 있는 객체 또는 원시타입을 저장하는 컨테이너.

왜 필요할까?

Null값을 가질 수 있는 인스턴스 같은 경우, 인스턴스 메소드에 접근할 때 해당 인스턴스가 Null일 경우 NPE(Null Pointer Exception)가 발생할 수 있다.

문제는, 이러한 결함을 컴파일 타임에 알 수 없고 해당 로직이 실행될 때서야 결함을 발견할 수 있다.(런타임)

소프트웨어의 특성 상 결함의 발견이 늦으면 늦을수록, 기하급수적으로 비용이 증가한다는 점에서 이러한 오류를 최대한 빠르게, 정확하게 찾아 내는 것이 중요하다.

프로그래머는 실수를 한다.

자바 8 이전까지 Null값을 가질 수 있는 인스턴스와 Null값을 가질 수 없는 인스턴스와의 구분을 하지 않았다.

nullable한 객체는 해당 값이 null일 경우와 not-null일 경우를 나눠 처리를 해야한다.

프로젝트의 복잡성이 커질 수록, nullable한 객체를 식별하기가 어려워진다.

Optional이라는 단어가 내포하듯, nullable한 객체를 명시적으로 표현할 수 있는 수단이 자바 8 이후로 추가되었다.

Optional

Java의 Optional은 nullable한 값을 가질 수 있는 객체를 래핑하는 식으로 구현되었다.

/* 내부 구현
public final class Optional<T> {
    /**
     * Common instance for {@code empty()}.
     */
    private static final Optional<?> EMPTY = new Optional<>();
    private final T value;

... 이하 생략 ...
}

이전까지는 nullable한 객체는 전부 if-else 기반의 분기를 이용하여 처리를 하였는데, Optional로 래핑한 객체는 좀 더 직관적으로 null처리를 할 수 있는 다양한 메소드를 제공한다.

Optional API

대표적인 메소드들만 소개하겠다.

  1. ifPresent() 또는 ifPresent(Consumer)
    Optional이 가지는 value가 null값이 아닐 때 boolean값을 리턴하거나 (단순 존재 유무 확인) 또는 해당 값을 이용하여 무엇인가를 처리하는 로직을 기술한다(Consumer 함수형 인터페이스)
Optional<SomeObject> a = Optional.of(new SomeObject(1, 2, 3, 4));

Optional<SomeObject> b = Optional.of(null);

/* 1. ifPresent() 사용 */
if (a.ifPresent()) {
	// 값이 존재할 때 값을 가져와서 처리한다.
    SomeObject temp = a.get();
    temp.doSomething();
} else {
	// 값이 존재하지 않을 때 ~ 
    // 대표적으로 exception을 던지거나, 기본 값을 할당, 다른 로직 사용을 고려할 수 있다.
}

/* 2. ifPresent(Consumer) 사용 */
// 1개의 인자를 받아 아무것도 리턴하지 않는 메소드를 참조
a.ifPresent(System.out::println);
b.ifPresent(System.out::println);
  1. orElse-- 계열의 메소드
    Optional이 가지는 value가 null일 경우에 처리할 수 있는 방법들을 기술할 수 있는 메소드를 제공한다.
Optional<SomeObject> a = Optional.of(new SomeObject(1, 2, 3, 4));

Optional<SomeObject> b = Optional.of(null);

// 1. 기본 값 지정 방식 (상수)
SomeObject temp1 = a.orElse(new SomeObject(0, 0, 0, 0)); 

// 2. 기본 값 지정 방식 (동적으로 값을 할당, Supplier사용)
Supplier<SomeObject> supplier = () -> {
	int seed = ThreadLocalRandom.current().nextInt();
    return new SomeObject(seed, seed + 1, seed + 2, seed + 3);
};
SomeObject temp2 = a.orElseGet(supplier);

// 3. 예외를 던지는 방식
SomeObject temp3 = a.orElseThrow(Exception::new);

// 등등..
  1. 그외 stream 관련 메소드(stream(),filter(Predicate), map(Function))
    여기서 filter(), map(Function)은 Optional 내부적으로도 구현되어 있는데 그 이유는 다음과 같다고 한다.

    Java's Optional class has a stream() method because it allows for a more fluent and expressive programming style. It allows developers to perform operations on an Optional object in a similar way to how they would perform operations on a Stream. This can make code more readable and easier to understand. Additionally, having the stream() method on Optional allows developers to use the full range of Stream operations (such as filter, map, and reduce) on Optional objects, providing a consistent and powerful API for working with Optional values.

Optional 권장 사항

1. Optional은 문법적으로는 어느 곳에서나 사용할 수 있지만, 메소드의 파라미터로 넘기는 것은 권장되지 않는다.

Optional자체가 value를 한번 래핑하는 인스턴스라는 점을 리마인드하자.
🌟 Optional자체도 null을 참조하고 있을 수 있다.

그래서 Optional을 parameter로 전달받는 메소드 내부에서는 Optional 자체에 대한 추가적인 null체크가 필요하다. 이럴 경우에 Nullable한 값을 검증하기 위해 Optional 그 자체에 대한 null체크도 포함되기 때문에, null로 전달되는 경우에는 한번만 검증해도 될 것을 추가적인 검증이 필요하게 된다.
참고

public String sayHello(Optional<String> name) {  // Noncompliant, name이라는 optional instance에 대한 검증이 추가적으로 필요하다.
  if (name == null || !name.isPresent()) {
    return "Hello World";
  } else {
    return "Hello " + name;
  }
}

public String sayHello(String name) {
  if (name == null) {
    return "Hello World";
  } else {
    return "Hello " + name;
  }
}

2. 인스턴스의 필드로 Optional을 사용하는 것은 좋지 않다.

이 경우는, 설계의 문제인데 Optional한 필드를 가진다는 것은 적절하게 클래스의 역할이 분리되지 않았다는 방증이 된다.

예를 들어, Car라는 클래스가 존재한다고 가정해보자.
Car는 가솔린 차, 전기 차로 크게 분류될 수 있어 내부에 type을 구분하는 필드를 두고, type에 따라 사용하는 필드가 다르기 때문에 battery, gasCapacity 라는 Optional 필드가 존재하는 상황이다.

이 경우에는, 나중에 Car라는 클래스에 새로운 feature가 추가되면 기존의 코드를 재수정해야한다. 또한, 지속적으로 기능을 추가 하여 복잡도가 높아질 경우 결함이 발생할 수 있다.
가장 심각한 점은, 내부적으로 사용되지 않는 필드가 이 클래스를 확장하는 다른 클래스에게도 전파된다는 것이다.

즉, 모든 클래스는 자신이 반드시 가져야하는 필드들을 포함해야한다.

또한, Optional은 Serializable을 구현하지 않았다!!


3. Optional을 사용할 경우, null을 함께 사용하는 것은 권장되지 않는다.
레퍼런스
Optional은 특정 값을 감싸는 래퍼 클래스이다. Optional이 감싸는 값이 null이라면, Optional.empty()라는 정적 메소드를 사용하여 null 값을 감싼 Optional을 가져와서 리턴하자.

자칫 null과 optional을 혼용하게 될 경우, Optional 그 자체를 null로 받는 실수를 하게 될 가능성이 높다. 자세한 내용은 레퍼런스를 참고하자.


4. Primitive type은 꼭 필요한 경우가 아닐 경우 Primitive Optional을 이용하자

실제 Optional<T>은 제너릭으로 선언되어 있으며, 타입 정보가 컴파일 타임에 소거되면 Object로서 내부적으로 사용된다. 즉, Optional의 타입 매개변수는 Object의 하위 타입만 가능하고 이를 위해서 primitive type(ex. int)같은 경우 래퍼 클래스(ex.Integer)로 구체화 해야하는데, 박싱되는 과정에서 오버헤드가 존재한다.

자주 참조되는 Primitive의 Optional은 성능에 치명적인 영향을 주므로, 이를 개선하고자 OptionalInt와 같은 primitive type 전용 Optional 클래스들이 존재한다.


5. Collection, Map, Stream, Array, Optional와 같은 컨테이너들은 Optional로 감싸지 않는 것을 권장한다.

Optional은 비싸다. 코드의 복잡성과 클린 아키텍처를 위해서 빈 컨테이너를 표현하는 정적 팩토리 메소드가 대부분의 컨테이너에서 구현되어 있다. (ex. Collections.emptyList(), emptyMap(),emptySet())


그외에 Optional이 등장하게 된 이유와 올바르게 사용하는 방법이 다음과 같이 잘 기술되어 있다.

profile
성장하는 개발자

0개의 댓글