[아이템33] 타입 안전 이종 컨테이너를 고려하라

2024. 10. 31. 13:05·1️⃣ 백앤드/이펙티브 자바

1. 타입 안전 이종 컨테이너

1-1. 이종 컨테이너

타입 안전하다는 말은 앞서 제네릭을 학습할 때 많이 들어봤는데, 이종 컨테이너는 무엇인가?

제네릭은 주로 동일한 타입을 다룰 수 있게 해주는데 반해, 이종 컨테이너를 사용하면 타입에 구애받지 않고 여러 가지 타입의 객체를 하나의 컨테이너에 안전하게 담을 수 있게 된다. 

Set 에는 원소의 타입을 뜻하는 타입 매개변수 1개, Map 에는 2개가 필요한데, 이종 컨테이너는 타입에 구해받지 않고 담아보자는 취지이다. 참고로 자바에서의 컨테이너란, 여러 객체나 데이터를 담아서 관리할 수 있는 객체로 List, Set, Map 등 모든 컬렉션을  포함한다. 아래 간단한 예시를 통해 살펴보자.

import java.util.HashMap;
import java.util.Map;

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

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

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

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

    favorites.putFavorite(String.class, "Hello, World!");
    favorites.putFavorite(Integer.class, 42);

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

    System.out.println("Favorite String: " + favoriteString);
    System.out.println("Favorite Integer: " + favoriteInteger);
}

이종 컨테이너는 흔히 키에 타입 정보를 결합하여 구현하고, 자바의 Class<T> 객체를 키로 사용하여 특정 타입에 해당하는 값을 저장하고 가져오는 방식이다. getFavorite 메서드가 호출되기 전까진 키 (Class<?>) 와 값 (Object) 사이엔 아무런 관계도 없다. 위 코드에서는 putFavorite 메서드는 특정 타입의 값을 저장하며, getFavorite 메서드를 통해 Object 타입인 값을 Class 객체가 가리키는 타입으으로 형변환 하여 타입을 안전하게 반환할 수 있다. 타입 안전한 Favorites 인스턴스는 여러 타입의 원소를 담을 수 있어 '타입 안전 이종 컨테이너'라 부를 수 있다.

매개변수화 타입 클래스의 cast 메서드

위에서 나온  getFavorite 메서드 내 cast 메서드를 좀 더 자세히 살펴보자. 주어진 인수가 Class 객체가 알려주는 타입인지 검사한다음, 맞다면 인수를 반환하고 아니면 ClassCastException 을 던진다. 그렇다면 왜 굳이 사용할까? 그 이유는 제네릭의 이점을 완벽히 활용했기 때문이다.

1-2. Favorites 의 두가지 제약

1-2-1. 첫 번째 제약

악의적으로 클라이언트가 Class 객체를 로 타입으로 넘기면 타입 안정성이 쉽게 깨진다. 컴파일 시 비검사 경고가 뜨긴 할 것이다. 

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

    // 로우 타입으로 Class 전달
    favorites.putFavorite((Class) Integer.class, "This is a String, not an Integer");

    // 안전하지 않은 로우 타입으로 저장된 값을 꺼내려 할 때 오류 발생
    Integer favoriteInteger = favorites.getFavorite(Integer.class);

    System.out.println("Favorite Integer: " + favoriteInteger);  // 예상 외의 오류 발생
}

이를 방지하고자, putFavorites 메서드에서 인수로 주어진 instance 타입이 type 으로 명시한 타입과 같은지 확인하자.

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

java.util.Collections 의 checkedSet, checkedList, checkedMap 같은 메서드들이 이 방식을 적용한 컬렉션 레퍼들이다. 예를 들어, Collection<Stamp> 에 Coin 을 넣으려 하면 ClassCastException 을 던진다. 제네릭과 로 타입을 섞어 사용하는 클라이언트가 컬렉션에 잘못된 타입의 원소를 넣지 못하게 도움을 준다.

1-2-2. 두 번째 제약

String 이나 String[] 은 저장 할 수 있어도, List<String> 과 같은 실체화 불가 타입을 사용하게 되면 컴파일 에러가 난다. Class 객체를 얻을 수 없기 때문이다. List<String>.class 라고 쓰면 문법 오류가 난다. 이를 위한 해결책은 없다. 

1-3. Favorites 타입 제한

Favorites 클래스의 getFavorite 와 putFavorite 는 어떤 Class 객체든 받아들이는데, 허용하는 타입을 제한하고 싶을 때는 한정적 타입 매개변수나 한정적 와일드카드를 사용해 한정적 타입 토큰을 활용해보자.

public class Favorites {
    // Number 타입 또는 그 하위 타입만 허용
    private Map<Class<? extends Number>, Number> favorites = new HashMap<>();

    // putFavorite 메서드: Number 타입의 객체만 추가 가능
    public <T extends Number> void putFavorite(Class<T> type, T instance) {
        favorites.put(type, instance);
    }

    // getFavorite 메서드: Number 타입의 객체만 가져올 수 있음
    public <T extends Number> T getFavorite(Class<T> type) {
        return type.cast(favorites.get(type));
    }

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

        favorites.putFavorite(Integer.class, 42);
        favorites.putFavorite(Double.class, 3.14159);

        Integer favoriteInteger = favorites.getFavorite(Integer.class);
        Double favoriteDouble = favorites.getFavorite(Double.class);

        System.out.println("Favorite Integer: " + favoriteInteger);
        System.out.println("Favorite Double: " + favoriteDouble);

        // favorites.putFavorite(String.class, "Hello"); // 컴파일 에러 발생
    }
}

 

favorites 맵의 키 타입을 Class<? extends Number>로 제한하여, Number와 그 하위 타입에 해당하는 Class 객체만 허용한다. 이로 인해 String.class와 같은 Number가 아닌 타입을 추가하려 하면 컴파일 에러가 발생한다. 그리고 putFavorite와 getFavorite 메서드에서 제네릭 타입을 Number의 하위 타입으로 한정하여. 오직 Number 하위 타입의 객체만 인스턴스화하여 저장하거나 가져오도록 보장하도록 할 수 있다.

 

'1️⃣ 백앤드 > 이펙티브 자바' 카테고리의 다른 글

[아이템35] ordinal 메서드 대신 인스턴스 필드를 사용하라  (0) 2024.10.31
[아이템34] int 상수 대신 열거 타입을 사용하라  (0) 2024.10.31
[아이템32] 제네릭과 가변인수를 함께 쓸 때는 신중하라  (0) 2024.10.30
[아이템31] 한정적 와일드카드를 사용해 API 유연성을 높이라  (0) 2024.10.28
[아이템30] 이왕이면 제네릭 메서드로 만들라  (1) 2024.10.24
'1️⃣ 백앤드/이펙티브 자바' 카테고리의 다른 글
  • [아이템35] ordinal 메서드 대신 인스턴스 필드를 사용하라
  • [아이템34] int 상수 대신 열거 타입을 사용하라
  • [아이템32] 제네릭과 가변인수를 함께 쓸 때는 신중하라
  • [아이템31] 한정적 와일드카드를 사용해 API 유연성을 높이라
HOZINU
HOZINU
주니어 백앤드 개발자의 세상만사 이모저모. 주로 개발 이야기를 다룸.
  • HOZINU
    백엔드 탐험 일지
    HOZINU
  • 전체
    오늘
    어제
  • 블로그 메뉴

    • ⛪ HOME
    • 🌍 GITHUB
    • 카테고리 (73)
      • 1️⃣ 백앤드 (72)
        • 이펙티브 자바 (72)
      • 2️⃣ CS (0)
        • 운영체제 (0)
        • 네트워크 기초 (0)
        • 네트워크 응용 (0)
        • SSL & PKI (0)
        • 기타 (0)
      • 3️⃣ 코딩테스트 (0)
      • 4️⃣ 개인공부 (0)
        • MSA (0)
        • REDIS (0)
      • 5️⃣ 일상이야기 (1)
  • 인기 글

  • 태그

    optional
    멤버클래스
    hashcode
    Cleaner
    finalizer
    빌더
    로타입
    try-with-resources
    의존객체
    컴포지션
    표준예외
    정적 팩터리 메서드
    CLONE
    equals
    싱글턴
    캡슐화
    계층구조
    정보은닉
    Comparable
    맥북
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
HOZINU
[아이템33] 타입 안전 이종 컨테이너를 고려하라
상단으로

티스토리툴바