Java Optional의 사용
런타임에 발생하는 객체없음(NullPointException)을 방지하기 위해서 java8 부터는 함수형 언어를 이용해서 존재하지 않을 수 도 있는 값이라는 Optional 타입을 만들어냈다.
그러나 Optional의 사용에는 주의해야 할 점 이 26가지나 존재하므로 유의해서 사용해야 한다.
Optional
Card card = cardRepository.findById(1).orElseThrow(new IllegalException("Card not found with id: " + 1))
Optional은 null이 될 수도 있는 값을 가진 객체를 감싸는 Wrapper 클래스 로 반환할 결과 값이 null(없음)임을 명백하게 표현할 필요가 있지만 null반환시에 에러가 발생할 가능성이 높은 곳에서 제한적으로 사용할 수 있다.
주의사항 ① .isPresent()-get() 대신 orElse() , orElseGet(), orElseThrow() 사용하기
Optional<Card> card = findById(1); //Wrong
if (card.isPresent()) {
Card c = card.get();
System.out.println(c.getNumber());
} else {
throw new CardNotFoundException("No card found with id: " + 1);
}
Card card = findById(1) //OK
.orElseThrow(() -> new CardNotFoundException(No card found with id: " + 1));
System.out.println(card.getNumber());
isPresent()를 사용해서 객체를 찾고 없으면 예외발생을 직접 작성해주는 것보다 orElseThrow(()->)를 사용해서 명시적으로 의도를 나타내고 간결하게 표현할 수 있으므로 orElse(), orElseGet(), orElseThrow()를 사용하도록 한다.
주의사항 ② orElse(new ... ) 대신 orElseGet( () -> new ... ) 사용하기
Card card = findById(1).orElse(UNKNOWN_CARD); //orElse()의 좋은 예
findById(1).orElse(new Card()); // Wrong
findById(1).orElseGet(() -> new Card("UNKNOWN")); // OK
이미 존재하는 객체를 반환하는 경우에는 orElse()의 사용이 적절 하다. 하지만 새로운 객체를 생성해서 반환하려고 하는 경우에 orElse(생성 메서드)를 사용시 orElse()의 사용보다 생성메서드가 먼저 사용되기 때문에 객체가 null일 때만 객체를 새로 만드는 것이 아니라 객체가 있던 없던 생성메서드를 실행하게 된다. 즉 orElse(생성메서드)를 사용할 때마다 값의 존재여부와 달리 불필요한 객체를 새로 만들어 내게 된다. 이렇게 기본값으로 반환할 값을 인자로 받는 orElse()와는 달리 orElseGet()은 값이 없는 경우에 반환할 값을 생성하는 람다를 인자로 받기 때문에 불필요한 객체 생성을 막을 수 있다.
주의사항 ③ 단지 값을 얻기 위해서라면 Optional 대신 null을 비교하자
Optional.ofNullable(status).orElse(READY); // Wrong
status != null ? status : READY; //OK
Optional은 Wrapper 클래스 이기 때문에 두 개의 참조를 가지므로 생성 비용이 비싸다. 단순히 값을 얻기 위해서라면 null비교를 사용하여 불필요한 Optional의 사용을 자제하도록 하자.
주의사항 ④ 비어있는 컬렉션을 반환하기 위해서라면 Optional 대신 빈 컬렉션을 사용하자
List<Card> cards = shape.getCards(); // Wrong
return Optional.ofNullable(cards);
List<Card> cards = shape.getCards(); // OK
return cards != null ? cards : Collections.emptyList();
Optional은 생성비용이 비싸기 때문에 컬렉션은 null이 아닌 비어있는 컬렉션을 반환하는 더 좋을 때가 많고 또한 컬렉션 타입의 복수의 결과를 반환하는 메서드가 결과없음(비어있음)을 가장 명확하게 나타나기 위해서 빈 컬렉션을 반환하는 것이 좋으므로 Optional 대신 빈 컬렉션을 반환하도록 하자.
주의사항 ⑤ Optional의 필드 사용 금지
class Card { // Wrong
Optional<String> number;
}
void printCardNumber(Optional<Card> card) { // Wrong
}
Optional은 반환을 위해 설계된 타입으로 Serializable도 아니기 때문에 Optional을 메서드의 인자로 사용하거나 클래스의 필드로 선언하는 것은 Optional 도입 의도에 반하는 패턴이 된다.
주의사항 ⑥ Optional을 컬렉션 원소로 사용 금지
Map<Integer, Optional<String>> cards = new HashMap<>(); // Wrong
cards.put(1, Optional.of("Diamond"));
cards.put(8, Optional.ofNullable(otherCard));
String diamond = cards.get(1).orElse("Diamond");
String other = cards.get(8).orElse("");
Map<Integer, String> cards = new HashMap<>(); // OK
cards.put(1, "Diamond");
cards.put(8, null);
String diamond = cards.getOrDefault(1, "Diamond");
String other = cards.computeIfAbsent(8, k -> "");
Optional의 생성 비용이 비싸므로 원소로 Optional을 원소로 사용하는 것이 아니라 꺼낼 때 null 체크를 하여 사용하는 것이 더 효과적이다. 추가로 Map의 경우 getOrDefault(), putIfAbsent(), computeIfAbsent(), computeIfPresent()와 같은 null 체크가 포함된 메서드를 제공하므로 이 메서드를 활용하자.
주의사항 ⑦ of(), ofNullable() 혼동 주의
of(대상)은 대상이 null일 경우에 NullPointerException이 발생하므로 확실하게 null이 아닌 경우에만 사용해야 하며 ofNullable(대상)은 대상이 null일 수도 있을 때만 사용해야 하고 null이 확실히 아닐 경우에는 of(대상)을 사용해야 한다.
주의사항 ⑧ 원시타입의 Optional에는 OptionalInt, OptionalLong, OptionalDouble 사용
OptionalInt int = OptionalInt.of(1);
OptionalLong long = OptionalLong.of(2L);
OptionalDouble double = OptionalDouble.empty();
원시타입을 Optional로 사용할 때에는 박싱과 언박싱을 거치면서 오버헤드가 발생되므로 반드시 Optional의 제네릭 타입에 맞춰야 하는 경우가 아니라면 int, long, double의 타입에는 내부 값을 Wrapper클래스가 아닌 원시타입으로 갖고 값 존재 여부를 나타내는 isPresent필드를 함께 갖는 구현체인 Optional원시타입의 사용을 고려하는 것이 좋다.
(참고한 사이트)
https://tecoble.techcourse.co.kr/post/2021-06-20-optional-vs-null/
https://dzone.com/articles/using-optional-correctly-is-not-optional