본문 바로가기

IT Book Summary/ModernJavaInAction

Chapter 11 null 대신 Optional 클래스

자바를 포함해 대부분의 언어 설계에는 null 참조개념을 포함한다.

 

11.1 값이 없는 상황을 어떻게 처리할까?

 

객체가 객체를 가지고 있는 중첩 구조일 경우 nullPointException 발생한다

 

1 - 보수적인 자세로 NullPointException 줄이기

 

필요한 곳에 다양한 null 확인 코드를 추가해 예외 문제를 해결해야 한다

 

변수를 접근할 때마다 null 체크하면서 들여쓰기 수준이 증가한다

public String getCarInsuranceName (Person person) {
    if (person != null) {
        Car car = person.getCar();
        if (car != null) {
            Insurance insurance = car.getInsurance();
            if (insurance != null) {
                return insurance.getName();
            }
        }
    }
    return "Unknown";
}

 

메서드안에 네개의 반환이 생긴경우. 출구가 많으면 유지보수가 어려워지는 문제가 있다.

 

public String getCarInsuranceName (Person person) {
    if(Person == null) {
        return "Unknown";
    }
    Car car = person.getCar();
    if(car == null) {
        return "Unknown";
    }
    Insurance insurance = car.getInsurance();
    if(insurance == null) {
        return "Unknown";
    }
    return insurance.getName();
}

 

2 - null 때문에 발생하는 문제

 

다음은 null의 이론적 실용적 문제들이다.

  • 에러의 근원이다
  • 코드를 어지럽힌다 : 가독성이 떨어짐
  • 아무 의미가 없다 : 정적 형식 언어에서 값이 없음을 표현하는 방법으로 적절하지 않음.
  • 자바 철학에 위배된다 : 자바는 모든 포인터를 숨겼지만 null 포인터가 예외로 남아있음.
  • 형식 시스템에 구멍을 만든다 : 정보를 포함하지 않으므로 모든 참조형식에 null 할당이 가능. 나중에 어떤의미인지 알기 힘듬.

3 - 다른 언어는 null 대신 무얼 사용하나?

 

그루비 : 내비게이션 연산자 (?.)

def carInsuranceName = person?.car?insurance?.name

이때 호출체인데 null인 참조가 있으면 결과로 null이 반환

 

하스켈, 스칼라: T 형식의 값을 갖거나 아니면 아무 값도 갖지 않을수 있다.

 

4 - Optional 클래스 소개

 

자바8 은 하스켈과 스칼라의 영향을 받아 java.util.Optional<T> 클래스 제공한다.

 

Optional은 선택형 값을 캡슐화하는 클래스.

 

값이 있으면 Optional 클래스는 값을 감싼고, 값이 없으면 Optional.empty 메서드로 Optional 을 반환한다.

 

Optional.empty 는 싱글턴 인스턴스를 반환하는 정적 팩토리 메서드이다.

 

null참조와 Optional.empty() 의 차이점은?

-> Optional.empty()는 Optional의 객체이므로 다양한 방식으로 활용 가능하다.

 

Optional을 이용하면 값이 없는 상황이 데이터에 문제가 있는것인지 알고리즘 버그인지 명확하게 구분 가능하다.

 

이를 언랩해서 값이 없을 수있는 상황에 적절히 대흥하도록 강제하는 효과가 있다.

 


11.3 Optional 적용 패턴

 

1 - Optional객체 만들기

 

빈 Optional 

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

 

null이 아닌 값으로 Optional 만들기

Optional.of 로 null이 아닌 값을 포함하는 Optional

 

Optional<Car> optCar = Optional.of(car);

car가 null이라면 NullPointException발생할것.

 

null값으로 Optional 만들기

null값을 저장할 수 있는 Optional

 

Optional<Car> optCar = Optional.ofNullable(car);

car가 null이면 빈 Optional 객체가 반환.

 

 

2 - 맵으로 Optional의 값을 추출하고 변환하기

 

객체의 정보를 추출할때 Optional의 사용법.

 

//이름 정보에 접근하기전에 insurance가 null인지 확인
String name = null;
if(insurance != null) {
    name = insurance.getName();
}

Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);
// map의 인수로 제공된 함수가 값을 바꿈. 스트림의 연산과 비슷.
// Optional이 비어있으면 아무일도 일어나지 않음.
// 메서드를 안전하게 호출할 수 있다.

 

3 - flatMap으로 Optional 객체 연결

 

스트림의 flatMap은 함수를 인수로 받아서 다른 스트림을 반환하는 메서드이다.

 

함수를 적용해 생성한 모든 스트림이 하나의 스트림으로 병합되어 평준화 된다.

 

즉, 이차원의 Optional이 하나의 Optional로 바뀐다.

 

ex) Optional로 자동차의 보험회사 이름 찾기.

 

public String getCarInsuranceName(Optional<Person> person) {
    return person.flatMap(Person::getCar)
                 .flatMap(Car::getInsurance)
                 .map(Insurance::getName)
                 .orElse("Unknown");
                 //비어있으면 기본값
}

 

null을 확인하느라 조건문을 추가하지않고 값이 없는 상황을 처리할 수 있다.

 

명시적으로 형식 시스템을 정의한다.

 

Optional을 이용한 Person/Car/Insurance 참조체인.

 

평준화 과정이란 두 Optional 을 합치는 기능을 수행하며 둘중 하나라도 null이면 빈 Optional을 생성하는 연산이다.

FlatMap을 빈 Optional에 호출하면 아무일도 하지않고 전체결과로 빈Optional을 그대로 반환된다.

 

 

도메인 모델에 Optional을 사용했을때 데이터를 직렬화할 수 없는 이유

자바 아키텍트인 브라이언 고츠는 Optional의 용도가 선택형 반환값을 지원하는 것이라고 단정지었다.

Optional 클래스는 필드 형식으로 사용할 것을 가정하지 않았으므로 Serializable 인터페이스를 구현하지 않는다.
따라서 직렬화를 사용하는 프레임워크에서 문제가 생길 수 있다.

직렬화 모델이 필요하다면 다음과 같이 Optional로 값을 반환받는 메서드를 추가하는 방식을 권장한다.

 

public class Person {
    private Car car;
    public Optional<Car> getCarAsOptional() {
        return Optional.ofNullable(car);
    }
}

 

4 - Optional 스트림 조작 

 

자바9에서는 Optional을 포함하는 스트림을 쉽게 처리하도록 stream() 메서드를 추가했다.

 

 

다음과 같은 절차를 stream() 메서드를 이용해 같은 결과를 얻어낼 수 있다.

Stream<Optional<String>> stream = ...
Set<String> result = stream.filter(Optional::isPresent)
                           .map(Optional::get)
                           .collect(toSet());

 

ex) 자동차를 소유한 사람들이 가입한 보험 회사의 이름을 포함하는 Set<String>을 반환하는 메서드 구현예제.

 

public Set<String> getCarInauranceNames(List<Person> persons) {
    return persons.stream()
                  .map(Person::getCar)
                  .map(optCar -> optCat.flatMap(Car::getInsurance))
                  .map(optIns -> optIns.map(Insurance::getName))
                  .flatMap(Optional::stream)
                  .collect(toSet());
}

//Optional이 비어있는지에 따라 0개 이상의 항목을 포함하는 스트림으로 변환.
//값을 포함하는 Optional을 언랩하고 비어있는 Optional은 건너뜀.

 

 

5 - 디폴트 액션과 Optional 언랩

 

이전 예시에서 빈 Optional 상황에 기본값을 반환하도록 orElse()를 사용했다.

다음의 다양한 방법으로 값을 읽을수 있다.

 

  • get()
    : 값을 읽는 간단한 메서드지만 안전하지 않다. 래핑된 값이 있으면 반환하고 없으면 NoSuchElementException 발생. 
  • orElse(T other)
    : 값을 포함하지 않을 때 기본값을 제공한다.
  • orElseGet(Supplier<? extends T> other)
    : orElse 메서드의 게으른 버전이다. Optional에 값이 없을 때만 Supplier가 실행된다. 비어있을때만 기본값을 생성하게 된다.
  • orElseThrow(Supplier<? extends X> exceptionSupplier)
    : Optional이 비어있을때 지정한 예외를 발생시킨다.
  • ifPresent(Consumer<? super T> consumer)
    : 값이 존재할 때 인수로 넘겨준 동작을 실행할 수 있다.
  • ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) - 자바9에서 추가됨
    : Optional이 비었을 때 실행할 수 있는 Runnable을 인수로 받음.

 

6 - 두 Optional 합치기 

 

ex) Person과 Car 정보를 이용해서 가장 저렴한 보험료를 제공하는 보험회사를 찾는 서비스.

 

public Insurance findCheapestInsurance(Person person, Car car) {
    // 보험회사가 제공하는 서비스 조회
    // 모든 결과 데이터 비교
    return cheapestCompany;
}

 

- 두 Optional을 인수로 받아 Optional<Insurance> 를 반환하는 null-safe 메서드를 구현해야 한다.

- 인수로 전달한 값 중 하나라도 비어있으면 빈 Optional<Insurance> 를 반환.

 

// isPresent 이용해 구현
public Optional<Insurance> nullSafeFindCheapestInsurance(
            Optional<Person> person, Optional<Car> car) {
    if(person.isPresent() && car.isPresent()){
        return Optional.of(findCheapestInsurance(person.get(), car.get()));
    } else {
        return Optional.empty();
    }
}

// 조건문을 사용하지 않고 map과 flapMap으로 구현
public Optional<Insurance> nullSafeFindCheapestInsurance(
            Optional<Person> person, Optional<Car> car) {
    return person.flatMap(p -> car.map(c -> findCheapestInsurance(p,c)));
}

 

Optional 이 비어있다면 인수로 전달한 람다 표현식이 실행되지 않고 그대로 빈 Optional 반환한다.

모두 값이 존재하면 메서드를 안전하게 호출한다.

 

7 - 필터로 특정값 거르기

객체의 메서드를 호출해서 어떤 프로퍼티를 확인해야 할때 사용.

 

ex) 보험회사 이름이 'CambridgeInsurance' 확인

 

// 조건문으로 단순 null 체크
Insurance insurance = ...;
if(insurance != null && "CambridgeInsurance".equal(insurance.getName())) {
    System.out.println("ok");
}

// Optional 객체에 filter 메서드 사용
Optional<Insurance> optInsurance = ...;
optInsurance.filter(ins -> "CambridgeInsurance".equals(insurance.getName()))
            .ifPresent(x -> System.out.println("ok"););

 

Optional은 최대 한 개의 요소를 포함할 수 있는 스트림과 같다.

Optional이 비어있으면 filter 연산은 아무동작도 하지 않고, 값이 있으면 프레디케이트 적용한다.

filter메서드는 프레디케이트를 인수로 받아 Optional 객체가 값을 가지며 일치하면 그 값을 반환, 아니면 빈 Optional객체 반환한다.

 

ex) person이 minAge 이상의 나이일 때만 보험회사 이름을 반환

 

public String getCarInsuranceName(Optional<Person> person, int minAge) {
    return person.filter(p -> p.getAge() >= minAge)
                 .flatMap(Person::getCar)
                 .flatMap(Car::getInsurance)
                 .map(Insurance::getName)
                 .orElse("Unknown");
}

 


11.4 Optional을 사용한 실용 예제

 

기본 자바 API에  Optional 기능을 활용할 수 있도록 코드에 작은 유틸리티 메서드를 추가하는 방식으로 실용예제를 살펴보자.

 

1 - 잠재적으로 null이 될 수 있는 대상을 Optional로 감싸기

 

Map<String, Object> 형식의 맵에 key로 접근시 문자열 key에 해당하는 값이 없으면 null 반환될 것이다.

    Object value = map.get("key"); 

map에서 반환하는 값을 Optional로 감싸서 이를 개선할 수 있다.

Optional<Object> value = Optional.ofNullable(map.get("key")); 

 

2 - 예외와 Optional 클래스

 

null 이 아닌 예외를 발생시키는 경우 

 

ex) 문자열을 정수로 바꾸지 못해 발생한 NumberFormatException 예외를 Optional 로 처리하기

 

kpublic static Optional<Integer> stringToInt(String s) {
    try{
        return Optional.of(Integer.parseInt(s));
    } catch (NumberFormatException e) {
        reutn Optional.empty(); // 예외 발생시 빈 Optional 반환
    }
}

 

3 - 기본형 Optional을 사용하지 말아야 하는 이유

 

스트림처럼 Optional도 기본형 특화 클래스 OptionalInt, OptionalLong, OptionalDouble 등을 제공한다.

 

하지만 Optional의 최대 요소 수는 한개 이므로 기본형 특화 클래스로 성능을 개선할 수 없다.

 

또한 기본형특화 클래스에는 map, filter 등희 메서드를 지원하지 않으므로 사용을 권장하지 않는다.

 

4 - 응용 

 

Properties props = new Properties();
props.setProperty("a", "5");
props.setProperty("b", "true");
props.setProperty("c", "-3");

// 메서드 시그니처
// Properties를 읽어 값을 초 단위 duration으로 해석.
public int readDuration(Properties props, String name)

// 지속 시간은 양수여야 하므로 문자열이 양의 정수이면 해당값 반환, 그 외에는 0 반환.
assertEqual(5, readDuration(param, "a"));
assertEqual(0, readDuration(param, "b"));
assertEqual(0, readDuration(param, "c"));
assertEqual(0, readDuration(param, "d"));

// 프로퍼티에서 지속 시간을 읽는 명령형 코드
public int readDuration(Properties props, String name) {
    String value = props.getProperty(name);
    if(value != null) {
        try {
            int i = Integer.parseInt(value);
            if (i>0) {
                return i;
            }
        }catch (NumberFormatException nfe) { }
    }
    return 0;
}

// Optional로 프로퍼티 지속시간 읽기
// 이전 예제에서 만든 OptionalUtility 활용해 Optional<Integer> 반환
public int readDuration(Properties props, String name) {
    return Optioinal.ofNullable(props.getProperty(name))
                    .flatMap(OptionalUtility::stringToInt)
                    .filter(x -> x > 0)
                    .orElse(0)
}

 


  • 자바8 에서는 값이 있거나 없음을 표현하는 클래스 java.util.Optional<T> 제공
  • 팩토리메서드 Optional.empty, Optional.of, Optional.Nullable 등을 이용해 Optional 객체를 만들 수 있다.
  • Optional 클래스는 스트림과 비슷한 연산을 수행하는 flatMap, filter, map 등의 메서드 제공.
  • Optional로 값이 없는 상황을 적절하게 처리하도록 강제할수 있음. 즉, 예상치 못한 예외를 방지 할 수 있다.
  • Optional을 활용하면 좋은 API 설계 가능. 메서드의 시그니처만 보고도 Optional 값이 사용되거나 반환되는지 예측 가능.
Optional 메서드 설명 of 값이 존재하면 값을 감싸는 Optional 반환, 값이 null이면 NullPointerException 발생
filter 값이 존재하고 프레디케이트와 일치하면 값을 포함하는 Optional 반환하고, 값이 없거나 일치하지 않으면 빈 Optional 반환. ofNullable 값이 존재하면 값을 감싸는 Optional 반환, 값이 null이면 빈 Optional 반환.
flatMap 값이 존재하면 인수로 제공된 함수를 적용한 결과 Optional 반환하고, 값이 없으면 빈 Optional 반환.  or 값이 존재하면 같은 Optional반환, 없으면 Supplier에서 만든 Optional 반환.
get 값이 존재하면 Optional이 감싸고 있는 값 반환, 값이 없으면 NoSuchElementException 발생. orElse 값이 존재하면 값을 반환하고, 없으면 기본값 반환.
ifPresent 값이 존재하면 지정된 Consumer 실행, 없으면 아무일도 일어나지 않음. orElseGet 값이 존재하면 값을 반환, 없으면 Supplier에서 만든 Optional을 반환. 
ifPresentOrElse 값이 존재하면 지정된 Consumer 실행, 없으면 아무일도 일어나지 않음. orElseThrow 값이 존재하면 값을 반환, 없으면 Supplier에서 생성한 예외를 반환.
isPresent 값이 존재하면 true 없으면 false stream 값이 존재하면 존재하는 값만 포함하는 스트림을 반환, 값이 없으면 빈 스트림을 반환.
map 값이 존재하면 제공된 매핑함수를 적용. empty 빈 Optional 인스턴스 반환