맞는데 왜 틀릴까..?

Java/Effective Java

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

안도일 2023. 5. 16. 03:55

타입 안전 이종 컨테이너란, 제네릭을 이용하여 서로 다른 타입의 객체를 담을 수 있는 컨테이너를 말한다.

즉, 컨테이너 내부에 저장된 객체들이 모두 같은 타입일 필요가 없으며, 다양한 타입의 객체들을 함께 보관하면서도 타입 안정성을 보장할 수 있다.

 

 

컨테이너(Container)

컨테이너란 객체를 담는 역할을 하는 클래스를 의미한다. 일반적으로 다수의 객체를 담을 수 있는 객체를 말하며, 주로 배열(Array)과 컬렉션(Collection)이 포함된다.

 


타입 안전 이종 컨테이너 패턴

 

 

컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하면 제네릭 타입 시스템이 값의 타입이 키와 같음을 보장해 준다.

 

타입별로 즐겨 찾는 인스턴스를 저장하고 검색할 수 있는 Favorites 클래스를 예로 들어보자.

 

 

Favorites API

 

public class Favorites{
    public <T> void putFavorite(Class<T> type, T instance);
    public <T> T getFavorite(Class<T> type);
}

 

Favorites 클래스 사용 예시

 

 

실행 결과

 

 

하나의 컨테이너 Favorite에 String, Integer, Class 인스턴스를 저장, 검색, 출력한다.

 

  • Favorites 인스턴스는 타입 안전하다.
  • 모든 키의 타입이 제각각이라, 일반적인 맵과 달리 여러 가지 타입의 원소를 담을 수 있다.

 

Favorites 구현

 

 

  • Map의 키가 와일드카드 타입이기 때문에 모든 키가 서로 다른 매개변수화 타입일 수 있다.
  • 값 타입이 단순히 Object이므로 이 맵은 키와 값 사이의 타입 관계를 보증하지 않는다.
  • getFavorite에서 favorites.get(type)의 타입은 맵의 값 타입인 Object 이므로 cast 메서드를 통해 Class 객체가 가리키는 타입으로 동적 형변환 한다.

 

class 리터럴

class 리터럴은 클래스나 인터페이스의 이름을 나타내는 데 사용되는 것으로, 다음과 같이 .class를 클래스 이름 뒤에 붙이는 방식으로 표기한다.

Class<?> clazz = MyClass.class;

 

cast 메서드

형변환 연산자의 동적 버전. 단순히 주어진 인수가 Class 객체가 알려주는 타입의 인스턴스인지를 검사한 다음, 맞다면 그 인수를 그대로 반환하고, 아니면 ClassCastException을 던진다.

cast 메서드의 시그니처 : public class Class<T> { T cast (Object obj); }
cast의 반환 타입은 Class 객체의 타입 매개변수와 같다. 

 

시그니처 (signature)

시그니처란 메서드나 생성자 등의 선언부를 말한다.

 

타입 토큰 (type token)

컴파일타임 타입 정보와 런타임 타입 정보를 알아내기 위해 메서드들이 주고받는 class 리터럴

 

Map의 키와 값의 타입 관계 보증

키와 값의 타입 관계를 보증한다는 것은, 맵에서 사용되는 키와 값의 타입이 맵의 선언부에서 명시한 타입과 일치해야 한다는 것을 의미한다. 예를 들어, Map<String, Integer>라는 맵이 있다고 했을 때, 이 경우 키는 String 타입이어야 하고, 값은 Integer 타입이어야 한다.

자바 컴파일러는 제네릭 타입 시스템을 통해 맵의 타입 관계를 검사하며, 맵을 사용하는 곳에서는 올바른 타입의 키와 값만 사용할 수 있도록 보증하는데, 이로써 컴파일러는 잘못된 타입을 사용하여 발생할 수 있는 런타임 오류를 사전에 방지하고, 타입 안정성을 확보할 수 있다.



지금의 Favorites 클래스에 알아두어야 할 제약 두 가지

 

악의적인 클라이언트가 Class 객체를 제네릭이 아닌 로 타입으로 넘기면 Favorites 인스턴스의 타입 안전성이 쉽게 깨진다.

 

Favorites가 타입 불변식을 어기는 일이 없도록 보장하려면 putFavorite 메서드에서 인수로 주어진 instance의 타입이 type으로 명시한 타입과 같은지 확인하면 된다.

아래와 같이 동적 형변환을 해주자.

 

 

 

실체화 불가 타입에는 사용할 수 없다.

 

  • 즐겨 찾는 String이나 String [ ]은 저장할 수 있지만 List<String>은 저장할 수 없다.
  • List<Integer>와 List<String>은 List.class 라는 같은 Class 객체를 공유하므로 List<String>용 Class 객체를 따로 얻을 수 없다.

 


 

한정적 타입 토큰

 

Favorites가 사용하는 타입 토큰은 비한정적이기 때문에 getFavorite와 putFavorite는 어떤 Class 객체든 받아들인다.

이 메서드들이 허용하는 타입을 제한하려면 한정적 타입 토큰을 사용하자.

 

 

AnnotatedElement 인터페이스에 선언된 메서드

 

public <T extends Annotation>
	T getAnnotation(Class<T> annotationType);

 

대상 요소에 달려 있는 애너테이션을 런타임에 읽어오는 기능을 한다.

여기서 annotationType 인수는 애너테이션 타입을 뜻하는 한정적 타입 토큰이다.

 

이 메서드는 토큰으로 명시한 타입의 애너테이션이 대상 요소에 달려있다면 그 애너테이션을 반환하고, 없다면 null을 반환한다.

 

 

Class<?> 타입의 객체를 한정적 타입 토큰을 받는 메서드에 넘기려면?

 

  • Class 클래스가 형변환을 안전하게 수행해 주는 인스턴스 메서드 asSubclass 메서드를 제공한다.
  • 호출된 인스턴스 자신의  Class 객체를 인수가 명시한 클래스로 형변환 한다.

asSubclass를 사용해 한정적 타입 토큰을 안전하게 형변환한다.

 


 

결론

 

  • 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되어 있다.
  • 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 타입 안전 이종 컨테이너를 만들 수 있다.
  • 타입 안전 이종 컨테이너는 Class를 키로 쓰며, 이런 식으로 쓰이는 Class 객체를 타입 토큰이라 한다.