옵셔널(Optional)

Walker·2022년 1월 2일
0

Java

목록 보기
5/5

public class Classes {

    private Integer id;
    private String title;
    private boolean closed;
    
    private Progress progress;

}

public class Progress {

    private Duration studyDuration;
    private boolean finished;
    
    public Progress() {};

    public Progress(boolean finished) {
        this.finished = finished;
    }

}

public class OptionalApp {

    public static void main(String[] args) {
        List<Classes> springClasses = new ArrayList<>();
        springClasses.add(new Classes(1, "spring boot", true));
        springClasses.add(new Classes(2, "spring jpa", true));
        springClasses.add(new Classes(3, "spring mvc", true));
    }
 	   
}

예제를 위해 Classes 클래스에 Progress라는 reference 타입의 필드를 추가하고
구동 App의 경우 생성 Classes 생성자에서 Progress 인스턴스를 주지 않은 상황이다.

System.out.println(springClasses.get(0).getProgress().getStudyDuration());

이런 상황에서 위와 같은 코드를 실행하면 progress가 null이라며
NPE(null point exception)
가 발생한다.

if (springClasses.get(0).getProgress() != null) {
    System.out.println(springClasses.get(0).getProgress().getStudyDuration());
}

자바 8 이전에는 위와 같은 Code(if(a != null))로 NPE를 방지하곤 했으나
번거롭기도 하고 어떤 요소가 어떤 상황에서 NPE가 발생할지 모르니
NULL 체크를 빼먹거나 아니면 불필요하게 체크하는 상황이 발생했다.

이러한 문제를 해결하기 위해 자바 8부터 나온 것이 Optional이다.

Optional은 일종의 Wrapping Class로 원하는 값을 감싸 적어도 null은 되지 않게 막아준다.

// return 타입에만 Optional을 쓰는 것이 권장됨!
public Optional<Progress> getProgress() {
    // null일 수 있는 경우
    return Optional.ofNullable(progress);
}

위의 경우에서 Optional을 사용하여 return type을 Optional로 바꾸고
ofNullable()로 return할 값을 Optional로 감싸주게 되면

getProgress()는 Progress가 아닌 Optional을 return하게 되고
설사 Progress가 null이더라도 빈 Optional을 리턴하기 때문에 NPE는 발생하지 않는다.

Optional<Progress> optionalProgress = springClasses.get(0).getProgress();
// Progress progress = springClasses.get(0).getProgress(); 컴파일 에러 발생

getter에서 Optional을 return type으로 선언해줬기 때문에
Progress로 받으려 하면 컴파일 에러가 발생해 이 객체가 null일 수 있겠다는 것도 미리 알 수 있다.

// 인자는 Supplier 형태
boolean isClassFinished = optionalProgress.orElseGet(OptionalApp::createProgress).isFinished();

private static Progress createProgress(){
    return new Progress(false);
}

물론 이것을 아는 것만으로는 부족하고 받아온 Optional 객체를 활용하는 것이 필요한데
이 중 가장 요긴하게 쓰일 수 있는 API가 orElseGet()으로 만약 null이 아니라라면 가져오고
또는(or) null이라면 원하는 동작을 수행
하게 정할 수 있다.
(유사한 API로 orElse(value)가 있으나 createProgress()가 null 여부에 상관없이 무조건 동작하게 되므로 문제가 발생할 수 있고 비용도 더 큼 > 가능하면 orElseGet() 권장)
참고 : https://mangkyu.tistory.com/70

if(optionalProgress.isPresent()) { 
    System.out.println("optionalProgress is not null");
}

optionalProgress.ifPresent(p -> System.out.println(p.isFinished()));

isPresent()는 말 그대로 해당 Optional이 null이 아니라는 것으로 flag 값에서 쓰기 좋을 듯 하다.
자바 11부터는 isEmpty()도 추가되었으나 !optionalProgress.isPresent()로도 표현 가능하다.

ifPresent()null이 아니라면 해당 요소를 가지고 작업을 진행하는 것으로 return은 불가하다.

여기까지가 주로 알아두면 되는 내용이고
아래는 부가적인 기능이나 Optional을 쓰지 말아야 하는 경우이다.

optionalProgress.orElseThrow(IllegalStateException::new);

orElseThrow()는 정말 어쩔 수 없이 null이면 예외를 던지는 것인데 어떤 때 써야 할지는 모르겠다.

if(progress.isPresent()) {
    System.out.println(progress.get().isFinished());
}

get()Optional안의 요소를 꺼내는 것으로 해당 요소가 null인 경우 NPE가 아니라 NoSuchElementException이 발생하므로 미리 null 여부를 확인하고 써야한다.

Optional<Classes> optionalClass = springClasses.stream()
                                  .filter(c -> c.getTitle().startsWith("spring")).findFirst();

filter()나 map()도 Optional과 어울리게 쓸 수 있는데
위의 경우 spring으로 시작하는 수업명이 없을 수도 있으니 Optional이 필요하다.

Optional<Classes> filterClass = optionalClass.filter(c -> c.isClosed());

Optional<Integer> integerClass = optionalClass.map(Classes::getId);

위와 같이 간단하게 사용도 가능하다.
filter() - 있다는 가정하에 걸러냄 없으면 빈 Optional return
map() - Optional이 아닌 타입을 꺼내는 경우

Optional<Optional<Progress>> progress = optionalClass.map(Classes::getProgress);
Optional<Progress> progress1 = progress.orElseThrow(IllegalStateException::new);
Progress progress2 = progress1.orElseThrow(IllegalStateException::new);

Optional<Progress> flatMapProgress = optionalClass.flatMap(Classes::getProgress);

만약 Progress 자체가 Optional이라 Optional을 이중으로 꺼내야한다면
flatMap()
을 활용하여 한번에 꺼낼 수 있다.

Optional.of(10);
OptionalInt.of(10);

Primitive Type을 Optional로 감쌀 때는 해당하는 메소드를 사용하는 것이 boxing에 쓰이는 리소스를 아낄 수 있다.

  1. 리턴값으로만 쓰기를 권장한다.
    (메소드 매개변수 타입, 맵의 키 타입, 인스턴스 필드 타입으로 쓰지 말자.)
  2. Optional을 리턴하는 메소드에서 null을 리턴하지 말자.
  3. Collection, Map, Stream, Array, Optional은 Optional로 감싸지 말 것.
    참조 : 더 자바8 강의노트

위와 같이 Optional은 제한적으로 필요한 곳에만 쓰는 것이 좋으며
각각의 이유까지 기억하기 어렵다면 Optional은 리턴값으로만 쓰자고 기억하자!

https://mangkyu.tistory.com/203

위 글에서도 Optional은 처음부터 제한적 목적으로 만들어졌으며 잘 알지 못하고
Optional을 남발하는 것은 오히려 자원 낭비, 에러 발생, 가독성 저하 등의 부작용을 만든다고 한다.

소위 말하는 Modern Java(람다식, 스트림 등)을 공부하면서 잘 사용하면 편리하겠다는 생각이 들면서도
어설프게 쓰면 오히려 안 쓰느니만 못하겠다는 우려가 들었다.
Optional의 경우도 좀 더 정확하게 알고 신중하게 적용하도록 주의해야겠다!

profile
I walk slowly, but I never walk backward. -Abraham Lincoln-

0개의 댓글