이번에 Optional을 정리하면서 여러 자료를 찾아보던 중 Optional과 관련된 포스팅 하나가 정말 정리가 잘되었다고 생각하게 되었다.
https://dzone.com/articles/using-optional-correctly-is-not-optional
https://www.latera.kr/blog/2019-07-02-effective-optional/
[Java] Optional 올바르게 사용하기
블로그에 정리...를 빙자한 배껴쓰기를 하면서 내 머리에도 정리가 될 수 있도록 꼼꼼히 읽으면서 정리하려고 한다.
Optional<Member> findById(Long id) {
// find Member from db
if (result == 0) {
return null;
}
}
좋은 예 :
Optional<Member> findById(Long id) {
// find Member from db
if (result == 0) {
return Optional.empty();
}
}
당연히 Optional 대신 null 을 반환하는 것은 Optional 의 도입 의도와 맞지 않는다.
Optional 은 내부 값을 null 로 초기화한 싱글톤 객체를 Optional.empty()
메소드를 통해 제공하고 있다.
위에서 말한 "결과 없음"을 표현해야 하는 경우라면 null 대신 Optional.empty() 를 반환하자.
Optional.get()
호출 전에 Optional 객체가 값을 가지고 있음을 확실히 할 것
Optional.get()
메소드를 통해 해당 값을 접근 할 수 있다.
만약 빈 Optional 객체에 get() 메소드를 호출한 경우 NoSuchElementException 이 발생하기 때문에 값을 가져오기 전에 반드시 값이 있는지 확인해야 한다.
값이 없는 경우, Optional.orElse() , Optional.orElseGet()을 통해 값을 제공한다.
이전 글에서 정리했던 것 처럼 orElse는 Optional에 값이 있던 없던 무조건 실행된다.
만약 값이 있다면 인자로 실행된 값은 무시되고 버려진다.
값이 없는 경우, Optional.orElseThrow()
를 통해 명시적으로 예외를 던질 것
예시
Member member = findById(1).orElseThrow(() -> new NoSuchElementException("Member Not Found"));
isPresent() - get() 은 orElse() 나 orElseXXX 등으로 대체
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());
Optional 을 생성자나 메소드 인자로 사용하지 말 것
Optional 을 생성자나 메소드 인자로 사용하면, 호출할 때마다 Optional 을 생성해서 인자로 전달해줘야 한다.굳이 비싼 Optional 을 인자로 사용하지 말고 호출되는 쪽에 null 체크 책임을 남겨두는 것이 좋다.
void increaseSalary (Member member,int salary){
if (member != null) {
member.increaseSalary(salary);
}
}
단지 값을 얻을 목적이라면 Optional 대신 null 비교
Optional 은 비싸기 때문에 과도하게 사용하지 말아야 한다.단순히 값 또는 null 을 얻을 목적이라면 Optional 대신 null 비교를 사용하자
return Optional.ofNullable(member).orElse(UNKNOWN);
//변경
return member != null ? member : UNKNOWN;
Optional 을 빈 컬렉션이나 배열을 반환하는 데 사용하지 말 것
컬렉션이나 배열로 복수의 결과를 반환하는 메소드가 "결과 없음"을 가장 명확하게 나타내는 방법은 대부분의 경우 빈(empty) 컬렉션 또는 배열을 반환하는 방법이다.
List<Member> members = team.getMember();
return Optional.ofNullable(members);
//변경
List<Member> members = team.getMembers();
return members != null ? members : Collections.emptyList();
JPA에서도 컬렉션을 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);
}
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);
원시 타입의 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 발생 안 함
내부 값 비교에는 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));
}