꾸준한 스터디
article thumbnail
  • 타입 안전 이종 컨테이너 : 한 타입의 객체만 담을 수 있는 컨테이너가 아니라 여러 다른 타입(이종)을 담을 수 있는 타입 안전한 컨테이너
  • 타입 토큰 : String.class 또는  Class<String>
  • 타입 안전 이종 컨테이너 구현 방법: 컨테이너가 아니라 "키"를 매개변수화 하라

 

컨테이너 : 다른 객체를 담을 수 있는 객체. 객체를 담고 관리할 수 있는 Map, Set, Optional 등이 있다. 개발자가 객체를 보다 쉽게 관리하고 처리할 수 있도록 도와주며, 자료구조를 다룰 수 있는 다양한 메서드를 제공한다.

 

이종 컨테이너 : 서로 다른 타입의 객체를 하나의 컨테이너에 담아 관리할 수 있는 자바 컬렉션. 이종 컨테이너를 사용하면 여러 타입의 객체를 일관된 방법으로 처리할 수 있어 코드의 유연성과 재사용성을 높일 수 있다.

 

 

지금껏 우리가 제네릭을 사용한 방식은 1가지 타입만 넣을 수 있는 컨테이너를 만드는 것이었다.

String을 넣으면 String, Integer를 넣으면 Integer

지정된 제네릭 객체로 사용하지 않으면 컴파일 에러가나니 타입에 안전하게 사용할 수 있다.

 

 

경우에 따라서 1개의 타입만 컨테이너에서 관리하는게 아닌 여러 타입을 관리해야 하는 경우도 있는데 가령 데이터베이스의 행은 여러개의 열을 가질 수 있는데 해당 열의 다양한 타입을 Type-safety 하게 관리하고 사용할 수 있게 하려면?

Map을 사용하여 key 값에 타입을 받을 수 있게 하고 타입에 따라 받는 value가 어떤것일지 모르니 Object로 받는다면?

여러 타입을 받을 순 있지만 타입에 안전하지 않다.

Object는 어떤 객체든 다 받을 수 있으니 String을 담으려고 했던 곳에 아무 객체나 다 들어가고 심지어 출력할 때 별도의 에러가 나지 않는다 key값에 해당하는 타입이 value에 들어간다는 그 무엇도 없으니.. 해당 타입 key 값에 들어있는 value만 출력할 뿐이다.

 

Class 객체는 제네릭 타입 객체이다.

컨테이너가 아닌 key 값에 제네릭을 사용하면

비 한정적 와일드 카드를 사용해서 어떠한 타입이던 다 받고 put 메서드와 get 메서드에 key 값으로 받은 타입만 매개변수로 올 수 있게 하면 클라이언트에서 맞지 않은 타입을 사용했을 때 컴파일 에러가 나게 된다.

 

get 메서드에서 제네릭 Object로 리턴할 때 명시적 형변환하는 방법을 사용해도 되나 그것 보다 Class 객체의 cast 메서드를사용하여 전달받은 객체를 검사받고 리턴하는 타입이 같은 타입인지 체크를 해주는 방법을 사용하는 것이 더 좋다.

타입 안전 이종 컨테이너

 

String.class 리터럴의 타입은 Class<String> 

Integer.class 리터럴의 타입은 Class<Integer>

컴파일 타임 타입정보와 런타임 타입 정보를 알아내기 위해 메서드들이 주고받는 class 리터럴을 타입 토큰(type token)이라 한다.

 

 

로우 타입으로 우회하여 타입 안전을 깨트리는 경우

Class로 다시 형변환을 하게 된다면 value 값에 Class 자체가 제네릭이 되니 value 에 다른 타입이 들어와도 컴파일 타임에서 오류를 잡아낼 수 없다. get 해서 가져올 때 String 타입이 아니니 에러가 난다.

값을 넣을 때 타입 체크를 하게 되면 런타임 실행시 에러가 나서 조금 더 빨리 오류를 잡아낼 수 있다.

 

 

컬렉션 리터럴은 파라미터화 할 수 없는데 사용할 수 있는 방법

 

 

슈퍼 타입 토큰

  • 상속을 사용한 경우 제네릭 타입을 알아낼 수 있다. 제네릭 타입이 제거되지 않아 reflection으로 정보를 알아온다.

Super<T> 클래스의 경우 클라이언트에서 선언한 String 객체가 컴파일 후 소거되고 value는 모든 타입을 받을 수 있는 Object로 컴파일되니 어떤 객체를 제네릭에 사용했는지 클라이언트에서 알기 어렵다.

 

하지만 Sub 클래스의 경우 Super 클래스를 상속받고 제네릭에 명시적으로 <String>을 사용했다면 어떤 객체를 사용했는지 알아오는 방법이 있다.

getGenericSuperClass() 메서드로 상속받은 Super<String> 객체의 정보를 가져오고 ParameterizedType로 타입을 변환하면 getActualTypeArguments() 으로 제네릭에 사용된 객체들을 배열로 받을 수 있는데 제네릭에 여러개가 들어갈 수 있으니.. 일단 String 한개이니 0번째 정보를 가져오면 String 객체가 쓰였다는 것을 알 수 있다.

 

상속을 사용하지 않고 제네릭 타입을 알아낼 수 있는 방법은 없다. 상속을 사용했다면 상속받은 객체의 제네릭 정보를 알아올 수 있다.

 

클래스를 만들지 않고 익명 클래스로 제네릭을 선언하며 클래스를 인스턴스화 하면 제네릭 정보를 알아낼 수 있다.

 

타입을 추론해 내는 abstract 클래스 해당 클래스를 상속받은 익명클래스의 제네릭 정보를 알아온다.

ParameterizedType이나 Type을 보면 reflect 패키지에 있는 객체인 것을 알 수 있다.

 

TypeRef 객체를 key 값으로 사용하고 익명클래스로 인스턴스화 하면 컬렉션 제네릭 객체별로 정보를 넣거나 가져올 수 있다. TypeRef 객체중 equals와 hashCode 메서드로 서로 다른 객체임을 인식해서 가져올 수 있다.

 

슈퍼타입토큰도 깨질 수 있는 여지가 있다.

favoriteList 메서드 내부에서 ref 변수에 담긴 TypeRef 객체는 동일한 객체를 사용하는데 integer 값을 넣고 String으로 꺼낼 때 형변환 Exception이 난다.

 

 

한정적 타입 토큰

  • 한정적 타입 토큰을 사용한다면, 이종 컨테이너에 사용할 수 있는 타입을 제한할 수 있다.
    • AnnotatedElement.<T extends Annotation> T
    • getAnnotation(Class<T> annotationClass);
  • asSubclass 메서드
    • 메서드를 호출하는 Class 인스턴스를 인수로 명시한 클래스로 형변환 한다.

 

애너테이션 API는 한정적 타입 토큰을 적극적으로 사용한다.

AnnotatedElement 인터페이스에 getAnnotation() 메서드는 대상 요소에 달려있는 애너테이션을 런타임에 읽어오는 기능을 한다. 리플렉션의 대상이 되는 타입들 클래스, 메서드, 필드 같이 프로그램 요소를 표현하는 타입들에서 구현한다.

<T extends Annotation> T getAnnotation(Class<T> annotationClass);

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

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

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

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

객체를 Class<? extends Annotation>으로 형변환할 수도 있지만 Class 객체에서 형변환을 안전하고 동적으로 수행해주는 인스턴스 메서드를 제공한다.

asSubclass 메서드로, 호출된 인스턴스 자신의 Class 객체를 인수가 명시한 클래스로 형변환한다. (형변환 된다는 것은 인수로 명시한 클래스의 하위 클래스)

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

asSubclass 메서드로 컴파일 시점에 타입을 알 수 없는 애너테이션을 런타임에 읽어낸다.

 

애너테이션은 런타임까지 살아있는 생명주기를 가져야 런타임시 읽어올 수 있다.

profile

꾸준한 스터디

@StudyRecord

포스팅이 유익하셨다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!