Optional 을 사용하는 목적

김성혁·2022년 3월 31일
0

Optional 클래스의 참조 변수에 데이터가 저장되어있지 않을 경우 NoSuchElementException이 발생합니다.

모던 자바 인 액션을 통해 프로그래밍 언어를 개발하면서 범했던 가장 큰 실수 중 하나가 null 값을 허용했던 것이라고 공부했었는데요. NPE(NullPointException)는 컴파일 타임에는 보이지 않다가 런타임에 터져서 골치아프게 되었습니다.

Optional은 null이 아닌 값을 포함하거나 포함하지 않을 수 있는 컨테이너 객체입니다. Optional 클래스를 활용하면 null을 처리할 수 있는 다양한 메서드가 제공되기 때문에 활용하면 비즈니스 로직 상 null 체크를 따로 해줄 필요가 없어 null 체크가 코드에서 난무하게 되는 복잡한 상황을 없애주고 null을 처리할 수 있는 방법을 제공합니다.

하지만 여기서 드는 의문점..! 데이터가 존재하지 않는다고 NoSuchElementException 을 던지면 그걸 또 에러 처리를 해줘야 하는데 NPE 에러 처리하는 거랑 무엇이 다를까요?

실제 데이터가 존재하지 않는 상황이 에러 상황인건지..

지금부터는 Optional을 제대로 알고 사용하는 방법에 대해 알아보고자 한다.

Optional은 “결과 없음”을 나타내는 명확한 방법이 필요한 라이브러리 메서드 반환 유형에 대한 제한된 매커니즘을 제공하기 위한 것이며 null을 사용하는 것은 에러를 유발할 가능성이 압도적으로 높다.


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 컨테이너 객체를 반환
  • Optional 객체를 null로 초기화하는 것은 Optional의 도입 의도와 맞지 않다.

Optional.get()을 호출하기 전에 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() 쌍은 평판이 좋지 않다.

값이 존재하지 않을 때, Optional.orElse()를 통해 이미 만들어진 디폴트 객체를 반환

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);

값이 존재하지 않을 때, Optional.orElseGet() 을 통해 존재하지 않는 디폴트 객체를 반환

orElseGet() 메서드는 파라미터 타입은 Supplier 입니다. Supplier 타입이기 때문에 Optional 값이 있을 때 필요하지 않는 객체 생성 및 실행 코드의 orElse() 성능 저하를 방지할 수 있습니다.

Optional<User> user = userRepository.findById(id).orElseGet(() -> new User("UNKOWN")); 

값이 존재하지 않을 때, Optional.orElseThrow()를 통해 NosuchElementException을 던져라

Optional 값이 존재하지 않을 때 NosuchElementException을 던진다. Java 버전 10부터는 인수 없이 orElseThrow() 메서드를 사용할 수 있습니다.

Optional<User> user = userRepository.findById(id).orElseThrow(NoSuchElementException::new);

Optional과 null 참조가 필요하다면, orElse(null)을 사용하라. 그렇지 않는다면, orElse(null)은 피해라

특정한 경우에 메서드 파라미터로 null 참조를 허용하는 method가 존재한다.

값이 있는 경우에 실행되고 값이 없는 경우에 실행이 되지 않게 만들기 위해서는 Optional.ifPresent()를 사용하라

Avoid:

Optional<User> user = ...;

if(user.isPresent()) {
	System.out.println("user: " + user.get());
}

Prefer:

Optional<User> user = ...;

user.ifPresent(System.out::println);

값이 존재할 경우 Optional을 소비하고, 값이 존재하지 않는다면 Empty-Based Action을 취하라. Optional.ifPresentOrElse()

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.or()

비어있지 않은 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"));

Optional.orElse / orElseXXX는 isPresent()-get() 쌍을 람다로 대체하는 가장 좋은 방법입니다.

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");

값을 얻으려는 단일 목적으로 Optional 의 메서드를 연결하지 마세요

남용하지 말라는 이야기

Avoid:

String status = ...;

return Optional.ofNullable(status).orElse("PENDING");

Prefer:

String status = ...;

return status == null ? "PENDING" : status;

Optional을 필드의 타입, 생성자의 인수, 세터의 인수, 메서드의 인수로 사용하지 마세요.

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;
    }
    ...
}

메서드의 인수로 사용되는 것은 불필요한 복잡성을 초래한다.

빈 컬렉션이나 배열을 위해 Optional을 사용하지 마세요.

대신 선호되는 방식은 빈 컬렉션이나 배열을 반환하는 것입니다.

  • Collections.emptyList(), emptyMap(), emptySet() 과 같은..

컬렉션에서 Optional의 사용은 피하세요.

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 객체로 채워지면 어떻게 하시겠습니까?

조금의 디자인 패턴을 가미하여 바꾸는 것도 괜찮은 방법이다.

Optional.of()와 Optional.ofNullable() 혼돈하지 않기

  • Optional.of(null)은 NPE를 던짐
    public static <T> Optional<T> of(T value) {
    	return new Optional<>(value);
    }
  • Optional.ofNullable(null)은 Optional.empty()를 반환
    public static <T> Optional<T> ofNullable(T value) {
    	return value == null ? empty() : of(value);
    }

자바에서 기본 제공하는 타입을 활용하는 경우 Non-Generic을 사용해라.

  • OptionalInt
  • OptionalLong
  • OptionalDouble
  • 박싱과 언박싱은 성능 저하를 유발할 수 있기 때문에 위의 유형을 사용하도록 한다.

동등성을 비교하는 경우 Optional을 벗겨낼 필요가 없습니다.

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);
	}
...
}

Optional도 Stream API 사용 가능

  • map 연산과 flatMap 연산은 정말 편리한 기능입니다.
  • filter를 사용하여 미리 정해진 규칙을 적용하는 것도 괜찮습니다.
  • 자바 9 버전부터 적용

자바 11 버전부터 추가된 isEmpty()는 boolean을 반환합니다.

<원문 참조>

26 Reasons Why Using Optional Correctly Is Not Optional - DZone Java

0개의 댓글