반응형

개요

 이전 포스팅에서 매개변수화 타입을 사용하는 클래스에 유연성을 더하기 위해 한정적 와일드카드를 사용했고, 그 결과 자식 타입까지 허용 가능하도록 구현하였다. 타입의 다형성을 활용한 것이다. 이는 다형성을 활용하지 못하는 타입은 접근이 불허하다는 한계가 있다는 뜻이다. 이 한계를 타입 안전 이종 컨테이너 방식으로 극복할 수 있다.

 


즐겨찾기 기능 구현

 타입별로 즐겨 찾는 인스턴스를 저장하고 조회할 수 있는 즐겨찾기 기능을 구현해보자. 

 

1. HashMap을 통한 구현

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

favorites.put(String.class, "hi");
favorites.put(Integer.class, 1);

String favoriteString = (String)favorites.get(String.class);
Integer favoriteInteger = (Integer)favorites.get(Integer.class);

System.out.println(favoriteString);
System.out.println(favoriteInteger);

 

HashMap을 통해 간단하게 구현할 수 있지만 단점이 있다.

 

단점 1. 타입 안전성을 보장받지 못한다.

 Key를 Integer.class로 하고, value를 String 타입으로 넣어도 컴파일 에러가 발생하지 않는다. 이에 따라 런타임 시 해시맵의 Integer.class의 값을 조회할 때 ClassCastException이 발생하게 된다.

favorites.put(String.class, "hi");
favorites.put(Integer.class, "bye"); // 컴파일 에러는 발생하지 않는다.

..

Integer favoriteInteger = (Integer)favorites.get(Integer.class); // 런타임 에러 발생 !

 

단점 2. 조회 시 정적 타입 캐스팅이 필요하다

 모든 타입을 받아야 하기에 Map의 값을 Object 타입으로 선언하였다. 이에 따라 값을 조회할 때 정적인 타입 캐스팅이 필요하다. Integer.class에 대한 값을 조회할 때는 Integer 타입으로, String.class에 대한 값을 조회할 때는 String 타입으로 개발자가 직접 캐스팅해야한다.

 

 

2. 타입 안전 이종 컨테이너 패턴을 통한 구현

타입 안전 이종 컨테이너 패턴
 키 값을 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하는 패턴이다. 이에 따라 키와 값의 타입이 보장된다.

 

 

public class Favorites {

    private final 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));
    }
}

 

 

 HashMap 인스턴스를 감싸는 Favorites라는 래퍼 클래스를 만들고, 값을 넣거나 뺄 때 '키' 값에 매개변수화 타입 값을 함께 제공하고 있다. 이 방식이 Map 방식의 단점을 모두 극복했는지 알아보자.

 

1. 타입 안정성을 보장받는다.

Favorites f = new Favorites();

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

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

 

 putFavorite 메서드에서 사용하는 제네릭 타입에 의해 내부 Map 인스턴스의 Key로 사용할 클래스 타입과 값이 모두 같은 타입임이 컴파일 타임에 보장된다.

 

만약 아래와 같이 String.class에 대해 123이라는 Integer 타입의 값을 사용한다면 메서드 시그니처에 맞지 않다는 컴파일 에러가 발생하게 된다. 즉, 기존 Map을 직접 사용한 방식과는 다르게 타입 안정성이 보장되고 있다.

f.putFavorite(String.class, 123); // 메서드 시그니처에 맞지 않는다는 컴파일 에러가 발생!

 

2. 조회 시 동적 타입 캐스팅이 가능하다.

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

 

 조회 시 Class 클래스의 cast() 메서드를 사용하고 있다. 이 메서드는 매개변수로 들어온 값을 자신의 타입으로 캐스팅 할 수 있다면 캐스팅 후 반환하고, 캐스팅이 불가능할 경우 ClassCastException을 반환하는 메서드이다.

 어차피 getFavorite 메서드의 파라미터로 들어온 클래스의 타입 T와 favorites.get(type)을 통해 조회한 클래스의 타입 T 는 일치할 수 밖에 없으므로 type.cast 메서드를 아주 적절하게 사용할 수 있는 부분이다.

 

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

 

물론 위와 같이 정적 형변환 방식으로 수정해도 되지만, 비검사 형변환이므로 '경고'가 발생하게 된다. 타입 안정성이 보장되는 부분이기에 @SuppressWarnings와 주석을 남겨야 한다.

 

 


제약 사항

Class 타입을 로 타입으로 넘길 경우 타입 안정성이 깨진다.

List<String> stringList = new ArrayList<>();
stringList.add("hi");

f.putFavorite(List<String>.class, stringList); // 컴파일 에러 발생

 

 List<String>과 List<Integer>에 대한 값을 Favorites 클래스로 관리하기 위해 putFavorite 메서드에 List<Class>.class 형태로 파라미터를 넘길 경우 경우 컴파일 에러가 발생한다. List<String>.class 구문 자체가 문법 에러를 발생시키기 때문이다.

 

List<String> stringList = new ArrayList<>();
stringList.add("hi");

List<Integer> integerList = new ArrayList<>();
integerList.add(1);

f.putFavorite(List.class, stringList);
f.putFavorite(List.class, integerList); // 덮어 씌워진다.

 

이를 해결하기 위해 위와 같이 로 타입으로 값을 넘기게 되면 List<String>과 List<Integer> 모두 같은 키를 공유하게 되므로 List.class 키에 대한 값은 마지막에 넣은 값으로 덮어 씌워지게 된다. List<String>과 List<Integer> 타입에 대한 값을 따로 관리하려 했던 목적을 이루지 못했다.

 


정리

 타입 매개변수를 사용하는 컬렉션 API는 한 컨테이너가 다룰 수 있는 타입의 수가 고정되어 있다. 하지만 타입 안정 이종 컨테이너 패턴을 사용하면 타입에 제약이 없는 컨테이너로 만들 수 있다.

 

 

 

반응형

+ Recent posts