Java

Optional 소개 (더 자바, Java 8 강의)

깊게 생각하고 최선을 다하자 2022. 9. 12. 16:09

1) Optional이란?

- Optional은 자바 8에 새로 추가된 인터페이스입니다. 

  Optional은 비어 있을 수도 있고, 값 하나만을 담고 있을 수도 있는 컨테이너 인스턴스의 타입입니다. 

  예시 코드를 통해 살펴보겠습니다.

public class OnlineClass{

   private Integer id;
   
   private String title;
   
   private boolean closed;
   
   public Progress progress;
   
   public OnlineClass(Integer id, String title, booelean closed){
     this.id = id;
     this.title = title;
     this.closed = closed;
   }
   
   public Progress getProgress(){
       return progress;
   }
}
import java.time.duration;

public class Progress{
     private Duration studyDuration;
     private boolean finished;   
     
     public Duration getStudyDuration(){
        return studyDuration;
     }
}
public class App{

    public static void main(String[] args){
        OnlineClass spring_boot = new OnlineClass(1, "spring boot", true);
        Duration studyDuration = spring_boot.getProgress().getStudyDuration();
        System.out.println(studyDuration);
    }
}

// 실행 결과
NullPointerException

- OnlineClass 객체인 spring_boot의 Progress 객체가 null이기 때문에 NullPointerException이 발생합니다. 

  이를 방지하기 위해서 보통 이런 식으로 코딩을 해왔습니다. 

public class App{

    public static void main(String[] args){
        OnlineClass spring_boot = new OnlineClass(1, "spring boot", true);
        Progress progress = spring_boot.getProgress();
        if(progress != null){
           System.out.println(progress.getStudyDuration());
        }
    }
}

- 이런 식으로 코딩을 할 때의 문제점은 에러를 만들기 좋다는 점입니다. 

  왜냐하면 널체크를 깜박할 수 있기 때문입니다. 

  또한 Null을 리턴하는 것 자체도 문제입니다. 

 

- Java 8 이전에는 Null인 경우에 에러를 던지는 방법이 있었습니다. 

  에러를 던지는 옵션의 문제는

  (1) RuntimeException을 던지는 것은 문제가 되지 않지만,

       CheckedException을 던지면 에러 처리가 강제되고 

  (2) 에러가 발생되면 자바는 StackTrace를 찍는데, 이 자체로 리소스를 쓰게 됩니다. 

- 예외는 진짜로 필요한 경우에만 쓰는 것이 좋고, 로직을 처리할 때 쓰는 것은 좋지 않습니다. 

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

- 그냥 Null을 리턴할 수도 있는데, 클라이언트 코드가 알아서 널체크를 하는 방법이 있습니다. 

public class OnlineClass{
   
   public Progress getProgress(){
       return progress;
   }
}
public class App{

    public static void main(String[] args){
        OnlineClass spring_boot = new OnlineClass(1, "spring boot", true);
        Progress progress = spring_boot.getProgress();
        if(progress != null){
           System.out.println(progress.getStudyDuration());
        }
    }
}

- 자바 8부터는 이것을 좀 더 명시적으로 표현할 수 있는 방법이 생겼습니다.   

  자바 8부터는 Null이 전달될 수 있는 경우에 Optional로 감싸서 리턴할 수 있습니다. 

  리턴 타입에만 쓰는 것으로 생각하면 좋습니다. 

  Optional은 여러 군데(ex) 파라미터)에 쓸 수 있지만,

  리턴 타입으로 쓰는 것만이 권장 사항입니다.  

public class OnlineClass{
   
   public Optional<Progress> getProgress(){
       return Optional.ofNullable(progress);
   }
}

- Optional을 사용하면 일종의 박스를 만들어서 객체를 담아 놓습니다. 

  이 객체는 Null일 수도 있고, 값을 가질 수도 있습니다.

  ofNullable 메소드는 객체가 Null일 수도 있는 경우에 쓰이고,

  of 메소드는 객체가 Null이 아닌 경우에 사용합니다. 

 

- Optional은 메소드 매개변수 타입으로 쓸 수 있지만, 

  리턴 타입으로 쓰는 것만이 권장됩니다.

  메소드 매개변수 타입으로 쓰면 다음과 같이 코드를 작성할 수 있습니다. 

public class OnlineClass{
   
   public void setProgress(Optional<Progress> progress){
       progress.ifPresent((p) -> {this.progress = p;});
   }
}

  - 위와 같이 코드를 작성하면 위험합니다. 

    왜냐하면 메소드를 호출할 때, Null을 호출할 수 있기 때문입니다. 

    메소드의 매개변수로 Null이 전달되면 NullPointerException이 발생할 수 있습니다. 

public class App{

    public static void main(String[] args){
        OnlineClass spring_boot = new OnlineClass(1, "spring boot", true);
        spring_boot.setProgress(null);
    }
}

- 따라서 오히려 널체크를 한 번 더 해야 하는 상황이 발생합니다. 

public class OnlineClass{
   
   public void setProgress(Optional<Progress> progress){
        if(progress.isPresent()){
             progress.ifPresent(p -> this.progress = p);
        }
   }
}

- 또한, Map의 Key 타입을 Optional로 쓰는 것도 안 좋은 사용법입니다. 

  그것은 Map 인터페이스의 특징을 깨뜨리는 것입니다.

  Map 인터페이스의 가장 큰 특징 중 하나가 Key가 Null을 가질 수 없다는 것입니다. 

 

- 클래스의 필드 타입을 선언할 때, Optional을 쓰는 것도 좋지 않습니다. 

  이것은 도메인 클래스의 설계와 관련된 문제입니다. 

  차라리 상하위 클래스를 쪼개거나, Delegation을 사용하는 것이 좋습니다. 

public class OnlineClass{

   private Integer id;
   
   private String title;
   
   private boolean closed;
   
   public Optional<Progress> progress;
   
   public OnlineClass(Integer id, String title, booelean closed){
     this.id = id;
     this.title = title;
     this.closed = closed;
   }
   
   public Progress getProgress(){
       return progress;
   }
}

 - 프리미티브 타입용 Optional은 따로 있습니다. (ex) OptionalInt, OptionaLong 등)

   Optional에 프리미티브 타입을 쓸 수 있지만, 박싱, 언박싱이 발생할 수 있으므로 권장되지 않습니다. 

   박싱, 언박싱이 발생하면 성능에 좋지 않습니다. 

// 박싱 & 언박싱 발생
Optional.of(10);  // x

OptionalInt.of(10); // o

- Optional을 쓰는 메소드에서 Null을 리턴하지 않는 것도 필요합니다.

  왜냐면 이는 Optional을 쓰는 의미를 퇴색시키기 때문입니다. 

public class OnlineClass{
   
   public Optional<Progress> getProgress(){
        return null; 
   }
}

- Null을 리턴하고 싶으면, Optional.empty()를 리턴하시면 됩니다. 

public class OnlineClass{
   
   public Optional<Progress> getProgress(){
        return Optional.empty();
   }
}

- Collection, Map, Stream, Array, Optional은 Optional로 또 다시 감싸지 않는 것이 좋습니다. 

  왜냐하면 Optional을 사용하지 않고도 비어 있다는 것을 표현할 수 있는 타입들이기 때문입니다. 

 

 

참고

백기선 더 자바, Java 8