Language/Java

Optional

JUNGKEUNG 2022. 7. 2. 17:28

Optional 이란?

오직 값 한 개가 들어있을 수도 없을 수도 있는 컨테이너

 

 

NullPointerException

개발하면서 가장 많이 괴롭히는 예외 중 하나가 NPE(NullPointerException) 이다.

이를 피하기 위해서는 어떤 방법이 있었을까?

Test t = null;
System.out.println("t = " + t.v); // NPE 발생

//예외 처리 로직
if( t == null) {
	System.out.println("Test is NULL");
} else {
	System.out.println(" t = " + t.v);
}

NPE를 해결하기 위하여 위와 같이 if...else 문으로 null을 검사하는 예외 처리 로직을 추가해야한다.

하지만 이 방법으로는 null값을 검사해야하는 변수들이 많아지고 로직이 복잡해지면, 코드가 지저분해지고 가독성은 떨어진다. 이를 해결 하기위하여 나온 것이 Optional 이다.

 

메소드에서 작업 중 특별한 상황에서 값을 제대로 리턴할 수 없는 경우 선택할 수 있는 방법 

  • 예외를 던진다. 
    • 에러가 발생하게되면 자바는 스택 트레이스를 찍는데, ( 이 에러가 발생하기 전까지의 어떠한 콜 스택을 거쳐서 에러가 발생하게 되었는지에 대한 정보) 이 자체로 리소스를 사용하는거여서 부담이 가기 때문에 필요할 때에만 사용해야지 로직을 처리할때 사용하는건 비용부담이 된다. 
  • null 를 리턴한다. 
    • 비용문제는 없지만 그 코드를 사용하는 클라이언트 코드가 주의해야한다.
  • Optional 을 리턴한다. 
    • 클라이언트에 코드에게 명시적으로 빈 값일 수도 있도있다는 걸 알려주고, 빈 값인 경우에 대한 처리를 강제한다.

 

Optional 주요 메서드 

Optional.empty(); // 비어있는 Optioanl 반환
Optional.of(T); // 파라미터로 전달된 값을 Optional로 감싸서 반환, null 불가
Optional.ofNullbale(T); // 파라미터로 전달된 값을 Optional로 감싸서 반환, null 허용
  • of() 메서드에서 null값을 전달하면 NPE를 던진다.
  • null값도 허용하는 Optional을 만드려면 ofNullable()를 사용하자.
  • 추가로 Optional을 반환하는 메서드에서는 절대 null을 반환하지말자
    Optional을 도입한 취지를 완전히 무시하는 행위이다.

 

Optional<T>.orElse("기본 값"); // Optional이 비어있는 경우 기본 값을 설정할 수 있다.
Optional<T>.orElseGet("기본 값"); // Optional이 비어있는 경우 기본 값을 설정할 수 있다.
Optional<T>.orElseThrow(Exception::new); // Optional이 비어있는 경우 Exceptoin을 던진다.
  • orElse() 사용 시 Optional에 값이 있든 없든 무조건 실행된다.
    따라서 orElse(new ...) 처럼 새로운 객체를 생성하는 경우에는 orElseGet()을 사용하자.
    • orElseGet()은 Supplier를 사용해 Optional에 값이 비어있을때만 새로 생성한다.

 

Optional.ifPresent(...); // Optional이 비어 있지 않으면 해당 값으로 로직 수행
Optional.isPresent(...); // Optional이 비어 있지 않으면 true 반환
  • isPresent()는 예외처리할때 유용하게 썼다.
    예로 회원가입 하는경우 클라이언트가 전달한 정보로 DB에서 조회했을 때 이미 회원이 존재한다면? isPresent()를 이용해 예외를 던질 수 있다.
  • ifPresent()는 null값을 전달할 경우 NPE를 던질 수 있다고 명시되어 있으니 주의하자

실습

package com.study.java8to11;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public class OptionalStudy {

  public static void main(String[] args) {
    List<OnlineClass> springClasses = new ArrayList<>();
    springClasses.add(new OnlineClass(1, "spring boot", true));
    springClasses.add(new OnlineClass(5, "rest api dev", false));


    Optional<OnlineClass> optional = springClasses.stream()
        .filter(oc -> oc.getTitle().startsWith("spring")) // 있을 수도 있고 없을 수도 있어서 리턴 타입은 Optional 이다.
        .findFirst();

    // [1] Optional 에 값이 있는지 없는지 확인하기
    boolean present = optional.isEmpty();
    boolean empty = optional.isPresent();

    // [2] Optional 에 있는 값 가져오기
    // [2-1] 비어있는 Optional 에서 무언가 꺼낼땐 NoSuchElementeException 발생
    // System.out.println(optional.get());
    // [2-2] 예외처리 적용 , 하지만 이렇게 get 으로 가져오고 isPresent 로 예외처리 하는 방식은 잘 사용되지 않는다.
    if (optional.isPresent()) {
      System.out.println(optional.get());
    }

    // [3] Optional에 값이 있는 경우 그 값을 가지고 ~를 하라.
    // ex) spring 으로 시작하는 수업이 있으면 id 를 출력하라
    optional.ifPresent(oc -> {
      System.out.println(oc.getId());
    });

    // [4] Optional에 값이 있으면 가져오고 없는 경우에 ~를 리턴하라
    // ex) jpa 로 시작하는 수업이 없다면 비어있는 수업을 리턴하라.
    // orElse(T) 함수 구현체가 아닌, 인스턴스를 매개변수로 주어야한다.
    // orElse 는 optional 에 값이 있던, 없던 무조건 실행된다.
    OnlineClass onlineClass = optional.orElse(createNewClasses());
    System.out.println(onlineClass.getTitle());

    // [5] Optional에 값이 있으면 가져오고 없는 경우에 ~를 하라.
    // ex) JPA로 시작하는 수업이 없다면 새로 만들어서 리턴하라.
    // orElseGet(Supplier)  supplier 로 구현해주면 된다. (람다 혹은 메서드 레퍼런스)
    // orElseGet 는 optional 에 값이 없는 경우에만 실행된다.
    // orElse 는 이미 만들어져 있는 인스턴스, 상수 들을 참고해서 사용할땐 orElse 가 적합
    // orElseGet 동적으로 무언가 작업해서 만들어내야 한다면 orElseGet 이 적합
    OnlineClass onlineClass2 = optional.orElseGet(OptionalStudy::createNewClasses);
    System.out.println(onlineClass2.getTitle());

    // 무언가 만들어줄 수 없는상황에는 orElseThrow
    // [6] Optional에 값이 있으면 가져오고 없는 경우 에러를 던져라.
    // default 로는 NoSuchElementException 을 던지지만, 원하는 에러가 있을경우 supplier 로 구현해주면 된다.
    OnlineClass onlineClass3 = optional.orElseThrow(IllegalArgumentException::new);

    // [7] Optional에 들어있는 값 걸러내기
    // 값이 있다는 가정하에 이벤트가 일어나며, 값이 없을경우 아무 일도 일어나지 않는다.
    Optional<OnlineClass> onlineClass4 = optional.filter(OnlineClass::isClosed);
    System.out.println(onlineClass4.isEmpty()); // true

    // [8] Optional에 들어있는 값 변환하기
    // [8-1] Optional map(Function)
    Optional<Integer> integer = optional.map(OnlineClass::getId); // id 타입을 담고있는 Optional 반환

    // [8-2] Optional flatMap(Function): Optional 안에 들어있는 인스턴스가 Optional인 경우 사용하면 편리하다.
    Optional<Progress> progress = optional.flatMap(OnlineClass::getProgress);

    // [8-3] Optional 안에 있는 값이 Optional 인데 map 을 사용하게 될 경우, 아래와 같이 두번 작업을 해야한다.
    Optional<Optional<Progress>> progress1 = optional.map(OnlineClass::getProgress);
    Optional<Progress> progress2 = progress1.orElse(Optional.empty());

    // 무조건 .get 으로 꺼내기 보다는 여태 배운 메소드를 활용하여 적용하자.

  }

  private static OnlineClass createNewClasses() {
    System.out.println("creating new online class");
    return new OnlineClass(10, "new class", false);
  }
}

 

 

주의할 것

  • 리턴값으로만 쓰기를 권장한다.
    • 메소드 매개변수 타입, 맵의 키의 타입, 인스턴스 필드 타입으로 쓰지 말자.
    • 맵의 키 타입에 Optional 을 준다는것은 맵의 특징을 무시하는 것이다. (키는 null 일 수 없다.)
  • Optional 을 리턴하는 메소드에서 null 을 리턴하지 말자. 
    • 리턴 할 것이 없다면 null 말고,  Optional.empty( ) 를 리턴하자.
  • 프리미티브 타입용 Optional 은 따로 있다. (OptionalInt, OptionalLong ...)
    • 프리미티브 타입에 Optional 을 사용할 경우 박싱, 언박싱이 발생한다. OptionalInt 을 사용하자.
  • Collection, Map, Stream Array, Optional 은 Optional 로 감싸지 말 것. 
    • 이미 해당 타입들은 비어있다는 것을 표현할 수 있는 컨테이너 성격을 가진 타입들이다.  

 

 

 

참고


'Language > Java' 카테고리의 다른 글

긴 함수(2)  (0) 2022.08.14
[리팩토링] 긴 함수  (0) 2022.08.06
Java8 Stream  (0) 2022.07.02
인터페이스 기본 메소드와 스태틱 메소드  (0) 2022.06.19
메소드 레퍼런스  (0) 2022.06.18