Optional 클래스의 참조 변수에 데이터가 저장되어있지 않을 경우 NoSuchElementException
이 발생합니다.
모던 자바 인 액션을 통해 프로그래밍 언어를 개발하면서 범했던 가장 큰 실수 중 하나가 null 값을 허용했던 것이라고 공부했었는데요. NPE(NullPointException)는 컴파일 타임에는 보이지 않다가 런타임에 터져서 골치아프게 되었습니다.
Optional은 null이 아닌 값을 포함하거나 포함하지 않을 수 있는 컨테이너 객체입니다. Optional 클래스를 활용하면 null을 처리할 수 있는 다양한 메서드가 제공되기 때문에 활용하면 비즈니스 로직 상 null 체크를 따로 해줄 필요가 없어 null 체크가 코드에서 난무하게 되는 복잡한 상황을 없애주고 null을 처리할 수 있는 방법을 제공합니다.
하지만 여기서 드는 의문점..! 데이터가 존재하지 않는다고 NoSuchElementException
을 던지면 그걸 또 에러 처리를 해줘야 하는데 NPE 에러 처리하는 거랑 무엇이 다를까요?
실제 데이터가 존재하지 않는 상황이 에러 상황인건지..
지금부터는 Optional을 제대로 알고 사용하는 방법에 대해 알아보고자 한다.
Optional은 “결과 없음”을 나타내는 명확한 방법이 필요한 라이브러리 메서드 반환 유형에 대한 제한된 매커니즘을 제공하기 위한 것이며 null을 사용하는 것은 에러를 유발할 가능성이 압도적으로 높다.
Avoid:
// 옵셔널의 의도와 맞지 않게 사용한 예
Optional<Integer> optional = null;
optional.get(); // NPE 발생
Prefer:
Optional<Integer> optional = Optional.empty();
optional.get(); // NoSuchElementException 발생
private static final Optional<?> EMPTY = new Optional<>();
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
Optional.empty()
Optional 객체를 null로 초기화하는 것은 Optional의 도입 의도와 맞지 않다.
Avoid:
Optional<Question> question = questionRepository.findById(id);
String title = question.get().getTitle();
Prefer:
Optional<Question> question = questionRepository.findById(id);
if(question.isPresent()) {
title = question.get().getTitle();
}
일반적으로 Optional 값이 존재한다는 것을 확인하기 위해 Optional.isPresent()을 사용하지만 isPresent()-get() 쌍은 평판이 좋지 않다.
isPresent()-get() 쌍을 대체할 수 있는 방법입니다. 하지만 중요한 점은 비어있지 않는 Optional을 가지고 있을 경우에도 orElse()의 매개변수가 평가된다는 점이다.
해당 메서드는 미리 선언된 디폴트 객체를 메서드 인자로 전달하여 사용하는데 객체의 생성 비용이 높을 경우에도
객체를 미리 생성하는 비용을 발생시키기 때문에 값 비싼 비용을 치를 수 있다.
// UNKOWN_USER is pre-defined object for case that no user is found that id matches
Optional<User> user = userRepository.findById(id).orElse(UNKOWN_USER);
orElseGet() 메서드는 파라미터 타입은 Supplier 입니다. Supplier 타입이기 때문에 Optional 값이 있을 때 필요하지 않는 객체 생성 및 실행 코드의 orElse() 성능 저하를 방지할 수 있습니다.
Optional<User> user = userRepository.findById(id).orElseGet(() -> new User("UNKOWN"));
Optional 값이 존재하지 않을 때 NosuchElementException을 던진다. Java 버전 10부터는 인수 없이 orElseThrow() 메서드를 사용할 수 있습니다.
Optional<User> user = userRepository.findById(id).orElseThrow(NoSuchElementException::new);
특정한 경우에 메서드 파라미터로 null 참조를 허용하는 method가 존재한다.
Avoid:
Optional<User> user = ...;
if(user.isPresent()) {
System.out.println("user: " + user.get());
}
Prefer:
Optional<User> user = ...;
user.ifPresent(System.out::println);
Avoid:
Optional<User> user = ...;
if(user.isPresent()) {
System.out.println("user: " + user.get());
} else {
System.out.println("User not found");
}
Prefer:
Optional<User> user = ...;
user.ifPresentOrElse(System.out::println, () -> System.out.println("User not found");
};
비어있지 않은 Optional일 경우, 때때로 Optional 자체를 반환할 수 있고 Optional이 비어있을 때 Option을 반환하는 다른 액션을 취할 수 있습니다.
Java 9 버전부터 추가된 스펙입니다.
Avoid:
// Avoid
Optional<Role> role = ...;
Optional<Role> defaultRole = Optional.of("USER");
if (role.isPresent()) {
return role;
} else {
return defaultRole;
}
// Avoid
Optional<Role> role = ...;
role.orElseGet(() -> Optional.<Role>of("USER"));
Prefer:
Optional<Role> role = ...;
role.or(() -> Optional.of("USER"));
isPresent()-get() 쌍을 통해 Optional을 사용하는 것은 람다 체인을 끊고 불필요한 if문을 만들기 때문에 코드를 오염시킬 수 있습니다.
Avoid:
List<User> users = ...;
Optional<User> user = users.stream()
.filter(u -> u.getHeight() > height)
.findFirst();
if (user.isPresent()) {
reutrn user.get().getHeight();
} else {
return "NOT FOUND";
}
Prefer:
List<User> users = ...;
return users.stream()
.filter(u -> u.getHeight() > height)
.findFirst()
.map(User::getHeight)
.orElse("NOT FOUND");
남용하지 말라는 이야기
Avoid:
String status = ...;
return Optional.ofNullable(status).orElse("PENDING");
Prefer:
String status = ...;
return status == null ? "PENDING" : status;
Optional의 사용 의도와 맞지 않고 Optional은 직렬화를 구현하지 않는다.
Optional은 다른 수준의 추상화로 객체를 감싼다. 이 경우에는 추가 상용구 코드로 감싼다.
Avoid:
public class Customer {
private final String name; // cannot be null
private final Optional<String> postcode; // optional field, thus may be null
public Customer(String name, Optional<String> postcode) {
this.name = Objects.requireNonNull(name, () -> "Name cannot be null");
this.postcode = postcode;
}
public Optional<String> getPostcode() {
return postcode;
}
...
}
Prefer:
public class Customer {
private final String name; // cannot be null
private final String postcode; // optional field, thus may be null
public Cart(String name, String postcode) {
this.name = Objects.requireNonNull(name, () -> "Name cannot be null");
this.postcode = postcode;
}
public Optional<String> getPostcode() {
return Optional.ofNullable(postcode);
}
...
}
위와 같은 Prefer 코드 역시도 Avoid 보다 선호되는 방식이지 대부분의 경우 Optional 대신 빈 컬렉션이나 배열을 반환하는 것을 선호한다.
Domain Model 엔티티에서 다음과 같이 사용되기도 한다.
Prefer:
@Entity
public class Customer implements Serializable {
private static final long serialVersionUID = 1L;
...
@Column(name="customer_zip")
private String postcode; // optional field, thus may be null
public Optional<String> getPostcode() {
return Optional.ofNullable(postcode);
}
public void setPostcode(String postcode) {
this.postcode = postcode;
}
...
}
메서드의 인수로 사용되는 것은 불필요한 복잡성을 초래한다.
대신 선호되는 방식은 빈 컬렉션이나 배열을 반환하는 것입니다.
Avoid:
// AVOID
Map<String, Optional<String>> items = new HashMap<>();
items.put("I1", Optional.ofNullable(...));
items.put("I2", Optional.ofNullable(...));
...
Optional<String> item = items.get("I1");
if (item == null) {
System.out.println("This key cannot be found");
} else {
String unwrappedItem = item.orElse("NOT FOUND");
System.out.println("Key found, Item: " + unwrappedItem);
}
Prefer(Java 8):
Map<String, String> items = new HashMap<>();
items.put("I1", "Shoes");
items.put("I2", null);
...
// get an item
String item = get(items, "I1"); // Shoes
String item = get(items, "I2"); // null
String item = get(items, "I3"); // NOT FOUND
private static String get(Map<String, String> map, String key) {
return map.getOrDefault(key, "NOT FOUND");
}
Optional 타입의 상품의 맵이 모두 null 값으로 채워지거나 상품이 아닌 다른 것들을 포함하는 Optional 객체로 채워지면 어떻게 하시겠습니까?
조금의 디자인 패턴을 가미하여 바꾸는 것도 괜찮은 방법이다.
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);
}
public final class Optional<T> {
...
...
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Optional)) {
return false;
}
Optional<?> other = (Optional<?>) obj;
return Objects.equals(value, other.value);
}
...
}
<원문 참조>
26 Reasons Why Using Optional Correctly Is Not Optional - DZone Java