Optional 정리

3 분 소요

Optional 사용하면서 참고할 내용 정리입니다.

기존 null check

아래와 같은 객체를 예시로 사용할 예정이다. 기존에 작성했었던 코드를 거의 그대로 들고왔다..;

public class Article {
    private Long id;
    private User author;
    private LocalDateTime createdTime;
    private LocalDateTime updatedTime;
    private Content content;
    private FileUrl imageUrl;
    private FileUrl videoUrl;
}

public class User {
    private UserName userName;
    private UserEmail userEmail;
    private UserPassword UserPassword;
}

public class UserEmail {
    private static final String EMAIL_PATTERN = "^[_a-z0-9-]+(.[_a-z0-9-]+)*@(?:\\w+\\.)+\\w{2,50}$";
    private static final String EMAIL_EXCEPTION_MESSAGE = "올바르지 않은 이메일입니다.";

    private String email;

    public UserEmail(String email) {
        Validator.checkValid(email, EMAIL_PATTERN, EMAIL_EXCEPTION_MESSAGE);
        this.email = email;
    }
}

기존에 null 확인을 위해 했던 일들을 보면 만약 author에서 user의 email을 읽어오는데 null check를 해야한다면 아래와 같이 작성 해야한다.

User author = ...;
public String getUserEmail(User user) {
    if (user != null) {
        UserEmail userEmail = user.getUserEmail();
        if (userEmail != null) {
            return userEmail.getEmail();
        }
    }
    return "Unknown";
}

Optional을 사용하면 아래와 같이 만들 수 있을 것이다.

public String getUserEmail(User user) {
    Optional<User> optUser = Optional.ofNullable(user);
    return optUser.flatmap(User::getUserEmail)
            .map(UserEmail::getEmail)
            .orElse("Unknown");
}

null 때문에 발생하는 문제

자바에서 null 때문에 발생하는 문제를 보면 아래와 같다.

  1. NullPointerException
  2. null check 로직 때문에 if(userEmail != null)과 같은 코드 때문에 코드 복잡도가 올라간다.
  3. null은 값이 없음을 의미하지 않는다. 아무 의미가 없다.
  4. 모든 레퍼런스에 null이 가능하기 때문에, 이 null이 퍼졌을 때 처음의 null이 어떤 의미인지 파악하기 힘들다.

Optional

Optional<T>라는 클래스가 있다. T에 값이 있으면, Optional은 값을 감싼다. 값이 없으면 Optional.empty로 특별한 싱글턴 인스턴스를 반환한다. 모든 null 레퍼런스를 Optional로 고치는 것은 바람직하지 않다. Optional은 더 이해하기 쉬운 API를 설계하도록 돕는 것이다. Optional 클래스는 필드 형식으로 사용할 것을 가정하지 않았으므로 Serializable 인터페이스를 구현하지 않는다.

Optional 적용

빈 Optional

Optional.empty로 빈 Optional 객체를 얻을 수 있다. 하지만 컬렉션의 경우에는 Optional 대신 비어있는 컬렉션을 반환하자.

컬렉션을 반환하는 Spring Data JPA Repository 메서드는 null을 반환하지 않고 비어있는 컬렉션(emptyList 같은..)을 반환해주므로 Optional로 감싸서 반환할 필요가 없다.

JPA - findAllById

null이 아닌 Optional

Optional.of()로 null이 아닌 Optional을 만들 수 있다. Optional<User> optAuthor = Optional.of(author)

여기서 author가 null이면 null pointer exception이 발생한다.

null이 가능한 Optional

Optional.ofNullable() 메서드는 null이면 빈 Optional이 반환된다. Optional<User> optAuthor = Optional.ofNullable(author)

author가 null이면 빈 Optional 객체가 나온다.

Optional 언랩

  1. get()
    • null이 반드시 아니라고 확신할 수 있는 상황이 아니면 쓰지말자. 기존의 null 체크 상황과 크게 다르지 않다.
  2. orElse(T other)
    • Optional이 값이 없을 때 default값을 제공한다.
  3. orElseGet(Supplier<? extends T> other)
    • Optional에 값이 없을 때만 Supplier가 실행된다.
  4. orElseThrow(Supplier<? extends T> other)
    • Optional에 값이 없을 때 exception을 던진다. 하지만 원하는 exception을 던질 수 있다.
  5. ifPresent(Consumer<? super T> consumer)
    • 값이 존재하면, Consumer로 어떤 동작을 할 수 있다.

필터로 특정값 거르기

User author = ...;
if (author != null && "Martin".equals(author.getName())) {
    System.out.println("Author Name is " + author.getName())
}

위와 같은 코드를 아래 코드처럼 구현할 수 있다.

Optional<User> author = ...;
author.filter(author -> "Martin".equals(author.getName()))
    .ifPresent(author -> System.out.println("Author Name is " + author.getName()));

이와 같은 방식으로 이름이 Martin이 들어간 유저로 필터링 할 수 있다. 만약 "Martin".equals(author.getName())가 false면 Optional은 빈 상태가 된다.

이 경우에서도 "Martin".equals(author.getName())으로 “Martin”이라는 문자열이 null일 확률은 없기 때문에, 앞에 왔다. null일 확률이 적은 것부터 호출되도록 하면 좋다.

잠재적으로 null이 될 수 있는 대상을 Optional로 감싸기

예를 들어 Map에서 get(key)를 했을 때 맵 안에 그 키에 해당하는 값이 없으면 null이 나온다. Map의 get을 바꿀 수는 없으니, 이 반환 타입을 Optional로 감싼 후 반환 할 수 있다.

Map<Long, User> userRepository;라는 맵을 가지고 있다고 해보면, Optional<User> user = Optional.ofNullable(userRepositor.get(1L))로 Map에서 꺼내는 값을 Optional로 처리 할 수 있게 된다.

예외와 Optional

예를 들어 Integer.parseInt(String)이라는 메서드 실행이 실패 할 경우 NumberFormatException이 발생하게 된다. 이를 깔끔하게 처리하기 위해서 아래 코드처럼 유틸 메서드를 만들어서 Optional로 처리할 수 있다.

public static Optional<Integer> stringToInt(String s) {
    try {
        return Optional.of(Integer.parseInt(s));
    } catch (NumberFormatException e) {
        return Optional.empty();
    }
}

기본형 Optional

Optional에도 OptionalInt, OptionalLong, OptionalDouble등의 클래스가 있다. 이와 같은 기본형 Optional 클래스들을 사용하면 Optional<Integer>, Optional<Long>, Optional<Double>처럼 boxing, unboxing이 발생하지 않는다. 대신 기본형 특화 Optional 클래스들은 map, flatMap, filter등을 지원하지 않는다.

정리

  • Optional is primarily intended for use as a method return type where there is a clear need to represent “no result,” and where using null is likely to cause errors. A variable whose type is Optional should never itself be null; it should always point to an Optional instance.
  • orElse() 보다는 orElseGet(() -> …)
  • 단순히 값 또는 null을 얻을 목적이라면 Optional 대신 null 비교를 쓰자. (컬렉션일 경우 비어있는 컬렉션을 쓰자)
  • Optional을 field, parameter로 사용금지 (호출되는 쪽에 null 체크의 책임을 남겨두는 것이 좋다.)
  • of(), ofNullable() 구분해서 사용
  • Integer, Long, Double의 경우 OptionalInt, OptionalLong, OptionalDouble 사용 (대신 map, flatMap, filter를 지원하지 않는다.)

참고자료

태그:

카테고리:

업데이트:

댓글남기기