[자바] Optional

동등성 비교

Optional은 값 기반(value-based) 클래스이므로 Optional 인스턴스에 대해 참조 동일성(equality)을 비교하거나 해시코드 값을 비교하는 등의 인스턴스의 식별 관련 연산을 수행하는 경우 의도치 않은 결과가 나올 수 있으므로 권장되지 않는다.


반환값으로 사용

메서드의 반환형 또는 getter의 반환형으로 Optional을 사용하는 경우 고려해야 할 사항이 몇가지 존재한다. Optional을 반환하는 메서드를 호출하는 쪽에서는 Optional 객체의 래핑된 값을 확인하기 위한 추가적인 메서드 호출이 필요하다. 따라서 라이브러리나 템플릿 엔진에서 객체의 속성값에 직접 접근하여 어떤 처리를 하는 경우 Optional에 대한 내부적인 처리가 정상적으로 가능한지 살펴봐야 한다.

Serializable를 상속한 객체의 속성을 Optional로 정의한 경우 직렬화 시 NotSerializableException 예외가 발생한다. Jackson 라이브러리를 사용하여 자바 객체를 JSON으로 직렬화 및 역직렬화할 경우에도 문제가 될 수 있다. 객체의 속성을 Optional로 정의하거나 getter 메서드의 반환형으로 사용하는 경우 직렬화 시 해당 속성이 제대로 직렬화되지 않으며 역직렬화 시에는 JsonMappingException 에러가 발생한다. 이를 해결하기 위해 별도의 라이브러리 사용이 필요하다.

JPA 엔티티 객체의 속성을 Optional로 정의할 경우 해당 객체를 영속화할 때 PersistenceException 에러가 발생한다. 속성은 원래의 타입으로 정의하고 getter의 반환형을 Optional로 정의할 수는 있지만 이 경우 속성값을 조회할 때만 Optional을 사용할 수 있게 된다.

위와 같은 이유로 객체의 속성 자체를 Optional로 정의하거나 해당 속성에 대한 getter의 반환형을 Optional로 정의하는 경우 속성이 원래의 타입으로 정의되었을 때와 동일하게 문제 없이 사용될 수 있는지 먼저 확인해야 한다.


안전 탐색 연산자(?.) 및 엘비스 연산자(?:)와 Optional 비교

null 역참조로 인한 예외 발생을 피하기 위해 사용하는 연산자(그루비나 코틀린에서 제공)와 자바의 Optional을 비교하면 다음과 같다.

예제 코드에서 사용할 객체의 구조는 다음과 같다.

public class Object1 {
  private Optional<Object2> object2;
  public Optional<Object2> getObject2() { ... }
}

public class Object2 {
  private Optional<Object3> object3;
  public Optional<Object3> getObject3() { ... }
}

public class Object3 {
  public String field;
  public String getField() { ... }
}


그루비나 코틀린의 안전 탐색 연산자(?.)는 참조하려는 변수가 null일 경우 표현식을 곧바로 null로 평가한다.

String result = object1?.getObject2()?.getObject3();


Optional로 유사한 코드를 작성하면 다음과 같다.

Optional<String> result = object1.flatMap(Object1::getObject2)
                    .flatMap(Object2::getObject3)
                    .map(Object3::getField);


참조하는 Optional이 비어있지 않다면(ifPresent()true이거나 empty()false이면) 변환 함수를 적용하며 객체 중 하나라도 비어있다면(ifPresent()false이거나 empty()true이면) 전체 표현식은 비어있는 Optional로 평가된다. 즉, 안전 탐색 연산자(?.)와 유사하게 참조하려는 변수가 비어있는 Optional일 경우 표현식을 곧바로 비어있는 Optional로 평가한다.

그루비나 코틀린의 엘비스 연산자(?:)는 참조하려는 변수가 null일 경우 표현식을 특정 값으로 평가한다.

String result = object1?.getObject2()?.getObject3() ?: "Default";


Optionalor() 메서드를 사용하여 유사한 코드를 작성하면 다음과 같다.

Optional<String> result = object1.flatMap(Object1::getObject2)
                    .flatMap(Object2::getObject3)
                    .map(Object3::getField)
                    .or(() -> "Default");


참조하는 Optional이 비어있지 않다면 변환 함수를 적용하며 객체 중 하나라도 비어있다면 전체 표현식은 특정 값 또는 객체를 가지는 Optional로 평가된다. 즉, 엘비스 연산자(?:)와 유사하게 참조하려는 변수가 비어있는 Optional일 경우 표현식을 특정 값을 가지는 Optional로 평가한다.

Optional이 아닌 래핑된 값 또는 객체를 반환받고자 하는 경우에는 다음과 같이 작성한다. OptionalorElse() 메서드를 사용하여 반환할 기본값을 명시한다.

String result = object1.flatMap(Object1::getObject2)
                    .flatMap(Object2::getObject3)
                    .map(Object3::getField)
                    .orElse("Default");


orElse()와 orElseGet()

orElse() 메서드와 orElseGet() 메서드 모두 Optional의 값이 존재하면 그대로 반환하고, 존재하지 않으면(Optional이 비어있으면) 특정 값을 반환한다.

orElse() 메서드의 파라미터는 제네릭 타입 파라미터이며 값 뿐만 아니라 값을 반환하는 표현식이 될 수 있다. orElseGet() 메서드의 파라미터는 Supplier 함수형 인터페이스이다. 두 메서드의 파라미터 타입은 다르지만 두 메서드 모두 어떤 값을 반환하는 메서드를 파라미터로 전달할 수 있고 값이 존재하지 않을 때 해당 메서드를 실행하여 값을 반환할 수 있으므로 큰 차이가 없는 것처럼 보인다.

String value = "Value";
String result = Optional.of(value).orElse("Default");
String value = "Value";
String result = Optional.of(value).orElse(getDefault());
String value = "Value";
String result = Optional.of(value).orElseGet(() -> getDefault());


하지만 두 메서드에는 중요한 차이점이 있다. 바로 orElse() 메서드의 경우 파라미터가 표현식일 때 값의 존재 여부에 관계 없이 표현식이 항상 실행된다는 것이다. 이는 값이 존재하지 않는 경우에만 파라미터로 전달한 표현식이 실행될 것이라는 예상과는 크게 다르다. 따라서 값이 존재함에도 불구하고 불필요한 메서드 호출이 이루어질 수 있으므로 주의해야 한다.

String value = null;
// 빈 값인 경우 getDefault() 메서드가 실행된다.
String result = Optional.of(value).orElse(getDefault());
String value = "Value";
// 빈 값이 아니지만 getDefault() 메서드가 실행된다.
String result = Optional.of(value).orElse(getDefault());


값이 존재하지 않는 경우에만 파라미터로 전달한 표현식이 실행되는 코드 구현은 orElseGet() 메서드를 통해 가능하다.

String value = null;
// 빈 값인 경우 getDefault() 메서드가 실행된다.
String result = Optional.of(value).orElseGet(getDefault());
String value = "Value";
// 빈 값이 아닌 경우 getDefault() 메서드는 실행되지 않는다.
String result = Optional.of(value).orElseGet(getDefault());


참고

Categories: ,

Updated:

Comments