Originally posted by **MyeoungDev** June 14, 2023
# 11.0.0 null 대신 Optional 클래스
- 1965년 토니 호어(Tony Hoare) 라는 영국 컴퓨터 과학자가 최초의 프로그래밍 언어 중 하나인 알골(ALGOL W) 를 설계하면서 처음 null 참조가 등장했다.
- ‘구현하기가 쉬웠기 때문에 null을 도입했다고 ‘ 라고 말하며 null이 세상에 등장하게 되었다.
- 따라서, null 과 관련된 예외들이 미친듯이 생겨났다.
- 토니 호어는 당시 null을 만든 결정을 가리켜 ‘십억 달러짜리 실수’라고 표현했다.
- 프로그래밍역사가 50년이 넘은 지금 해당 액수는 더 클것이다. 이것이 null이다.
# 11.1 값이 없는 상황을 어떻게 처리할까?
```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();
}
```
위의 코드는 상당히 문제가 많다.
`person` 객체에 `car` 가 존재하지 않는다면?
`car` 객체는 존재하더라도 `insurance` 가 존재하지 않는다면?
이는 런타임에 `NullPointerException` 을 발생시킬 문제가 다분한 코드이다.
## 11.1.1 보수적인 자세로 NullPointerException 줄이기
매우매우 보수적으로 `NullPointerException` 을 줄이는 코드를 봐보자.
```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";
}
```
위와 같이 작성이 가능하지만, 모든 변수가 null인지 의심하므로 변수를 접근할 때마다 중첩된 if문이 추가되면서 코드 들여쓰기 수준이 증가한 모습이다.
이와 같은 **반복 패턴(recurring pattern)코드**를 **깊은 의심(Deep Doubt)** 라고 부른다.
이게 반복되면 **코드 구조가 엉망이되고, 가독성이 떨어진다.**
```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();
}
```
앞서 본 코드를 위의 코드처럼 변경할 수 있다.
하지만, 이 코드또한 좋은 코드는 아니다.
중첩 if문을 없애면서 들여쓰기가 존재하지는 않지만, **하나의 메서드에 4개의 출구**가 생겼기 때문이다.
따라서, 각 return 문들 때문에 유지보수가 힘들어지게 된다.
## 11.1.2 null 때문에 발생하는 문제
- **에러의 근원이다.**
- `NullPointerException` 은 자바에서 가장 흔히 발생하는 에러다.
- **코드를 어지럽힌다.**
- 때로는 **중첩된 null 확인 코드를 추가**해야 하므로 **null 때문에 코드 가독성이 떨어진다.**
- **아무 의미가 없다.**
- **null은 아무 의미도 표현하지 않는다.** 특히 정적 형식 언어에서 값이 없음을 표현하는 방법으로는 적절하지 않다.
- **자바 철학에 위배된다.**
- **자바는 개발자로부터 모든 포인터를 숨겼다.**
- 하지만, 예외가 있는데 그것이 바로 null 포인터다.
- **형식 시스템에 구멍을 만든다.**
- **null은 무형식**이며 정보를 포함하고 있지 않으므로 **모든 참조 형식에 null을 할당할 수 있다.**
- 이런 식으로 **null이 할당되기 시작하면서** 시스템의 다른 부분으로 **null이 퍼졌을 때** 애초에 **null이 어떤 의미로 사용되었는지 알 수 없다.**
## 11.1.3 다른 언어는 null 대신 무얼 사용하나?
- 그루비(Groovy)
- 안전 내비게이션 연산자 (Safe Navigation Operator ?.)
- `def carinsuranceName = person?.car?.insurance?.name`
- 호출체인에 null이 있으면 결과로 null 반환
- 하스켈(Haskell)
- 선택형 값(Optional Value)을 지정할 수 있는 `Maybe` 형식
- 스칼라(Scala)
- T 형식의 값을 갖거나 아무 값도 갖지 않을 수 있는 `Option[T]` 형식 제공.
# 11.2 Optional 클래스 소개
Java 8은 하스켈과 스칼라의 영향을 받아서 `java.util.Optional` 라는 새로운 클래스를 제공한다.
`Optional` 은 **선택형값을 캡슐화하는 클래스이다.**
![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/56027263-8091-4e38-a7ed-0c1b7aa063bf)
값이 있으면 `Optional` 클래스는 값을 감싼다.
반면, 값이 없으면 `Optional.empty` 메서드로 `Optional` 을 반환한다.
`Optional.empty()` 는 `Optional.empty` 는 `Optional` 의 특별한 싱글턴 인스턴스를 반환하는 정적 팩토리 메서드다.
### 그렇다면 null과 Optional.empty()는 무엇이 다를까?
- null을 참조하려 하면 `NullPointerException` 이 발생한다.
- `Optional.empty()` 는 `Optional` 객체이므로 이를 다양한 방식으로 활용할 수 있다.
- `Optional` 클래스를 사용하면 값이 없을 수 있음을 명시적으로 보여준다. 즉 semantic이 명확해진다.
- `Optional` 을 이용하면 값이 없는 상황에 우리 데이터에 문제가 있는 것인지 아니면 알고리즘의 버그인지 명확하게 구분할 수 있다.
- 반면, 일반 객체 형식을 사용할 경우 null 참조가 할당될 수 있는데 이것이 올바른 값인지 잘못된 값인지 판단할 수 없다
- 그러나, 모든 null 참조를 `Optional` 로 대치하는 것은 바람직하지 않다.
- `Optional` 의 역할은 더 이해하기 쉬운 API를 설계하도록 돕는 것이다.
- 즉, 메서드의 시그니처만 보고도 선택형값인지 여부를 구별할 수 있다.
- `Optional` 을 언랩해서 값이 없을 수 있는 상황에 적절하게 대응하도록 강제하는 효과가 있다.
## 11.3 Optional 적용 패턴
`Optional` 형식을 이용해서 도메인 모델의 의미를 더 명확하게 만들 수 있으며, null 참조 대신 값이 없는 상황을 표현할 수 있다.
## 11.3.1 Optional 객체 만들기
- **빈 Optional**
```java
/* 정적 팩토리 메서드 Optional.empty()로 빈 Optional 객체를 얻을 수 있다. */
Optional optCar = Optional.empty();
```
- **null이 아닌 값으로 Optional 만들기**
```java
/* 정적 팩토리 메서드 Optional.of로 null이 아닌 값을 포함하는 Optional을 만들 수 있다. */
Optional optCar = Optional.of(car);
```
- **null값으로 Optional 만들기**
```java
/* 정적 팩토리 메서드 Optional.ofNullable로 null값을 저장할 수 있는 Optional을 만들 수 있다.
* car가 null이면 빈 Optional 객체가 반환된다.
*/
Optional optCar = Optional.ofNullable(car);
```
## 11.3.2 맵으로 Optional의 값을 추출하고 변환하기
`Optional` 에서 `get` 메서드를 이용하면 안에 존재하는 값을 가져올 수 있다.
그러나, `Optional` 이 비어있으면 `get` 을 호출했을 때 예외가 발생한다.
즉, `Optional` 을 잘못 사용하면 결국 null을 사용했을 때와 같은 문제를 겪을 수 있다.
따라서 다른 방법을 사용하는 것이 좋다.
```java
/* 일반적인 코드 */
String name = null;
if(insurance != null) {
name = insurance.getName();
}
/* 위와 같은 유형의 패턴에서 사용할 수 있는 Optional.map */
Optional optInsurance = Optional.ofNullable(insurance);
// Optional이 값을 포함하면 map의 인수로 제공된 함수가 값을 바꾼다.
// 비어있으면 아무 일도 일어나지 않는다.
Optional name = optInsurance.map(Insurance::getName);
```
## 11.3.3 flatMap으로 Optional 객체 연결
위와 같은 코드에서는 필드값을 `Optional` 로 변경해서 사용하는 것이 좋다.
각 필드값이 null일 수 있다는 것을 명시적으로 표현이 가능하고, 그에 대한 처리를 사용자에게 강제할 수 있기 때문이다.
따라서 Person, Car 클래스를 이와 같이 변경해서 사용할 수 있다.
```java
public class Person {
private Optional car;
public Optional getCar() {
return car;
}
}
public class Car{
private Optional insuarnce;
public Optional getInsuarnce() {
return insuarnce;
}
}
```
그리고 `map` 메서드를 이용하여 `getCarInsuranceName` 메서드를 아래와 같이 변경할 수 있다.
```java
/* 변경 전 코드 */
public String getCarInsuranceName(Person person) {
return person.getCar().getInsurance().getName();
}
/* 변경 후 코드 */
Optional name =
optPerson.map(Person::getCar)
.map(Car::getInsurance)
.map(Insurance::getName);
```
그러나, 위의 코드는 컴파일 되지 않는다.
`optPerson` 의 경우 `Optional` 이므로 `map` 메서드를 호출할 수 있다.
그러나, 그 이후 변수는 `Optional>` 으로 이중으로 `Optional` 객체로 감싸져 있는 형태를 띄게 된다.
따라서, 그 이후 메서드 체인에서 `map` 메서드를 호출하지 못하게 된다.
그렇다면 이 메서드를 어떻게 변경해야 될까?
정답은 `Optional.flatMap` 메서드를 이용하여 `Stream.flatMap` 메서드 처럼 이중 `Optional` 객체를 1차원 `Optional` 객체로 변경하여 사용할 수 있다.
```java
/* Optional.flatMap 을 사용한 코드 */
public String getCarInsuranceNameOptional(Optional person) {
return person.flatMap(Person::getCar)
.flatMap(Car::getInsurance)
.map(Insurance::getName)
.orElse("Unknown"); // Default Value
}
```
![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/41efaf1e-a7cb-49c8-98e1-58ad4aed0688)
> 도메인 모델에 Optional을 사용했을 때 데이터를 직렬화할 수 없는 이유
>
> 자바 언어 아키텍트인 브라이언 고츠(Brian Goetz)는 `Optional` 의 용도가 선택형 반환값을 지원하는 것이라고 명확하게 지정하였다.
>
> 따라서, `Optional` 클래스는 필드 형식으로 사용할 것을 가정하지 않았으므로 `Serializable` 인터페이스를 구현하지 않는다.
>
> 즉, 우리 도메인 모델에 `Optional` 을 사용한다면 직렬화(Serializable) 모델을 사용하는 도구나 프레임워크에 문제가 생길 수 있다.
>
> 이와 같은 단점에도 불구하고 여전히 `Optional` **을 사용해서 도메인 모델을 구성하는 것이 바람직하다고 책에서는 기술하고 있다.
> 특히 객체 그래프에서 일부 또는 전체 객체가 null일 수 있는 상황이라면 더욱 그렇다고 한다.
>
> 만약, 직렬화 모델이 필요하다면 이와같이** `Optional` **값을 반환받을 수 있는 메서드를 추가하는 방식을 권장한다고 한다.**
>
> 참고로, **Jackson 라이브러리**는 jackson-modules-java8 project 프로젝트부터 **Optional을 지원**하여, **empty일 경우에는 null**을 **> > 값이 있는 경우**에는 **값을 꺼내서 처리**하도록 지원하고 있다
****
>
```java
public class Person {
private Car car;
public Optional getCarAsOptional() {
return Optional.ofNullable(car);
}
}
```
## 11.3.4 Optional 스트림 조작
Java 9에서 `Optional` 을 포함하는 스트림 값을 쉽게 처리할 수 있도록 `Optional` 에 `stream()` 메서드를 추가 했다.
`Optional` 스트림을 값을 가진 스트림으로 변환할 때 이 기능을 유용하게 활용할 수 있다.
```java
public Set getCarInsuranceNames(List persons) {
return persons.stream()
// 사람 목록을 각 사람이 보유한 자동차의 Optional 스트림으로 변환
.map(Person::getCar)
// FlatMap 연산을 이용해 Optional 을 해당 Optional 로 변환
.map(optCar -> optCar.flatMap(Car::getInsurance))
// Optional 를 해당 이름의 Optional 으로 매핑
.map(optIns -> optIns.map(Insurance::getName))
// Stream>을 현재 이름을 포함하는 Stream 으로 변환
.flatMap(Optional::stream)
// 결과 문자열을 중복되지 않은 값을 갖도록 집합으로 수집
.collect(Collectors.toSet());
}
```
위의 예제는 `Optional` 로 값이 감싸져 있으므로 기존의 Stream 보다 과정이 조금 더 복잡해 졌다.
사람이 차를 갖고있지 않거나 또는 차가 보험에 가입되어 있지 않아 결과가 비어있을 수 있다.
`Optional` 덕분에 이런 종류의 연산을 `null` 걱정없이 안전하게 처리할 수 있지만 마지막 결과를 얻으려면 빈 `Optional` 을 제거하거 값을 언랩해야 한다는 문제점이 존재한다.
## 11.3.5 디폴트 액션과 Optional 언랩
빈 `Optional` 인 상황에서 기본값을 반환하도록 `orElse` 로 `Optional` 을 읽었다.
`Optional` 클래스는 이 외에도 인스턴스에 포함된 값을 읽는 다양한 방법을 제공한다.
- `get()`
- 값을 읽는 가장 간단한 메서드면서 동시에 **가장 안전하지 않은 메서드**.
- 래핑된 **값이 존재하면 값을 반환**, **없으면** `NoSuchElementException`
- `Optional` 에 값이 반드시 있다고 가정할 수 있는 상황이 아니면 사용하지 않는 것이 바람직함.
- `orElse()`
- `Optional` 이 **값을 포함하지 않을 때 기본값을 제공.**
- `orElseGet(Supplier extends T other>)`
- `orElse` 메서드에 대응하는 Lazy 버전의 메서드이다.
- `Optional` 에 값이 없을 때만 `Supplier` 가 실행되기 때문이다.
- 디폴트 값 생성 시간이 오래걸리거나, 기본값이 반드시 필요한 상황에 사용한다.
- `orElseThrow(Supplier extends X> consumer)`
- `Optional` 이 **비어있을 때 예외를 발생**시킨다.
- `ifPresent(Consumer super T> consumer)`
- 값이 존재할 때 인수로 넘겨준 동작을 실행할 수 있다.
- 값이 없으면 아무 일도 일어나지 않는다.
- `ifPresentOrElse(Consumer super T> action, Runnable emptyAction)`
- `Optional` 이 비었을 때 실행할 수 있는 `Runnable` 을 인수로 받는다는 점만 `ifPresent` 와 다르다.
## 11.3.6 두 Optional 합치기
두개의 인자를 매개변수로 받는 복잡한 비즈니스 로직이 구현된 메서드가 있다고 가정하겠다.
```java
public Insurance findCheapestInsurance(Person person, Car car) {
// 다양한 보험회사가 제공하는 서비스 조회
// 모든 결과 데이터 비교
return cheapestCompany;
}
```
위의 메서드에서 두 인자를 `Optional` 로 받아서 null-safe 한 메서드를 구현할 경우는 아래와 같다.
```java
public Optional nullSafeFindCheapestInsurance(
Optional,
Optional car) {
if(person.isPresent() && car.isPresent) {
return Optional.of(findCheapestInsurance(person.get(), car.get()));
} else {
return Optional.empty();
}
}
```
위 메서드의 장점은 `person` 과 `car` 의 시그니처만으로 둘 다 아무 값도 반환하지 않을 수 있다는 정보를 명시적으로 보여준다는 것이다.
그렇다면 앞서 얘기했던 `map` 과 `flatMap` 을 이용하여 코드를 변경할 수 있지 않을까?
```java
public Optional nullSafeFindCheapestInsurance(
Optional person,
Optional car) {
return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c)));
}
```
(솔직히 말해서 바로 위와 같은 코드를 짜내기는 숙련이 부족한것 같다.)
- 첫 번째 `Optional` 에 `flatMap` 을 호출했으므로 첫 번째 `Optional` 이 비어있다면 인수로 전달한 람다 표현식이 실행되지 않고 그대로 빈 `Optional` 을 반환한다.
- 반면 `person` 값이 있으면 `flatMap` 메서드에 필요한 `Function` 의 입력으로 `person` 을 사용한다.
- 두 번째 `Optional` 에 `map` 을 호출하므로 `Optional` 이 `car` 값을 포함하지 않으면 `Function` 은 빈 `Optional` 을 반환하므로 결국 해당 메서드는 빈 `Optional` 을 반환하게 된다.
- 마지막으로 모두 존재하면 `map` 메서드로 전달한 람다 표현식이 `findCheapestInsurance` 메서드를 안전하게 호출 할 수 있다.
## 11.3.7 필터로 특정값 거르기
`Optional` 의 `filter` 메서드는 `Predicate` 를 인수로 받아, 조건과 일치하면 그 값을 반환하고 그렇지 않으면 빈 `Optional` 객체를 반환한다.
`filter` 메서드를 통해 특정 값을 가져올 수 있게 된다.
```java
Optional optInsurance = ...;
optInsurance.filter(insurance ->
"CambridgeInsurance".equals(insurance.getName()))
.ifPresent(x -> System.out.println("ok"));
```
```java
/* minAge 이상의 나이를 먹은 사람만 선택하도록 Perdicate를 설정해서
* filter 메서드에 전달하는 방식으로 Optional filtering 하는 예제
*/
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");
}
```
# 11.4 Optional을 사용한 실용 예제
## 11.4.1 잠재적으로 null이 될 수 있는 대상을 Optional로 감싸기
```java
/* Map 에서 key 값으로 데이터 조회하기 */
Object value = map.get("key");
/* to Optional */
Optional
Discussed in https://github.com/FeGwan-Training/FeGwan/discussions/24