Java

[Java] Optional 알고 사용하기

kittity 2024. 7. 25. 23:42

예전에 작성해둔 코드를 수정하다 보니, 갑자기 Optional이 눈에 띄었다.

JPA로 데이터를 조회할 때, 이전과 달라진 정책으로 이전에는 유일하게 한 건의 데이터만 존재해 식별하던 값이 이제는 여러 값이 존재할 수 있는 상황이 되어 Optional을 사용하는 부분이 수정이 필요해졌다.

 

문득 Optional의 올바른 사용법이 궁금해졌다. 과연, Optional은 어떤 의도로 만들어진 걸까? 어떻게 사용해야 올바르게 사용한 것일까?

 

이전의 나는 Optional을 Null값에 대한 존재 여부를 판단하는 것의 편리함을 생각하며 단순하게 사용해왔다.

 

Optional을 만든 사람의 사용 의도는 무엇일까? 주의해야 할 점은 없을까?

Optional은 Null 에 대한 여부를 판단하는 로직에 사용할 수록 좋은 걸까? Side Effect 는 없을까?

값이 여러 건 조회 된다면 어떤 결과가 나올까?

데이터를 조회할 때 객체, Optional, List 어떤걸 사용해야 좋을까?

Optional을 효율적으로 사용하는 방법은 어떤 걸까?

 

조금 더 생각해보고 하나씩 정리해보고자 한다.


목차
  1. Optional의 개념과 사용 의도
    1. Optional이란?
    2. Optional의 사용 의도
  2. Optional 활용하기
    1. Optional 객체 초기화
    2. Optional의 대표적인 메서드
  3. Optional을 사용한 데이터 조회
    1. Optional을 사용한 데이터 조회시 이점
    2. Optional을 사용해 데이터를 조회했지만 여러 건 이라면?
  4. 현명한 Optional 사용을 위하여
    1. Optional 사용 가이드

1. Optional의 개념과 사용 의도

Optional이란?

Java 8부터 도입된 Optional<T> 클래스는 Null을 안전하게 사용할 수 있도록, Null이 올 수 있는 참조 객체를 Wrapping하는 Wrapper 클래스이다.

 

Optional의 사용 의도

공식문서에는 Optional에 대해 다음과 같이 설명되어 있다.

A container object which may or may not contain a non-null value. If a value is present, isPresent() returns true and get() returns the value. Additional methods that depend on the presence or absence of a contained value are provided, such as orElse() (returns a default value if no value is present) and ifPresent() (performs an action if a value is present).

This is a value-based class; use of identity-sensitive operations (including reference equality (==), identity hash code, or synchronization) on instances of Optional may have unpredictable results and should be avoided.

null이 아닌 값을 포함하거나 포함하지 않을 수 있는 컨테이너 개체입니다. 값이 있으면 isPresent() true를 반환하고 get()을 반환합니다.

또는 **Else()(값이 없는 경우 기본값 반환), ifPresent()(값이 있는 경우 액션 수행)**와 같이 포함된 값의 유무에 따라 달라지는 추가적인 방법이 제공됩니다.

이 클래스는 값 기반 클래스입니다. Optional 인스턴스에서 ID에 민감한 작업(기준 동일성(==), ID 해시 코드 또는 동기화 포함)을 사용하면 예측할 수 없는 결과가 발생할 수 있으므로 피해야 합니다.

 

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.

Optional은 주로 "결과 없음"을 나타낼 필요가 분명히 있고 null을 사용하면 오류가 발생할 가능성이 있는 메서드 반환 유형으로 사용하기 위한 것입니다. 유형이 Optional인 변수는 절대 null이 아니며 항상 Optional 인스턴스를 가리켜야 합니다.

 

 

stackoverflow에 java 아키텍트인 Brian Goetz 는 Optional 사용에 대해 다음과 같은 글을 남겼다.

물론 사람들은 그들이 원하는 대로 할 것입니다. 하지만 우리는 이 기능을 추가할 때 분명한 의도가 있었고, 그것은 많은 사람들이 원하는 만큼 일반적인 목적의 Maybe 유형이 아니었습니다

우리의 의도는 "결과 없음"을 표현할 명확한 방법이 필요한 라이브러리 메서드 반환 유형에 제한된 메커니즘을 제공하는 것이었고, 그러한 유형에 null을 사용하는 것은 오류를 유발할 가능성이 압도적으로 높았습니다.

예를 들어, 배열의 결과나 리스트의 결과로 반환하는 것에 사용해서는 안 되며, 대신 빈 배열이나 리스트를 반환해야 합니다. 어떤 것의 필드나 메서드 매개 변수로 사용해서는 안 됩니다.


일상적으로 getter의 반환 값으로 사용하는 것은 분명히 과용(over-use)
이라고 생각합니다.

Optional은 피해야 한다는 것은 잘못된 것이 아니며, 많은 사람들이 원하는 것이 아닙니다. 따라서 우리는 과도한 사용의 위험에 대해 상당히 우려했습니다.

(절대 null이 아님을 증명할 수 없는 한 Optional.get을 호출하지 마십시오. 대신 orElse 또는 ifPresent와 같은 안전한 방법 중 하나를 사용하십시오.)

 

 

위의 내용을 통해 Optional에 대해 정리해보자. 이 부분이 전체 글의 핵심이다.

  • Optional이 만들어진 의도는 “결과 없음”을 나타낼 필요가 있고, null을 사용할 경우 오류가 발생할 가능성이 있는 메서드의 반환 유형으로 사용하기 위한 것이다.
  • 절대 null이 아님을 증명할 수 없는 한 get()을 사용하지 말 것. Optional은 isPresent() 가 true일 때 값이 있는 것으로 get()으로 반환할 수 있다. Else() , ifPresent() 등의 추가적인 방법도 제공된다.
  • 일상적으로 getter의 반환 값으로 사용하는 것은 과용이다. 예를 들어 다음과 같은 경우가 있다.
    • 배열, 리스트의 결과로 반환하지 말아야 하며, 대신 빈 배열이나 리스트를 반환해야 한다.
    • 필드나 메서드의 매개변수로 사용하면 안된다.

 

2. Optional 활용하기

Optional 객체 초기화

Optional 객체를 초기화할 때는 다음과 같은 방법을 사용한다.

  • Optional.empty() : null 값으로 초기화
  • Optional.of() : null이 아닌경우
  • Optional.ofNullable() : null 또는 객체가 있을 수 있는 경우
  • 예시
    // null값으로 초기화
    Optional<String> emptyObj = Optional.empty();
    
    // null이 아닌 경우
    Optional<String> object1 = Optional.of("test");
    
    // null 또는 객체가 있을 수 있는 경우
    Optional<String> object2 = Optional.ofNullable(null);
    ​

 

Optional의 대표적인 메서드

Optional의 대표적인 메서드는 다음과 같다.

  • filter() : 값이 존재하고 값이 지정한 값과 일치하면 반환하고, 그렇지 않으면 빈 값을 반환.
  • map() : 입력값을 다른 값으로 변환하는 기능.
  • isPresent() : 객체가 존재하는지 확인. 객체가 존재하면 true.
  • isEmpty() : isPresent 와 반대로 객체가 존재하지 않는지 확인. 객체가 존재하지 않으면 true.
  • ifPresent() : 객체가 존재한다면 그 객체를 입력값으로 주는 기능.
  • ifPresentOrElse() : 객체가 존재할 경우 해당 람다식 결과를 반환하고, 존재하지 않을 경우 파라미터의 람다식 결과값을 반환.
  • get() : 최종적인 객체를 Optional에서 꺼내는 기능. 객체가 존재하지 않다면 NPE가 발생.
  • orElse() : 최종적으로 객체가 비어있으면 기본값으로 제공할 객체를 설정하는 기능.
  • orElseGet() : 저장된 값이 존재하면 그 값을 반환하고, 값이 존재하지 않으면 파라미터의 람다식 결과값을 반환.
  • orElseThrow() : 저장된 값이 존재하면 그 값을 반환하고, 값이 존재하지 않으면 파라미터의 예외를 발생.

 

3. Optional을 사용한 데이터 조회

Optional을 사용한 데이터 조회시 이점

데이터를 단 건 조회할 경우, 데이터 존재 여부에 따라 Optional의 메서드를 이용해 Null 처리를 쉽게 할 수 있다.

단, 데이터가 여러개 존재할 경우에는 List를 사용하거나 예외처리가 되어야한다.

 

Optional을 사용해 데이터를 조회했지만 여러 건 이라면?

만약 Optional을 이용해 데이터를 조회했는데 여러건이 조회된다면 다음과 같이 NonUniqueResultException이 발생한다.

query did not return a unique result: 17; nested exception is javax.persistence.NonUniqueResultException:

 

4. 현명한 Optional 사용을 위하여

Optional 사용 가이드

앞서 ‘Optional의 사용 의도’에 대해 정리한 내용이 핵심이다. 추가로 Optional을 올바르게 사용하는 26가지에 대해 정리된 글이 있어 그 중 몇 가지만 정리해 보고자 한다.

  1. Optional을 초기화하고 싶다면, null이 아니라 Optional.empty()를 사용해야 한다. Optional은 단지 컨테이너나 박스일 뿐이고, null로 초기화시키는 것은 무의미하다.
    • Prefer
      public Optional<Cart> fetchCart() {
          Optional<Cart> emptyCart = Optional.empty();
          ...
      }      
      ​
  2. optional.get()을 호출하기 전에 optional에 값이 있는지 확인해야 한다.
    • Prefer
      if (cart.isPresent()) {
          Cart myCart = cart.get();
          ... 
      } else {
          ... 
      }
      ​
  3. 값이 없는 경우 Optional.orElse() 메서드를 통해 이미 구성된 기본 값을 반환한다.
    • Optional.orElse() 메서드는 isPresent()-get() 메서드 쌍의 대안이다.
    • 그러나, 주의할 점은 orElse() 메서드의 인자는 Optional 객체가 존재할 경우에도 무조건 실행되어 비용이 생긴다는 점이다.
    • 이러한 이유로 새 객체 생성이나 새로운 연산을 유발하지 않고 이미 구성된 경우에만 userElse()를 사용하고, 다른 상황에서는 Optional.orElseGet() 을 사용하는게 좋다.
    • Avoid
      public static final String USER_STATUS = "UNKNOWN";
      ...
      public String findUserStatus(long id) {
      
          Optional<String> status = ... ; // prone to return an empty Optional
      
          if (status.isPresent()) {
              return status.get();
          } else {
              return USER_STATUS;
          }
      }
      ​
       
    • Prefer
      public static final String USER_STATUS = "UNKNOWN";
      ...
      public String findUserStatus(long id) {
          Optional<String> status = ... ; 
          return status.orElse(USER_STATUS);
      }
      
  4. 값이 없는 경우, Optional.orElseGet() 메서드를 통해 기본 값을 반환한다.
    • Optional.orElseGet() 메서드는 isPresent()-get() 메서드 쌍의 대안이다.
    • orElse()는 값이 존재할 때도 무조건 실행되었으나, orElseGet()은 값이 존재하지 않을 경우에만 실행된다.
    • Avoid
      public String computeStatus() {
          ... // some code used to compute status
      }
      
      public String findUserStatus(long id) {
          Optional<String> status = ... ; // prone to return an empty Optional
          // computeStatus() is called even if "status" is not empty
          return status.orElse(computeStatus()); 
      }
      
      public String computeStatus() {
          ... // some code used to compute status
      }
      
      public String findUserStatus(long id) {
          Optional<String> status = ... ; // prone to return an empty Optional
          // computeStatus() is called even if "status" is not empty
          return status.orElse(computeStatus()); 
      }
      ​
    • Prefer
      public String computeStatus() {
          ... // some code used to compute status
      }
      
      public String findUserStatus(long id) {
          Optional<String> status = ... ; // prone to return an empty Optional
          // computeStatus() is called only if "status" is empty
          return status.orElseGet(this::computeStatus);
      }
      ​
  5. orElse나 orElseXXX은 람다에서 isPresent()-get() 쌍을 완벽하게 대체한다.
    • Avoid
      List<Product> products = ... ;
      Optional<Product> product = products.stream()
          .filter(p -> p.getPrice() < price)
          .findFirst();
      
      if (product.isPresent()) {
          return product.get().getName();
      } else {
          return "NOT FOUND";
      }
      
      Optional<Cart> cart = ... ;
      Product product = ... ;
      ...
      if(!cart.isPresent() || !cart.get().getItems().contains(product)) {
          throw new NoSuchElementException();
      }  
      ​
    • Prefer
      Optional<Cart> cart = ... ;
      Product product = ... ;
      ...
      cart.filter(c -> c.getItems().contains(product)).orElseThrow();
      
      Optional<Cart> cart = ... ;
      Product product = ... ;
      ...
      cart.filter(c -> c.getItems().contains(product)).orElseThrow();
      ​
       
  6. 값이 없는 경우, Optional.orElseThrow() 를 통해 명시적으로 예외를 던질 것.
    • Avoid
      public String findUserStatus(long id) {
          Optional<String> status = ... ; // prone to return an empty Optional
          if (status.isPresent()) {
              return status.get();
          } else {
              throw new NoSuchElementException();        
          }
      }
      ​
    • Prefer
      public String findUserStatus(long id) {
          Optional<String> status = ... ; // prone to return an empty Optional
          return status.orElseThrow(IllegalStateException::new);
      }
      
      public String findUserStatus(long id) {
          Optional<String> status = ... ; // prone to return an empty Optional
          return status.orElseThrow(IllegalStateException::new);
      }
      ​
       
  7. 값이 있는 경우에 이를 사용하고 없는 경우에 아무 동작도 하지 않는다면, Optional.ifPresent()를 활용할 것.
    • Avoid
      Optional<String> status = ... ;
      ...
      if (status.isPresent()) {
          System.out.println("Status: " + status.get());
      }
      ​
    • Prefer
      Optional<String> status ... ;
      ...
      status.ifPresent(System.out::println); 
      ​
  8. Optional을 필드의 타입 또는 생성자의 매개변수로 사용하지 말 것.
  9. Optional을 빈 컬렉션이나 배열을 반환하는 데 사용하지 말 것
    • Avoid
      public Optional<List<String>> fetchCartItems(long id) {
          Cart cart = ... ;    
          List<String> items = cart.getItems(); // this may return null
          return Optional.ofNullable(items);
      }
      ​
    • Prefer
      public List<String> fetchCartItems(long id) {
          Cart cart = ... ;    
          List<String> items = cart.getItems(); // this may return null
          return items == null ? Collections.emptyList() : items;
      }
      ​
  10. Optional의 컬렉션을 사용하지 말 것
    • Avoid
      Map<String, Optional<String>> items = new HashMap<>();
      items.put("I1", Optional.ofNullable(...));
      items.put("I2", Optional.ofNullable(...));
      ...
      Optional<String> item = items.get("I1");
      if (item == null) {
          System.out.println("This key cannot be found");
      } else {
          String unwrappedItem = item.orElse("NOT FOUND");
          System.out.println("Key found, Item: " + unwrappedItem);
      }
      ​
    • Prefer
      Map<String, String> items = new HashMap<>();
      items.put("I1", "Shoes");
      items.put("I2", null);
      ...
      // get an item
      String item = get(items, "I1");  // Shoes
      String item = get(items, "I2");  // null
      String item = get(items, "I3");  // NOT FOUND
      
      private static String get(Map<String, String> map, String key) {
        return map.getOrDefault(key, "NOT FOUND");
      }
      ​

참고 자료

Optional (Java SE 9 & JDK 9 )

Should Java 8 getters return optional type?

26 Reasons Why Using Optional Correctly Is Not Optional - DZone

Optional 이론편 [ 자바 (Java) 기초 ]

[Java] 언제 Optional을 사용해야 하는가? 올바른 Optional 사용법 가이드 - (2/2)

 

728x90

'Java' 카테고리의 다른 글

[Java] Java 21 특징  (0) 2025.01.31
[Java] Java 17 특징  (1) 2025.01.30
[Java] Java 11 특징  (0) 2025.01.30
[실전 자바 - 기본편] 다형성2  (0) 2024.09.09
[실전 자바 - 기본편] 다형성1  (0) 2024.09.09