diff --git a/docs/ch11/ch11.md b/docs/ch11/ch11.md new file mode 100644 index 0000000..c7eff36 --- /dev/null +++ b/docs/ch11/ch11.md @@ -0,0 +1,543 @@ +# Chapter 11 : null 대신 Optional 클래스 + +## 전체 핵심 + +Chapter 11의 주제는 **null 대신 값의 존재 여부를 명시적으로 표현하는 Optional 클래스**(`java.util.Optional`)이다. + +자바에서 값이 없는 상황은 전통적으로 null 참조로 표현해왔다. 하지만 null은 NullPointerException의 근원이고, null 확인 코드가 반복되면서 코드를 어지럽힌다. 자바 8은 하스켈의 Maybe, 스칼라의 Option[T] 같은 **선택형값** 개념의 영향을 받아 Optional 클래스를 도입했다. + +이 장의 핵심은 다음과 같다. + +> 값이 있거나 없을 수 있는 상황을 Optional로 모델링하고, map / flatMap / filter 같은 스트림 스타일 연산으로 null 확인 코드 없이 안전하게 값을 처리한다. 궁극적인 목적은 **메서드 시그니처만 보고도 선택형값 여부를 알 수 있는 더 좋은 API 설계**다. + +예를 들어 중첩 null 확인으로 가득한 코드가, + +```java +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"; +} +``` + +Optional을 사용하면 다음처럼 조건 분기 없이 표현된다. + +```java +public String getCarInsuranceName(Optional person) { + return person.flatMap(Person::getCar) + .flatMap(Car::getInsurance) + .map(Insurance::getName) + .orElse("Unknown"); +} +``` + +--- + +## 11.1 값이 없는 상황을 어떻게 처리할까? + +1965년 토니 호어(Tony Hoare)가 알골(ALGOL W)을 설계하면서 처음 null 참조를 도입했다. '구현하기 쉬웠기 때문에' 도입했지만, 훗날 그는 이 결정을 **'십억 달러짜리 실수'**라고 표현했다. + +다음처럼 자동차와 자동차 보험을 갖고 있는 사람 객체를 중첩 구조로 구현했다고 하자. + +```java +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; } +} +``` + +다음 코드는 어떤 문제가 있을까? + +```java +public String getCarInsuranceName(Person person) { + return person.getCar().getInsurance().getName(); +} +``` + +차를 소유하지 않은 사람은 `getCar()`가 null을 반환하고, 이어지는 `getInsurance()` 호출에서 **NullPointerException**이 발생한다. person 자체가 null일 수도 있다. + +### 11.1.1 보수적인 자세로 NullPointerException 줄이기 + +대부분의 프로그래머는 null 확인 코드를 추가해서 이 문제를 해결하려 한다. + +**시도 1 : 깊은 의심(deep doubt)** + +모든 변수를 접근할 때마다 중첩 if로 null을 확인하는 방식이다. 들여쓰기 수준이 계속 증가하면서 코드 구조가 엉망이 되고 가독성이 떨어진다. + +**시도 2 : 너무 많은 출구** + +중첩 if 대신 null 변수를 만나면 즉시 "Unknown"을 반환하는 방식이다. 하지만 메서드에 출구(return)가 네 개나 생겨서 유지보수가 어려워지고, 같은 문자열이 반복되면서 오타 등의 실수가 생길 수 있다. + +```java +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 확인을 깜빡하면 바로 에러로 이어진다. 값이 있거나 없음을 표현할 수 있는 더 좋은 방법이 필요하다. + +### 11.1.2 null 때문에 발생하는 문제 + +- **에러의 근원이다** : NullPointerException은 자바에서 가장 흔히 발생하는 에러다. +- **코드를 어지럽힌다** : 중첩된 null 확인 코드 때문에 가독성이 떨어진다. +- **아무 의미가 없다** : null은 아무 의미도 표현하지 않는다. 정적 형식 언어에서 값이 없음을 표현하는 방법으로 적절하지 않다. +- **자바 철학에 위배된다** : 자바는 개발자로부터 모든 포인터를 숨겼지만, null 포인터는 예외다. +- **형식 시스템에 구멍을 만든다** : null은 무형식이므로 모든 참조 형식에 할당될 수 있다. 시스템 곳곳으로 null이 퍼지면 애초에 어떤 의미로 사용되었는지 알 수 없다. + +### 11.1.3 다른 언어는 null 대신 무얼 사용하나? + +**그루비**는 안전 내비게이션 연산자(safe navigation operator) `?.`를 도입했다. 호출 체인에 null인 참조가 있으면 예외 없이 결과로 null이 반환된다. + +```groovy +def carInsuranceName = person?.car?.insurance?.name +``` + +자바 7에서도 비슷한 제안이 있었지만 채택되지 않았다. NullPointerException이 발생하면 null 확인 if문을 추가해서 쉽게 해결할 수 있지만, 이는 문제의 본질을 해결하는 것이 아니라 문제를 뒤로 미루고 숨기는 것이다. 반면 그루비의 안전 내비게이션 연산자를 이용하면 부작용을 최소화하면서 null 예외 문제를 더 근본적으로 해결할 수 있다. + +**하스켈**은 선택형값을 저장할 수 있는 `Maybe` 형식을, **스칼라**는 값이 있거나 없을 수 있는 `Option[T]` 구조를 제공한다. 형식 시스템이 값의 존재 여부 확인을 강제하므로 null 관련 문제가 일어날 가능성이 줄어든다. + +자바 8은 이 '선택형값' 개념의 영향을 받아 `java.util.Optional`를 제공한다. + +--- + +## 11.2 Optional 클래스 소개 + +**Optional은 선택형값을 캡슐화하는 클래스**다. 값이 있으면 Optional 클래스가 값을 감싸고, 값이 없으면 `Optional.empty` 메서드로 빈 Optional 객체를 반환한다. + +null 참조와 `Optional.empty()`는 의미상 비슷하지만 실제로는 차이가 크다. null을 참조하려 하면 NullPointerException이 발생하지만, `Optional.empty()`는 Optional 객체이므로 다양한 방식으로 활용할 수 있다. + +Optional로 데이터 모델을 재정의하면 다음과 같다. + +```java +public class Person { + private Optional car; // 차를 소유했을 수도, 안 했을 수도 있다 + public Optional getCar() { return car; } +} + +public class Car { + private Optional insurance; // 보험에 가입했을 수도, 안 했을 수도 있다 + public Optional getInsurance() { return insurance; } +} + +public class Insurance { + private String name; // 보험회사에는 반드시 이름이 있다 + public String getName() { return name; } +} +``` + +주목할 점은 보험회사 이름은 `Optional`이 아니라 **String 그대로**라는 것이다. 보험회사는 반드시 이름을 가져야 하기 때문이다. 이름이 없는 보험회사를 발견했다면 null 확인 코드를 추가할 것이 아니라, 이름이 없는 이유(데이터 문제인지 알고리즘 버그인지)를 밝혀서 문제를 해결해야 한다. + +즉 Optional의 역할은 **모든 null을 대체하는 것이 아니라**, 값이 없을 수 있는 상황을 형식으로 명시해서 더 이해하기 쉬운 API를 설계하도록 돕는 것이다. 메서드 시그니처만 보고도 선택형값인지 여부를 구별할 수 있다. + +--- + +## 11.3 Optional 적용 패턴 + +### 11.3.1 Optional 객체 만들기 + +Optional 객체를 만드는 세 가지 정적 팩토리 메서드가 있다. + +```java +Optional optCar = Optional.empty(); // 빈 Optional +Optional optCar = Optional.of(car); // car가 null이면 즉시 NPE 발생 +Optional optCar = Optional.ofNullable(car); // car가 null이면 빈 Optional 반환 +``` + +- `Optional.of` : null이 아닌 값을 감쌀 때 사용한다. null을 넘기면 **즉시** NullPointerException이 발생한다(사용 시점이 아니라 생성 시점에 에러를 알 수 있다). +- `Optional.ofNullable` : null일 수도 있는 값을 감쌀 때 사용한다. + +### 11.3.2 맵으로 Optional의 값을 추출하고 변환하기 + +객체의 정보를 추출하기 전에 null 확인을 하는 패턴은 매우 흔하다. + +```java +String name = null; +if (insurance != null) { + name = insurance.getName(); +} +``` + +Optional의 `map` 메서드로 이 패턴을 대체할 수 있다. + +```java +Optional optInsurance = Optional.ofNullable(insurance); +Optional name = optInsurance.map(Insurance::getName); +``` + +Optional을 **최대 요소 개수가 한 개 이하인 컬렉션**으로 생각하면 스트림의 map과 개념적으로 같다. Optional이 값을 포함하면 함수가 값을 바꾸고, 비어있으면 아무 일도 일어나지 않는다. + +### 11.3.3 flatMap으로 Optional 객체 연결 + +map만으로 호출 체인을 만들면 컴파일되지 않는다. + +```java +Optional optPerson = Optional.of(person); +Optional name = + optPerson.map(Person::getCar) // Optional> 가 되어버림 + .map(Car::getInsurance) // 컴파일 에러! + .map(Insurance::getName); +``` + +`getCar`가 `Optional`를 반환하므로 map의 결과는 `Optional>`, 즉 **중첩 Optional**이 된다. 스트림의 flatMap처럼 Optional의 `flatMap`은 이차원 Optional을 일차원으로 평준화한다. + +```java +public String getCarInsuranceName(Optional person) { + return person.flatMap(Person::getCar) + .flatMap(Car::getInsurance) + .map(Insurance::getName) + .orElse("Unknown"); // 결과 Optional이 비어있으면 기본값 사용 +} +``` + +호출 체인 중 어떤 메서드가 빈 Optional을 반환하면 전체 결과로 빈 Optional이 반환되고, 아니면 보험회사 이름을 포함하는 Optional이 반환된다. 마지막 단계의 `getName()`은 String을 반환하므로 flatMap이 아닌 map을 쓴다. + +또한 메서드가 `Optional`을 인수로 받으면, 이 메서드를 사용하는 모든 사람에게 **인수가 빈 값일 수 있음을 문서화해서 제공하는 것**과 같다. + +> 💡 **도메인 모델에 Optional을 사용했을 때 데이터를 직렬화할 수 없는 이유** +> +> Optional 설계자는 Optional을 선택형 반환값 용도로만 가정했기 때문에 Optional은 `Serializable` 인터페이스를 구현하지 않는다. 따라서 도메인 모델의 필드로 Optional을 쓰면 직렬화 모델을 사용하는 도구나 프레임워크에서 문제가 생길 수 있다. 직렬화 모델이 필요하다면 필드는 일반 형식으로 두고 Optional로 값을 반환하는 메서드를 추가하는 방식을 권장한다. +> +> ```java +> public class Person { +> private Car car; +> public Optional getCarAsOptional() { +> return Optional.ofNullable(car); +> } +> } +> ``` + +### 11.3.4 Optional 스트림 조작 + +자바 9에서는 Optional에 `stream()` 메서드가 추가되었다. 값이 있으면 그 값만 포함하는 스트림을, 없으면 빈 스트림을 반환한다. + +사람 목록에서 자동차를 소유한 사람들이 가입한 보험회사 이름 집합을 구하는 예제를 보자. + +```java +public Set getCarInsuranceNames(List persons) { + return persons.stream() + .map(Person::getCar) // Stream> + .map(optCar -> optCar.flatMap(Car::getInsurance)) // Stream> + .map(optIns -> optIns.map(Insurance::getName)) // Stream> + .flatMap(Optional::stream) // Stream (빈 Optional 제거) + .collect(toSet()); +} +``` + +세 번의 변환을 거치면 `Stream>`을 얻는데, 사람이 차가 없거나 차가 보험에 가입되어 있지 않으면 빈 Optional이 섞여 있다. `flatMap(Optional::stream)` 한 번의 연산으로 값을 포함하는 Optional은 언랩하고 빈 Optional은 건너뛸 수 있다. + +stream() 없이 하려면 다음처럼 filter와 map을 순서대로 사용해야 한다. + +```java +Set result = stream.filter(Optional::isPresent) + .map(Optional::get) + .collect(toSet()); +``` + +### 11.3.5 디폴트 액션과 Optional 언랩 + +Optional에 저장된 값을 읽는 다양한 방법을 제공한다. + +- `get()` : 가장 간단하지만 **가장 안전하지 않은** 메서드다. 값이 없으면 NoSuchElementException을 발생시킨다. 값이 반드시 있다고 가정할 수 있는 상황이 아니면 사용하지 않는 것이 바람직하다. 결국 중첩 null 확인 코드와 크게 다르지 않다. +- `orElse(T other)` : 값이 없을 때 기본값을 제공한다. +- `orElseGet(Supplier other)` : orElse의 **게으른** 버전. 값이 없을 때만 Supplier가 실행된다. 기본값 생성에 시간이 걸리거나, 값이 비었을 때만 기본값을 생성해야 하는 상황에 사용한다. +- `orElseThrow(Supplier exceptionSupplier)` : 값이 없으면 예외를 발생시킨다. get과 비슷하지만 발생시킬 예외의 종류를 선택할 수 있다. +- `ifPresent(Consumer consumer)` : 값이 존재할 때만 인수로 넘겨준 동작을 실행한다. 값이 없으면 아무 일도 일어나지 않는다. +- `ifPresentOrElse(Consumer action, Runnable emptyAction)` : **자바 9 추가.** Optional이 비었을 때 실행할 Runnable을 추가로 받는다는 점만 ifPresent와 다르다. + +### 11.3.6 두 Optional 합치기 + +`Optional`과 `Optional`를 인수로 받아 가장 저렴한 보험회사를 찾는 null 안전 버전 메서드를 구현한다고 하자. `isPresent`를 이용하면 다음처럼 구현할 수 있다. + +```java +public Optional nullSafeFindCheapestInsurance( + Optional person, Optional car) { + if (person.isPresent() && car.isPresent()) { + return Optional.of(findCheapestInsurance(person.get(), car.get())); + } else { + return Optional.empty(); + } +} +``` + +시그니처만으로 둘 다 값이 없을 수 있다는 정보를 보여주지만, 구현은 null 확인 코드와 다르지 않다. map과 flatMap을 이용하면 언랩 없이 한 줄로 재구현할 수 있다. + +```java +public Optional nullSafeFindCheapestInsurance( + Optional person, Optional car) { + return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c))); +} +``` + +person이 비어있으면 flatMap의 람다가 실행되지 않고 빈 Optional이 반환된다. car가 비어있으면 map이 빈 Optional을 반환한다. 둘 다 존재할 때만 findCheapestInsurance가 안전하게 호출된다. + +### 11.3.7 필터로 특정값 거르기 + +보험회사 이름이 'CambridgeInsurance'인지 확인하려면 기존에는 null 확인 후 getName을 호출해야 했다. + +```java +Insurance insurance = ...; +if (insurance != null && "CambridgeInsurance".equals(insurance.getName())) { + System.out.println("ok"); +} +``` + +Optional의 `filter` 메서드로 재구현할 수 있다. + +```java +Optional optInsurance = ...; +optInsurance.filter(insurance -> + "CambridgeInsurance".equals(insurance.getName())) + .ifPresent(x -> System.out.println("ok")); +``` + +filter는 프레디케이트를 인수로 받는다. Optional이 값을 가지며 프레디케이트와 일치하면 그 값을 반환하고, 그렇지 않으면 빈 Optional을 반환한다. Optional이 비어있으면 아무 동작도 하지 않는다. + +filter를 flatMap 체인 앞단에 조합할 수도 있다. 예를 들어 person이 minAge 이상의 나이일 때만 보험회사 이름을 반환하려면 다음처럼 구현한다. + +```java +public String getCarInsuranceName(Optional person, int minAge) { + return person.filter(p -> p.getAge() >= minAge) + .flatMap(Person::getCar) + .flatMap(Car::getInsurance) + .map(Insurance::getName) + .orElse("Unknown"); +} +``` + +프레디케이트 적용 결과가 false면 값이 사라지고 빈 Optional이 되므로, 이후 체인이 모두 건너뛰어져 "Unknown"이 반환된다. + +**표 11-1 Optional 클래스의 메서드** + +| 메서드 | 설명 | +| --- | --- | +| empty | 빈 Optional 인스턴스 반환 | +| filter | 값이 존재하며 프레디케이트와 일치하면 값을 포함하는 Optional을, 아니면 빈 Optional 반환 | +| flatMap | 값이 존재하면 인수로 제공된 함수를 적용한 결과 Optional을 반환하고, 값이 없으면 빈 Optional 반환 | +| get | 값이 존재하면 값을 반환하고, 없으면 NoSuchElementException 발생 | +| ifPresent | 값이 존재하면 지정된 Consumer를 실행하고, 없으면 아무 일도 일어나지 않음 | +| ifPresentOrElse | 값이 존재하면 Consumer를 실행하고, 없으면 emptyAction(Runnable)을 실행 (자바 9) | +| isPresent | 값이 존재하면 true, 없으면 false 반환 | +| map | 값이 존재하면 제공된 매핑 함수를 적용 | +| of | 값이 존재하면 값을 감싸는 Optional을 반환하고, null이면 NullPointerException 발생 | +| ofNullable | 값이 존재하면 값을 감싸는 Optional을 반환하고, null이면 빈 Optional 반환 | +| or | 값이 존재하면 같은 Optional을 반환하고, 없으면 Supplier에서 만든 Optional 반환 (자바 9) | +| orElse | 값이 존재하면 값을 반환하고, 없으면 기본값 반환 | +| orElseGet | 값이 존재하면 값을 반환하고, 없으면 Supplier에서 제공하는 값 반환 | +| orElseThrow | 값이 존재하면 값을 반환하고, 없으면 Supplier에서 생성한 예외 발생 | +| stream | 값이 존재하면 그 값만 포함하는 스트림을, 없으면 빈 스트림 반환 (자바 9) | + +--- + +## 11.4 Optional을 사용한 실용 예제 + +호환성 때문에 기존 자바 API는 Optional을 적절하게 활용하지 못하고 있다. 하지만 코드에 작은 유틸리티 메서드를 추가하는 방식으로 Optional 기능을 활용할 수 있다. + +### 11.4.1 잠재적으로 null이 될 수 있는 대상을 Optional로 감싸기 + +기존 자바 API는 요청한 값이 없으면 null을 반환한다. 예를 들어 Map의 get 메서드는 키에 대응하는 값을 찾지 못하면 null을 반환한다. get의 시그니처는 고칠 수 없지만 반환값을 Optional로 감쌀 수 있다. + +```java +Optional value = Optional.ofNullable(map.get("key")); +``` + +### 11.4.2 예외와 Optional 클래스 + +값을 제공할 수 없을 때 null 대신 예외를 발생시키는 API도 있다. 전형적인 예가 `Integer.parseInt(String)`으로, 문자열을 정수로 바꾸지 못하면 NumberFormatException을 발생시킨다. 거추장스러운 try/catch 대신 빈 Optional로 해결할 수 있도록 유틸리티 메서드를 만들 수 있다. + +```java +public static Optional stringToInt(String s) { + try { + return Optional.of(Integer.parseInt(s)); // 변환 가능하면 값을 포함하는 Optional + } catch (NumberFormatException e) { + return Optional.empty(); // 그렇지 않으면 빈 Optional + } +} +``` + +이런 메서드들을 포함하는 유틸리티 클래스 `OptionalUtility`를 만들어두면 필요할 때 재사용할 수 있다. + +### 11.4.3 기본형 Optional을 사용하지 말아야 하는 이유 + +스트림처럼 Optional도 `OptionalInt`, `OptionalLong`, `OptionalDouble` 등 기본형 특화 클래스를 제공한다. 하지만 사용을 권장하지 않는다. + +- 스트림과 달리 **Optional의 최대 요소 수는 한 개**이므로 기본형 특화로 성능을 개선할 수 없다. +- 기본형 특화 Optional은 `map`, `flatMap`, `filter` 등을 지원하지 않는다. +- 기본형 특화 Optional로 생성한 결과는 다른 일반 Optional과 혼용할 수 없다. + +### 11.4.4 응용 + +프로그램 설정 인수로 Properties를 전달하고, 프로퍼티 값을 초 단위의 지속 시간으로 해석한다고 하자. 지속 시간은 양수여야 하므로 양의 정수면 해당 정수를 반환하고 그 외에는 0을 반환한다. + +```java +Properties props = new Properties(); +props.setProperty("a", "5"); +props.setProperty("b", "true"); +props.setProperty("c", "-3"); + +assertEquals(5, readDuration(props, "a")); // 양수로 변환 가능 → 5 +assertEquals(0, readDuration(props, "b")); // 숫자가 아님 → 0 +assertEquals(0, readDuration(props, "c")); // 음수 → 0 +assertEquals(0, readDuration(props, "d")); // 프로퍼티 없음 → 0 +``` + +명령형으로 구현하면 if문과 try/catch 블록이 중첩되면서 복잡하고 가독성도 나쁘다. + +```java +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을 이용하면 하나의 유연한 코드로 재구현할 수 있다. + +```java +public int readDuration(Properties props, String name) { + return Optional.ofNullable(props.getProperty(name)) + .flatMap(OptionalUtility::stringToInt) + .filter(i -> i > 0) + .orElse(0); +} +``` + +프로퍼티가 없으면 ofNullable이 빈 Optional을 반환하고, 숫자가 아니면 stringToInt가 빈 Optional을 반환하고, 음수면 filter가 값을 제거한다. 이 과정 중 하나라도 빈 Optional이 되면 orElse에 의해 기본값 0이 반환된다. Optional과 스트림에서 사용한 이런 방식은 여러 연산이 서로 연결되는 **데이터베이스 질의문**과 비슷한 형식을 갖는다. + +--- + +## 11.5 마치며 + +1. 역사적으로 프로그래밍 언어에서는 **null 참조로 값이 없는 상황을 표현**해왔다. +2. 자바 8에서는 값이 있거나 없음을 표현할 수 있는 클래스 **`java.util.Optional`** 를 제공한다. +3. 팩토리 메서드 `Optional.empty`, `Optional.of`, `Optional.ofNullable` 등을 이용해서 Optional 객체를 만들 수 있다. +4. Optional 클래스는 스트림과 비슷한 연산을 수행하는 `map`, `flatMap`, `filter` 등의 메서드를 제공한다. +5. Optional로 값이 없는 상황을 적절하게 처리하도록 **강제**할 수 있다. 즉, 예상치 못한 null 예외를 방지할 수 있다. +6. Optional을 활용하면 더 좋은 API를 설계할 수 있다. 즉, 사용자는 **메서드의 시그니처만 보고도** Optional값이 사용되거나 반환되는지 예측할 수 있다. + +--- + +## 퀴즈 + +**문제 1.** 다음 코드는 컴파일되지 않는다. 이유를 설명하고, 컴파일되도록 고치시오. (getCar는 `Optional`를, getInsurance는 `Optional`를 반환한다.) + +```java +Optional optPerson = Optional.of(person); + +Optional name = + optPerson.map(Person::getCar) + .map(Car::getInsurance) + .map(Insurance::getName); +``` + +
+정답 + +getCar가 `Optional`를 반환하므로 map의 결과는 `Optional>`이 된다. 즉 중첩 Optional이 생기기 때문에 다음 map(Car::getInsurance)을 적용할 수 없어 컴파일 에러가 발생한다. + +flatMap을 사용하면 변환 결과인 중첩 Optional이 일차원으로 평준화된다. 마지막 getName은 String을 반환하므로 map을 그대로 쓴다. + +```java +Optional name = + optPerson.flatMap(Person::getCar) + .flatMap(Car::getInsurance) + .map(Insurance::getName); +``` + +
+ +**문제 2.** 다음은 두 개의 Optional을 인수로 받아 가장 저렴한 보험회사를 찾는 null 안전 버전의 메서드다. isPresent와 get을 사용하지 않고(언랩하지 않고), 이 장에서 배운 메서드만으로 한 줄로 재구현하시오. + +```java +public Optional nullSafeFindCheapestInsurance( + Optional person, Optional car) { + if (person.isPresent() && car.isPresent()) { + return Optional.of(findCheapestInsurance(person.get(), car.get())); + } else { + return Optional.empty(); + } +} +``` + +
+정답 + +flatMap과 map을 조합하면 어떤 조건문도 사용하지 않고 한 줄로 구현할 수 있다. + +```java +public Optional nullSafeFindCheapestInsurance( + Optional person, Optional car) { + return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c))); +} +``` + +person이 비어있으면 flatMap에 전달한 람다가 실행되지 않고 그대로 빈 Optional을 반환한다. car가 비어있으면 map이 빈 Optional을 반환한다. 둘 다 값이 있을 때만 findCheapestInsurance가 안전하게 호출된다. + +
+ +**문제 3.** 다음은 Properties에서 프로퍼티 값을 읽어 양의 정수면 그 값을, 그 외에는(프로퍼티 없음, 숫자 아님, 음수) 0을 반환하는 명령형 코드다. Optional의 기능과 유틸리티 메서드 `OptionalUtility.stringToInt`(변환 실패 시 빈 Optional 반환)를 이용해서 하나의 체인으로 재구현하시오. + +```java +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; +} +``` + +
+정답 + +```java +public int readDuration(Properties props, String name) { + return Optional.ofNullable(props.getProperty(name)) + .flatMap(OptionalUtility::stringToInt) + .filter(i -> i > 0) + .orElse(0); +} +``` + +프로퍼티가 없으면 ofNullable이 빈 Optional을 반환하고, 숫자가 아니면 stringToInt가 빈 Optional을 반환하며, 음수면 filter가 값을 제거한다. 이 중 하나라도 빈 Optional이 되면 orElse가 기본값 0을 반환하고, 그렇지 않으면 Optional에 들어 있는 양수를 반환한다. + +