[Ch11] null 대신 Optional 클래스
NullPointerException
을 유발하는 null 객체를 보완하기 위한 Optional 에 대해서 알아본다.
값이 없는 상황을 처리하는 방법
보수적인 방법 : 중첩된 If 문
가장 흔하고 쉽게 null을 피하는 방법은 여러 if문을 통해 null 체크를 직접하는 것이다. (지겹다 :cry:)
public String getCarInsuranceName(Person person) {
if(person != null) {
Car car = person.getCar();
if(car != null) {
Insurance insurance = car.getInsurance();
if(insurance != null) {
return inserance.getName()''
}
}
}
return "Unknown"
}
이 메서드는 모든 변수가 null인지 의심하므로 “깊은의심 (deep doubt)” 라고도 부른다. 이를 반복하다보면 코드의 구조가 엉망이 되고 가독성도 떨어진다.
자바 8의 선택 : java.util.Optional
자바8에서는 ‘선택형값’ 개념으로 Optional
null 일 수 있는 값을 Optional
@Getter
public class Person {
private Optional<Car> car;
@Getter
public static class Car {
private Optional<Insurance> insurance;
}
@Getter
public static class Insurance {
private String name; // 필수로 값이 있을 경우에는 Optional 로 감싸지 않는다.
}
}
이처럼 Optional 의 역할은, 값이 없을 수 있는 상황에 적절하게 대응하도록 강제하는 효과가 있다.
Optional 적용 패턴
Optional 객체 만들기
Optional.of()
또는 Optional.ofNullble()
, Optional.empty()
로 Optional 객체를 만들 수 있다.
맵으로 Optional의 값을 추출하고 변환하기
Optional 로 랩한 객체더라도, 값이 없는데 get()
메서드를 통해 값을 가져오려고 하면 에러가 난다. 이럴 경우 map
메서드를 활용하면 된다.
Optional이 값을 포함하면 map의 인수로 제공된 함수가 값을 바꾼다. 비어있다면 아무일도 일어나지 않는다.
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);
다시 예제로 돌아와서, 아래 코드를 어떻게 Optional 을 활용해서 바꿀 수 있는지 알아보자
public static getCarInsuranceName(Person person) {
return person.getCar().getInsurance().getName();
}
Person 객체의 내부에 있는 name 을 가져오려면 아래의 값에 접근해야 하며, NPE를 피해야한다.
Person -> Optional
map
메서드를 사용하면 Optional<Optional
만약 map 메서드를 이용했다면 아래와 같은 에러가 뜬다.
Optional<String> name = person.map(Person::getCar)
.map(Person.Car::getInsurance)
.map(Person.Insurance::getName);
reason: no instance(s) of type variable(s) exist so that Optional
스트림의 flatMap
은 함수를 인수로 받아서 다른 스트림으로 반환하는 메서드. 값이 존재하면 인수로 제공된 함수를 적용한 결과 Optional을 반환하고, 값이 없으면 빈 Optional을 반환한다.
Optional<String> name = person.flatMap(Person::getCar)
.flatMap(Person.Car::getInsurance)
.map(Person.Insurance::getName);
이렇게 NPE를 피하기 위해 if-else 문이 아닌, Optional 의 스트림을 활용해서 값을 도출하는 방법을 알아봤다. Optional을 인수로 받거나, 반환하는 메서드를 정의한다면 개발자들은 “아 이 값은 null 일 수 있구나!” 라고 생각할 수 있어야 되겠다 :)
flatMap 에 대해서
flatMap
에 대해 조금 더 노트하고 싶은 내용이 있다. flatMap
은 두 Optional 을 합치는 기능을 수행하면서, 둘 중 하나라도 null 이면 빈 Optional을 생성하는 연산이다.
flatMap 을 빈 Optional에 호출하면 아무 일도 일어나지 않고 그대로 반환된다. 반면 Optional이 Person을 감싸고 있다면, flatMap에 전달된 Function이 Person에 적용된다. Person.Car::getInsurance
라는 Function을 적용한 결과가 이미 Optional 이므로 flatMap 메서드는 결과를 그대로 반환할 수 있다. 반면 마지막에 map
메서드를 쓴 이유는 name 은 이미 String 이기 때문에 flatMap을 쓸 필요가 없어서이다.
Optional 스트림 조작
자바 9에서는 Optional을 포함하는 스트림을 쉽게 처리할 수 있도록 Optional 에 stream() 메서드를 추가했다.
public static Set<String> getCarInsuranceNames(List<Person> persons) {
return persons.stream()
.map(Person::getCar)
.map(car -> car.flatMap(Person.Car::getInsurance))
.map(ins -> ins.map(Person.Insurance::getName))
.flatMap(Optional::stream) // Stream<Optional<String>> dmf Stream<String> 으로 평준화
.collect(Collectors.toSet());
}
각각의 name 을 담은 Stream 을 생성하고, Set 으로 변환했다. 하지만 요소들 중에서 빈 Optional 값이 포함되어 있으므로, 이를 필터하기 위해 filter
메서드를 추가해준다.
return persons.stream()
.map(Person::getCar)
.map(car -> car.flatMap(Person.Car::getInsurance))
.map(ins -> ins.map(Person.Insurance::getName))
.filter(Optional::isPresent) // 값이 있는것만 filter 해준다
.map(Optional::get) // 값이 있는것만 위에서 필터했으므로, 바로 get으로 가져와도 된다.
.collect(Collectors.toSet());
두 Optional 합치기
Person 과 Car 정보를 이용해서 가장 저렴한 보험료를 제공하는 보험회사를 찾는 메서드를 작성해야 한다고 가정하자
여기서 Person, Car 둘 중에 하나라도 비어 있으면 빈 Optional
앞서 배운 flatMap 메서드를 이용하면 언랩하지 않고 구현할 수 있다. 왜냐하면 첫번째 Optional 에 flatMap 을 호출했으므로, 만약 person이 비어 있다면 인수로 전달한 람다 표현식이 실행되지 않고 그대로 빈 Optional 을 반환한다.
두 번째 Optional 인 car에 map을 호출하므로 Optional 이 car 값을 포함하지 않으면 Function은 빈 Optional을 반환한다.
마지막으로 두 Optional 이 모두 값이 존재하면 map 메서드로 전달한 람다 표현식이 findcheapestInsurance 메서드를 안전하게 호출할 수 있다.
public static Optional<Person.Insurance> nullSafeFindCheapestInsurance(Optional<Person> person, Optional<Person.Car> car) {
return person.flatMap(p -> car.map(c -> findcheapestInsurance(p, c)));
}
public static Person.Insurance findcheapestInsurance(Person p, Person.Car c) {
return null;
}
filter 로 특정값 거르기
Optoinal 자체에서도 filter 메서드로 값을 필터링할 수 있다.
Optional<Insurance> optInsurance = ...;
optInsurance.filter(insurance -> "CambridgeInsurance".equals(insurance.getName()))
.ifPresent(x -> System.out.println("ok"));
댓글남기기