티스토리 뷰

Back-end/JAVA

Optional 활용

지나짱-* 2022. 12. 11. 20:32

팀원과 대화를 하다가 Optional를 활용했을 때의 장단점에 관해 얘기를 하게 되었다. 그래서 Optional 활용에 대해 알아보았다.

 

1. Optional 변수에 null을 할당하지 않기

나쁜 예:

Optional<Person> findById(Long id) {
    // find person from db
    if (result == 0) {
        return null;
    }
}

좋은 예:

Optional<Person> findById(Long id) {
    // find person from db
    if (result == 0) {
        return Optional.empty();
    }
}

반환 값으로 null을 사용하는 것이 위험하기 때문에 등장한 것이 Optional이다. 당연히 Optional 객체 대신 null을 반환하는 것은 Optional의 도입 의도와 맞지 않는다.

Optional은 내부 값을 null로 초기화한 싱글턴 객체를 Optional.empty() 메서드를 통해 제공하고 있다. 위에서 인용한 답변과 같이 “결과 없음”을 표현해야 하는 경우라면 null을 반환하는 대신 Optional.empt()를 반환하여 값을 처리하면 된다.

 

2. Optional.get() 호출 전에 Optional 객체가 값을 가지고 있는지 확인하기

Optional을 사용한다면 그 안에 들어있는 값은 Optional.get() 메서드를 통해 접근할 수 있다. 만약 빈 Optional 객체에 get() 메서드를 호출한 경우 NoSuchElementException이 발생한다. 때문에 Optional 객체에서 값을 가져오기 전에는 이후에 소개할 API들을 통해 반드시 값이 있는지 확인해야 한다.

나쁜 예:

Optional<Person> maybePerson = findById(4);
String name = maybePerson.get().getName();

피해야 하는 예: (나쁘다고만은 할 수 없지만 이후에 소개할 Optional의 API를 활용하면 동일한 로직을 더 간단하게 처리할 수 있다. Optioanl을 이해하고 있다면 가독성 면에서도 더 낫기 때문에 꼭 필요한 경우가 아니라면 피하는 것이 좋다.)

Optional<Person> maybePerson = findById(4);
if (myabePerson.ifPresent()) {
    return maybeperson.get();
}
return UNKNOWN_PERSON;

좋은 예:

Person person = findById(4).orElseThrow(PersonNotFoundException::new);
String name = person.getName();

 

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

결과가 없는 상황에 대해 null 대신 Optional을 사용하기로 했으니, 이전에는 null을 반환했던 “값이 없는” 상황을 처리할 방법

  • 기본값 반환
  • 예외 던지기

Optional 객체에 값이 있는지는 Optional.isPresent() 메서드를 통해 확인할 수 있다.

Optional.orElse() 메서드는 “기본값 반환”에 해당하는 메서드이다. Optional 객체의 값이 없는 경우에 orElse의 인자로 명시된 값을 대신 반환한다.

좋은 예:

// UNKNOWN_PERSON is pre-defined object for case that no person is found that id matches
Person person = findById(4).orElse(UNKNOWN_PERSON);

주의할 점은 orElse 메서드의 인자는 Optional 객체가 비어있지 않은 경우에도 평가된다.

주의:

findById(4).orElse(new Person());

위의 코드는 findById가 반환한 Optional 객체가 비어있지 않은 경우에도 Person 생성자를 호출한다. 이런 경우에 공통으로 사용할 수 있는 객체를 미리 생성해서 사용하는 것이 좋다

 

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

값이 없는 경우에 매번 새로운 객체를 반환해야 하는 경우에는 Optional.orElseGet()을 사용할 수 있다. orElse가 기본값으로 반환할 을 인자로 받는 것과 달리, orElseGet()은 값이 없는 경우 이를 대신해 반환할 값을 생성하는 람다를 인자로 받는다.

좋은 예:

findById(4).orElseGet(() -> new Person("UNKNOWN")); // construct with named 'UNKNOWN'

Optional의 값이 없는 경우에만 인자로 전달된 코드가 실행된다. 이 방법으로 매번 새로운 객체를 생성하는 경우에는 실제로 값이 없는 경우에만 객체를 생성한다. Map.computeIfXXX와 유사하다고 볼 수 있다.

 

5. 값이 없는 경우, Optional.orElseThrow()를 통해 명시적으로 예외를 던질 것

값이 없는 경우, 기본값을 반환하는 대신 예외를 던져야 하는 경우도 있다. 이 경우에는 Optional.orElseThrow()를 사용할 수 있다.

findById(4).orElseThrow(() -> new NoSuchElementException("Person not found"));

 

6. 값이 있는 경우에 이를 사용하고 없는 경우에 아무 동작도 하지 않는다면, Optional.ifPresent()를 활용

Optional.ifPresent()는 Optional 객체 안에 값이 있는 경우 실행할 람다를 인자로 받는다. 값이 있는 경우에 실행되고 값이 없는 경우에는 실행되지 않는 로직에 ifPresent를 활용할 수 있다.

좋은 예:

findById(4).ifPresent((user) -> System.out.println(user.getName()));

 

7. ifPresent-get은 orElse나 orElseXXX 등으로 대체

Optional 객체로부터 값의 유무를 확인한 뒤 값을 사용하는 패턴은 앞에서 소개한 다양한 API들로 대체할 수 있다.

피해야 하는 예:

Optional<Person> maybePerson = findById(4);
if (maybePerson.isPresent()) {
    Person person = maybePerson.get();
    System.out.println(person.getName());
} else {
    throw new NoPersonFoundException("No person found id maches: " + 4);
}

좋은 예:

Person person = findById(4)
    .orElseThrow(() -> new NoPersonFoundException("No person found id maches: " + 4));
System.out.println(person.getName());

 

8. Optional을 필드의 타입으로 사용하지 말 것

Optional은 반환 타입을 위해 설계된 타입이다. 뿐만 아니라 Serializable도 아니기 때문에 Optional을 (생성자와 세터를 포함한) 메서드의 인자로 사용하거나 클래스의 필드로 선언하는 것은 Optional의 도입 의도에 반하는 패턴이다.

나쁜 예:

class Person {
    Optional<String> address;
}

혹은

void printUserName(Optional<Person> maybePerson) {
    // ...
}

좋은 예:

class Person {
    String address = "";
}

혹은

void printUserName(Person person) {
    // ...
}

 

 

9. Optional을 빈 컬렉션이나 배열을 반환하는 데 사용하지 말 것

컬렉션이나 배열을 통해 복수의 결과를 반환하는 메서드가 “결과 없음”을 가장 명확하게 나타내는 방법은 빈(empty) 컬렉션 또는 배열을 반환하는 방법이다.

나쁜 예:

Optional<List<Person>> findByLastName(String lastName) {
    // ...
}

좋은 예:

List<Person>> findByLastName(String lastName) {
    // ...
}

 

10. Optional의 컬렉션을 사용하지 말 것

컬렉션에 Optional을 사용하는 경우는 Optional을 사용하지 않으면서 더 좋은 방법으로 개선할 수 있는 경우가 많다.

- 간단한 단어 수를 세는 코드

class WordCounter {
  private Map<String, Optional<Integer>> wordCounts = new HashMap<>();

  void addCount(String word) {
    wordCounts.put(word, Optional.of(wordCounts.get(word)
      .orElse(0) + 1));
  }

  Optional<Integer> getCount(String word) {
   return wordCounts.get(word);
  }
}

각 단어(키)에 대한 카운트(값)가 없으면 기본값 0으로 초기화한 뒤 값에 1을 더한 값을 다시 집어넣는 과정으로 각 단어에 대한 카운트를 저장한다.

하지만 Map의 타입 인자를 Optional<Integer> 대신 Integer로 바꿔도 충분히 간단한 코드로 작성할 수 있다.

class WordCounter {
  private Map<String, Optional<Integer>> wordCounts = new HashMap<>();

  void addCount(String word) {
    wordCounts.putIfAbsent(word, 0);
    wordCounts.computeIfPresent(word, (word, cnt) -> cnt + 1);
  }

  int getCount(String word) {
    return Optional.ofNullable(wordCounts.get(word))
      .orElse(0);
  }
}

각 단어의 카운트(값)가 존재하지 않으면 0으로 초기화한 뒤 computeIfPresent를 통해 기존 값에 1씩 더한다는 점을 명확히 알 수 있다.

getCount 메서드도 Optional을 반환하는 대신 Map.get의 반환 값을 Optional로 만든 뒤 값이 비어있는 경우 기본값인 0을 반환하도록 했다.

기존 구현에서는 매번 새로운 값을 감싸는 Optional 객체를 addCount 메서드가 호출될 때마다 생성하게 된다. 이런 코드는 컬렉션이 굉장히 많은 항목을 담는 상황이 돼서야 메모리 문제를 일으키는 원인이 될 수도 있다.

정리하면, Optional을 컬렉션의 타입 인자로 사용하는 경우는, 대부분 Optional을 사용하지 않는 더 좋은 방법이 있는 경우가 많기 때문에, Optional이 정말 필요한지 고민해보고 신중히 사용해야 한다.

 

11. 원시 타입의 Optional에는 OptionalInt, OptionalLong, OptionalDouble 사용을 고려할 것

원시 타입(primitive type)을 Optional로 사용해야 할 때는 박싱과 언박싱을 거치면서 오버헤드가 생기게 된다.

반드시 Optional의 제네릭 타입에 맞춰야 하는 경우가 아니라면, int, long, double 타입에는 OptionalXXX 타입 사용을 고려하는 것이 좋다. 이들은 내부 값을 래퍼 클래스가 아닌 원시 타입으로 갖고, 값의 존재 여부를 나타내는 isPresent 필드를 함께 갖는 구현체들입니다.

때문에 기존의 Optional 타입에 사용할 때와 비교하면 박싱과 언박싱에서 생기는 오버헤드를 줄였다는 점에서 장점이 있습니다.

좋은 예:

OptionalInt maybeInt = OptionalInt.of(2);
OptionalLong maybeLong = OptionalLong.of(3L);
OptionalDouble maybeDouble = OptionalDouble.empty();

 

12. 내부 값의 비교에는 Optional.equals 사용을 고려할 것

@Override
public boolean equals(Object obj) {
  if (this == obj) {
      return true;
  }

  if (!(obj instanceof Optional)) {
      return false;
  }

  Optional<?> other = (Optional<?>) obj;
  return Objects.equals(value, other.value);
}

기본적인 참조 확인과, 타입 확인 이후에 두 Optional의 동치성은 내부 값의 equals 구현이 결정한다. 즉 Optional 객체 maybeA와 maybeB의 두 내부 객체 a와 b에 대해, a.equals(b)가 true이면 maybeA.equals(maybeB)도 true이며 그 역도 성립한다

나쁜 예:

// returns false if both person object is absent
boolean comparePersonById(long id1, long id2) {
  Optional<Person> maybePersonA = findById(id1);
  Optional<Person> maybePersonB = findById(id2);
  if (!maybePersonA.isPresent() && !maybePersonB.isPresent()) { return false; }
  if (maybePersonA.isPresent() && maybePersonB.isPresent()) {
      return maybePersonA.get().equals(maybePersonB.get());
  }
  return false;
}

좋은 예:

// returns false if both person objects are absent
boolean comparePersonById(long id1, long id2) {
  Optional<Person> maybePersonA = findById(id1);
  Optional<Person> maybePersonB = findById(id2);
  if (!maybePersonA.isPresent() && !maybePersonB.isPresent()) { return false; }
  return findById(id1).equals(findById(id2));
}

 

13. 변환에 map과 flatMap 사용을 고려할 것

Optional에도 map과 flatMap 메서드가 있다. 이를 활용하면 스트림처럼 함수형 스타일로 코드를 작성할 수 있다.

map의 경우 스트림의 map과 동일한 형태로 다른 값으로 변환하는 과정이다.

class Address {
    public static Address of(String text) { /* ... */ }
}

Address getUserAddress(long id) {
  findById(id)
      .map(Person::getAddress)
      .map(Address::of)
      .orElseGet(Address::emptyAddress());
}

map 메서드는 매퍼 함수의 반환 값을Optional.ofNullable에 인자로 전달하여 Optional 객체로 만든다.

flatMap은 map과 비슷하지만 인자로 전달되는 매퍼 함수의 반환 타입이 Optional이어야 한다는 점이 다르다. 다음과 같은 경우에 사용할 수 있다.

class Address {
    // 반환 타입이 Optional
    public static Optional<Address> of(String text) { /* ... */ }
}

Address getUserAddress(long id) {
  findById(id)
      .map(Person::getAddress)
      .flatMap(Address::of)
      .orElseGet(Address::emptyAddress());
}

Address.of의 반환 타입이 Optional일 때 map 메서드를 사용하면 만들어지는 타입은 Optional<Optional<Address>>가 된다.

살펴본 것과 같이 매퍼 함수가 Optional 객체를 생성할 책임을 갖는 경우에는 flatMap을, 그렇지 않은 경우에는 map을 활용한다.

 

14. 값에 대해 미리 정의된 규칙(제약사항)이 있는 경우에는 filter 사용을 고려할 것

Optional.filter도 스트림처럼 값을 필터링하는 역할을 한다. 인자로 전달된 predicate이 참인 경우에는 기존의 내부 값을 유지한 Optional이 반환되고, 그렇지 않은 경우에는 비어 있는 Optional을 반환한다.

유저네임에 대한 몇 가지 제약 사항을 검증하는 기능을 아래 메서드를 활용하여 다음과 같이 구현해볼 수 있다.

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

'Back-end > JAVA' 카테고리의 다른 글

Lock  (0) 2023.01.30
Exception의 종류와 발생원인  (0) 2022.12.12
통합 테스트(Integration Test)  (0) 2022.12.04
NullPointerException 처리 방법  (0) 2022.11.21
로그 사용 - log4j.properties  (0) 2022.11.15
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함