본문 바로가기
Java/Effective Java 3E

[이펙티브자바 3판] ITEM33. 타입 안전 이종 컨테이너를 고려하라

by 잭피 2020. 10. 25.

이번장의 핵심은...

컬렉션 API로 대표되는 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정

하지만, 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 타입 안전 이종 컨테이너를 만들 수 있음 타입 안전 이종 컨테이너는 Class를 키로 쓰며, 이런 Class 객체를 타입 토큰이라 한다

또한, 직접 구현한 키 타입도 쓸 수 있다

예) db 행(컨테이너)을 표현한 DatabaseRow 타입에는 제네릭 타입인 Column<T>를 키로 사용할 수 있다


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

컨테이너 대신 키를 매개변수화한 다음, 컨테이너 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하는 방법

→ 제네릭 타입 시스템이 값의 타입이 키와 같음을 보장

예) 타입별로 즐겨 찾는 인스턴스를 저장하고 검색할 수 있는 클래스

// 타입 안전 이종 컨테이너 패턴 - API
public class Favorites {
	public <T> void putFavorite(Class<T> type, T instance);
	public <T> T getFavorite(Class<T> type);
}

 

위 클래스를 사용하는 클라이언트

즐겨 찾는 String, Integer, Class 인스턴스를 저장, 검색, 출력

// 코드 33-2 타입 안전 이종 컨테이너 패턴 - 
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());
		// Java cafebabe Favorites 출력
}
자바의 prinf에서 %n은 플랫폼에 맞는 줄바꿈 문자로 자동으로 대체
(대부분 플랫폼에서 \n이 되겠지만, 모든 플랫폼이 그렇지는 않다)

Favorites 인스턴스는 타입이 안전함 (String 요청 시, 다른 타입을 반환할 일은 없음)

또한, 일반 맵과 달리 여러 가지 타입 원소를 담을 수 있다

따라서 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));
    }  
}

1. favorites 맵의 모든 키가 서로 다른 매개변수화 타입일 수 있다

ex) Class<String>, Class<Integer>

 

2. favoites 맵의 값 타입은 Object

→ 키와 값 사이의 타입 관계를 보증하지 않음

즉, 모든 값이 키로 명시한 타입임을 보증하지 않는다

 

3. putFavortie - 주어진 Class 객체와 즐겨찾기 인스턴스를 추가해 관계를 지어 넣음

타입 링크 정보는 버려지지만, getFavorite 메서드에서 이 관계를 다시 살릴 수 있다

 

4. getFavorite - 주어진 Class 객체에 해당하는 값을 favoirtes 맵에서 꺼냄

이 객체가 바로 반환해야 할 객체가 맞지만, 잘못된 컴파일타임 타입을 가짐

해당 객체의 값 타입인 Object이나, 우리는 T로 바꿔 반환해야 한다.

→ Class의 cast 메서드를 사용해 이 객체 참조를 Class 객체가 가르키는 타입으로 동적 형변환cast 메서드 : 형변환 연산자의 동적 버전

인수가 Class 객체가 알려주는 타입의 인스턴스인지 검사하고 맞으면 그 인수 그대로 반환하고 아니면 ClassCastException을 반환

즉, favorites 맵 안의 값은 해당 키의 타입과 항상 일치함을 알고 있다

 

cast 메서드 사용 이유? (단지 인수를 그대로 반환하기만 하는데??)

→ cast 메서드의 시그니처가 Class 클래스가 제네릭이라는 이점을 완벽히 활용하기 때문이다

// cast의 반환 타입은 Class 객체의 타입 매개변수와 같음
public class Class<T> {
	T cast(Object obf);
}

즉, Favorites를 타입 안전하게 만드는 비결이다

 

 

악의적인 클라이언트가 Class 객체를 (제네릭이 아닌) 로 타입으로 넘기면 Favorites 인스턴스의 타입 안정성이 쉽게 깨진다 (컴파일할 때 비검사 경고가 뜸)

// 호출하기전까진 아무 문제 없다가 ClassCastException (컴파일타임에 잡지못함)
f.putFavorite((Class)Integer.class, "Integer 인스턴스가 아님");
int favoriteInteger = f.getFavorite(Integer.class);

// 컴파일도 되고 동작도 함
HashSet<Integer> set = new HashSet<>();
((HashSet).set).add("문자열");

Favorites 클래스가 타입 불변식을 어기는 일이 없도록 보장하려면

putFavorite 메서드에서 인수로 주어진 instance의 타입이 type으로 명시한 타입과 같은지 확인

그 방법으론 동적 형변환을 사용하자

// 동적 형변환으로 런타임 타입 안정성 확보
public <T> void putFavorite(Class<T> type, T instance) {
	favorites.put(Objects.requireNonNull(type), type.cast(instance));
}

예) Collections 의 checkedSet, checkedList, checkedMap 같은 메서드가 위 방식을 적용한 컬랙션 래퍼이다

이 정적 팩터리들은 컬랙션(or 맵)과 1~2개의 Class 객체를 받음

모두 제네릭이라 Class 객체와 컬렉션의 컴파일타임 타입이 같음을 보장

또한 이 래퍼들은 내부 컬렉션들을 실체화한다

제네릭과 로 타입을 섞어 사용하는 애플리케이션에서 클라이언트 코드가 컬렉션에 잘못된 타입의 원소를 넣지 못하게 추적하는 데 도움을 준다

Favoirtes 클래스에 알아두어야 할 제약 2가지

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

다시 말해, 즐겨 찾는 String이나 String[]은 저장할 수 있어도 즐겨 찾는 List<String>은 저장할 수 없다

List<String>을 저장하려는 코드는 컴파일되지 않음 (List<String>.class (문법오류) - List<String>용 Clss 객체는 얻을 수 없음)

List<String>, List<Integer> → List.class 라는 같은 Class 객체를 공유함

 

해결

슈퍼 타입 토큰

슈퍼 타입을 토큰으로 사용하겠다! 라는 의미

제네릭 정보가 지워지는 문제 때문에 슈퍼 타입 토큰 기법이 생김

제네릭 정보가 컴파일시 런타임시 다지워지지만 제네릭 정보를 런타임시 가져올 방법이 존재하는데,

제네릭 클래스를 정의한 후, 그 제네릭 클래스를 상속받으면 런타임시에는 제네릭 정보를 가져올 수 있다

예시) http://wonwoo.ml/index.php/post/1807

 

Super type token - 머루의개발블로그

오늘은 Super type token에 대해서 알아보자. Super type token을 알기전에 일단 type token을 알아야 되는데 type token 이란 간단히 말해서 타입을 나타내는 토큰(?)이다. 예를들어 String.class는 클래스 리터럴

wonwoo.ml

리터럴(Literal) 이란 데이터 그 자체 객체 리터럴 → immutable class(불변 클래스) 해당 클래스는 한번 생성하면 객체 안의 데이터는 변하지 않음 만약 변경이 일어난다면 새로운 객체를 생성함 Immutable Class는 레퍼런스 타입의 객체이기 때문에 heap영역에 생성 ex) 대표적으로 String, Boolean, Integer, Float, Long 등
보통 기본형 데이터를 의미하지만, 특정 객체(Immutable class, VO class) 한에서는 리터럴이 될 수 있다

* 참고 

토비의봄#02. Super Type Token

 

토비의봄#02. Super Type Token

1. Generics java5에서 부터 추가된 제네릭은 타입을 파라미터로 만들어 넘어오는 파라미터에 따라 다른 타입이 되게끔한다. 이로서 얻을 수 있는 이득은 타입이 동적으로 변하게 되기때문에 개발자

multifrontgarden.tistory.com

토비의 봄 TV 2회 - 수퍼 타입 토큰

스프링에선 ParameterizedTypeReference라는 클래스로 미리 구현해놓음

// 슈퍼 타입 토큰 적용
Favorites f = Favorites();
List<String> pets = Arrays.asList("개", "고양이", "앵무");
f.putFavorite(new TypeRef<List<String>>(){}, pets);
List<String> iistofStrings = f.getFavorite(new TypeRef<List<String>>(){});
TypeRef<List<String>>(){});
단, 슈퍼 타입 토큰도 완벽하지 않으니 주의해서 사용하자 완벽히 만족스러운 우회로는 없다

Favorites가 사용하는 타입 토큰은 비한정적

즉, getFavorite과 put은 어떤 class 객체든 받아들임

 

한정적 타입 토큰

타입을 제한하고 싶을 때 한정적 타입 토큰을 활용

한정적 타입 토큰이란 한정적 타입 매개변수나 한정적 와일드카드를 사용하여 표현 가능한 타입을 제한하는 타입 토큰

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

(대상 요소에 달려 있는 애너테이션을 런타임에 읽어 오는 기능을 함)

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

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

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

즉, 애너테이션된 요소는 그 키가 애너테이션 타입인, 타입 안전 이종 컨테이너이다

 

Class<?> 타입의 객체가 있고, 이를 한정적 타입 토큰을 받는 메서드에 넘길려면?

객체에 Class<? extends Annotation>으로 형변환 할 수 있지만,

이 형변환은 비검사이므로 컴파일할 때 경고가 뜰 것이다

→ Class 클래스가 이런 형변환을 안전하게 동적으로 수행해주는 인스턴스 메서드를 제공한다

→ asSubclass 메서드

호출된 인스턴스 자신의 Class 객체를 인수가 명시한 클래스로 형변환한다

형변환에 성공하면 인수로 받은 클래스 객체를 반환하고, 실패하면 ClassCastException을 던짐

예) 컴파일 시점에 타입을 알 수 없는 애너테이션을 asSubclass 메서드를 사용해 런타임에 읽어냄

static Annotation getAnnotation(AnnotatedElement element,
                                    String annotationTypeName) {
        Class<?> annotationType = null; // 비한정적 타입 토큰
        try {
            annotationType = Class.forName(annotationTypeName);
        } catch (Exception ex) {
            throw new IllegalArgumentException(ex);
        }
        return element.getAnnotation(
                annotationType.asSubclass(Annotation.class));
    }

오류나 경고 없이 컴파일됨

 

이 글은 “이펙티브 자바 3판” 책 내용을 정리한 글입니다.

만약 저작권 관련 문제가 있다면 “shk3029@kakao.com”로 메일을 보내주시면, 바로 삭제하도록 하겠습니다.

 

댓글