이펙티브 자바 #item33 타입 안전 이종 컨테이너를 고려하라

임현규·2023년 2월 10일
0

이펙티브 자바

목록 보기
33/47
post-custom-banner

타입 안전 이종 컨테이너 (type safe heterogeneous container)

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를 유연하게 저장하고 확장할 수 있다.

예제를 통해 조금 더 알아보자

시나리오

1. 객체의 property가 많고 자주 바뀌는 경우

객체의 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로 사용하는것이 조금 더 나을 수 있다.

profile
엘 프사이 콩그루
post-custom-banner

0개의 댓글