슬기로운 개발생활

[Java] Optional 올바르게 사용하기

by coco3o
반응형

개요

Java 언어 설계자인 Brian Goetz는 Optional 을 만든 의도를 다음과 같이 공식 API 문서에 작성해 두었다.

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 타입을 자주 사용하고 있는데, 어느 부분에 Optional 을사용하는 것이 적절하거나 그렇지 않은지를 명확히 구분하기 어렵다는 생각이 들었고, Optional 을 올바르게 사용하기 위한 26가지의 방법을 제공하는 글이 있어 일부를 번역해서 정리하고자 한다.(원문)
참고1, 참고2


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

Optional 은 내부 값을 null 로 초기화한 싱글톤 객체를 Optional.empty() 메소드를 통해 제공하고 있다.
위에서 말한 "결과 없음"을 표현해야 하는 경우라면 null 대신 Optional.empty() 를 반환하자.


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) 에서 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() 나 orElseXXX 등으로 대체할 것

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 의 동치성은 내부 값의 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. Optionalnull 할당 금지

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. 원시 타입의 OptionalOptionalInt , OptionalLong , OptionalDouble 사용

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

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

블로그의 정보

슬기로운 개발생활

coco3o

활동하기