Optional 관련

Chooooo·2023년 9월 4일
0

TIL

목록 보기
14/22
post-thumbnail

😎 Optional 관련

메소드가 반환할 결과 값이 '없음'을 명백하게 표현할 필요가 있고, null 을 반환하면 에러가 발생할 가능성이 높은 상황에서 메소드의 반환 타입으로 Optional 을 사용하자는 것이 Optional 을 만든 주된 목적이다.

Optional 타입의 변수의 값은 절대 null 이어서는 안 되며, 항상 Optional 인스턴스를 가리켜야 한다.

Spring Data JPA 사용 시 Repository에서 리턴 타입을 Optional로 바로 받을 수 있도록 지원하고 있다.

  • Optional을 사용하면 반복적인 null 체크를 줄일 수 있기 때문에 잘 사용하면 매우 편리하다.

Optional 올바르게 사용하기

1. 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의 도입 의도와 맞지 않는다.

2. Optional.get() 호출 전에 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 을 이해하고 있다면 가독성 면에서도 더 낫기 때문에 꼭 필요한 경우가 아니라면 피하는 것이 좋다.

3. 값이 없는 경우, Optional.orElse()를 통해 이미 생성된 기본 값(객체)을 반환할 것

  • 좋은 예 :
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(...) 는 ... 가 새 객체 생성이나 새로운 연산을 유발하지 않고 이미 생성되었거나 계산된 값일 때만 사용해야 한다.
  • 매번 새로운 객체를 생성해야 한다면 4번 방법을 참고하자.

4. 값이 없는 경우, Optional.orElseGet()을 통해 이를 나타내는 객체를 제공할 것

  • 피해야 하는 예 :
Member member = findById(1).orElse(new Member()); // 값이 있던 없던 new Member()는 무조건 실행됨
  • 좋은 예 :
Member member = findById(1).orElseGet(Member::new);

orElseGet(Supplier) 에서 SupplierOptional 에 값이 없을 때만 실행된다. 따라서 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()나 orElseXXXX 등으로 대체할 것

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 로 감싸서 반환 할 필요가 없다.

😎 spring data JPA에서의 반환

  • 나쁜 예 :
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 체크 하는 것이 좋다.

특히 MapgetOrDefault() , 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 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);
}
  • Optional 을 활용한 방식 :
boolean isValidName(String username) {
    return Optional.ofNullable(username)
        .filter(this::isIncludeSpace)
        .filter(this::isOverLength)
        .filter(this::isDuplicate)
        .isPresent();
}

여기에는 어느 방법이 맞다고 단정하기 어렵기 때문에 (가독성 등을 고려하여) 상황에 따라 최선이라고 생각되는 방법을 찾는게 중요할 것이다.

😎 정리

  1. Optional 에 null 할당 금지

  2. Optional.get() 호출 전에 값을 가지고 있음을 확실히

  3. 값이 없을 땐 orElse() , orElseGet() , orElseThrow() 처리

  4. 값이 없는 경우 아무 동작도 하지 않는다면 ifPresent() 활용

  5. isPresent() - get() 은 orElseXXX 등으로 대체

  6. 필드의 타입 및 생성자나 메소드 인자로 Optional 사용 금지

  7. 단지 값을 얻는 목적이면 Optional 대신 null 비교

  8. Optional 대신 빈 컬렉션 반환

  9. Optional 을 컬렉션의 원소로 사용 금지

  10. of() 와 ofNullable() 혼동 금지

  11. 원시 타입의 Optional 은 OptionalInt , OptionalLong , OptionalDouble 사용

  12. 내부 값 비교는 Optional.equals 사용을 고려

  13. 제약 사항이 있는 경우 filter 사용 고려

😁 get() 사용 관련

// 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);
                           

😁 Optional에서 값 바로 받기

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;
    }

😁 ifPresent() 사용

   // 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());
    });
profile
back-end, 지속 성장 가능한 개발자를 향하여

0개의 댓글