peaches-book-study / effective-java

이펙티브 자바 3/E
0 stars 2 forks source link

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

Open hyunsoo10 opened 3 months ago

hyunsoo10 commented 3 months ago

Chapter : 5. 제네릭

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

Assignee : hyunsoo10


🍑 서론

제네릭은 Set<E>, ThreadLocal<T> 등의 단일 원소 컨테이너에도 흔히 사용되는데, 문제될건 없지만 더 유연한 수단이 필요할 때도 종종 있다. 예를 들면 데이터베이스의 행은 임의 개수의 열을 가질 수 있는데, 모든 열을 타입 안전하게 이용할 수 있으면 so cool

이렇게 하기 위한 방법은 컨테이너 대신 키를 매개변수화한 다음에 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하면된다. 이렇게 하면 제네릭 타입 시스템이 값의 타입이 키와 같음을 보장해준다. 이러한 설계 방식을 타입 안전 이종 컨테이너 패턴이라 한다.

타입 안전 이종 컨테이너❓❓❓ 말이 어려운데 쉽게 말하면 여러 다른 종류들로 이루어진 값을 저장하는 타입 안전한 객체라고 생각하면된다.

🍑 본론

example

타입 안전 이종 컨테이너 ❌

// 데이터베이스 컬럼을 표현하는 클래스
class Column {
    private String name;

    public Column(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

// 데이터베이스 행을 표현하는 클래스
class DatabaseRow {
    // 컬럼과 값을 매핑하는 Map
    private Map<Column, Object> data = new HashMap<>();

    // 컬럼과 값을 매핑하여 저장
    public void put(Column column, Object value) {
        data.put(column, value);
    }
    public Object get(Column column) {
        return data.get(column);
    }
}

public class Main {
    public static void main(String[] args) {
        // 데이터베이스 행 생성
        DatabaseRow row = new DatabaseRow();

        // 데이터베이스 컬럼 객체 생성
        Column ageColumn = new Column("age");
        Column nameColumn = new Column("name");

        // 데이터베이스 행에 컬럼과 값을 매핑하여 저장
        row.put(ageColumn, 30);
        row.put(nameColumn, "John Doe");

        // 데이터베이스 행에서 컬럼 값을 가져와 출력
        // 이 부분에서는 컴파일러가 타입을 확인할 수 없기 때문에 형변환
        int age = (int) row.get(ageColumn);
        String name = (String) row.get(nameColumn);

        // 출력
        System.out.println("Age: " + age);
        System.out.println("Name: " + name.toUpperCase());

         // 이 부분에서 런타임 에러가 발생할 수 있음
        // 예를 들면 이런 식으로 잘못된 타입으로 형변환하여 런타임 에러를 발생시킴
        String age2 = (String) row.get(ageColumn); // 잘못된 형변환으로 런타임 에러 발생
        System.out.println("Age: " + age2.toUpperCase());
    }
}

타입 안전 이종 컨테이너 ⭕

// 제네릭 클래스를 사용하여 컨테이너를 생성
class Container<K, V> {
    private Map<K, V> map = new HashMap<>();

    // 키와 값을 매핑하여 저장하는 메서드
    public void put(K key, V value) {
        map.put(key, value);
    }

    // 키를 사용하여 값을 가져오는 메서드
    public V get(K key) {
        return map.get(key);
    }
}

//데이터베이스 행을 표현하는 클래스
class DatabaseRow {
    // 데이터베이스 컬럼을 키로 사용하는 타입 안전 이종 컨테이너
    private Container<Column<?>, Object> data = new Container<>();

    // 데이터베이스 컬럼과 값을 매핑하여 저장하는 메서드
    public <T> void putColumn(Column<T> column, T value) {
        data.put(column, value);
    }

    // 데이터베이스 컬럼을 키로 사용하여 해당 컬럼의 값을 가져오는 메서드
    public <T> T getColumnValue(Column<T> column) {
        // 컨테이너에서 해당 컬럼을 키로 사용하여 값을 가져옴
        return (T) data.get(column);
    }
}

// 데이터베이스 컬럼을 표현하는 제네릭 클래스
class Column<T> {
    private String name;

    public Column(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

public class Main {
    public static void main(String[] args) {
        // 데이터베이스 행 생성
        DatabaseRow row = new DatabaseRow();

        // 데이터베이스 컬럼 객체 생성
        Column<Integer> ageColumn = new Column<>("age");
        Column<String> nameColumn = new Column<>("name");

        // 데이터베이스 행에 컬럼과 값을 매핑하여 저장
        row.putColumn(ageColumn, 30);
        row.putColumn(nameColumn, "John Doe");

        // 데이터베이스 행에서 컬럼 값을 가져와 출력
        System.out.println("Age: " + row.getColumnValue(ageColumn));
        System.out.println("Name: " + row.getColumnValue(nameColumn).toUpperCase());

        // 잘못된 형변환으로 컴파일 에러 발생
        // 런타임 이전에 에러를 잡을 수 있음
        String age = (String) row.getColumnValue(ageColumn);
        System.out.println("Age: " + age.toUpperCase());
    }
}

🍑 결론

컬렉션 API로 대표되는 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되어 있다. 하지만 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 타입 안전 이종 컨테이너를 만들 수 있다. 타입 안전 이종 컨테이너는 Class를 키로 쓰며, 이런 식으로 쓰이는 Class 객체를 타입 토큰이라 한다. 또한, 직접 구현한 키 타입도 쓸 수 있다. 예컨대, 데이터베이스의 행(컨테이너)을 표현한 DatabaseRow 타입에는 제네릭 타입인 Column<T>를 키로 사용할 수 있다.


📍 참고

Favorites 클래스

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

참고: cast 메서드

public final class Class<T> {
    ...
    @SuppressWarnings("unchecked")
    public T cast(Object obj) {
        if (obj != null && !isInstance(obj))
            throw new ClassCastException(cannotCastMsg(obj));
        return (T) obj;
    }
    ....
}

main 메서드

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

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

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

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

Referenced by