Optional

Java 8에 새로 생긴 인터페이스로 라이브러리 메서드가 반환할 결과값이 없음을 명백하게 표현할 필요가 있는 곳에서 제한적으로 사용할 수 있는 메커니즘을 제공하기 위해 새로 생겨났다.

Java api doc의 API 노트를 보면 다음과 같이 설명하고 있다. Optional은 주로 결과 없음을 나타낼 필요성이 명확하고 null을 사용하면 오류가 발생할 수 있는 메소드 반환 유형으로 사용하도록 고안되었다.

한마디로 비어있을 수도 있고, 어떠한 값 하나만 담고 있을수도 있는 인스턴스의 타입


등장 배경

1. 참조형 멤버변수 와 NPE

  1. 런타임에 NPE(NullPointerException)라는 예외를 발생시킬 수 있다.
  2. NPE 방어를 위해서 들어간 null 체크 로직 때문에 코드 가독성과 유지 보수성이 떨어진다.
/* OnlineClass.java */
public Progress progress;
    public Progress getProgress() {
        return progress;
}

    public void setProgress(Progress progress) {
  	    this.progress = progress;
    }
}

/* OptionalTestApp.java */
OnlineClass spring_boot = new OnlineClass(1, "spring boot", true); // 이슈상황 → 참조형 멤버 변수 사용 시 초기화 되지 않아 null 값을 참조 할 수 있다.

Duration studyDuration = spring_boot.getProgress().getStudyDuration();  // NullPointExecption 발생
System.out.println(studyDuration);

모든 객체타입은 null을 갖을 수 있기 때문에 해당 값이 null인지 아닌지는 run time에 정확히 알지 못할 수가 있어 NPE를 발생시킬 여지가 있다.


Optional 등장 이전 해결방법

  1. 사전에 null 체크

    public Progress getProgress() {
        if (progress == null) {
    		throw new IllegalStateException();
        }
        return progress;
    }
    

    이는 에러에 대한 스택트레이스를 뽑게 되고 이또한 리소스 낭비가 될 수 있다.

  2. 클라이언트 코드에서 null 체크

    Progress progress = spring_boot.getProgress();
         if (progress != null) {
            System.out.println(progress.getStudyDuration());
         }
     }
    

    사용하는 시점에 null을 체크하는 방법도 존재하는데 이러면 get 메서드마다 null을 체크하는 코드를 작성해줘야 하고, 비즈니스로직보다 null 체크 코드가 더 길어져 가독성이 떨어질 수 있다.

이러한 문제들을 스칼라나 하스켈과 같은 함수형 언어들은 존재할지 안 할지 모르는 값을 표현할 수 있는 타입을 가지고 있고 자바도 처음 만들어졌을떄 존재하지 않는 값을 표현하기 위해 null을 만들었다면 이번에는 위 언어들의 컨셉을 모티브로 Optional API를 만들고 null 처리를 Optional에게 위임하는 방법으로 해결하고자 추가되었다.


장점

  1. NPE를 유발할 수 있는 null을 직접 다루지 않아도 된다.
  2. 수고롭게 null 체크를 직접 하지 않아도 된다.
  3. 명시적으로 해당 변수가 null일 수도 있다는 가능성을 표현할 수 있다. (따라서 불필요한 방어 로직을 줄일 수 있다!)


주의사항 (sub. Optional 올바르게 사용하는 법)

1. Optional 변수에 null 대신 Optional.empty() 사용

Optional<Car> optionalCar = null;

Optional<Car> optionalCar = Optional.empty();

Optional도 결국 객체이기 때문에 null이 들어갈 수 있는데 이는 또다른 NPE를 유발하는 코드이기 때문에 emtpy()메서드로 비어있는 값을 정의하자.


2. Optional 값을 꺼내쓰기 전에 값이 있는지 확인하기

Optional<Car> optionalCar = Optional.empty();

Car car = optionalCar.get(); //NoSuchElementException 발생

Optional은 값이 비어있을 수도 있기 때문에 사용하기전에 존재한다는 것을 증명해야 하는데 일반적으로는 isPresent()후에 get()을 사용할 수 있지만 코드도 길어지고 한번에 사용할 수 있는 API를 제공하기 때문에 이를 사용하는 것이 바람직하다.

//isPresent - get
if(optionalCar.isPresent()){
    return optionalCar.get();
}else {
    return null;
}

//orElse
return optionalCar.orElse(null);

//orElseGet
return optionalCar.orElseGet(() -> null);

이때 orElseGet()은 Supplier를 인자로 받으며, 값이 없을때에 해당 supplier가 수행된다. 하지만 orElse()는 Optional로 감싸고 있는 객체타입을 인자로 받으며 값이 있더라도 내부가 수행되고 사용되지 않는 경우 해당 객체를 지우게 되어 필요없는 오버헤드가 발생한다.

//source code
Optional<String> optStr = Optional.of("Hi");
optStr.orElse(new String("hi1"));
optStr.orElseGet(() -> new String("hi1"));

//bytecode
 L1
    LINENUMBER 9 L1
    ALOAD 1
    NEW java/lang/String
    DUP
    LDC "hi"
    INVOKESPECIAL java/lang/String.<init> (Ljava/lang/String;)V
    INVOKEVIRTUAL java/util/Optional.orElse (Ljava/lang/Object;)Ljava/lang/Object;
    POP
L2
    LINENUMBER 11 L2
    ALOAD 1
    INVOKEDYNAMIC get()Ljava/util/function/Supplier; [
      // handle kind 0x6 : INVOKESTATIC
      java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
      // arguments:
      ()Ljava/lang/Object;,
      // handle kind 0x6 : INVOKESTATIC
      study/OptionalEx.lambda$main$0()Ljava/lang/String;,
      ()Ljava/lang/String;
    ]
    INVOKEVIRTUAL java/util/Optional.orElseGet (Ljava/util/function/Supplier;)Ljava/lang/Object;
    POP

    ...
private static synthetic lambda$main$0()Ljava/lang/String;
 L0
    LINENUMBER 11 L0
    NEW java/lang/String
    DUP
    LDC "hi"
    INVOKESPECIAL java/lang/String.<init> (Ljava/lang/String;)V
    ARETURN
    MAXSTACK = 3
    MAXLOCALS = 0
}

예를 들어 위와 같은 코드를 작성했을때 optStr은 null이 아니라 new String(“hi1”)가 실행되지 않을 것 같지만 바이트코드를 보면 새로 문자열을 생성했다가 POP하는 것을 볼 수 있고, orElseGet()은 우리가 lambda에서 봤던것처럼 static 메서드로 생성하여 호출되는 타이밍에 이를 실행해 객체를 생성하는 것을 볼수있어 orElse는 필요없는 오버헤드에 주의해야한다. 하지만 반드시 이미 생성되어있는 객체를 반환하는 것이라면 orElse()를 사용하는 것이 좋을 수도 있다.


//before
if(optionalCar.isPresent()){
    return optionalCar.get();
}else {
    throw new NoSuchElementException();
}

//after
optionalCar.orElseThrow();

예외처리를 하는 경우에도 isPresent()를 통한 예외처리보다는 orElseThrow()를 이용하여 예외처리를 하는 것이 바람직하다. 인자로 Supplier를 통해 특정 Exception을 던질 수 있는데 아무것도 주지 않으면 기본적으로 NoSuchElementException을 던진다.


3. Optional이 있을때만 이를 소비하여 무언가를 할때는 isPresent()가 아닌 ifPresent()를 활용

//before
if(optionalCar.isPresent()){
    System.out.println(optionalCar.get());
}

//after
optionalCar.ifPresent(System.out::println);

이도 orElse()와 같은 메서드들과 같은 이유로 별도로 API를 제공하기 때문에 필요없는 조건절을 추가하지 말고 API를 활용해보자.


4. 컬렉션은 Optional대신 비어있는 컬렉션을 사용하자.

//before
List<Car> cars = carFactory.getCars();
return Optional.ofNullable(cars);

//after
List<Car> cars = carFactory.getCars();
return cars != null ? cars : Collections.emptyList();

컬렉션들은 자체적으로 비어있는 값을 표현할 수 있는 자료구조이다. 때문에 Optional을 사용할 필요가 없다.


5. 컬렉션은 Optional로 감싸지말자.

컬렉션 자체가 데이터를 감싼 형태의 객체이고 이도 충분한 API를 제공하기 때문에 한번더 Optional로 감싸면 필요없는 오버헤드가 발생한다. JPA의 메서드를 생성할때도 JPA자체적으로 비어있는 컬렉션을 반환해주므로 Optional로 감쌀 필요가 없다.

//bad
Optional<List<Car>> findAllByCompanyName(String name);

//good
List<Car> findAllByCompanyName(String name);


6. 컬렉션,Map의 원소로 Optional을 사용하지 말자.

컬렉션이나 Map에 들어가는 값들은 애초에 null이 아님을 전제조건으로 만들어진 자료구조이기 때문에 Optional을 사용할 필요가 없다.


7. 단일 값을 얻기 위한 목적으로 메서드 체이닝을 하지말자.

//bad
String status = "on";
return Optional.ofNullable(status).orElse("PENDING");

//good
status == null ? "PENDING" : status;

Optional을 사용하는 것은 결국 또하나의 래퍼객체를 사용하는 것이므로 단순한 로직이라면 그냥 코딩하자. 구관이 명관이라했다.


8. Optional을 필드로 사용하지말자.

public class Car {
    private Optional<Navigation> navigation = Optional.empty(); //bad
}

Optional은 애초에 필드로 사용할 목적으로 만들어지지 않아 Serializable도 구현하지 않았기 때문에 사용을 지양해야 한다.


9. Optional을 메서드,생성자 인자로 사용하지말자.

//bad
public void render(Optional<Renderer> renderer){
    renderer.orElseThrow(() -> new IllegalArgumentException("null 일 수 없습니다."));
    //...
}

//good
public void render(Renderer renderer){
    if(renderer == null){
        throw new IllegalArgumentException("null 일 수 없습니다.");
    }
    //...
}

이러한 방법은 불필요하게 코드를 복잡하게 할 뿐아니라 이를 호출하는 쪽에서도 Optional 생성을 강제하게 하는 것이다. 또한, Optional은 하나의 객체로 이를 호출하는 것이 결코 비용이 저렴하지 않다.


10. null이 확실하면 ofNullable()이 아닌 of()를 사용하자.

//bad
Optional.ofNullable("NULL일 수 없지!");

//good
Optional.of("NULL일 수 없지!");
//ofNullable
public static <T> Optional<T> ofNullable(T value) {
        return value == null ? empty() : of(value);
}

ofNullable은 내부적으로 보면 삼항연산자를 통해 비어있지 않는 경우 of를 호출하는 것을 볼 수있다. 그렇기 때문에 이러한 연산을 조금이라도 줄일 수 있기 때문에 of를 사용하자.


11. Optional의 타입이 Primitive타입이면 OptionalInt,OptionalLong, OptionalDouble을 사용하자

//bad
Optional<Integer> max = Optional.of(10);
for(int i=0; i < max.get(); i++)

//good
OptionalInt max = OptionalInt.of(10);
for(int i=0; i < max.getAsInt(); i++)

//byte code
L0
    LINENUMBER 10 L0
    BIPUSH 10
    INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
    INVOKESTATIC java/util/Optional.of (Ljava/lang/Object;)Ljava/util/Optional;
    ASTORE 0
L1
    LINENUMBER 12 L1
    BIPUSH 10
    INVOKESTATIC java/util/OptionalInt.of (I)Ljava/util/OptionalInt;
    ASTORE 1

내부적으로 Integer.valueOf()를 통해 한번 boxing이 일어나는 것을 볼 수 있고 또 이를 사용할때 unboxing이 일어나기 때문에 OptionalInt를 사용하는 것이 좋다.


12. Optional을 리턴하는 메서드에서 null을 리턴하지 말자.

public static Optional<String> hi(){
        return null;
}

당연한거지만 Optional도 객체이기 때문에 null을 리턴이 가능한데 이렇게 리턴하게 되면 Optional을 사용하는 것이 의미가 없기 때문에 null을 리턴하지 말자.



Optional API

1. Optional 생성

  1. 선언하기

    제네릭을 제공하기 때문에, 변수를 선언할 때 명기한 타입 파라미터에 따라서 감쌀 수 있는 객체의 타입이 결정된다.

    Optional<Order> maybeOrder; // Order 타입의 객체를 감쌀 수 있는 Optional 타입의 변수
    Optional<Member> optMember; // Member 타입의 객체를 감쌀 수 있는 Optional 타입의 변수
    Optional<Address> address; // Address 타입의 객체를 감쌀 수 있는 Optional 타입의 변수
    

    변수명은 그냥 클래스 이름을 사용하기도 하지만 maybeopt와 같은 접두어를 붙여서 Optional 타입의 변수라는 것을 좀 더 명확히 나타내기도 한다.

  2. 객체 생성하기

    • Optional.empty() : null을 담고 있는, 한 마디로 비어있는 Optional 객체를 얻어온다.

      Optional<Member> maybeMember = Optional.empty();
      

      이 비어있는 객체는 Optional 내부적으로 미리 생성해놓은 싱글턴 인스턴스이다.

    • Optional.of() : null이 아닌 객체를 담고 있는 Optional 객체를 생성

      Optional<Member> maybeMember = Optional.of(aMember);
      

      null이 넘어올 경우, NPE를 던지기 때문에 주의해서 사용하자!

    • Optional.ofNullable() : null인지 아닌지 확신할 수 없는 객체를 담고 있는 Optional 객체를 생성

      Optional<Member> maybeMember = Optional.ofNullable(aMember);
      Optional<Member> maybeNotMember = Optional.ofNullable(null);
      

      Optional.empty()Optional.ofNullable(value)를 합쳐놓은 메소드로 이해하면 편하다. null이 넘어올 경우, NPE를 던지지 않고 Optional.empty()와 동일하게 비어 있는 Optional 객체를 얻어온다.

      해당 객체가 null인지 아닌지 자신이 없는 상황에서는 이 메소드를 사용하자.


2. Optional 값 여부 확인

  1. isPresent() : 값이 있으면 true, 없으면 false
  2. isEmpty() : Java11부터 제공하며 값이 없으면 true, 값이 있으면 false


3. Optional 값 가져오기

아래 메소드들은 모두 Optional이 담고 있는 객체가 존재할 경우 동일하게 해당 값을 반환하지만, Optional이 비어있는 경우(null을 담고 있는 경우), 다르게 작동한다.

  1. get() : null일 경우 NoSuchElementException 발생히고 가급적 사용을 하지 않는 것을 권장한다.

  2. orElse(T other) : null일 경우 넘어온 인자(T) 를 반환한다.

  3. orElseGet(Supplier<? extends X>) : null일 경우 넘어온 함수형 인자를 통해 생성된 객체를 반환한다.

    orElse(T other)의 게으른 버전으로 비어있는 경우에만 함수가 호출되기 때문에 orElse(T other) 대비 성능상 이점을 기대할 수 있다.

  4. orElseThrow(Supplier<? extends X> exceptionSupplier) : null일 경우 넘어온 함수형 인자를 통해 생성된 예외(를 던진다. (default : NoSuchElementException)

  5. ifPresent(Consumer) : 값이 있는 경우 값을 가지고 Consumer 함수 동작한다.

    // 1. get()
    OnlineClass onlineClass = optional.get(); // NoSuchElementException 발생
    
    // 2. isPresent() + get() = 먼저 확인후 꺼낸다  번거롭다
    if (optional.isPresent()) {
    	OnlineClass onlineClass = optional.get();
    	System.out.println(onlineClass.getTitle());
    }
    
    // 3. ifPresent(Consumer) = 값이 있는 경우만 함수가 동작한다!
    optional.ifPresent(oc -> System.out.println(oc.getTitle()));
    


4. Optional 필터/변환

  1. Optional map(Function)

     // AS-WAS
     //주문을 한 회원이 살고 있는 도시를 반환하는 메소드다. (기본값은 Seoul 이다.)
     public String getCityOfMemberFromOrder(Order order) {
         if (order == null) {
             return "Seoul";
         }
         Member member = order.getMember();
         if (member == null) {
             return "Seoul";
         }
         Address address = member.getAddress();
         if (address == null) {
             return "Seoul";
         }
         String city = address.getCity();
         if (city == null) {
             return "Seoul";
         }
         return city;
     }
    
     // TO-BE
     public String getCityOfMemberFromOrder(Order order) {
         return Optional.ofNullable(order)
                 .map(Order::getMember)
                 .map(Member::getAddress)
                 .map(Address::getCity)
                 .orElse("Seoul");
     }
    

    기존의 방식으로 null check를 하면서 코드를 작성하면 사방에서 return 해줘야 하여 가독성이 떨어지고, 유지보수가 좋지 않은 코드가 있다.

    하지만 Optional과 map을 이용하면 전통적인 NPE 방어 패턴에 비해 훨씬 간결하고 명확해진다!

    우선 기존에 존재하던 조건문들이 모두 사라지고 Optional의 수려한(fluent) API에 의해서 단순한 메소드 체이닝으로 모두 대체된다.

    • 메소드 체이닝 설명
      1. ofNullable() 정적 팩토리 메소드를 호출하여 Order 객체를 Optional로 감싸고 혹시 Order 객체가 null인 경우를 대비하여 of() 대신에 ofNullable()을 사용하는 것이다.
      2. 3번의 map() 메소드의 연쇄 호출을 통해서 Optional 객체를 3번 변환한다. 매 번 다른 메소드 레퍼런스를 인자로 넘겨서 Optional에 담긴 객체의 타입을 바꿔준다.
      3. 마무리 작업으로 orElse() 메소드를 호출하여 이 전 과정을 통해 얻은 Optional이 비어있을 경우, 디폴트로 사용할 도시 이름을 세팅해주면 된다.
  2. Optional filter(Predicate)

    if (obj != null && obj.do() ...)
    

    Java8 이 전에 NPE 방지를 위해서 위와 같이 null 체크로 시작하는 if 조건문 패턴을 많이 사용해왔고 이러한 패턴을 이용해서 주어진 시간(분) 내에 생성된 주문을 한 경우에만 해당 회원 정보를 구하는 메소드를 작성해보면 아래와 같다.

    public Member getMemberIfOrderWithin(Order order, int min) {
        if (order != null && order.getDate().getTime() > System.currentTimeMillis() - min * 1000) {
            return order.getMember();
        }
    }
    

    위 코드의 문제점은 두가지가 존재하게 된다. 첫번째로 if 조건문 내에 null 체크와 비지니스 로직이 혼재되어 있어서 가독성이 떨어진다는 점이다. 두번째로는 null을 리턴할 수 있기 때문에 메소드 호출부에 NPE 위험을 전파하고 있다는 것이다.


    이런 문제점 해결하고자 filter를 적용하면 아래와 같이 코드를 작성 할 수 있다.

    public Optional<Member> getMemberIfOrderWithin(Order order, int min) {
        return Optional.ofNullable(order)
                .filter(o -> o.getDate().getTime() > System.currentTimeMillis() - min * 1000)
                .map(Order::getMember);
    }
    

    Optional과 filter를 이용하면 if 조건문 없이 메소드 연쇄 호출만으로도 좀 더 읽기 편한 코드를 작성할 수 있을 뿐만 아니라, 메소드의 리턴 타입을 Optional로 사용함으로써 호출자에게 해당 메소드가 null을 담고 있는 Optional을 반환할 수도 있다는 것을 명시적으로 알려준다.

  3. Optional flatMap(Function) : Optional 안의 인스턴스가 Optional인 경우 사용하면 편리하며 Stream에서 사용하는 경우와 비슷하게 Optional을 한번 분리해서 쪼개주는 걸 뜻한다.





Reference

백기선 인프런 강의 : 더 자바, Java8

https://dzone.com/articles/using-optional-correctly-is-not-optional