Computer Science

All About Java Optional

예상치 못한 Null Pointer Exception에 대응하기 위해, 우리는 가끔 이런 코드를 짜곤 합니다.

if (a != null && a.getA() != null) {
    // TO DO
}

이렇듯 null은 코드를 길어지게 하지만 꼭 막아야하는 불편함을 제공하죠.

여기서, 우리는 자바 8에서 도입된 Optional 을 사용할 수 있습니다.

Optional

객체 생성하기

먼저, 반드시 값이 있어야하는 객체의 경우에는 다음과 같이 생성할 수 있습니다.

Optional<String> optional = Optional.of("hi");

값이 null일 수도 있는 경우에는 다음과 같이 객체를 생성할 수 있습니다.

Optional<String> optioanl = Optioanl.ofNullable(null);

그렇다면, 비어있는 Optional 객체는 어떻게 생성할까요?

Optional<String> emptyOptional = Optional.empty();

예제와 함께 Optional을 이용하여 코드를 더욱 효율적으로 만들어보겠습니다.

/* 학교 */
public class School {
    private Student student;
    private String name;
}

/* 학생 */
public class Student {
    private String name;
    private Long Id;
    private Address address;
}

/* 주소 */
public class Address {
    private String street;
    private String city;
    private String zipcode;
}

학교는 학생을 필드로 갖고, 학생은 주소를 필드로 갖습니다. 위와 같은 상황에서 우리는 학교의 학생이 살고있는 도시를 알고 싶습니다.

public String getCityOfStudentFromSchool(School school){
    return school.getStudent().getAddress().getCity();
}

위 코드를 통해 발생할 수 있는 NPE는 다음과 같습니다.

  1. 학교가 null인 경우
  2. school.getStudent()null인 경우
  3. school.getStudent().getAddress()null인 경우
  4. school.getStudent().getAddress().getCity()null인 경우

우리는 이러한 어이없이 많은 NPE 발생 경우에 대비하기 위해 모든 경우에 대해 중첩 Null 체크를 하는 등의 방법으로 NPE를 방어할 수 있습니다. 하지만, 이 방법이 아닌 Optional을 이용한다면 더욱 효율적으로 이를 방어할 수 있습니다.

public String getCityOfStudentFromSchool(School school) {
    return Optional.ofNullable(school) // 학교가 null 일 수 있는 경우를 감싸주었어요.
            .map(School::getStudent) // map의 연쇄 호출을 통해 Optional에 담긴 객체의 타입을 바꿔 주었어요.
            .map(Student::getAddress)
            .map(Address::getCity)
            .orElse("Empty"); // 혹시라도 Optional 객체가 비어있는 경우에는 비어있다는 디폴트 값을 넣어주었어요.
}

Optional 객체를 생성한 후 사용 가능한 메서드

  • Filter
// Optional 개체가 Joanne 과 같다면 Joanne을, 아니면 No Joanne을 출력합니다.
Optional.of("Joanne").filter((val) -> "Joanne".equals(val)).orElse("NO Joanne");
  • Map
// Map 함수를 통해 입력 값을 다른 값으로 변환합니다.
String stringValue = Optional.of(1).map(Integer::toString).orElseThrow(Exception::new);
  • flatMap

mapper 함수를 통해 입력 값을 다른 값으로 변환하지만, 시그니처의 매개변수가 Optional(U)인 점이 Map과 다릅니다. 즉, flatMap() 메서드가 반환해야하는 값은 Optional객체입니다.

  • ifPresent / isPresent
/* ifPresent
* 연산을 끝낸 후 값이 비어있지 않다면 입력값으로 주어집니다.
*/
Optional.of("Joanne").ifPresent((val) -> {
    // 수행합니다.
});

Optional.ofNullable(null).ifPresent((val) -> {
    // 수행하지 않습니다.
});


/* isPresent
* 최종 연산을 끝낸 후 객체가 존재하는지 여부를 판별합니다.
*/
Optional.ofNullable("Joanne").isPresent(); //true
Optional.ofNullable("J").filter((v) -> "Joanne".equals(v)).isPresent(); //false
  • get

최종적으로 연산을 끝낸 후 객체를 꺼냅니다. 이 때, 비어있는 객체였다면 예외가 발생합니다.

  • orElse / orElseGet / orElseThrow

최종적으로 연산을 끝낸 후에도 비어있다면 ~ 이후의 내용을 수행합니다.

/* orElse
* 최종 연산 후 비어있는 객체라면 디폴트로 제공할 객체를 지정합니다. 
* 여기서 result는 "default" 를 나타냅니다.
*/
String result = Optional.OfNullable(null).orElse("default");


/* orElseGet
* 최종 연산 후 비어있는 객체라면 기본값으로 제공할 Supplier를 지정합니다.
*/
Optional.ofNullable("input").filter("output"::equals).orElseGet(() -> "default");

/* orElseThrow
* 최종 연산 후 비어있는 객체라면 예외를 발생시킵니다.
*/
Optional.ofNullable("input").filter("output"::equals).orElseThrow(Exception::new);

참고자료