🙋 들어가며
자바 기반의 백엔드 개발자가 Optional
을 제대로 접하는 순간은 JPA의 query method
를 사용할 때라고 생각합니다.Optional
이 사용하기 편한 객체이지만 사용할 때 주의점을 요합니다.
이 글에서는 Optional
에 대한 저의 고찰과 올바르게 사용하는 방법에 대해 이야기해 보겠습니다.
🧐 Optional의 목적
모든 사물들은 각자에게 주어진 알맞은 쓰임이 존재합니다.
예를 들어서, 책은 읽기 위해서 존재합니다. 라면 받침으로 쓰는 것은 본디 목적을 잃는 것입니다. (물론 라면 받침으로 씁니다)
그렇다면 Optional
의 쓰임은 무엇일까요? 자바 설계자가 언급한 의도를 가져왔습니다.
Our intention was to provide a limited mechanism for library method return types where there needed to be a clear way to represent "no result", and using null for such was overwhelmingly likely to cause errors.
요약하면 결과 없음
을 나타내는 제한된 메커니즘을 제공하기 위해 탄생했습니다.
같은 이야기가 자바 공식 문서의 API NOTE에도 나와있습니다.
💵 Optional은 비싸다?
저는 자바를 공부하고 지금까지 Optional이 비싸다는 이야기를 많이 들었습니다.
실제로 어떤 점에서 비싸다고 표현하는지 제대로 알아보고자 확인해 보았습니다.
다음은 가장 많이 사용하는 정적 팩토리 메서드의 내부 구현입니다.
제가 봤을 때는 비싸다는 생각이 들지 않았습니다. 또한, 주요 메서드들의 구현을 봐도 크게 비싸다는 생각이 들지 않았습니다.
그렇다면 비싸다는 이야기는 어디서 나온 것일까요?
저는 상대적인 관점에서 나온 표현이라고 생각합니다.
자바에서 기본적으로 제공하는 Wrapper
타입은 객체로 한번 감싸기 때문에 기본 타입보다 비싸다고 표현을 합니다.
Optional
도 하나의 Wrapper 클래스로 볼 수 있습니다.
따라서 no result
를 표현하기 위해서 한번 감싸고, 검증하거나 꺼내는 추가 동작을 하므로 비싸다고 표현한 것입니다.
이제 어떻게 올바르게 사용할 수 있을지 살펴보겠습니다.
😎 Optional 바르게 사용하는 습관
1️⃣ 생성할 때 고민하자!
앞서 말했듯이 Optional
의 목적은 "no result"를 표현하기 위해서입니다.
따라서, 표현 이외의 목적으로 생성하면 안 된다고 생각합니다. 다음이 대표적인 잘못된 사용의 예시들입니다.
// 1. null을 할당한다.
Optional<User> nullUser = null;
// 2. 단순한 값을 반환하기 위해서 Optional 사용한다.
return Optional.ofNullable(status).orElse(UserStatus.WITHDRAW);
// 3. 비어있는 값을 반환하기 위해서 Optional 사용한다.
List<User> users = userRepository.findAll();
return Optional.ofNullable(users);
null
을 할당해 주는 것은 당연히 Optional
의 목적과 위배됩니다.
불필요한 객체 생성은 메모리를 낭비하는 일입니다. 근데 2번, 3번 예시는 불필요하게 객체를 생성하고 있습니다.
특히 JPA는 비어있는 컬렉션을 반환해 주기 때문에 이것을 굳이 객체를 생성해서 넘겨주지 않아도 됩니다.
3번은 제가 참고한 이펙티브 자바(item 54, 55)에도 좋은 내용이 있습니다. (살짝 다른 예이긴 합니다.)
다음과 같이 수정해 볼 수 있을 것 같습니다.
// 1. 내부적으로 캐싱되어있는 값을 사용한다.
Optional<User> nullUser = Optional.empty();
// 2. null을 비교하고 넘겨준다.
if (status == null) {
return UserStatus.WITHDRAW;
}
return status;
// 3. 빈 컬렉션이므로 바로 넘겨준다.
return userRepository.findAll();
2️⃣ 생성했다면 적극적으로 사용하자!
목적에 부합해서 사용하기로 결심했다면 적극적으로 사용해야 합니다. 바로 안 좋은 예시들을 보겠습니다.
// 1. optionalUser가 null일 수도 있다.
Optional<User> optionalUser = userRepository.findById(...);
return optionalUser.get();
// 2. 불필요한 isPresent()-get() 사용한다.
Optional<User> optionalUser = userRepository.findById(...);
if (optionalUser.isPresent()) {
return optionalUser.get();
}
return null;
// 3. orElse()를 계속 호출한다.
Optional<User> optionalUser = userRepository.findById(...);
return optionalUser.orElse(new User(...));
// 4. 불필요하게 get()을 호출한다.
Optional<User> optionalUser1 = userRepository.findById(...);
Optional<User> optionalUser2 = userRepository.findById(...);
assertEquals(optionalUser1.get(), optionalUser2.get());
// 5. 메서드들을 제대로 활용하지 못한다.
Optional<String> optionalName = ...;
if (optionalName.isPresent()) {
return Optional.of(new User(optionalName.get()));
}
return Optional.empty();
1번은 Optional
이 감싼 값이 없을 수도 있기 때문에 null
이 아님을 확인하고 넘겨야 합니다.
2번은 isPresent()
로 null
이 아님을 확인하지만 Optional
에는 이미 좋은 메서드들을 지원하고 있습니다.orElse()
, orElseGet()
, orElseThrow()
와 같은 메서드를 사용하는 것을 추천드립니다.
3번은 orElse()
의 문제점입니다. null
의 상관없이 항상 생성자를 호출합니다.
따라서 orElse()
을 사용할 때는 미리 캐싱되어 있는 값 혹은 생성하지 않아도 되는 값을 리턴해야 합니다.
4번과 5번은 이미 내부적으로 구현되어 있는 메서드를 활용하면 더 좋은 코드로 발전할 수 있습니다.
특히 자바 9부터 stream
, map
, filter
그리고 isEmpty
등을 다양하게 지원하고 있습니다. (isEmpty : 자바 11)
다음과 같이 수정해 볼 수 있을 것 같습니다.
// 1. isPresent()로 확인하고 넘겨준다.
Optional<User> optionalUser = userRepository.findById(...);
if (optionalUser.isPresent()) {
return optionalUser.get();
}
return null;
// 2. orElse, orElseGet, orElseThrow를 사용한다.
Optional<User> optionalUser = userRepository.findById(...);
return optionalUser.orElse(null);
// 3. orElseGet()으로 한번만 호출한다.
Optional<User> optionalUser = userRepository.findById(...);
return optionalUser.orElseGet(User::New);
// 4. 구현되어있는 메서드를 사용한다. (equals)
Optional<User> optionalUser1 = userRepository.findById(...);
Optional<User> optionalUser2 = userRepository.findById(...);
assertEquals(optionalUser1.equals(optionalUser2));
// 5. 구현되어있는 메서드를 사용한다. (map)
Optional<String> optionalName = ...;
return optionalName.map(User::New);
3️⃣ 일시적으로 사용하자!
계속 언급했듯이 목적에 맞게 사용하는 것이 중요합니다. 보여드릴 예시는 이 목적을 지키지 않았습니다.
모두 결과 없음
을 표현해 null-safe
하게 사용하려는 목적이 아닙니다.
// 1. 필드 값으로 사용한다.
public class User {
private Optional<String> name; // bad..!
}
// 2. 컬렉션 내부에서 사용한다.
Map<Long, Optional<String>> map = ...;
// 3. 파라미터로 사용한다.
public void validateName(Optional<String> name) {
if (name.isPresent()) {
...
}
}
😋 정리
이외에도 더 좋은 팁들이 있지만 실질적으로 사용할 일이 크게 없을 것 같아서 제외했습니다.
소개해드린 단계별로 스텝을 밟아 만들어진 이유에 맞게 잘 사용해 보면 좋을 것 같습니다! 😋😋😋
-reference
https://dzone.com/articles/using-optional-correctly-is-not-optional
https://docs.oracle.com/javase/9/docs/api/java/util/Optional.html
https://stackoverflow.com/questions/26327957/should-java-8-getters-return-optional-type/26328555#26328555
이펙티브 자바 : item 54, 55
😋 지극히 개인적인 블로그지만 댓글과 조언은 제 성장에 도움이 됩니다 😋
'언어 공부 > Java' 카테고리의 다른 글
[Java] equals()와 hashCode()를 같이 재정의하자! (2) | 2023.07.20 |
---|---|
[Java] 가변성 (feat. Generic) (0) | 2023.07.05 |
[Java] 빌드툴 (feat. Gradle) (0) | 2023.06.05 |
[Java] Reflection (0) | 2023.01.11 |
[Java] 멀티쓰레드 프로그래밍 (22.12.05 updated) (0) | 2022.12.05 |
댓글