Java 언어 설계자인 Brain Goetz는 Optional을 만든 의도를 아래와 같이 작성했다.
API Note:
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.
메소드가 반환할 결과 값이 '없음'을 명백하게 표현할 필요가 있고, null 을 반환하면 에러가 발생할 가능성이 높은 상황에서 메소드의 반환 타입으로 Optional 을 사용하자는 것이 Optional 을 만든 주된 목적이다.
Optional 타입의 변수의 값은 절대 null 이어서는 안 되며, 항상 Optional 인스턴스를 가리켜야 한다.
Optional의 기능을 이해하는 것도 중요하지만, 해당 기능의 의도를 올바르게 파악하고 사용해야한다.
Optional을 올바르게 사용하는 방법
1. Optional 변수에 절대로 null을 할당하지 마라.
나쁜 예시
Optional<Member> findById(Long id) {
// find Member from datebase
if (result == 0) {
return null;
}
}
좋은 예시
Optional<Member> findById(Long id) {
// find Member from database
if (result == 0) {
return Optional.empty();
}
}
null로 반환하는 것이 위험하기 때문에 Optional이 등장하였다. 따라서 null을 반환하는 것은 Optional의 도입 의도와 맞지 않다.
Optional은 내부 값을 null로 초기화한 싱글톤 객체를 Optional.empty() 메서드를 통해 제공한다.
따라서 결과가 없다는 표현을 null 대신 Optional.empty()로 표현하자.
2. Optional.get() 호출 전에 Optioanl 객체가 값을 가지고 있음을 확실하게 해라.
Optional을 사용하면 그 안의 값은 Optional.get() 메소드를 통해 접근 할 수 있는데,
만약 빈 Optional 객체에 get() 메소드를 호출한 경우 NoSuchElementException 이 발생하기 때문에 값을 가져오기 전에 반드시 값이 있는지 확인해야 한다.
나쁜 예시
Optional<Member> optionalMember = findById(1);
String name = optionalMember.get().getName();
지양해야 하는 예시
Optional<Member> optionalMember = findById(1);
if (optionalMember.isPresent()) {
return optionalMember.get();
} else {
throw new NoSuchElementException();
}
좋은 예시
Member member = findById(1).orElseThrow(MemberNotFoundException::new);
String name = member.getName();
지양해야 하는 예의 경우엔 반드시 나쁘다고만은 할 수 없지만,
이후에 소개할 Optional의 API를 활용하면 동일한 로직을 더 간단하게 처리할 수 있다.
Optional을 이해하고 있다면 가독성 면에서도 더 낫기 때문에 꼭 필요한 경우가 아니라면 피하는 것이 좋다.
3. 값이 없는 경우, Optional.orElse()를 통해 이미 생성된 기본 값을 반환해라
좋은 예시
Member EMPTY_MEMBER = new Member();
...
Member member = findById(1).orElse(EMPTY_MEMBER);
주의할 점은 orElse 메소드의 인자는 Optional 객체가 존재할 때도 평가된다는 점이다.
주의 점
Member member = findById(1).orElse(new Member());
orElse(...)에서... 는 Optional 에 값이 있든 없든 무조건 실행된다.
method1(method2()) 이 실행되면 method2()는 method1() 보다 먼저 그리고 언제나 실행된다.
따라서 orElse(new...) 에서도 new ... 가 무조건 실행되는 것이 당연하다.
값이 없으면 orElse() 의 인자로서 실행된 값이 반환되므로 실행한 의미가 있지만,
Optional에 값이 있으면 orElse() 의 인자로서 실행된 값이 무시되고 버려진다.
따라서 orElse(...)는 ... 가 새 객체 생성이나 새로운 연산을 유발하지 않고 이미 생성되었거나 계산된 값일 때만 사용해야 한다.
매번 새로운 객체를 생성해야 한다면 4번 방법을 참고하자.
4. 값이 없는 경우, Optional.orElseGet()를 통해 이를 나타내는 객체를 제공해라
지양해야 하는 예시
Member member = findById(1).orElse(new Member()); // 값이 있던 없던 new Member()는 무조건 실행됨
좋은 예시
Member member = findById(1).orElseGet(Member::new);
orElseGet(Supplier)에서 Supplier는 Optional에 값이 없을 때만 실행된다. 따라서 Optional에 값이 없을 때만 새 객체를 생성하거나 새 연산을 수행하므로 불필요한 오버헤드가 없다.
5. 값이 없는 경우, Optional.orElseThrow()를 통해 명시적으로 예외를 던져라
값이 없는 경우, 기본 값을 반환하는 대신 예외를 던져야 하는 경우도 있다.
이 경우에는 Optional.orElseThrow()를 사용하자.
Member member = findById(1).orElseThrow(() -> new NoSuchElementException("Member Not Found"));
자바 10부터는 orElseThrow()의 인수 없이도 사용할 수 있다.
6. 값이 있는 경우에 이를 사용하고 없는 경우에 아무 동작도 하지 않는다면, Optional.ifPresent()를 활용하라
지양해야 하는 예시
Optional<Member> optionalMember = findById(1);
if(optionalMember.isPresent()) {
System.out.println("member : " +optionalMember.get());
}
좋은 예시
Optional<Member> optionalMember = findById(1);
optionalMember.ifPresent(System.out::println);
Optional.ifPresent()는Optional 객체 안에 값이 있는 경우 실행할 람다를 인자로 받는다.
값이 있는 경우에 실행되고 값이 없는 경우에는 실행되지 않는 로직에 ifPresent()를 활용할 수 있다.
7. isPresent() - get()은 orElse() 나 orElseXX 등으로 대체해라
Optional 객체로부터 값의 유무를 확인한 뒤 사용하는 패턴은 앞에서 소개한 다양한 API들로 대체할 수 있다.
지양해야 하는 예시
Optional<Member> optionalMember = findById(1);
if(optionalMember.isPresent()) {
System.out.println("member : " +optionalMember.get());
} else {
throw new MemberNotFoundException("Member Not Found id : " + 1);
}
좋은 예시
Member member = findById(1)
.orElseThrow(() -> new MemberNotFoundException("Member not found id : " + 1));
System.out.println("member : " + member.get());
8. Optional을 필드의 타입으로 사용하지 마라
나쁜 예시
public class Member {
private Optional<String> name;
}
좋은 예시
public class Member {
private String name;
}
Optional 은 반환 타입을 위해 설계된 타입이다.
Optional을 클래스의 필드로 선언하거나 (생성자와 세터를 포함한) 메소드의 인자로 사용하는 것은 Optional의 도입 의도에 반하는 패턴이다.
9. Optional을 생성자나 메서드 인자로 사용하지 마라
Optional을 생성자나 메소드 인자로 사용하면, 호출할 때마다 Optional을 생성해서 인자로 전달해줘야 한다.
굳이 비싼 Optional을 인자로 사용하지 말고 호출되는 쪽에 null체크 책임을 남겨두는 것이 좋다.
나쁜 예시
void increaseSalary(Optional<Member> member, int salary) {
member.ifPresent(member -> member.increaseSalary(salary));
}
//call the method
increaseSalary(Optional.ofNullable(member), 10);
좋은 예시
void increaseSalary(Member member, int salary) {
if(member != null) {
member.increaseSalary(salary);
}
}
//call the method
increaseSalary(member, 10);
10. 단지 값을 얻으려는 목적이라면 Optional 대신 null 비교를 해라
Optional 은 비싸기 때문에 과도하게 사용하지 말아야 한다.
단순히 값 또는 null을 얻을 목적이라면 Optional 대신 null 비교를 사용하자
나쁜 예시
return Optional.ofNullable(member).orElse(UNKNOWN);
좋은 예시
return member != null ? member : UNKNOWN;
11. Optional을 빈 컬렉션이나 배열을 반환하는 데 사용하지 마라
컬렉션이나 배열로 복수의 결과를 반환하는 메소드가 "결과 없음"을 가장 명확하게 나타내는 방법은 대부분의 경우 빈(empty) 컬렉션 또는 배열을 반환하는 방법이다.
이러한 상황에 빈 컬렉션이나 배열 대신 Optional을 사용해서 얻는 이점이 있는지 고민해 본다면Optional을 컬렉션이나 배열에 사용하는 것이 옳은지에 대한 답을 찾을 수 있다.
나쁜 예시
List<Member> members = team.getMember();
return Optional.ofNullable(members);
좋은 예시
List<Member> members = team.getMembers();
return members != null ? members : Collections.emptyList();
마찬가지 이유로 Spring Data JPA Repository 메소드 선언 시 다음과 같이 컬렉션을 Optional로 감싸서 반환하는 것은 좋지 않다.
컬렉션을 반환하는 Spring Data JPA Repository 메소드는 null을 반환하지 않고 비어있는 컬렉션을 반환해 주므로Optional로 감싸서 반환할 필요가 없다.
나쁜 예시
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<List<Member>> findAllByNameContaining(String keyword);
}
좋은 예시
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findAllByNameContaining(String keyword);
}
12. Optional을 컬렉션의 원소로 사용하지 마라
컬렉션에 Optional을 원소로 사용하지 말고 원소를 꺼낼 때나 사용할 때 null 체크하는 것이 좋다.
특히 Map 은 getOrDefault() , putIfAbsent() , computeIfAbsent() , computeIfPresent()처럼null 체크가 포함된 메소드를 제공하므로, Map의 원소로 Optional을 사용하지 말고 Map 이 제공하는 메소드를 활용하는 것이 좋다.
나쁜 예시
Map<String, Optional<String>> sports = new HashMap<>();
sports.put("100", Optional.of("BasketBall"));
sports.put("101", Optional.ofNullable(someOtherSports));
String basketBall = sports.get("100").orElse("BasketBall");
String unknown = sports.get("101").orElse("");
좋은 예시
Map<String, String> sports = new HashMap<>();
sports.put("100", "BasketBall");
sports.put("101", null);
String basketBall = sports.getOrDefault("100", "BasketBall");
String unknown = sports.computeIfAbsent("101", k -> "");
13. Optional.of()와 Optional.ofNullable을 혼동하지 마라
of(X)는 X 가 null 이 아님이 확실할 때만 사용해야 하며, X 가 null 이면 NullPointerException이 발생한다.
ofNullable(X) 은 X가 null 일 가능성이 있을 때 사용해야 하며, X 가 null 이 아님이 확실하면 of(X)를 사용해야 한다.
나쁜 예시
return Optional.of(member.getName()); // member의 name이 null 이면 NPE 발생
return Optional.ofNullable(MEMBER_STATUS);
좋은 예시
return Optional.ofNullable(member.getName());
return Optional.of(MEMBER_STATUS);
14. 원시 타입의 Optional 에는 OptionalInt, OptionalLong, OptionalDouble의 사용을 고려해라
원시 타입(primitive type)을 Optional로 사용하면 Boxing과 UnBoxing을 거치면서 오버헤드가 생기게 된다.
반드시 Optional의 제네릭 타입에 맞춰야 하는 경우가 아니라면 int, long, double타입에는 OptionalXXX타입 사용을 고려하는 것이 좋다. 이들은 내부 값을 래퍼 클래스가 아닌 원시 타입으로 갖고, 값의 존재 여부를 나타내는 isPresent 필드를 함께 갖는 구현체들이다.
나쁜 예시
Optional<Integer> cnt = Optional.of(10); // boxing 발생
for(int i = 0; i < cnt.get(); i++) { ... } // unboxing 발생
좋은 예시
OptionalInt cnt = OptionalInt.of(10); // boxing 발생 안 함
for(int i = 0; i < cnt.getAsInt(); i++) { ... } // unboxing 발생 안 함
15. 내부 값 비교는 Optional.equals 사용을 고려하자
기본적인 참조 확인과 타입 확인 이후에 두 Optional의 동치성은 내부 값의 equals 구현이 결정한다.
즉, Optional 객체 maybeA와maybeB의 두 내부 객체 a와b에 대해 a.equals(b)가 true 이면 maybeA.equals(maybeB) 도 true이며 그 역도 성립한다. 굳이 내부 값의 비교만을 위해 값을 꺼낼 필요는 없다는 의미이다.
나쁜 예시
boolean compareMemberById(long id1, long id2) {
Optional<Member> maybeMemberA = findById(id1);
Optional<Member> maybeMemberB = findById(id2);
if(!maybeMemberA.isPresent() && !maybeMemberB.isPresent()) { return false; }
if (maybeMemberA.isPresent() && maybeMemberB.isPresent()) {
return maybeMemberA.get().equals(maybeMemberB.get());
}
return false;
}
좋은 예시
boolean compareMemberById(long id1, long id2) {
Optional<Member> maybeMemberA = findById(id1);
Optional<Member> maybeMemberB = findById(id2);
if(!maybeMemberA.isPresent() && !maybeMemberB.isPresent()) { return false; }
return findById(id1).equals(findById(id2));
}
16. 값에 대해 미리 정의된 규칙이 있는 경우에는 filter사용을 고려하자
Optional.filter 도 스트림처럼 값을 필터링하는 역할을 한다.
인자로 전달된 predicate이 참인 경우에는 기존의 내부 값을 유지한 Optional 이 반환되고,
그렇지 않은 경우 비어 있는 Optional을 반환한다.
username에 대한 몇 가지 제약 사항을 검증하는 기능을 아래 메소드를 활용하여 다음과 같이 구현해 볼 수 있다.
기존 예시
boolean isValidName(String username) {
return isIncludeSpace(username)
&& isOverLength(username)
&& isDuplicate(username);
}
Optional을 활용한 방식
boolean isValidName(String username) {
return Optional.ofNullable(username)
.filter(this::isIncludeSpace)
.filter(this::isOverLength)
.filter(this::isDuplicate)
.isPresent();
}
여기에는 어느 방법이 맞다고 단정하기 어렵기 때문에 상황에 따라 최선이라고 생각되는 방법을 찾는 게 중요하다
정리
- Optionl에 null 할당 금지
- Optional.get() 호출 전에 값을 가지고 있음을 확실하게 하라
- 값이 없을 땐 orElse() , orElseGet() , orElseThrow() 처리
- 값이 없는 경우 아무 동작도 하지 않는다면 ifPresent() 활용
- isPresent() - get() 은 orElseXXX 등으로 대체
- . 필드의 타입 및 생성자나 메소드 인자로 Optional 사용 금지
- 단지 값을 얻는 목적이면 Optional 대신 null 비교
- Optional 대신 빈 컬렉션 반환
- Optional을 컬렉션의 원소로 사용 금지
- of()와ofNullable() 혼동 금지
- 원시 타입의 Optional 은 OptionalInt , OptionalLong , OptionalDouble 사용
- 내부 값 비교는 Optional.equals 사용을 고려
- 제약 사항이 있는 경우 filter 사용 고려
'JAVA' 카테고리의 다른 글
[JAVA] 불변 객체를 사용해야 하는 이유 (2) | 2023.05.17 |
---|---|
다양한 가비지 컬렉션(Garbage Collection) 알고리즘 (0) | 2023.05.15 |
[JAVA] Private 메서드 테스트 방법 및 지양하는 이유 (0) | 2023.05.15 |
[JAVA] 가비지 컬렉션(Garbage Collection)의 개념 및 동작 원리 (0) | 2023.05.14 |
[JAVA] JVM 동작원리 및 기본 개념 (1) | 2023.05.14 |