Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# 아이템 33. 타입 안전 이종 컨테이너를 고려하라

## 개요

제네릭은 `List<String>`, `Map<String, Integer>`와 같이 컨테이너 자체에 타입 매개변수가 적용된다.

> 따라서 한 컨테이너는 보통 타입의 수가 제한된다.
>

하지만, 우리는 다양한 타입의 객체를 저장하면서도 타입 안전성을 유지해야 할 때가 있다.

~~그런가?~~

다행히 해법은 있다.

바로 **타입 안전 이종 컨테이터 패턴(type safe heterogeneous container pattern)**이다.

<aside>
💡

**타입 안전 이종 컨테이너 패턴?**

: 컨테이너 대신 **키를 매개변수화** 한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하는 방식.

</aside>

설명을 읽어도 이해가 잘 되지 않으니 코드로 살펴보자.

## 예제

우리는 타입 별로 즐겨찾는 인스턴스를 저장하고 검색할 수 있는 `Favorates` 클래스를 만들 것이다.

> String → “문자”, Integer → 123
>

구현하는 방법을 알아보자.

### 1. 각 타입의 `Class` 객체를 매개변수화한 키 역할로 사용하자.

- `class`의 리터럴 타입이 `Class`가 아닌 `Class<T>`이기 때문에 가능하다.

> `String.class` → `Class<String>`, `Integer.class` → `Class<Integer>`
>
- 이때 컴파일타임 타입 정보와 런타임 타입 정보를 알아내기 위해 메서드들이 주고 받는 class 리터럴을 **타입 토큰(type token)**이라 한다.

```java
// 타입 안전 이종 컨테이너 API
public class Favorites {
public <T> void putFavorite(Class<T> type, T instance);
public <T> T getFavorite(Class<T> type);
}
```

```java
// 사용 예시
public static void main(String[] args) {
Favorites f = new Favorites();

f.putFavorite(String.class, "Java");
f.putFavorite(Integer.class, 0xcafebabe);
f.putFavorite(Class.class, Favorites.class);

String favoriteString = f.getFavorite(String.class);
int favoriteInteger = f.getFavorite(Integer.class);
Class<?> favoriteClass = f.getFavorite(Class.class);

System.out.printf("%s %x %s%n", favoriteString, favoriteInteger, favoriteClass.getName());
}
```

### 2. 값을 넣을 때와 뺄 때 해당 키의 타입을 함께 확인

```java
// 타입 안전 이종 컨테이너 구현
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();

public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), type.cast(instance));
}

public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
}
```

구현한 코드를 하나하나 살펴보자.

**먼저, `Map<Class<?>, Object>` 를 보면**

- 키: 와일드카드를 써서 `put`이 불가능할 것 같지만, **맵이 아니라 키가 와일드카드 타입**이므로 `Class<String>, Class<Integer>`식으로 될 수 있다.
- 값: 단순히 `Object`를 씀으로써 모든 값이 키로 명시한 타입임을 보증하지 않는다.

→ 형변환을 명시적으로 할 필요가 없다.

**다음으로 `putFavorite()`를 보면**

- `Class`의 `cast()`를 통해 동적 형변환을 사용하여 런타임 타입 안전성을 확보했다.

**마지막으로 `getFavorite()`는**

- 주어진 `Class` 객체에 해당하는 값을 `favorites` 맵에서 꺼내고,
- `Class`의 `cast()` 메서드를 사용해 객체 참조를 `Class` 객체가 가리키는 타입으로 동적 형변환한다.

<aside>
💡

**cast?**

: 형변환 연산자의 동적 버전. 단순히 주어진 인수가 Class 객체가 알려주는 타입의 인스턴스인지를 검사한 다음, 맞다면 그 인수를 그대로 반환, 아니면 `ClassCastException`을 던진다.

```java
public class Class<T>{
T cast(Object obj);
}
```

cast는 다음과 같이 반환 타입을 Class 객체의 타입 매개변수와 동일하게 한다.

</aside>

> 위처럼 cast를 사용해 런타임 타입 안전성을 확보한 것으로 `java.util.Collections`에 `checkedSet, checkedList, checkedMap`과 같은 메서드가 있다.
>

## 제약사항

`List<String>`과 같은 실체화 불가 타입은 사용 불가능하다.

> 즉, `String`이나 `String[]`은 저장할 수 있어도 `List<String>`은 저장할 수 없다.
>

> 이는 `List`의 리터럴이 `List.class`를 공유하기 때문이다!
>

## 해결책 - 슈퍼 타입 토큰

옮긴이에 따르면 제네릭 타입도 다룰 수 있게 하는 **슈퍼 타입 토큰**이라는 게 있다.

```java
// 슈퍼 타입 토큰 예시
Type listOfStringType = new TypeRef<List<String>>() {}.getType();
```

글이 길어지므로 궁금하다면 따로 알아보도록 하자.

## 결론

- 단일 원소 타입만을 지원하는 제네릭의 한계를 넘어서는 방법이다.
- 타입 토큰(Class<T>)을 키로 사용하여 타입 안전성을 보장한다.
- API가 다양한 타입을 지원해야 할 때 사용하면 좋다.
- 실무에서는 데이터베이스 행, 애너테이션, XML 문서 등을 사용할 때 이 패턴이 활용된다.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
- [x] 30. 이왕이면 제네릭 메서드로 만들라 (태태)
- [x] 31. 한정적 와일드카드를 사용해 API 유연성을 높이라 (모리)
- [x] 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라 (태태)
- [ ] 33. 타입 안전 이종 컨테이너를 고려하라 (모리)
- [x] 33. 타입 안전 이종 컨테이너를 고려하라 (모리)
- [ ] 34. int 상수 대신 열거 타입을 사용하라 (태태)
- [ ] 35. ordinal 메서드 대신 인스턴스 필드를 사용하라 (모리)
- [ ] 36. 비트 필드 대신 EnumSet을 사용하라 (태태)
Expand Down