메소드가 반환할 결과 값이 '없음'
을 명백하게 표현할 필요가 있고, null
을 반환하면 에러가 발생할 가능성이 높은 상황에서 메소드의 반환 타입으로 Optional
을 사용하자는 것이 Optional 을 만든 주된 목적이다.
Optional 타입의 변수의 값은 절대 null 이어서는 안 되며, 항상 Optional 인스턴스를 가리켜야 한다.
Spring Data JPA 사용 시 Repository에서 리턴 타입을 Optional
로 바로 받을 수 있도록 지원하고 있다.
Optional
을 사용하면 반복적인 null 체크를 줄일 수 있기 때문에 잘 사용하면 매우 편리하다.나쁜 예
: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();
}
}
😉 반환 값으로 null을 사용하는 것이 위험하기 때문에 등장한 것이 Optional
이다.
😉 당연히 Optional 대신 null을 반환하는 것은 Optional의 도입 의도와 맞지 않는다.
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 을 이해하고 있다면 가독성 면에서도 더 낫기 때문에 꼭 필요한 경우가 아니라면 피하는 것이 좋다.
public static final String MEMBER_STATUS = "UNKNOWN";
...
Member member = findById(1).orElse(MEMBER_STATUS);
Member EMPTY_MEMBER = new Member();
...
Member member = findById(1).orElse(EMPTY_MEMBER);
⚽ 주의할 점은 orElse
메소드의 인자는 Optional
객체가 존재할 때도 평가된다는 점이다.
Member member = findById(1).orElse(new Member());
아마도 이름 때문이겠지만 orElse(new ...)
를 써보면, new ... 는 Optional
에 값이 없을 때만 실행될 것 같은 착각이 드는데,
orElse(...)
에서 ... 는 Optional 에 값이 있든 없든 무조건 실행된다.
method1(method2())
이 실행되면 method2() 는 method1() 보다 먼저 그리고 언제나 실행된다.
따라서 orElse(new ...)
에서도 new ...
가 무조건 실행되는 것이 당연하다.
⚽ 값이 없으면 orElse()
의 인자로서 실행된 값이 반환되므로 실행한 의미가 있지만,
⚽ Optional 에 값이 있으면 orElse() 의 인자로서 실행된 값이 무시되고 버려진다.
orElse(...)
는 ... 가 새 객체 생성이나 새로운 연산을 유발하지 않고 이미 생성되었거나 계산된 값일 때만 사용해야 한다.피해야 하는 예
: Member member = findById(1).orElse(new Member()); // 값이 있던 없던 new Member()는 무조건 실행됨
좋은 예
:Member member = findById(1).orElseGet(Member::new);
⚽ orElseGet(Supplier)
에서 Supplier
는 Optional
에 값이 없을 때만 실행된다. 따라서 Optional
에 값이 없을 때만 새 객체를 생성하거나 새 연산을 수행하므로 불필요한 오버헤드가 없다.
값이 없는 경우, 기본 값을 반환하는 대신 예외를 던져야 하는 경우도 있다. 이 경우에는 Optional.orElseThrow()
를 사용하자.
Member member = findById(1).orElseThrow(() -> new NoSuchElementException("Member Not Found"));
피해야 하는 예
: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() 를 활용 할 수 있다.
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());
나쁜 예
:public class Member {
private Optional<String> name;
}
좋은 예
:public class Member {
private String name;
}
개요에서 다뤘 듯 Optional
은 반환 타입을 위해 설계된 타입이다.
Optional 을 클래스의 필드로 선언하거나 (생성자와 세터를 포함한) 메소드의 인자로 사용 하는 것은 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);
⚽ Optional 은 비싸기 때문에 과도하게 사용하지 말아야 한다.
단순히 값 또는 null 을 얻을 목적이라면 Optional
대신 null
비교를 사용하자
나쁜 예
:
return Optional.ofNullable(member).orElse(UNKNOWN);
좋은 예
:return member != null ? member : UNKNOWN;
⚽ 컬렉션이나 배열로 복수의 결과를 반환하는 메소드가 "결과 없음"
을 가장 명확하게 나타내는 방법은 대부분의 경우 빈(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);
}
⚽ 컬렉션에 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 -> "");
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);
⚽ 원시 타입(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 객체 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));
}
Optional.filter
도 스트림처럼 값을 필터링 하는 역할을 한다.
인자로 전달된 predicate이 참인 경우에는 기존의 내부 값을 유지한 Optional 이 반환되고,
그렇지 않은 경우 비어 있는 Optional 을 반환한다.
username에 대한 몇 가지 제약 사항을 검증하는 기능을 아래 메소드를 활용하여 다음과 같이 구현해 볼 수 있다.
boolean isIncludeSpace(String str) { /* ... */ } // check if string includes white space
boolean isOverLength(String str) { /* ... */ } // check if length of string is over limit
boolean isDuplicate(String str) { /* ... */ } // check if string is duplicates with already registered
boolean isValidName(String username) {
return isIncludeSpace(username)
&& isOverLength(username)
&& isDuplicate(username);
}
boolean isValidName(String username) {
return Optional.ofNullable(username)
.filter(this::isIncludeSpace)
.filter(this::isOverLength)
.filter(this::isDuplicate)
.isPresent();
}
여기에는 어느 방법이 맞다고 단정하기 어렵기 때문에 (가독성 등을 고려하여) 상황에 따라 최선이라고 생각되는 방법을 찾는게 중요할 것이다.
Optional 에 null 할당 금지
Optional.get() 호출 전에 값을 가지고 있음을 확실히
값이 없을 땐 orElse() , orElseGet() , orElseThrow() 처리
값이 없는 경우 아무 동작도 하지 않는다면 ifPresent() 활용
isPresent() - get() 은 orElseXXX 등으로 대체
필드의 타입 및 생성자나 메소드 인자로 Optional 사용 금지
단지 값을 얻는 목적이면 Optional 대신 null 비교
Optional 대신 빈 컬렉션 반환
Optional 을 컬렉션의 원소로 사용 금지
of() 와 ofNullable() 혼동 금지
원시 타입의 Optional 은 OptionalInt , OptionalLong , OptionalDouble 사용
내부 값 비교는 Optional.equals 사용을 고려
제약 사항이 있는 경우 filter 사용 고려
// AS-IS
@Override
public Sample getSample(final Long id) {
// 값이 없으면 NoSuchElementException
return sampleRepository.findById(id).get();
}
.get()
의 경우 결과없이 null일 경우 NoSuchElementException
발생
😁 orElseThrow()
를 통해 값이 없을 경우 예외를 던져주거나
orElse
, orElseGet
를 통해 값이 없을 경우 값을 지정할 수 있다.
//Optional 결과 값이 없을 때 처리
// throw Exception
return sampleRepository.findById(id)
.orElseThrow(IllegalArgumentException::new);
return sampleRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("no such data");
// null Return
return sampleRepository.findById(id)
.orElse(null);
// 비어있는 객체로 Return
return sampleRepository.findById(id)
.orElseGet(Sample::new);
Repository
에서 Optional
을 반환하는 경우 원하는 값이 있으면 원하는 객체로 받고 없으면 Exception처리
를 하는 패턴을 사용
Optional
은 NPE 방어를 위한 코드를 쉽게 사용하기 위해 사용
// AS-IS
@Override
@Transactional
public Sample updateSample(Long sampleId) {
Optional<Sample> sample = sampleRepository.findById(sampleId);
if(!sample.isPresent()) {
throw new IllegalArgumentException();
}
// 위의 코드를 orElseThrow 사용을 통해 한줄로 줄일 수 있음
...
return sample.get();
}
🎈 이걸 바꾼다면 !!
// TO-BE
@Override
@Transactional
public Sample updateSample(Long sampleId) {
Sample sample = sampleRepository.findById(sampleId)
.orElseThrow(IllegalArgumentException::new); .
...
return sample;
}
// AS-IS
Optional<User> user = userRepository.findById(userId);
if(user.isPresent()) {
vo.setUsername(user.get().getUsername());
vo.setUserNm(user.get().getName());
}
// TO-BE
Optional<UserVO> user = userRepository.findById(userId);
user.ifPresent(u -> {
vo.setUsername(u.getUsername());
vo.setUserNm(u.getName());
});