무지를 아는 것이 곧 앎의 시작

Java

타입 안전 이종 컨테이너

Alex96 2022. 4. 5. 21:49

아이템33 타입 안전 이종 컨테이너

타입 안전 이종 컨테이너란?🧐

컨테이너

Set<E>, Map<K, V>, List<T> 등 여러 요소를 담는 컬렉션들과
ThreadLocal<T>, AtomicReference<T>등 단일원소를 담는 클래스들.

타입 안전 이종 컨테이너

여러 타입을 안전하게 담을 수 있는 컨테이너

List<Integer>는 오직 Integer 타입만 담을 수 있음. 그럼 여러 타입을 담게 하려면 어떻게 해야할까?

Object에 저장하기

public class ObjectContainer {
    public static void main(String[] args) {
        List<Object> container = new ArrayList<>();
        container.add("1");
        container.add(1);
        container.add(1L);
        container.forEach(System.out::println);
    }
}

하지만, 위 방법대로 사용할 경우 해당 컨테이너에서 얻은 객체가 어떤 타입인지 알 수 없기 때문에 언제 런타임 에러가 발생할지 모른다. 위 코드의 경우 sysout이 Object 타입으로 파라미터를 받기 때문에 런타임 에러가 발생하지 않는 것이다.

클래스 타입을 Key로 잡아서 사용하면?

public class ClassTypeKeyContainer {
    public static void main(String[] args) {
        Map<Class<?>, Object> container = new HashMap<>();
        container.put(Integer.class, 1);
        container.put(String.class, "1");

        String item = (String) container.get(String.class);
    }
}

타입을 예상할 수는 있다. 하지만 컨테이너에 값을 넣을 때 다음과 같이 넣는다면..?

public class ClassTypeKeyContainer {
    public static void main(String[] args) {
        Map<Class<?>, Object> container = new HashMap<>();
        container.put(Integer.class, "1");

        Integer item = (Integer) container.get(Integer.class);
    }
}

컴파일 에러가 발생하지 않고 런타임 에러가 발생한다. Integer 키에 String 값을 넣었기 때문에 형변환시에 ClassCastException이 발생한다.

타입 안전 이종 컨테이너

앞선 두 케이스에서는 컨테이너에 여러 타입을 담을 수는 있었지만, 타입 안정성을 보장할 수 없었다. 타입 안전성을 보장하는 타입 안전 이종 컨테이너에 대해 알아보자.

제네릭을 활용한 타입 안전 이종 컨테이너

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

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

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

클라이언트 코드

public static void main(String[] args) {
    Favorites f = new Favorites();

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

    String favoriteString = f.getFavorite(String.class);
    int favoriteInteger = f.getFavorite(Integer.class);

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

제네릭을 이용해서 putFavorite메서드를 구현했다. 타입 키에 맞지 않으면 메서드에서 컴파일 에러를 발생시킨다.
즉, 위에 타입 안정성을 보장하지 못한 케이스에서 Object에 맞지 않는 타입을 넣는 경우를 배재했다.

그리고 마찬가지로 제네릭으로 getFavorite메서드에서 메서드를 호출하는 클라이언트가 따로 캐스팅이 필요 없도록 했다. Classcast() 메서드를 사용해서 입력된 키에 맞는 타입이 자동으로 반환된다.

cast() : 형변환 연산자의 동적 버전


타입 안전 이종 컨테이너가 완전하지 못한 이유...🥲

악의적인 클라이언트가 Class객체를 제네릭 없이 로타입으로 넘기는 경우


위의 클라이언트 코드에서 컨테이너에 다음처럼 담는다고 가정하면,

f.putFavorite((Class)Integer.class, "I'm malicious Alexander HaHaHa!!!");
int favoriteInteger = f.getFavorite(Integer.class);

들어갈 땐 아무 문제 없이 들어가지만, 꺼낼 때 Integer타입으로 꺼내려하면 ClassCastException이 발생한다.

이런 경우를 예방하려면 컨테이너에 담는 메서드에서 형변환 검사를 추가하는 방법이 있는데,

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

이렇게 구현할 경우 컨테이너에 넣을 때 ClassCastException이 발생한다. 어쨌든 컴파일 타임에 잡아낼 수는 없다.

실체화 불가 타입에는 사용할 수 없다.


어떤 말이냐면 List<String>같은 타입의 경우 저장할 수 없다. 이유는 List<String>Class객체를 얻을 수 없기 때문이다. List<String>.class는 문법 오류이다. List.class라는 같은 Class객체를 쓰기 때문에 만약 List.classList<String>List<Integer>를 모두 허용해서 사용하면.... 컨테이너는 제 기능을 할 수 없을 것이다. (로타입을 쓰면 안되는 이유)

'Java' 카테고리의 다른 글

옵셔널(Optional) 클래스  (0) 2022.05.02
상태 패턴 정리  (0) 2022.04.12
멤버 클래스는 되도록 static으로  (2) 2022.03.30
Stream.forEach() vs for-each  (0) 2022.03.14
표준 함수형 인터페이스  (0) 2022.03.08