Skip to content

Commit

Permalink
[8회차] 아이템 54, 55 - 호티(윤주호)
Browse files Browse the repository at this point in the history
[8회차] 아이템 54, 55 - 호티(윤주호)
  • Loading branch information
Dobby-Kim authored Jun 7, 2024
2 parents 95a49bf + aff9065 commit f39022c
Show file tree
Hide file tree
Showing 2 changed files with 368 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# 아이템 54: null이 아닌, 빈 컬렉션이나 배열을 반환하라

다음은 주변에서 흔히 볼 수 있는 메서드다

**컬렉션이 비었으면 null을 반환한다. (절대 따라 하면 안되는 코드이다)**

```java
private final List<Cheese> cheesesInStock = ...;

/**
* @return 매장 안의 모든 치즈 목록을 반환한다.
* 단, 재고가 하나도 없다면 null을 반환한다.
*/
public List<Cheese> getCheeses() {
return cheesesInStock.isEmtpy() ? null
: new ArrayList<>(cheesesInStock);
}
```

재고가 없다고 해서 특별히 취급할 이유는 없다. 그럼에도 이 코드처럼 null을 반환한다면, 클라이언트는 이 null을 처리하는 코드를 추가로 작성해야 한다.

```java
List<Cheese> cheeses = shop.getCheeses();
if (cheeses != null && cheeses.contains(Cheese.STILTON))
...
}
```

null을 반환하려면 반환하는 쪽에서도 이 상황을 특별히 취급해줘야 해서 코드가 더 복잡해진다.

### **빈 컨테이너를 할당하는 데도 비용이드니**
**빈 컨테이너보다는 null을 반환하는 것이 나을까?**

이는 두 가지 면에서 틀린 주장이다.

1. 성능 분석 결과 이 할당이 성능 저하의 주범이라고 확인되지 않는 한
이정도의 성능 차이는 신경 쓸 수준이 못 된다.
`→ retrun new ArraryList<>(cheeseInStock);`
2. 빈 컬렉션과 배열은 굳이 새로 할당하지 않고도 반환할 수 있다.
**예시 :** `(ex. Collections.emptyList, Collections.emptySet, Collections.emptyMap)`

가능성은 작지만, 사용 패턴에 따라 빈 컬렉션 할당이 성능을 눈에 띄게 떨어뜨릴 수도 있다.
해법은 간단하다, 불변 컬렉션을 반환하는 것이다. 왜냐하면 불변 객체는 자유롭게 공유해도 안전하기 때문이다.
`(ex. Collections.emptyList, Collections.emptySet, Collections.emptyMap)`

단, 이 역시 최적화에 해당하니 꼭 필요할 때만 사용하자.
최적화가 필요하다고 판단되면 수정 전화 후의 성능을 측정하여 실제로 성능이 개선되는지 꼭 확인하자

---

이는 배열을 사용할 때도 마찬가지다. 절대 null을 반환하지 말고 길이가 0인 배열을 반환하라.

```java
private static final Cheese[] EMTPY_CHEESE_ARRAY = new Cheese[0];

public Cheese[] getCheeses() {
return cheesesInStock.toArray(**EMTPY_CHEESE_ARRAY**);
}
```

> **`<T> T[] toArray(T[] a)`**: 이 메소드는 제네릭을 사용하여, 지정된 타입의 배열을 인자로 받고, 리스트의 요소들을 이 배열에 담아 반환합니다.
**인자로 전달된 배열의 길이가 리스트의 요소 수보다 크거나 같으면, 리스트의 요소들이 이 배열에 복사되고 반환됩니다**.
**배열의 길이가 리스트의 요소 수보다 작다면, 메소드는 새로운 배열을 생성하여 그 배열에 요소들을 복사한 후 반환합니다.**
>
>
> ```java
> java코드 복사
> List<String> list = new ArrayList<>();
> list.add("apple");
> list.add("banana");
> String[] array = list.toArray(new String[0]); // 빈 배열을 인자로
>
> ```
>
> 위 예제에서는 `String[]` 타입의 배열을 반환합니다. 배열 크기가 리스트의 크기보다 작은 경우 (여기서는 0), `toArray` 메소드는 적절한 크기의 새 배열을 만들어서 반환합니다. 이 방식은 타입 안전성을 제공하고, 반환된 배열을 추가적인 타입 캐스팅 없이 바로 사용할 수 있게 해줍니다.
>
길이 0짜리 배열을 미리 선언해두고 매번 그 배열을 반환하면 된다. 길이 0인 배열은 모두 불변이기 때문이다.
단순히 성능을 개선할 목적이라면 `toArray`에 넘기는 배열을 미리 할당하는 건 추천하지 않는다.
오히려 성능이 떨어진다는 연구결과가 있다
> 특정 상황에서는 `toArray(new T[0])` 방식이 `toArray(new T[size])`보다 빠를 수 있습니다, 이는 내부적으로 최적화되어 있기 때문입니다.
>
```java
return cheesesInStock.toArray(new Cheese[**cheesesInStock.size()**]);
```
# **정리**

- null이 아닌, 빈 배열이나 컬렉션을 반환하라.
- null을 반환하는 API는 사용하기 어렵고 오류 처리 코드도 늘어난다.
그렇다고 성능이 좋은 것도 아니다.
274 changes: 274 additions & 0 deletions 08장/아이템_55/옵셔널_반환은_신중히_하라_호티.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
# 아이템 55: 옵셔널 반환은 신중히 하라

## 반환값이 없을 때 취할 수 있는 방법 2가지 (JAVA 8 전)

### 1. 예외 던지기

### 문제점

- 예외는 진짜 예외적인 상황에서만 사용해야 한다.
- 예외를 생성할 때 **스택 추적 전체**를 캡처하므로 비용이 만만치 않다.

### 2. null을 반환(반환 타입이 객체 참조일 경우)

### 문제점

- 별도의 null 처리 코드를 추가해야 한다.
- null 처리를 무시하고 반환된 null은 언젠가 `NullPointerException`이 발생할 수 있다.

## Optional (JAVA 8 이후)

- null이 아닌 T 타입 참조를 하나 담거나, 혹은 아무것도 담지 않을 수 있다.
- 아무것도 담지 않은 옵셔널 - '`비었다`',
어떤 값을 담은 옵셔널 - '`비지 않았다`'고 한다.
- 옵셔널은 원소를 최대 1개 가질 수 있는 '불변' 컬렉션이다.
Optional<T>가 Collection<T>를 구현하지는 않았지만, 원칙적으로는 그렇다.
- 예외를 던지는 메서드보다 유연하고 사용하기 쉬우며, null을 반환하는 메서드보다 오류 가능성이 적다.

### 예제

> **코드 55-1 컬렉션에서 최댓값을 구한다(컬렉션이 비어있으면 예외를 던진다)**
>
```java
public static <E extends Comparable<E>> E max (Collection<E> c) {
if(c.isEmpty())
throw new IllegalArgumentException("빈 컬렉션");

E result = null;
for (E e : c )
if (result==null || e.compareTo(result) > 0)
result = Objects.requireNonNull(e);

return result;
}
```

> **코드 55-2 컬렉션에서 최댓값을 구해 Optional로 반환한다.**
>
```java
public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
if (c.isEmpty())
return **Optional.empty();**

E result = null;
for (E e : c)
if(result==null||e.compareTo(result) > 0)
result = Objects.requireNonNull(e);

return **Optional.of(result);**
}
```

- 빈 옵셔널은 **`Optional.Empty()`**로 만들고, 값이 든 옵셔널은 **`Optional.of(value)`**로 생성했다.
- `Optional.of(value)`에 null을 넣으면 `NullPointerException`을 던지니 주의하자.
- null 값도 허용하는 옵셔널을 만들려면 `Optional.ofNullable(value)`를 사용하면 된다. 
**옵셔널을 반환하는 메서드에서는 절대 null을 반환하지 말자.
→ 옵셔널을 도입한 취지를 완전히 무시하는 행위**

> **코드 55-3 컬렉션에서 최댓값을 구해 Optional로 반환한다 - 스트림 버젼**
>
```java
public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
return c.stream().max(Comparator.naturalOrder());
}
```

- 스트림의 종단 연산 중 상당수가 옵셔널을 반환한다.

---

### 그렇다면 null을 반환하거나 예외를 던지는 대신 옵셔널 반환을 선택해야 하는 기준은 무엇인가?

**옵셔널은 검사 예외와 취지가 비슷하다(아이템 71)** 

### 즉, 반환값이 없을 수도 있음을 API 사용자에게 명확하게 알려주어야 한다.

- 비검사 예외를 던지거나 `null`을 반환한다면 API 사용자가 그 사실을 인지하지 못해 끔찍한 결과가 나온다.
- 하지만 검사 예외를 던지면 클라이언트에서는 반드시 이에 대처하는 코드를 작성해 놓아야 한다.

### 메서드가 옵셔널을 반환한다면 클라이언트는 값을 받지 못했을 때 취할 행동을 선택해야 한다.

> 코드 55-4 옵셔널 활용1 -
**`orElse` :** **기본값을 정해둘 수 있다**
>
```java
String lastWordInLexicon = max(words).orElse("단어 없음...");
```

> 코드 55-5 옵셔널 활용2 -
**`orElseThrow` : 원하는 예외를 던질 수 있다**
>
```java
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
```

- 위 코드처럼 상황에 맞는 예외를 던질 수 있다.
- 실제 예외가 아니라 예외 팩터리를 건넨 것에 주목하자. 이렇게 하면 예외가 실제로 발생하지 않는한 **예외 생성 비용은 들지 않는다.**

> 코드 55-6 옵셔널 활용 3 -
**`get` : 항상 값이 채워져 있다고 가정한다**
>

```java
Element lastNobleGas = max(Elements.NOBLE_GASES).get();
```

- 옵셔널에 항상 값이 없다면 NoSuchElementException이 발생할 것이다.

이외에 이따금 **기본값을 설정하는** 비용이 아주 커서 부담이 되면, `Supplier`를 인수로 받는 `orElseGet`을 사용하면 **값이 처음 필요할 때 `Supplier`를 사용해 생성하므로 초기 설정 비용을 낮출 수 있다.
(→ orElseThrow와 비슷하게 팩터리를 넘긴 느낌)**

이외에 filter, map, flatMap, ifPresent 메서드가 있다.

---

여전히 적합한 메서드를 찾지 못했다면 `isPresent` 메서드를 살펴보자

### isPresent

- 안전 벨브 역할의 메서드로, 옵셔널이 채워져 있으면 true를, 비어 있으면 false를 반환한다.
- 신중히 사용해야 하는데, 실제로 isPresent를 쓴 코드 중 상당수는 앞서 언급한 메서드들로 대체할 수 있으며, 그렇게 하면 더 짧고 명확하고 용법에 맞는 코드가 되기 때문이다.

> 부모 프로세스의 프로세스 ID를 출력하거나, 부모가 없다면 “N/A”를 출력하는 코드
>

```java
Optional<ProcessHandle> parentProcess = ph.parent();
System.out.println("부모 PID:" + (parentProcess.**isPresent()** ?
String.valueOf(parentProcess.get().pid()) : "N/A"));
```

이 코드는 `Optional`의 `map`을 사용하여 다음처럼 다듬을 수 있다.

### map

```java
System.out.println("부모 PID:" + ph.parent()
.map(h-> String.valueOf(h.pid))).orElse("N/A");
```

스트림을 사용한다면 옵셔널들을 `Stream<Optional<T>>`로 받아서, 그 중 채워진 옵셔널들에서 값을 뽑아 `Stream<T>`에 건네 담아 처리하는 경우가 드물지 않다.

### filter

```java
streamOfOptionals
.filter(Optional::isPresnet)
.map(Optional::get)
```

`Optional`에 값이 있다면, 그 값을 꺼내서 스트림에 매핑한다.

### flatMap

자바 9에서는 Optional에 stream() 메서드가 추가되었다. 이 메서드는 OptionalStream으로 변환해주는 어댑터다.
옵셔널에 값이 있으면 그 값을 원소로 담은 스트림으로, 값이 없다면 빈 스트림으로 변환한다.

```java
streamOfOptionals
.flatMap(Optional::stream)
```

### flatMap 설명

Java의 스트림 API에서 `flatMap` 메소드는 매우 유용한 변환 연산자 중 하나입니다. 이 메소드는 스트림의 각 요소를 다른 스트림으로 변환하고, 이렇게 생성된 모든 스트림을 하나의 스트림으로 평탄화(flatten)합니다. 이를 통해 복잡한 구조를 갖는 컬렉션을 단일 스트림으로 간편하게 변환할 수 있습니다.

### 사용법

`flatMap`은 `Function`을 매개변수로 받습니다. 이 함수는 스트림의 각 요소를 새로운 스트림으로 매핑합니다. 그리고 `flatMap`은 이 매핑된 스트림들을 모두 하나의 "평탄화된" 스트림으로 합치게 됩니다.

### 예제

예를 들어, 리스트의 리스트 구조가 있을 때 모든 요소를 단일 리스트로 평탄화할 수 있습니다.

```java
java코드 복사
List<List<String>> listOfLists = Arrays.asList(
Arrays.asList("apple", "banana"),
Arrays.asList("carrot", "daikon"),
Arrays.asList("eggplant", "fig")
);

List<String> flatList = listOfLists.stream()
.flatMap(List::stream)
.collect(Collectors.toList());

System.out.println(flatList);

```

이 코드는 `listOfLists`의 각 리스트를 스트림으로 변환(`List::stream`)하고, `flatMap`을 통해 이러한 스트림들을 하나의 스트림으로 평탄화하여 모든 요소를 포함하는 단일 리스트를 생성합니다. 결과적으로 출력은 `["apple", "banana", "carrot", "daikon", "eggplant", "fig"]`가 됩니다.

### 사용 시나리오

`flatMap`은 주로 아래와 같은 시나리오에서 사용됩니다:

1. **다중 레벨 컬렉션 평탄화**: 중첩된 리스트나 맵 등의 복잡한 데이터 구조를 단일 스트림으로 평탄화할 때 유용합니다.
2. **데이터 변환**: 각 요소가 여러 개의 결과를 생성할 수 있는 변환을 적용할 때. 예를 들어, 문자열에서 단어로 분리하여 모든 단어의 스트림을 생성할 때 사용할 수 있습니다.
3. **옵셔널 처리**: `Optional` 객체의 스트림에서, 값이 존재하는 경우만 처리하고 싶을 때 `flatMap`을 사용하여 비어있지 않은 값을 추출할 수 있습니다.

```java
java코드 복사
List<Optional<String>> options = Arrays.asList(Optional.of("apple"), Optional.empty(), Optional.of("banana"));

List<String> filteredList = options.stream()
.flatMap(Optional::stream)
.collect(Collectors.toList());

```

이 예에서는 `Optional` 객체가 비어있지 않은 경우에만 값을 포함하는 리스트를 생성합니다.

### 결론

`flatMap`은 중첩된 스트림이나, 여러 값을 포함하는 객체를 처리할 때 매우 강력한 도구입니다. 이를 통해 복잡한 데이터 구조를 간단하고 효과적으로 다룰 수 있으며, 데이터를 쉽게 변환하고 합칠 수 있습니다.

### Optional를 사용하면 안되는 경우

- **컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입은 옵셔널로 감싸면 안 된다.**
- **빈 `Optional<List>`를 반환하기보다는 빈 `List`를 반환하는게 좋다.**
**(아이템 54) 빈 컨테이너를 그대로 반환하면 클라이언트에 옵셔널 처리 코드를 넣지 않아도 된다.**
- 참고로 `ProcessHandle.Info`인터페이스의 `arguments` 메서드는 `Optional<String[]>`을 반환하는데, 이는 예외적인 경우이니 따라 하지 말자

## 언제 Optional를 사용해야 하는가?

- **결과가 없을 수 있으며, 클라이언트가 이 상황을 특별하게 처리해야 한다면 Optional를 반환한다.**

### 성능 이슈

- Optional도 엄연히 새로 할당하고 초기화해야 하는 객체이고, 그 안에서 값을 꺼내려면 메서드를 호출해야 하니 한 단계를 더 거치는 셈이다.
그래서 성능이 중요한 상황에서는 옵셔널이 맞지 않을 수 있다.
- 박싱된 기본 타입을 담는 옵셔널은 기본 타입 자체보다 무거울 수밖에 없다.
- 값을 두 겹이나 감싸기 때문이다.
그래서 자바 API 설계자들은 int, long, double 전용 옵셔널 클래스들을 준비해놨다. 바로 OptionalInt, OptionalLong, OptionalDouble이다.
이 옵셔널들도 Optional가 제공하는 메서드를 거의 다 제공한다.
- 이렇게 대체제까지 있으니 **박싱된 기본 타입을 담은 옵셔널을 반환하는 일은 없도록 하자.** 
상대적으로 덜 중요한 Boolean, Byte, Character, Short, Float은 예외일 수 있다. → `OptionalXXX` 형태로 제공되지 않을 수 있다.

### 하지 말아야 할 것

- 옵셔널을 맵의 값으로 사용하면 절대 안 된다.
- 그리 한다면 맵 안에 키가 없다는 사실을 나타내는 방법이 두 가지가 된다.
- 하나는 키 자체가 없는 경우고,
- 다른 하나는 키는 있지만 그 키가 속이 빈 옵셔널`(ex : Option.empty())`인 경우다. 쓸데없이 복잡성만 높여서 혼란과 오류 가능성을 키울 뿐이다.
- **옵셔널을 컬렉션의 키, 값, 원소나 배열의 원소로 사용하는 게 적절한 상황은 거의 없다.**

### Optional을 인스턴스 필드에 저장해두는게 필요할 때가 있을까?

### 가끔은 적절한 상황도 있긴하다

- 특정 클래스의 필드 중 상당수가 필수가 아닐 때
- 또한 그 필드들이 기본타입이라 값이 없음을 나타낼 방법이 없을 때
- 이런 클래스의 선택적 필드들의 `getter` 메서드들이 옵셔널을 반환하게 해주면 좋았을 것이다.
- 따라서 이때는 필드 자체를 옵셔널로 선언하는 것도 좋은 방법이다.

### 핵심정리

- 값을 반환하지 못할 가능성이 있고, 호출할 때마다 반환값이 없을 가능성을 염두에 둬야 하는 메서들라면 옵셔널을 반환해야 하는 상황일 수 있다.
- 하지만 옵셔널 반환에는 성능 저하가 뒤따르니, 성능에 민감한 메서드라면 null을 반환하거나 예외를 던지는 편이 나을 수 있다.
- **옵셔널을 반환값 이외의 용도로 쓰는 경우는 매우 드물다.**

0 comments on commit f39022c

Please sign in to comment.