지난주부터 Optional에 대해 어떻게 설명하면 좋을까 고민을 많이 해보고 이리저리 찾아도 봤습니다.
처음에는 단순히 null처리를 쉽게할 수 있는 람다식 문법 중 하나겠구나 생각하면서 써왔습니다.
근데 주의사항이 무려 20가지가 넘는 글도 보이고해서 "이거...그냥 단순 null처리가 아닌데?"라는 생각에 바싹 조사를 했습니다. 오늘은 Optional 사용 시 주의해야할 점을 핵심만 요약해 알아보겠습니다.
1. Optional은 함수의 반환 타입에서 사용하도록 설계되었다.
ID에 따라 User 객체를 가져오는 메서드를 예로 들겠습니다. 이때 자동차 저장소에는 ID에 해당하는 Car 객체가 없을수도 있습니다.
Car car = CarRepository.findById();
if(car != null) { //다음 로직 진행 전
...
}
해당 소스만 보면 Car 객체가 null인지 아닌지도 알수 없을 뿐더러 NPE가 발생할지도 모르니 null 체크도 하고있습니다.
그렇다면 Optional을 사용한다면 어떨까요?
Optional<Car> carOpt = CarRepository.findById();
이와 같이 Optional의 목적은 반환 값이 null일수도 있다는 메시지를 명시적으로 전달하기 위해 만들어졌습니다.
또한 부수적으로 null 체크를 아주 편리하게 다룰수 있는 메서드들까지 제공해주죠.
Optional을 매개변수나 생성자, 멤버변수 등에 남용하는 경우도 있는데 이런 사용은 또 다른 에러를 야기할 수도 있으니 우리는 설계의도에 맞도록 사용해야 합니다.
설계의도에 따라 직렬화는 지원하지 않으며 Getter의 경우엔 남용될 수 있으니 사용을 지양하는 것이 좋다고 합니다.
2. Collection은 Optional 대신 빈 값을 반환하자.
public Optional<List<Car>> findCars() {
...
return Optional.ofNullable(cars);
}
반환 타입이 Optional<List<Car>>인게 보이시나요? 해당 함수를 호출하고난 후에도 Optional을 언래핑하고 List를 언래핑하고...사용이 굉장히 불편할 것입니다.
Collection에서는 null 대신 Empty Collection을 반환하는 것이 좋습니다.
public List<Car> findCars() {
...
return (cars != null) ? cars : Collections.emptyList();
}
이렇게 사용한다면 호출하는 곳은 물론 메서드도 훨씬 간결해진 것을 볼수 있습니다.
3. orElse는 null과 관계없이 항상 실행된다.
이 둘은 Optional을 언래핑하여 값을 얻어내는 메서드들입니다. 우선 해당 메서드들의 내부구현을 보여드리겠습니다.
public T orElse(T other) {
return value != null ? value : other;
}
public T orElseGet(Supplier<? extends T> supplier) {
return value != null ? value : supplier.get();
}
Lazy Evalution에 대한 개념을 가지고 계신 분이라면 왜 orElse가 항상 실행되는지 이해하셨을 겁니다.
쉽게 말해서 orElse는 null과 관계없이 항상 값을 만들어 냅니다. 반대로 orElseGet은 null일 때만 Supplier 함수가 실행됩니다.
이를 지연평가라고 하는데 어려운 개념이 아니니 궁금하신 분들은 구글링 한번 해보시길 바랍니다.
4. equals는 내부값을 비교하도록 오버라이딩 되었다.
@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을 서로 언래핑하여 동등비교를 할 필요가 없습니다.
5. filter, map, stream을 사용하여 내부의 값을 조작하자.
우선 해당 메서드들을 사용하지 않고 Optional을 언래핑하여 내부의 값을 조작해봅시다.
Car car = CarRepository.findById().orElseGet(Car::new); //unwrapping
String carName = car.getName();
if(StringUtils.isEmpty(carName)) {
carName = "default";
}
Optional의 메서드들을 이용하면 훨씬 더 보기 편하게 내부의 값을 다룰수 있습니다.
String carName = CarRepository.findById()
.map(Car::getName)
.filter(s -> !s.isBlank())
.orElse("default");
이때 만약 CarRepository.findById()의 옵셔널 내부의 값이 null이라면 연산의 결과는 Empty Optional을 반환합니다.
따라서 orElse가 실행되고 carName에는 default가 들어갑니다.
Optional은 스트림으로도 변경할 수 있고 이는 또 Stream API들과 연관되어 사용할수도 있습니다. 엄청 편리하죠?
그렇지만 위에서 이야기했다 싶히 Optional은 비싼 자원이고 함부로 남용해서는 안 됩니다.
정말 필요할 때 설계대로 주의사항을 살펴가며 사용해야할 것입니다.
참고
https://stackoverflow.com/questions/26327957/should-java-8-getters-return-optional-type
https://www.baeldung.com/java-optional-or-else-optional
https://www.baeldung.com/java-optional-return
'Java > Grammer' 카테고리의 다른 글
Stream API 정리 (0) | 2022.06.04 |
---|---|
입출력 스트림 (2) - File I/O (0) | 2022.05.24 |
입출력 스트림 (1) - System.out.println와 I/O (0) | 2022.05.22 |
ShallowCopy와 DeepCopy 완전 정복 (0) | 2022.05.07 |
객체의 중복/정렬 그리고 Collection (0) | 2022.04.24 |