optional

Posted by yunki kim on November 22, 2021

  optional은 java 8에서 새로 추가된 인터페이스이다. optional은 비어있거나 하나의 값을 담을 수 있는 컨테이너 인스턴스 타입이다.

  아래와 같은 코드가 있다고 가정해 보자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package me.skullkim.optional;
 
/**
 * <h1>온라인 수업에 대한 정보</h1>
 * stream api 실습을 위한 클래스
 *
 * @author yunki kim
 * @version 1.0
 * @since 2021-11-21
 */
 
public class OnlineClass {
 
    /** 수업 고유 번호 */
    private Integer classId;
 
    /** 수업 제목 */
    private String title;
 
    /** 수업 종료 여부 */
    private boolean closed;
 
    /** 수업을 수강한 기간 */
    private Progress progress;
 
    /**
     * OnelineClass 생성자
     * @param classId 수업 고유 번호
     * @param title 수업 제목
     * @param closed 수업 종료 여부
     */
    public OnlineClass(Integer classId, String title, boolean closed) {
        this.classId = classId;
        this.title = title;
        this.closed = closed;
    }
 
    /**
     * 수업 고유 번호를 반환
     * @return Integer 수업 고유 번호
     */
    public Integer getClassId() {
        return classId;
    }
 
    /**
     * 수업 제목을 반환
     * @return String 수업 제목 반환
     */
    public String getTitle() {
        return title;
    }
 
    /**
     * 수업 종료 여부 반환
     * @return boolean 수업 종료 여부
     */
    public boolean isClosed() {
        return closed;
    }
 
    /**
     * 수업을 수강한 기간을 반환
     * @reutrn Progress 수업을 들은 기간 정보
     */
    public Progress getProgress() {
        return progress;
    }
}
cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package me.skullkim.optional;
 
import java.time.Duration;
 
/**
 * <h1>과목을 수료한 기간, 수료완료 여부 저장</h1>
 *
 * @author yunkiKim
 * @version 1.0
 * @since 2021-11-22
 */
 
public class Progress {
 
    /**수강 기간*/
    private Duration studyDuration;
 
    /**수강 완료 여부*/
    private boolean finished;
 
    /**
     * 수강 기간을 가져온다
     * @return Duration 수강 기간
     */
    public Duration getStudyDuration() {
        return studyDuration;
    }
 
    /**
     * 수강 기간을 set한다
     * @param studyDuration 수강 기간
     */
    public void setStudyDuration(Duration studyDuration) {
        this.studyDuration = studyDuration;
    }
}
 
cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class App {
 
    /**
     * optional 실습이 진행되는 함수
     * @param args Unused
     */
    public static void main(String[] args) {
        List<OnlineClass> springClasses = new ArrayList<>();
        springClasses.add(new OnlineClass(1"spring boot"true));
        springClasses.add(new OnlineClass(2"sprint data jpa"true));
        springClasses.add(new OnlineClass(3"spring mvc"false));
        springClasses.add(new OnlineClass(4"spring core"false));
        springClasses.add(new OnlineClass(5"rest api development"true));
 
        OnlineClass spring_boot = new OnlineClass(1"spring boot"true);
        Progress progress = spring_boot.getProgress();
        if(progress != null) {
            System.out.println(progress);
        }
    }
}
cs

  App에 있는 main함수에서 progress를 가져온 다음에 조건문으로 null이 아니면 출력을 한다고 해보자. 이 코드는 null을 체크하는 것을 망각할 수 있기 때문에 에러를 발생할 확률이 높아진다. 또 한 null을 반환하는 것 역시 문제가 된다. 

  java8에서는 이렇게 null인 값이 전달될 가능성이 있는 경우에 Optional을 사용해 null여부를 판별할 수 있다. 만약 Optional.ofNullable()의 인자가 빈 값이라면 Optional.empty를 반환한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ...
public class OnlineClass {
 
   // ...
    /**
     * 수업을 수강한 기간을 반환
     * @reutrn Progress 수업을 들은 기간 정보
     */
    public Optional<Progress> getProgress() {
        return Optional.ofNullable(progress);
    }
}
 
 
public class App {
 
     // ...
    public static void main(String[] args) {
        // ...
        Optional<Progress> progress = spring_boot.getProgress();
        System.out.println(progress);
    }
}
cs

  Optional.ofNullable()대신에 Optional.of()를 사용할 경우 Optional을 사용하지 않는 경우와 같다.

  Optional은 되도록이면 반환값에만 사용하는 것이 좋다. Optional을 파라미터에서 사용했다고 생각해 보자. 

1
2
3
4
5
public void setProgress(Optional<Progress> progress) {
    if(progress.isPresent()) {
        progress.ifPresent((p) -> this.progress = p);
    }
}
cs

  이 경우 개발자는 파라미터로 Optional<progress> 타입의 데이터가 아닌 null을 넘길 수 도 있다. null을 넘기면 조건문의 조건에서 nullPointerException이 발생하게 된다. 또 한 조건을 progress != null이라 해도 굳이 한단계를 더 거쳐햐 하는 문제가 생기게 된다.

  map의 키값에 Optional을 사용하는것 또한 문제가 된다. map에서 중요한 조건 중 하나는 key값이 반드시 존재하는 것이다. 따라서 Optional을 사용해 키가 존재할 수 도 있고 아닐 수도 있다고 명시하는 것은 어불성설이다.

  인스턴스 필드타입으로도 Optional을 사용하면 안된다. 

 primitive-type용 optional이 별도로 존재한다. 일반 Optional을 사용해 primitive-type을 사용할 수 도 있지만 이 경우 Optional 내부에서 boxing, unboxing이 발생한다.

  Optional을 반환하는 메서드에서 null을 반환하면 안된다. Optional을 반환한다는 것은 반환 값이 null이 아님을 의미한다. 따라서 null을 반환한다면 Optional을 사용하는 의미가 없어진다. 만약 정말로 반환할 것이 없다면 Optional.empty()를 사용하자.

  Container type(Collection, Map, Stream, Arrya, Optional)은 Optional로 감싸지 말자. 이들은 자체적으로 empty여부를 판별할 수 있다.

Optional API

  java 내부의 api에서도 Optional을 사용하는 경우가 다수 존재한다. 예를 들어 stream의 terminal operator에서도 Optional을 사용하는 경우가 종종 존재한다.

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
    List<OnlineClass> springClasses = new ArrayList<>();
    springClasses.add(new OnlineClass(1"spring boot"true));
    springClasses.add(new OnlineClass(2"sprint data jpa"true));
    springClasses.add(new OnlineClass(3"spring mvc"false));
    springClasses.add(new OnlineClass(4"spring core"false));
    springClasses.add(new OnlineClass(5"rest api development"true));
 
    Optional<OnlineClass> spring = springClasses.stream()
            .filter((springClass) -> springClass.getTitle().startsWith("spring"))
            .findFirst();
}
cs

  Optional.isPresent()

    값이 존재하면 true, 없으면 false를 반환한다

  Optional.isEmpty()

    값이 없으면 false, 있으면 true

  Optional.get()

      Optional에 있는 값을 가져온다. 하지만 get()의 경우 값이 null이면 NoSuchElementException에러가 뜨게 된다. 

  Optional.ifPresent()

      Optional에 값이 존재할때 콜백을 수행한다.

  Optional.orElse()

      Optional에 값이 존재하면 그 값을 반환하고 파라미터로 넘긴 로직을 수행한다. 없으면 파라미터로 넘긴 값을 반환한다.

   Optional.orElseGet()

      Optional에 값이 존재하면 그 값을 반환하고 없으면 파라미터로 넘긴 조릭을 수행한다.

   Optional.orElseThrow()

 

      Optioanl에 값이 존재하면 그 값을 반환하고 없으면 예외처리를 한다. default로 NoSuchElementException을 throw하지만 원한다면 에러를 정의해 쓸 수 있다.

    Oprional.filter()

       값을 걸러낸다

     Optional.map()

        들어 있는 값을 변환한다

      Optional.flatMap()

         들어 있는 값이 Optional일 경우 flat하게 변환한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class App {
 
    public static void main(String[] args) {
        List<OnlineClass> springClasses = new ArrayList<>();
        springClasses.add(new OnlineClass(1"onlineClass boot"true));
        springClasses.add(new OnlineClass(5"rest api development"true));
 
        Optional<OnlineClass> onlineClass = springClasses.stream()
                .filter((springClass) -> springClass.getTitle().startsWith("rest"))
                .findFirst();
        System.out.println(onlineClass.isPresent());
        System.out.println(onlineClass.isEmpty());
 
        //ifPresent,
        onlineClass.ifPresent((oClass) -> System.out.println(oClass.getTitle()));
 
        //orElse
        System.out.println(onlineClass.orElse(createNewClass()).getTitle());
 
        System.out.println("===========");
        //orElseGet
        System.out.println(onlineClass.orElseGet(App::createNewClass));
 
        //orElseThrow
        System.out.println(onlineClass.orElseThrow(IllegalAccessError::new).getTitle());
 
        //filter
        Optional<OnlineClass> oClass = onlineClass.filter(OnlineClass::isClosed);
        System.out.println(oClass);
 
        //map
        Optional<Integer> integer =  onlineClass.map(OnlineClass::getClassId);
        System.out.println(integer);
 
        //flatMap
        Optional<Progress> progress = onlineClass.flatMap(OnlineClass::getProgress);
        System.out.println(progress);
    }
 
    private static OnlineClass createNewClass() {
        System.out.println("Create New Class");
        return new OnlineClass(10"new class"false);
    }
}
 
cs