33. 타입 안전 이종 컨테이너를 고려하라
1. 타입 안전 이종 컨테이너 패턴 소개:
1. 컨테이너가 무엇일까?
컨테이너(Container)는 데이터를 저장하고 관리하는 구조이다.
저장하는 데이터의 개수에 따라 두 가지로 구분된다.
여러 개의 원소를 저장하는 컨테이너 → Set, Map, List
단일 원소를 저장하는 컨테이너 → ThreadLocal, AtomicReference
컨테이너는 데이터를 정리하고 효율적으로 접근할 수 있도록 해주는 역할을 한다.
2. 이 패턴이 없을 때 개발자들은 어떻게 기능을 사용했을까?
과거에는 제네릭이 없었기 때문에, 모든 데이터를 Object 타입으로 저장하는 방법을 사용했다.
비-제네릭 컬렉션(raw type collections)을 사용해야 했다.
예제 1: ArrayList에 여러 타입의 데이터 저장
List container = new ArrayList(); // 비-제네릭 리스트 사용
container.add("Hello"); // String 저장
container.add(123); // Integer 저장
예제 2: HashMap에 여러 타입의 데이터 저장
Map map = new HashMap(); // 키와 값의 타입을 지정하지 않음
map.put("stringKey", "Hello");
map.put("integerKey", 123);
각기 다른 타입의 데이터를 저장할 때, 타입별로 별도의 컬렉션을 만들어야 했다.
List<String> stringList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
3. 해당 기능 사용 시, 어떤 불편한 점이 있었을까?
🚨 타입 안전성이 부족했다.
컴파일 타임에 타입 검사가 불가능했다.
Object 타입으로 저장되었기 때문에, 잘못된 타입이 들어가도 컴파일러가 감지하지 못했다.
데이터를 꺼낼 때 강제 형변환(Casting)이 필요했다.
String str = (String) container.get(0); // 강제 형변환 필요
Integer num = (Integer) container.get(1);
문제: 어떤 데이터가 들어있는지 보장되지 않으므로, 형변환이 안전하지 않다.
잘못된 형변환을 하면 ClassCastException 발생 가능성이 있었다.
String str = (String) container.get(1); // Integer를 String으로 변환하려다 오류 발생!
// Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
코드 가독성과 유지보수성이 떨어졌다.
컬렉션이 많아지면, 각 컬렉션이 어떤 타입을 저장하는지 관리하기 어려웠다.
데이터 저장/조회 시 형변환 코드가 반복적으로 필요했다.
🚀 기존 방식 (비-제네릭 컬렉션) - 타입 안정성 없음
import java.util.*;
public class RawTypeCollection {
public static void main(String[] args) {
Map map = new HashMap(); // (1) 타입을 지정하지 않음 (비-제네릭 컬렉션)
map.put("name", "Alice"); // (2) String 저장
map.put("age", 25); // (3) Integer 저장
String name = (String) map.get("name"); // (4) 형변환 필요 (문제없음)
Integer age = (Integer) map.get("age"); // (5) 형변환 필요 (문제없음)
String invalid = (String) map.get("age"); // (6) 잘못된 형변환 (런타임 오류 발생)
System.out.println(name);
System.out.println(age);
}
}
🚨 실행 결과 (잘못된 형변환 시 발생할 오류)
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
4. 불편한 점을 어떻게 개선해서 이 패턴이 만들어진 걸까?
🚀 비-제네릭 컬렉션의 문제를 해결하기 위해 "키를 매개변수화하는 방식"을 도입했다.
📌 개선 방법: 컨테이너가 아니라 "키"를 제네릭으로 매개변수화
컨테이너 자체를 타입 지정하는 대신, 저장할 때 "키"에 타입을 포함시키는 방식.
각 타입의 Class 객체를 "키"로 사용하여, 키마다 특정 타입만 저장되도록 강제했다.
예제 코드:
Class<String> key = String.class; // key의 타입은 Class<String>
Class<Integer> key2 = Integer.class; // key2의 타입은 Class<Integer>
String.class의 타입은 Class,
Integer.class의 타입은 Class.
이 점을 활용하여 "타입을 보장하는 컨테이너"를 만들 수 있다.
5. 타입 안전 이종 컨테이너 패턴이란?
🚀 개념 정리
컨테이너 자체가 아닌 "키"를 제네릭으로 매개변수화하는 방식.
하나의 컨테이너에서 여러 타입의 데이터를 안전하게 저장하고 꺼낼 수 있다.
컴파일러가 키와 값의 타입을 체크할 수 있으므로, ClassCastException 같은 문제를 방지할 수 있다.
2. 타입 안전 이종 컨테이너 패턴의 간단한 예시 - Favorites 클래스:
1. Favorites 클래스 소개
Favorites 클래스는 타입별로 즐겨 찾는 인스턴스를 저장하고 검색할 수 있는 간단한 예시입니다
코드 예시 1 (Favorites 클래스 - API (인터페이스 정의))
public class Favorites {
public <T> void putFavorite(Class<T> type, T instance);
public <T> T getFavorite(Class<T> type);
}
public class Favorites
Favorites 클래스 선언.
이 클래스는 제네릭 타입을 안전하게 저장하고 조회하는 컨테이너 역할을 한다.
public void putFavorite(Class type, T instance);
특정 타입의 데이터를 저장하는 메서드 선언.
Class type: 저장할 데이터의 타입 (ex. String.class, Integer.class)
T instance: 저장할 데이터 (ex. "Java", 123)
매개변수화된 키(Class)를 이용하여 타입 정보를 유지할 수 있도록 함.
public T getFavorite(Class type);
특정 타입의 데이터를 가져오는 메서드 선언.
Class type: 조회할 데이터의 타입 (ex. String.class)
반환 타입이 T → 타입 안전하게 값을 반환함.
저장 시 사용한 Class 키를 기반으로 값을 가져올 수 있음.
코드 예시 2 (Favorites 클래스 -클라이언트 (사용 코드))
public static void main(String[] args) {
Favorites f = new Favorites(); // (1)
f.putFavorite(String.class, "Java"); // (2)
f.putFavorite(Integer.class, 0xcafebabe); // (3)
f.putFavorite(Class.class, Favorites.class); // (4)
String favoriteString = f.getFavorite(String.class); // (5)
int favoriteInteger = f.getFavorite(Integer.class); // (6)
Class favoriteClass = f.getFavorite(Class.class); // (7)
System.out.printf("%s %x %s%n", favoriteString, favoriteInteger, favoriteClass.getName()); // (8)
}
코드 (1)
Favorites 클래스의 인스턴스 f를 생성
f를 통해 여러 타입의 데이터를 저장하고 검색할 예정.
코드(2)
키로 String.class를 사용하여 "Java"라는 문자열 저장
즉, "Java" 값이 String.class 키에 연결되어 저장됨.
코드(3)
키로 Integer.class를 사용하여 0xcafebabe(16진수) 저장
0xcafebabe는 10진수로 변환하면 3405691582
즉, Integer.class 키에 3405691582 값이 저장됨.
코드(4)
키로 Class.class를 사용하여 Favorites.class 저장
즉, Class.class 키에 Favorites.class 값이 저장됨.
이 값은 Class 타입이다.
코드(5)
String.class 키로 저장된 데이터를 가져옴.
반환 타입이 String이므로, 타입 캐스팅 없이 바로 String을 받을 수 있음.
코드(6)
Integer.class 키로 저장된 데이터를 가져옴.
반환 타입이 int이므로, 타입 캐스팅 없이 바로 정수값을 받을 수 있음.
코드(7)
Class.class 키로 저장된 데이터를 가져옴.
favoriteClass의 타입이 Class이므로, getName()을 호출하면 "Favorites"가 출력됨.
코드(8)
%s → String 값 ("Java") 출력
%x → 16진수로 변환 (0xcafebabe) 출력
%s → 클래스 이름 ("Favorites") 출력
코드 예시 3 (Favorites 클래스 - 구현)
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>(); // (1)
public <T> void putFavorite(Class<T> type, T instance) { // (2)
favorites.put(Objects.requireNonNull(type), instance); // (3)
}
public <T> T getFavorite(Class<T> type) { // (4)
return type.cast(favorites.get(type)); // (5)
}
}
코드(1)
favorites라는 맵(Map) 필드 선언
Class → 키로 Class 객체 사용 (어떤 클래스 타입도 저장 가능하도록 사용)
Object → 값은 어떤 타입이든 저장 가능 (모든 객체가 Object를 상속받기 때문)
코드(2)
putFavorite 메서드: 특정 타입의 데이터를 저장하는 메서드
Objects.requireNonNull(type)
type이 null이면 예외 발생 (안전성 보장)
favorites.put(type, instance);
키(type)에 대한 값(instance)을 Map에 저장
코드(3)
getFavorite 메서드: 특정 타입의 데이터를 검색하는 메서드
favorites.get(type)
type 키에 해당하는 데이터를 가져옴
type.cast(...)
올바른 타입으로 변환 (Class의 cast 메서드 활용)
컴파일러가 타입을 보장하므로, 형변환 없이 안전하게 값 반환
📌 코드 전체 흐름 정리
Favorites 클래스는 제네릭 기반의 타입 안전 이종 컨테이너
데이터를 저장할 때 타입 정보를 Class 키로 사용
데이터를 검색할 때 키로 타입을 전달하여 올바른 타입으로 반환
Class를 활용하여 런타임에도 타입 정보를 유지할 수 있음
컴파일러가 타입을 보장하므로 ClassCastException이 발생하지 않음
타입 토큰(Type Token)이란?
1️⃣ 타입 정보가 사라지는 문제 (Type Erasure)
📌 제네릭의 한계: 컴파일 이후에 타입 정보가 사라짐
자바의 제네릭은 타입 소거(Type Erasure) 과정을 거친다.
즉, 컴파일러가 제네릭 타입을 확인한 후, 런타임에서는 실제 타입 정보를 유지하지 않음.
🚀 예제 1: 타입 소거 확인
import java.util.ArrayList;
import java.util.List;
public class TypeErasureExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(stringList.getClass() == intList.getClass()); // true 출력
}
}
✅ List과 List는 다른 타입처럼 보이지만,
✅ 런타임에서는 동일한 ArrayList로 취급됨.
📌 왜냐하면 컴파일 이후에는 제네릭 타입 정보가 지워지기 때문!
List → List
List → List
🚨 즉, 런타임에서는 T의 타입을 알 수 없음.
2️⃣ 타입 토큰(Type Token) - 이 문제를 해결하는 방법
🚀 Class를 이용해서 런타임에 타입 정보를 유지하는 방법
타입 정보가 사라지는 문제(Type Erasure)를 해결하려면, 런타임에 타입을 유지해야 함.
이를 위해 Class를 활용하면 제네릭 타입 정보를 런타임에도 유지할 수 있다.
📌 예제 2: 타입 토큰 활용
public class TypeTokenExample {
public static <T> void printType(Class<T> type) {
System.out.println("전달된 타입: " + type.getName());
}
public static void main(String[] args) {
printType(String.class); // 출력: 전달된 타입: java.lang.String
printType(Integer.class); // 출력: 전달된 타입: java.lang.Integer
}
}
✅ Class를 사용하면, 런타임에서도 타입 정보를 유지할 수 있다!
✅ 즉, 컴파일 후에도 T가 String인지 Integer인지 알 수 있다.
3️⃣ 결론
✅ 컴파일 후 제네릭 타입 정보가 사라지는 문제(Type Erasure)가 있음.
✅ 이 문제를 해결하려면 Class를 활용해야 함.
✅ Class를 이용해 런타임에도 타입 정보를 유지할 수 있음.
✅ 이 방식이 Favorites 클래스처럼 "타입 안전 이종 컨테이너"를 만들 때 필수적으로 사용됨.
🚀 ➡ "타입 토큰(Type Token)은 런타임에 제네릭 타입 정보를 유지하기 위한 Class 객체이다!" 🚀
발표자료 https://byumm315.atlassian.net/wiki/external/ZmJmYTI5N2JiN2U5NDViM2E5ODk4NDg1NTFiNDk5YTg
Last updated