Post

(Effective Java) 5장 제네릭

[!info] 제네릭의 본질은, 런타임에 ClassCastException이 발생하는 것을 막아주어 타입 안전성을 높이는데 있다.

아이템 26. raw 타입은 사용하지 마라 (제네릭 쓸 때 <>를 빼먹지 않도록 주의해라)

  • raw타입이란 List<>가 아니라 그냥 List로, 제네릭을 타입 지정 없이 쓰는 것을 말한다.
    • 이걸로 받으면 아무 타입이나 담을 수 있게 된다. 이는 타입 안전성을 해치게 된다.
    • 예를 들어 List로 캐스팅해서 쓰면, List<String>에 Integer를 넣을 수 있다!
1
2
3
4
5
6
List<String> a = new ArrayList<>();
a.add("qwer");
List b = a;
b.add(Integer.valueOf(333));
System.out.println(b.get(0));
System.out.println(b.get(1));
  • List<Object>List는 다르다. 이는 변성을 지정하지 않으면 List<String> 등과 호환 되지 않는다.
  • 어떤 제네릭 타입이든 받고 싶은 경우 비한정적 와일드카드 List<?>를 사용한다.
    • 여기에는 null 말고 어떤 원소도 넣을 수 없다는 점이 그냥 List와의 차이점이다.
    • 원소를 넣을 수 없으니 타입 안전성을 해치지 않는다.
  • raw 타입을 사용해야 하는 경우는 크게 두 가지다.
    • List.class 이를 타입 토큰(type token)이라 부른다. <>를 쓰면 접근이 안된다.
    • o instanceof List로 검사해야 하는 경우. 런타임에는 제네릭 타입 정보가 지워지기 때문에. 이 때 검사 후 형변환은 그냥 List가 아니라 List<?>로 해야 한다!

아이템 27. 비검사 경고를 제거하라

  • 비검사 경고란 warning: [unchecked] 를 말하는데 casting할 때 검사 안했다고 뜨는 경고.

아이템 28. 배열보다는 리스트를 사용하라

  • 제네릭은 무공변이나, 배열은 공변이다.
  • 배열은 다른 타입을 넣으려고 할 때, 런타임에 에러가 발생하지만 제네릭은 컴파일 타임에 발생해서 좀 더 안전하다.
  • 실체화 불가 타입에 대한 배열은 생성할 수 없다.
    • non-reifiable type
    • new List<E>[], new List<String>[], new E[] 는 컴파일 타임에 generic array creation 에러가 발생한다.
    • 이런 타입들을 실체화 불가 타입(non-reifiable type) 이라고 한다. 런타임에 타입 정보가 없어지는 타입들.
    • 제네릭 배열이 허용된다고 가정하면 런타임에 ClassCastException이 발생할 수 있는데, 이는 컴파일 타임에 타입 에러를 체크하겠다는 제네릭의 취지와 어긋나기 때문이다.
    • 단, E[] 는 사용해야 하는 경우 우회해서 사용하기도 한다. (아이템 29)

아이템 29. 이왕이면 제네릭 타입으로 만들라 (제네릭 배열 우회 생성 방법)

  • 유틸리티 클래스(Stack 등) 만들 때는 제네릭 타입으로 만드는 것이 좋다.
  • 내부에서 가지고있는 컨테이너 변수는 제네릭 배열을 사용해도 되고, 제네릭 리스트를 사용해도 된다.
    • 아이템 28에서 배열 보다는 리스트를 사용하라고 했지만, 경우에 따라 리스트를 사용할 수 없는 경우도 있다.
      • 성능은 배열이 우세하다. 실제로 HashMap같은건 성능 때문에 내부적으로 배열을 쓴다.
      • ArrayList 같은 제네릭 타입을 만드는 경우에도 결국은 기본 타입인 배열을 사용해야 한다.
  • 제네릭 배열 우회 생성 방법1 17번 라인에서 캐스팅해주는 이유는, E는 실체화 불가 타입이라 new E[1]이 불가능하기 때문.
  • 제네릭 배열 우회 생성 방법2 선언은 Object로 하고 28번 라인에서 쓸 때 (E)로 캐스팅.
    • 1이 좀 더 보편적인 방법이다.
    • 1은 가독성이 좋다. E[] 타입으로 선언하기 때문에 확실히 알 수 있다.
    • 1은 캐스팅도 생성해서 담을 때 한 번만 해도 된다. 반면 2는 접근 시 마다 캐스팅해줘야 한다.
    • 경우에 따라서 1은 배열의 런타임 타입이 컴파일 타입과 달라 힙 오염(아이템32)을 일으킬 가능성도 있다.

아이템 30. 이왕이면 제네릭 메서드로 만들라

  • 제네릭 싱글턴 팩터리 패턴
  • 재귀적 타입 한정
    • 예제) <E extends Comparable<E>>
    • “Comparable를 구현한 클래스만 E가 될 수 있다."
    • 근데 사실, 이 보다는 <E extends Comparable<? super E>>를 쓰는 편이 좋다.(아이템 31)

아이템 31. 한정적 와일드카드를 사용해 API 유연성을 높여라

  • [Coding/Kotlin Java] - [Kotlin] 제네릭 : 변성(variance), 타입 프로젝션(type projection)
  • 생산자나 소비자용 입력 파라미터에 <? extends|super T> 를 잘 활용하라는 내용.
    • 제네릭은 기본적으로 무공변이니까. 공변성을 지정해서 더 유연하게 만들어 줘라.
  • 와일드카드 적용 원칙 PECS : T의 producer라면-extends, consumer라면-super
    • 생산이란 T 인스턴스를 [조건에 맞는걸 찾아 반환하거나, 내부 컬렉션에 추가하거나] 하는 등의 활동을 의미한다. T 인스턴스를 새로 생성한다는 의미가 아니다.
    • 반대로 소비란 [내부의 T 인스턴스를 외부 컬렉션에 추가] 하는 등의 활동을 의미한다.
1
2
3
4
5
6
7
8
9
class AnimalList<T> {
    ArrayList<T> animals = new ArrayList<>();  // T can be Tiger
   public AnimalList(Collection<? extends T> producer) {
        animals.addAll(producer);  // producer can be Collection<WhiteTiger>
    }
   public void export(Collection<? super T> consumer) {
        consumer.consume(animals);  // consumer can be Collection<Animal>
    }
}
  • 주의) 리턴 타입을 적을 때는 extends, super 등 한정적 와일드카드를 쓰면 안된다. 클라이언트 코드에서도 와일드카드 타입을 써야 하기 때문.
    • 타입 매개변수 T 자체를 제한하는건 괜찮음.예제
  • 비교 가능한(Comparable) 클래스만 E가 될 수 있도록 하려면, 이렇게 쓴다.
    • <E extends Comparable<? super E>> E
    • super를 안쓰고 그냥 <E extends Comparable<E>>로 지정하는 경우, Comparable을 간접적으로 상속하는 케이스는 실제로는 비교 가능하지만 E가 될 수 없다.
    • 간접적으로 상속한다는건 Comparable을 상속한 다른 인터페이스를 구현하는 경우. Comparable이 조부모가 됨.
    • Comparator도 마찬가지다.

아이템 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라

  • 가변인수로 제네릭을 받는 경우 힙 오염(heap pollution)이 발생할 수 있음.예시
    • 가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 하나 만들어진다.
    • 실체화 불가 타입을 가변 인수로 받는 경우(List<String>...이나 T...) 실체화 불가 타입에 대한 배열이 만들어진다는 뜻이다.
    • 제네릭 등 실체화 불가 타입의 배열은 원래는 불가하다. 타입 안전성이 깨질 수 있기 때문에.
      • 여기서 타입 안전성이 깨진다는건 “런타임에 ClassCastException이 발생할 수 있다.”를 의미
      • 그래서 컴파일 타임에 경고로 알려준다.
  • 제네릭 배열을 직접 만드는 것은 막아 두었으면서, 가변인수에서 생성하는건 경고로 끝내는 이유는?
    • T... 같은게 실무에서 매우 유용하기 때문!
    • 실제로 List.of(E...), Arrays.asList(T... a) 등 자바 라이브러리도 이런 메서드를 여럿 제공한다.
  • 위에서 컴파일 타임에 경고가 발생한다고 했는데, 이런 메서드에 대한 경고는 본적이 없다. 이는 @SafeVarargs 애너테이션을 붙여 주었기 때문이다.
  • 규칙 : 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 메서드를 정의할 때는 @SafeVarargs를 붙여준다.
    • 안전하지 않아도 붙이라는 뜻이 아니라, varargs 메서드를 만들거면 무조건 안전하게 만들라는 뜻이다.
    • 안전한 제네릭 varargs 메서드란 다음 두 조건을 만족하는 것을 말한다.
      • varargs 매개변수 배열에 아무것도 저장하지 않는다.
      • 그 배열(혹은 복제본)을 외부에 노출하지 않는다. (메서드 내에서만 사용하고 리턴하거나 전달하지 않는다.)
    • 안전하지 않은예시
      • 여기서 toArray()에 String을 넘기면서 리턴 타입이 String[]일 것으로 예상하겠지만, 리턴 타입은 Object[] 다.
        • 그래서 attribute에 담으면서 ClassCastException이 발생한다.
      • toArray가 T…를 받는데 이는 사실 배열로 넘어간다 . 그래서 컴파일러는 pickTwo에 T 인스턴스를 담을 배열을 만드는 코드를 생성하고, 이 배열을 toArray에 전달하게 된다.
      • 이 때 이 배열이 Object[]타입이 되는데, pickTwo에 어떤 타입의 객체를 넘기더라도 담을 수 있는 가장 구체적인 타입이 Object[]이기 때문이다.
        • pickTwo에서 <T extends String>으로 제한을 해주면 이 args의 타입이 String[]으로 바뀐다!
      • 그래서 args는 Object[] 타입이 되고, 이걸 그대로 리턴하면 Object[] 타입이 리턴된다.

아이템 33. 타입 안전 이종 컨테이너를 고려하라

  • type safe heterogeneous container pattern
  • 원래 자바같은 언어에서 Map<K, V> 같은걸 생각해보면 한 컨테이너에 한 가지 타입의 변수들만 담을 수 있다.
  • 타입 안전 이종 컨테이너는 여러 타입을 담을 수 있는 컨테이너를 말한다. 타입 안전하게!
  • Map<Class<?>, Object> 이런 식으로 컨테이너 키를 타입으로 사용하고, 불러올 때도 타입을 지정해서 불러오는 식이다.
  • 예시
    • Class 정보를 사용하는 방식이기 때문에 List<String>같은건 저장할 수 없다. List나 List<String>이나 같은 Class 이기 때문에 구분이 안된다.
    • super type token을 이용하면 List<String> 같은 것도 저장 가능하긴 한데, 썩 완벽하진 않다.
      • 스프링의 ParameterizedTypeReference가 바로 super type token이다.
  • 가끔 쓸일이 있을 것 같다. 실제로 클래스에 붙이는 애너테이션은 interface로 되어 있기 때문에 타입 안전 이종 컨테이너에 타입을 키로 해서 보관된다.예시
    • getAnnotation(Class<T>) 같은 메서드를 호출하면 타입으로 컨테이너를 뒤져서 반환해준다.
    • Class<?>Class<? extends Annotation>으로 형변환 해야 하는 경우, 당연히 경고가 발생하므로 .asSubclass()를 이용한다.
  • java.util.Collections.checkedSet, checkedList, checkedMap 같은 녀석들이 타입 안전 이종 컨테이너 방식으로 구현된 컬렉션의 래퍼인데, 제네릭과 raw 타입을 섞어 사용하는 어플리케이션에서 실수하지 않도록 도와준다.

+추가1. 클래스 내에서 T가 전역적으로 쓰일게 아니라면, Method T가 낫다.

1
2
3
4
5
6
7
@Component
public class CyberSourceClient {

    public <T> void runTransaction(T request) {
// 여기서 Class T를 쓸지 Method T를 쓸지 고민이 좀 됐는데, 클래스 내에서 T가 전역적으로 쓰일게 아니라면, generic 범위는 작은 것이 좋지.
// 사용하는 측에서도 method T가 사용하기 더 편하고. class T는 각 타입 마다 각각 다른 CyberSourceClient 사용해줘야 하니까
// 더군다나 <T, R> 이라면? class T,R은 경우의 수가 너무 많아 난감해진다.
This post is licensed under CC BY 4.0 by the author.