-
아이템 33. 타입 안전 이종 컨테이너를 고려하라JAVA/Effective java 2021. 3. 22. 22:27
- 제네릭은 Set<E>, Map<K,V> 등의 컬렉션과 ThreadLocal<T>, AtomicReference<T> 등의 단일원소 컨테이너에도 흔히 쓰인다.
- 예컨대 Set에는 원소의 타입을 뜻하는 단 하나의 타입 매개변수만 있으면 되며, Map에는 키와 값의 타입을 뜻하는 2개만 필요한 식으로 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수가 제한된다.
- 하지만 더 유연한 수단이 필요할 때도 종종 있다.
- 데이터베이스의 행은 임의 개수의 열을 가질 수 있는데, 모두 열을 타입 안전하게 이용할 수 있다면 멋질 것이다.
- 쉬운 해법은 컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하면 된다.
- 이렇게 하면 제네릭 타입 시스템이 값의 타입이 키와 같음을 보장해줄 것이며 이런 설계 방식을 타입 안전 이종 컨테이너 패턴이라 한다.
- 간단한 예로 타입별로 즐겨 찾는 인스턴스를 저장하고 검색할 수 있는 Favorites 클래스를 생각해보자.
- 각 타입의 Class 객체를 매개변수화한 키 역할로 사용하면 되는데, 이 방식이 동작하는 이유는 class의 클래스가 제네릭이기 때문이다.
- class 리터럴의 타입은 Class가 아닌 Class<T>다.
- Favorites 인스턴스는 타입 안전하다. String을 요청했는데 Integer를 반환하는 일은 절대 없다.
- 또한 모든 키의 타입이 제각각이라, 일반적인 맵과 달리 여러 가지 타입의 원소를 담을 수 있다.
- 따라서 Favorites는 타입 안전 이종 컨테이너라 할 만하다.
- Favorites가 사용하는 private 맵 변수인 favorites 타입은 Map<Class<?>, Object>이다.
- 비한정적 와일드카드 타입이라 이 맵 안에 아무것도 넣을 수 없다고 생각할 수 있지만, 사실은 그 반대다.
- 와일드카드 타입이 중첩되었다는 점을 깨달아야 한다. 맵이 아니라 키가 와일드카드 타입인 것이다.
- 이는 모든 키가 서로 다른 매개변수화 타입일 수 있다는 뜻으로, 첫 번째는 Class<String>>, 두 번째는 Class<Integer> 식으로 될 수 있으며 다양한 타입을 지원하는 힘이 여기서 나온다.
- 또 favorites 맵의 값 타입은 단순히 Object라는 것이다.
- 이 맵은 키와 값 사이의 타입 관계를 보증하지 않는다는 것이다. 사실 자바의 타입 시스템에서는 이 관계를 명시할 방법이 없다.
- 하지만 우리는 이 관계가 성립함을 알고 있고, 즐겨찾기를 검색할 때 그 이점을 누리게 된다.
- putFavorite 구현은 아주 쉬운데 Class 객체와 즐겨찾기 인스턴스를 favorites에 추가해 관계를 지으면 끝이다.
- '타입 링크' 정보는 버려지는데 즉 그 값이 그 키 타입의 인스턴스라는 저옵는 알 수 없다. 하지만 getFavorite 메서드에서 이 관계를 되살릴 수 있으니 상관 없다.
- getFavorite 코드는 favorites 맵의 값 타입인 Object를 Class의 cast 메서드를 이용해 Class 객체가 가리키는 타입으로 동적 형변환한다.
- cast 메서드가 단지 인수를 그대로 반환한다면 굳이 왜 사용하는 걸까?
- cast 메서드의 시그니처가 Class 클래스가 제네릭이라는 이점을 완벽히 활용하기 때문이다.
- 다음 코드에서 보듯 cast의 반환 타입은 Class 객체의 타입 매개변수와 같다.
지금의 Favorites 클래스에는 알아두어야 할 제약이 두가지 있다.
첫 번째, 악의적인 클라이언트가 Class 객체를 제네릭이 아닌 raw 타입으로 넘기면 Favorites 인스턴스의 타입 안정성이
깨진다.
- Favorites가 타입 불변식을 어기는 일이 없도록 보장하려면 putFavorite 메서드에서 인수로 주어진 instance의 타입이 type으로 명시한 타입과 같은지 확인하면 되며 다음 코드와 같이 동적 형변환을 쓰면 된다.
두 번째 제약은 실체화 불가 타입에는 Favorites 클래스를 사용할 수 없다는 것이다.
- 즉 즐겨찾는 String이나 String[]은 저장할 수 있어도 즐겨 찾는 List<String>은 저장할 수 없다.
- List<String>을 저장하려는 코드는 컴파일되지 않을 것이다. List<String> 용 클래스 객체를 얻을 수 없기 때문이다.
- List<String>과 List<INteger>는 List.class라는 같은 Class 객체를 공유하므로, 만약 List<String>.class 와 List<Integer>.class를 허용해서 둘 다 똑같은 타입의 객체 참조를 반환한다면 Favorites 객체의 내부는 아수라장이 될 것이다.
- 이 두번째 제약을 슈퍼 타입 토큰으로 해결하려는 시도도 있다.
- 스프링 프레임워크에서는 아예 ParameterizedTypeReference라는 클래스로 미리 구현해놓았다.
- 본문 예제의 Favorites에 슈퍼 타입 토큰을 적용하면 다음 코드처럼 제네릭 타입도 저장할 수 있다.
- Favorites가 사용하는 타입 토큰은 비한정적이다. 즉 getFavorite과 putFavorite은 어떤 Class 객체든 받아들인다.
- 때로는 이 메서드들이 허용하는 타입을 제한하고 싶을 수 있는데, 한정적 타입 토큰을 활용하면 가능하다.
- 예를 들어 다음은 AnnotatedElement 인터페이스에 선언된 메서드로 대상 요소에 달려 있는 애너테이션을 런타임에 읽어 오는 기능을 한다.
- 이 메서드는 리플렉션의 대상이 되는 타입들, 클래스(java.lang.Class<T>, 메서드(java.lang.reflect.Method), 필드와 같이 프로그램 요소를 포현하는 타입들에서 구현된다.
- 여기서 annotationType 인수는 애너테이션 타입을 뜻하는 한정적 타입 토큰이다.
- 이 메서드는 토큰으로 명시한 타입의 애너테이션이 대상 요소에 달려 있다면 그 애너테이션을 반환하고, 없다면 nul을 반환한다.
- 즉 애너테이션된 요소는 그 키가 애너테이션 타입인, 타입 안전 이종 컨테이너인 것이다.
- Class<?> 타입의 객체가 있고 이를 한정적 타입 토큰을 받는 메서드에 넘기려면 Class 클래스의 asSubclass 메서드를 활용하자.
- 호출된 인스턴스 자신의 Class 객체를 인수가 명시한 클래스로 형변환하는 인스턴스 메서드이며 형변환에 성공시 인수로 받은 클래스 객체를 반환하고 실패시 ClassCastException을 던진다.
핵심 정리
- 컬렉션 API로 대표되는 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되어 있다.
- 하지만 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 타입 안전 이종 컨테이너를 만들 수 있다.
- 타입 안전 이종 컨테이너는 Class를 키로 쓰며, 이런 식으로 쓰이는 Class 객체를 타입 토큰이라 한다.
- 또한, 직접 구현한 키 타입도 쓸 수 있다. 데이터베이스의 행을 표현한 DatabaseRow 타입에는 제네릭 타입인 Column<T>를 키로 사용할 수 있다.
참고 자료
'JAVA > Effective java' 카테고리의 다른 글
아이템 35. ordinal 메서드 대신 인스턴스 필드를 사용하라 (0) 2021.03.26 아이템34. int 상수 대신 열거 타입을 사용하라 (0) 2021.03.23 아이템 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라. (0) 2021.03.20 31. 한정적 와일드카드를 사용해 API 유연성을 높이라. (0) 2021.03.16 30. 이왕이면 제네릭타입으로 만들라2 (0) 2021.03.15