Skip to content

Latest commit

 

History

History
146 lines (101 loc) · 7.3 KB

33. 타입 안전 이종 컨테이너를 고려하라.md

File metadata and controls

146 lines (101 loc) · 7.3 KB

타입 안전 이종 컨테이너?

  • Type Safe Heterogeneous Container ⇒ 타입 안전한 여러가지를 담을 수 있는 컨테이너
  • 여기서 컨테이너란 List, Set, Map 같이 여러 요소를 담을 수 있는 클래스를 의미함.
  • 기존의 컨테이너에서 사용되는 제네릭은 사용할 수 있는 개수가 한정됨. 예컨데, List 는 List<String> 와 같이 하나의 타입만을, Map 은 Map<String, Integer> 같이 한번에 두개의 타입만을 사용할 수 있다.
    • 제네릭 타입이 정해졌을 때, 컨테이너에는 해당 타입의 요소만 관리할 수 있게됨.
  • 타입을 조금 더 유연하게 여러개를 사용하고 싶을 때, 타입 안전 이종 컨테이너를 사용한다.

엥, 제네릭 타입을 Object 로 하면 되지 않나요?

가능 하긴 함. 하지만, 컨테이너로부터 얻은 객체의 타입을 정확히 알기 힘듦. 컴파일 에러는 없을지라도 런타임 시 에러가 발생하지 않는다는 보장은 없음.

Set<Object> everything = new HashSet<>();

everything.add("Hello");
everything.add(123);
everything.add(1234L);
everything.add(1.00);

everything.forEach(System.out::println);
// 에러 없이 잘 작동함

Key 를 타입으로, Value 를 값으로 받는 Map 을 사용해볼까?

Map<Class<?>, Object> map = new HashMap<>();

map.put(Integer.class, 13);
map.put(String.class, "hello");

System.out.println((int) map.get(Integer.class)); // 13

Integer.class 의 타입은 Class, String.class 의 타입은 Class 이다.

위 처럼 코드를 작성하게 되면, Key 에는 타입이 Value 에는 값이 들어가게 되어 값의 타입을 추측할 수 있게 된다. 하지만 아래처럼 누군가 키와 밸류를 서로 다른 타입으로 집어 넣는다면?

map.put(Integer.class, "I am not a number");
System.out.println((int) map.get(Integer.class));
// 런타임 에러 : ClassCastException

컴파일 타임엔 알 수 없는 런타임 에러가 발생한다. Value 의 타입이 Object 이기 때문에 타입 캐스팅을 해줘야하기 때문.

컴파일 타임에 타입 안전을 보장하자! ⇒ 타입 안전 이종 컨테이너

public class Box {
    private Map<Class<?>, Object> favorites = new HashMap<>();

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

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

위 코드가 타입 안전 이종 컨테이너의 구현이다.

put 메소드

// Box.java
public <T> void put(Class<T> type, T instance) {
    favorites.put(Objects.requireNonNull(type), instance);
}

// Application.java
Box box = new Box();
box.put(Integer.class, 123);
box.put(Integer.class, "abcd"); // 컴파일 에러

Objects.requireNonNull 은 매개변수가 null 일경우 NullPointerException 을 발생시킨다. 아니라면, 매개변수를 그대로 반환한다.

아까 언급했듯 Integer.class 의 타입은 Class<Integer> 이고, String.class 의 타입은 Class<String> 이라고 했다. 따라서 위 코드와 같이 put 을 호출하게되면, 타입 추론을 통해 TInteger 가 된다.

따라서 마지막 줄은 타입이 다르므로 (T는 Integer 인데, 매개변수가 String) 컴파일 에러가 발생한다.

get 메소드

// Box.java
public <T> T get(Class<T> type) {
    return type.cast(favorites.get(type));
}

// Application.java
System.out.println(
        box.get(Integer.class)
);

get 메소드에 type.cast 는 동적으로 타입 캐스팅을 해주는 메소드이다. 현재 매개변수로 Integer.class 를 받아오므로, get 메소드 내부에서 자동으로 Integer 로 타입캐스팅을 해준다. 따라서 외부에서 별도로 타입 캐스팅을 해줄 필요가 없다.

제약사항

1. Raw 타입의 전달

box.put((Class) Integer.class, "abc 되지롱");

위와 같이 Integer.class(Class) 로 캐스팅해 Class<Integer> 가 아닌 Class 인 Raw 타입으로 전달하게 되면, 타입 안정성이 깨지게된다. 위 코드는 잘못된 타입이 전달되었지만 컴파일 타임에도, 런타임에도 오류가 발생하지 않는다.

이를 막기 위해 put 메소드에서도 형변환 검사를 추가해줘야 한다.

public <T> void put(Class<T> type, T instance) {
    favorites.put(Objects.requireNonNull(type), type.cast(instance));
		// instance 도 type.cast 를 추가해줌.
}

2. 실체화 불가 타입은 사용 불가능

(관련 아이템: 28. 배열보다는 리스트를 사용하라)

StringString[] 은 가능하지만, List<String> 용 Class 객체는 얻을 수 없다. 따라서 List<String> , Set<Integer> 등은 컴파일 할 수 없다.

box.put(Set<Integer>.class, Set.of(1, 2, 3, 4)); // 컴파일 불가능

정리

컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하여 제너릭 타입 시스템이 값의 타입이 키와 같음을 보장해주는 설계 방식을 타입 안전 이종 컨테이너 패턴이라고 한다.

참고

기타 관련 아이템