Optional 정리
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 때문에 발생하는 문제를 보면 아래와 같다.
- NullPointerException
- null check 로직 때문에
if(userEmail != null)
과 같은 코드 때문에 코드 복잡도가 올라간다. - null은 값이 없음을 의미하지 않는다. 아무 의미가 없다.
- 모든 레퍼런스에 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로 감싸서 반환할 필요가 없다.
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 언랩
- get()
- null이 반드시 아니라고 확신할 수 있는 상황이 아니면 쓰지말자. 기존의 null 체크 상황과 크게 다르지 않다.
- orElse(T other)
- Optional이 값이 없을 때 default값을 제공한다.
- orElseGet(Supplier<? extends T> other)
- Optional에 값이 없을 때만 Supplier가 실행된다.
- orElseThrow(Supplier<? extends T> other)
- Optional에 값이 없을 때 exception을 던진다. 하지만 원하는 exception을 던질 수 있다.
- 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를 지원하지 않는다.)
댓글남기기