Map 또는 Set에 사용하는 타입보다 훨씬 유연하고 확장 가능한 타입을 제공하고 싶을 때가 있다. 예를 들면 임의의 데이터 베이스를 생각해보자 각 레코드별로 임의의 컬럼을 가질 수 있다. 이 때 임의의 컬럼들을 제네릭을 활용해 type-safe하게 가져오고 저장할 수 있다면 IDE를 최대한으로 활용할 수 있고 컴파일 단계에서 쉽게 캐치할 수 있다. 이 때 타입 안전 이종 컨테이너를 활용한다.
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));
}
}
Favorites 클래스의 멤버 변수중 favorites를 살펴 보면 key로 Class<?>
를 받음을 알 수 있다. 이를 통해 여러 타입을 favorites에 저장할 수 있다. 그리고 타입별로 저장된 인스턴스를 호출할 수 있다.
Favorites 코드는 얼핏 보면 이런 코드는 왜 쓰지? 의문을 제기할 수 있다. 이 코드는 여러 타입을 담을 수 있는 객체지만 해당 타입의 Key를 객체로 커스터마이징하면 Favorites의 properties를 유연하게 저장하고 확장할 수 있다.
예제를 통해 조금 더 알아보자
객체의 property가 많다면 getter / setter가 귀찮도록 많아지고, property가 자주 바뀌는 상황이라면 수정할 코드가 많아진다. 예제를 통해 살펴보자
public enum UserProperty {
NAME,
AGE,
PHONE_NUMBER,
ADDRESS
}
public class User {
private final Map<UserProperty, Object> properties = new EnumMap<>(
UserProperty.class);
public void setProperty(UserProperty property, Object value)
{
properties.put(property, value);
}
public Object getProperty(UserProperty property)
{
return properties.get(property);
}
}
enum으로 perperty list를 만들고 enum에 있는 정보를 이용해 properties를 구성했다. 그리고 enumMap을 활용해 효율성을 높였다.
이 코드는 컴파일시는 문제가 없지만 런타임에 ClassCastException이 던져질 가능성이 있다. 그 이유는 Object로 저장되는데 String으로 저장하고 Integer로 type을 변경할 수 있기 때문이다. 좀 더 타입에 안전하도록 바꿀 필요가 있다.
그러나 enum의 문제점은 제네릭을 활용할 수 없다. 그렇기 때문에 class로 바꾸어서 위의 코드를 타입 안전 이종 컨테이너 형태로 만들어보자.
public class UserProperty<T> {
private final T name;
private final Class<T> type;
public UserProperty(T value, Class<T> type) {
this.name = value;
this.type = type;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
UserProperty<?> that = (UserProperty<?>) o;
return Objects.equals(name, that.name) && Objects.equals(type, that.type);
}
@Override
public int hashCode() {
return Objects.hash(name, type);
}
@Override
public String toString() {
return "UserProperty{" +
"value=" + name +
", type=" + type +
'}';
}
public T getName() {
return name;
}
public Class<T> getType() {
return type;
}
}
public class User {
private final Map<UserProperty<?>, Object> properties = new HashMap<>();
public <T> void setProperty(UserProperty<T> property, T value)
{
properties.put(property, value);
}
public <T> T getProperty(UserProperty<T> property)
{
return property.getType().cast(properties.get(property));
}
@Override
public String toString() {
return "User{" +
"properties=" + properties +
'}';
}
}
해당 코드의 문제점은 T을 원시 데이터 타입으로 사용하면 property 자체가 제한될 수 있다는 점이다. property의 특성을 제대로 활용하려면 래핑 객체를 T로 사용하는것이 조금 더 나을 수 있다.