CHAP11 - Modern-Java-in-Action/Online-Study GitHub Wiki
- Optional Class doc
- Optional 장점: 좋은 API를 설계 + null 포인터 예외 감소
이 장의 내용
- null 참조의 문제점과 null을 멀리해야 하는 이유
- null 대신 Optional : nuII로 부터 안전한 도메인 모델 재구현하기
- Optional 활용 : null 확인 코드 제거하기
- Optional에 저장된 값을 확인하는 방법
- 값이 없을 수도 있는 상황을 고려하는 프로그래밍
팩트
- 널포인터 에러 너무 많이 겪었다.
- null을 만든 사람도 자바에서 null 포인터를 만든 것은 실수라고 말한다.
null 때문에 어떤 문제가 발생할 수 있는지 간단한 예제로 살펴보자.
// Person -> Car -> Insurance
public class Person {
private Car car;
public Car getCar() { return car; }
}
public class Car {
private Insurance insurance;
public Insurance getInsurance() { return insurance; }
}
public class Insurance {
private String name;
public String getName() { return name; }
}
// -----------------------------------
// 다음과 같이 자동차 보험 이름을 가져오려고 했을때,
// 무슨 문제가 있을까?
public String getCarInsuranceName(Person person) {
return person.getCar().getInsurance().getName();
}
- deep deoubt (깊은 의심): 아래 if문 반복된 패턴
// 시도 1
// 하나라도 null 참조가 있으면 "Unknown" 리턴
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";
}
// ---------------------------------------
// 시도 2
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();
}
짧게말해, null로 값이 없다는 사실을 표현하는 것은 좋은 방법이 아니다.
- 다른 언어 null 문제 해결 예시
- 하스켈: Maybe
- 스칼라: Option[T]
- 그루비: 안전 내비게이션 연산자(?.)
// 그루비 예시
def carInsuranceName = person?.car?.insurance?.name
- 자바에선, java.util.Optional
- Optional 클래스가 객체가 있든 null이든 감싸준다.
// BEFORE
public class Person {
private Car car;
public Car getCar() { return car; }
}
public class Car {
private Insurance insurance;
public Insurance getInsurance() { return insurance; }
}
public class Insurance {
private String name;
public String getName() { return name; }
}
// ---------------------------------------------------------
// AFTER
public class Person2 {
private Optional<Car> car;
public Optional<Car> getCar() { return car; }
}
public class Car2 {
private Optional<Insurance> insurance;
public Optional<Insurance> getInsurance() { return insurance; }
}
public class Insurance2 {
private String name;
public String getName() { return name; } // 보험 객체 존재시 이름이 반드시 있어야함으로, 리턴형이 Optional이 아니다.
}
모든 null 참조를 Optional로 대치하는 것 은 바람직하지 않다. Optional의 역할은 더 이해하기 쉬운 API를 설계하도록 돕는 것이다. 즉, 메서드의 시그니처만 보고도 선택형값인지 여부를 구별할 수 있다.
Optional로 감싼 값을 실제로 어떻게 사용할 수 있을까? 일단, Optional 객체를 만들어보자.
// 1 빈 Optional 객체 만들기
Optional<Car2> optCar1 = Optional.empty();
// 2 null이 아닌 값으로 Optional 만들기
// null 이 아님을 확실할 경우
Optional<Car2> optCar2 = Optional.of(car);
// 3 null값으로 Optional 만들기
Optional<Ca2r> optCar3 = Optional.ofNullable(car);
주의: get() 을 이용하여 Optional의 값을 가져올 수 있는데, Optional이 비어있으면 호출시 예외가 발생한다. 즉, Optional 사용시 결국 null을 사용했을 때 와 같은 문제를 겪을 수 있다.
// BEFORE
String name = null;
if(insurance != null){
name = insurance.getName();
}
// AFTER
// 보험 객체를 Optional로 만들고,
// Optional로 감싸진 객체의 getName을 실행한다.
// 보험 객체가 널이면 name에는 Optional empty가 들어가고
// 값이 있을 경우 name에는 값이 들어간다.
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);
// ------------------------------------
// 보험 이름: aaaa로
Insurance insurance1 = new Insurance("aaaa");
Optional<Insurance> optInsurance1 = Optional.ofNullable(insurance1);
Optional<String> 보험1 = optInsurance1.map(Insurance::getName);
// // 보험 이름: 지정 x
Insurance insurance2 = new Insurance();
Optional<Insurance> optInsurance2 = Optional.ofNullable(insurance2);
Optional<String> 보험2 = optInsurance2.map(Insurance::getName);
System.out.println(보험1); // 결과: Optional[aaaa]
System.out.println(보험2); // 결과: Optional.empty
- 스트림의 flatMap 메서드와 Optional의 flatMap 메서드는 유사하다.
- map과 flatmap 시그니처 차이
![image-20220123111801353](D:\0 Google Drive\03 스터디 모임\모던자바\5주차\11장.assets\image-20220123111801353.png)
![image-20220123111743582](D:\0 Google Drive\03 스터디 모임\모던자바\5주차\11장.assets\image-20220123111743582.png)
// 컴파일 에러 예시
// map -> map -> map 을 호출하는데, Optional<Optional<Optional<Car>>> 이렇게 중첩된다
Optional<Person2> optPerson = Optional.of(person2);
Optional<String> name =
optPerson.map(Person2::getCar)
.map(Car2::getInsurance)
.map(Insurance2::getName);
// 스트림의 flatMap과 비슷하게, 옵셔널에서 flatMap 사용하면 중첩이 풀리면서 일차원으로 만들어진다.
// null 체크하는 if문 없이 코드 간결
public String getCarInsuranceName(Optional<Person> person) {
return person.flatMap(Person::getCar) // Optional을 이용한 참조 체인
.flatMap(Car::getInsurance)
.map(Insurance::getName)
.orElse("Unknown"); // 값이 없다면
}
// ------------------------------------------
// 실행 예시
// 차 없는 사람 생성
Person2 person = new Person2(Optional.empty());
Optional<Person2> optPerson = Optional.of(person);
System.out.println(getCarInsuranceName3(optPerson));
// 결과: Unknown
- 호출의 논리적 과정
- 1단계: Person에 Function을 적용 (getCar 메서드가 펑션)
- flatmap 사용 이유: Optional이 계속 중첩됨으로 평준화 한다.
- 평준화 과정: 두 Optional을 합치는 기능을 수행하면서 둘 중 하나라도 null이면 빈 Optional을 생성
- flatmap 사용 이유: Optional이 계속 중첩됨으로 평준화 한다.
- 2단계: 1단계와 동일하게 작동하고, Optional 리턴
- 3단계: 스트링 반환하므로, flatmap 사용 필요 x
- 1단계: Person에 Function을 적용 (getCar 메서드가 펑션)
도메인 모델에 Optional을 사용했을 때 데이터를 직렬화할 수 없지만, 이런 단점에도 저자 Optional을 사용해서 도메인 모델을 구성하는 것이 바람직하다고 말한다.
- 자바9, Optional에 stream() 메서드를 추가
- flatMap(Optional::stream): Stream<Optional>을 현재 이름을 포함하는 Stream으로 변환
// 사람들이 가입한 보험회사 이름들을 리턴
public static Set<String> getCarInsuranceNames(List<Person2> persons) {
return persons.stream()
.map(Person2::getCar) // 어떤 사람은 자동차를 가지지 않을 수도 있다.
.map(optCar -> optCar.flatMap(Car2::getInsurance))
.map(optIns -> optIns.map(Insurance2::getName))
.flatMap(Optional::stream)
.collect(Collectors.toSet()); // 중복 제거 기능도
}
// ------------------------------------------------
System.out.println(getCarInsuranceNames(personList)); // [a, b, c]
// 총 7명 중에, 차 있는 4명 중 1명만 빼고 보험을 들었고 (보험명: a,b,c), 다른 3명은 차가 없다.
static List<Person2> personList = List.of(
new Person2(Optional.of(new Car2(Optional.of(new Insurance2("a"))))),
new Person2(Optional.of(new Car2(Optional.of(new Insurance2("b"))))),
new Person2(Optional.of(new Car2(Optional.of(new Insurance2("c"))))),
new Person2(Optional.of(new Car2(Optional.empty())))),
new Person2(Optional.empty()),
new Person2(Optional.empty()),
new Person2(Optional.empty())
);
Optional에서 stream 메서드를 제공하지 않았다면, Optional의 isPresent 메서드를 사용해 가며, 직접 처리해줘야하는 번거로움이 있었을것이다. (옵셔널에 스트림 생겨서 좋다!)
- Optional unwrap 방법들
- orElse("디폴트값"): 값이 없을때 기본값 제공.
- get(): 값 바로 제공. 값이 없다면, NoSuchElementException. 그러니 조심 사용.
- orElseGet(): 값이 없을때만, Supplier가 실행. Optional이 비어있을 때만 기본값을 생성하고 싶을때 사용.
- orElseThrow(): get과 비슷
- ifPresent(): 값이 존재할 때 인수로 넘겨준 동작을 실행. 없으면 실행 x.
- ifPresentOrElse(): Optional이 비었을 때 실행할 수 있는 Runnable을 인수로 받는다는 점만 ifPresent와 다르다.
// ifPresentOrElse 예시
Optional<Integer> op = Optional.empty();
try {
op.ifPresentOrElse(
(value) -> {
System.out.println( "Value is present, its: " + value);
},
() -> {
System.out.println("Value is empty");
});
} catch (Exception e) {
System.out.println(e);
}
// 결과: Value is empty
복잡한 비즈니스 로직 가정: 가장 보험료 저렴한 회사 찾기
// 사람에 따라 보험료 다를테니,
// 가장 저렴한 보험료 찾기
public Insurance findCheapestInsurance(Person person, Car car) {
// 로직: 다양한 보험회사가 제공하는 서비스 조회 후 모든 결과 데이터 비교
return cheapestCompany;
}
Optional를 반환하는 버전을 구현해야 할때
// 아래 구현 코드는 null 확인 코드와 크게 다른 점이 없다.
// 퀴즈: 개선해 보자
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();
}
}
Optional 언랩하지 않고 두 Optional 합치기
정답
public Optional<Insurance> nullSafeFindCheapestInsurance(
Optional<Person> person, Optional<Car> car) {
return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c)));
}
객체의 메서드 호출해서 어떤 프로퍼티를 확인해야 할 때가 있다.
// 보험회사 이름 CambridgeInsurance인지 확인
Insurance insurance = ...;
if(insurance != null && "CambridgeInsurance".equals(insurance.getName())){
System.out.println("ok");
}
filter 사용: 조건에 맞으면 값을 반환하고 그렇지 않으면 빈 Optional 객체를 반환
// Optional으로 재구현
Optional<Insurance> optInsurance = ...;
optInsurance.filter(insurance ->
"CambridgeInsurance".equals(insurance.getName()))
.ifPresent(x -> System.out.println("ok"));]
Optional filtering
특정 나이 이상 되는 사람들이 가입한 보험을 찾아와 보자. (minAge 이상 일때만)
public String getCarInsuranceName(Optional<Person> person, int 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");
}
기존의 코드를 재구현하는데 도움이 되는 다양한 기법을 확인해보자.
Object value = map.get("key");
// ------------------------------------
// get의 시그니처를 고칠 순 없지만, 받은값을 감쌀 수 있다.
Optional<Object> value = Optional.ofNullable(map.get("key"));
- Integer.parseInt()는 값을 제공할 수 없을 때 null을 반환하는 대신 예외를 발생시킨다.
- e.g. Integer.parseInt("hello") -> NumberFormatException 에러
// 이와 같은 유틸리티 클래스르 많이 만들자!
public static Optional<Integer> stringToInt(String s) {
try {
return Optional.of(Integer.parseInt(s));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
// stringToInt("hello") -> 에러 나지 않는다. 옵셔널의 empty를 받는다.
- Optionallnt, OptionalLong, OptionalDouble은 사용 자제하기.
- map, flatMap,filter 등을 지원하지 않는다.
- 기본형은 기존 Optional 혼용 사용 불가
- 예, OptionalInt를 반환한다면 이를 다른 Optional 의 flatMap에 메서드 참조로 전달할 수 없다.
실제 업무에서 어떻게 사용하는지 보자.
// 프로그램 설정 인수로 프로퍼티 전달 예제
// 키를 주고 안에 값이 양수인것만 값을 가져오자. otherwise 0
@Test
public void testMap() {
Properties props = new Properties();
props.setProperty("a", "5");
props.setProperty("b", "true");
props.setProperty("c", "-3");
assertEquals(5, readDurationImperative(props, "a"));
assertEquals(0, readDurationImperative(props, "b"));
assertEquals(0, readDurationImperative(props, "c"));
assertEquals(0, readDurationImperative(props, "d"));
}
public static int readDurationImperative(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로 프로퍼티에서 지속 시간 읽기
@Test
public void testMap() {
Properties props = new Properties();
props.setProperty("a", "5");
props.setProperty("b", "true");
props.setProperty("c", "-3");
assertEquals(5, readDurationWithOptional(props, "a"));
assertEquals(0, readDurationWithOptional(props, "b"));
assertEquals(0, readDurationWithOptional(props, "c"));
assertEquals(0, readDurationWithOptional(props, "d"));
}
public static int readDurationWithOptional(Properties props, String name) {
return Optional.ofNullable(props.getProperty(name))
.flatMap(OptionalUtility::stringToInt)
.filter(i -> i > 0)
.orElse(0);
}