[자바] 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";
Optional
의 or()
메서드를 사용하여 유사한 코드를 작성하면 다음과 같다.
Optional<String> result = object1.flatMap(Object1::getObject2)
.flatMap(Object2::getObject3)
.map(Object3::getField)
.or(() -> "Default");
참조하는 Optional
이 비어있지 않다면 변환 함수를 적용하며 객체 중 하나라도 비어있다면 전체 표현식은 특정 값 또는 객체를 가지는 Optional
로 평가된다. 즉, 엘비스 연산자(?:
)와 유사하게 참조하려는 변수가 비어있는 Optional
일 경우 표현식을 특정 값을 가지는 Optional
로 평가한다.
Optional
이 아닌 래핑된 값 또는 객체를 반환받고자 하는 경우에는 다음과 같이 작성한다. Optional
의 orElse()
메서드를 사용하여 반환할 기본값을 명시한다.
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());
Comments