새소식

독서/이펙티브 자바 3판

CH04 제네릭

  • -

step26: Raw 타입은 사용하지 말라

 

 클래스와 인터페이스 선언에 타입 매개변수가 쓰이면, 이를 제네릭 클래스 혹은 제네릭 인터페이스라 한다. 각각의 제네릭 타입은 일련의 매개변수화 타입을 정의한다. 먼저 클래스 이름이 나오고, 이어서 꺾쇠괄호 안에 실제 타입 매개변수들을 나열한다. List<String>은 원소의 타입이 String인 리스트를 뜻하는 매개변수화 타입이다. 여기서 String이 정규 타입 매개변수 E에 해당하는 실제 타입 매개변수다.

 

 마지막으로, 제네릭 타입을 하나 정의하면 그에 딸린 로 타입(raw type)도 함께 정의된다. 로 타입이란 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않은 때를 말한다. 예로 List<E>의 Raw type은 List다.

 

 Raw type을 쓰면 제네릭이 안겨주는 안전성과 표현력을 모두 잃게 된다. 그럼 애초에 왜 만들어 놓은걸가?

바로 호환성 때문이다. 과거의 기존 코드를 모두 수용하면서 제네릭을 사용 하는 새로운 코드와도 맞물려 돌아가야 했기에, 로 타입을 사용하는 메서드에 매개변수화 타입의 인스턴스를 넘겨도 동작해야만 했던 것이다. 이 마이그레이션 호환성을 위해 로 타입을 지원하고 제네릭 구현에는 소거 방식을 사용하기로 했다. 

 

 

 

정리

 

로 타입을 사용하면 런타임에 예외가 일어날 수 있으니 사용하면 안된다. Set<Object>는 어떤 타입의 객체도 저장할 수 있는 매개변수화 타입이고, Set<?>는 모든 모종의 타입 객체만 저장할 수 있는 와일드카드 타입이다. 그리도 이들의 로 타입인 Set은 제네릭 타입 시스템에 속하지 않는다. Set<Object>와 Set<?>은 안전하지만, 로 타입인 Set은 안전하지 않다.

 

 

step27: 비검사 경고를 제거하라

 

  제네릭을 사용하기 시작하면 수많은 컴파일러 경고를 보게 될 것이다. 비검사 형변환 경고, 비검사 메서드 호출 경고, 비검사 매개변수 가변인수 타입 경고, 비검사 변환 경고 등이다. 할 수 있는한 모든 비검사 경고를 제거하라. 모두 제거한다면 그 코드는 타입 안정성이 보장된다.

즉 런타임에 ClassCastException이 발생할 일이 없고, 의도한 대로 잘 동작하리라 확신할 수 있다.

 

 경고를 제거할 수는 없지만 타입 안전하다고 확신할 수 있다면 @Suppress Warnings("unchecked") 애너테이션을 달아 경고를 숨기자. 대신 타입 안전함을 검증하지 않은 채 경고를 숨기면 스스로에게 잘못된 보안 인식을 심어주는 꼴이다. 코드는 경고없이 컴파일 되겠지만, 런타임에는 여전히 ClassCastException을 던질 수 있다. 

 

 @SuppressWarnings 애너테이션은 개별 지역변수 선언부터 클래스 전체까지 어떤 선언에도 달 수 있다. 하지만 @SuppressWarnings 애너테이션은 항상 가능한 한 좁은 범위에 적용하자. 보통 변수 선언, 아주 짧은 메서드, 생성자가 될 것이다. 심각한 경고를 놓칠 수 있기에 절대로 클래스 전체에 적용해서는 안된다.

 

 @SuppressWarnings("unchecked") 애너테이션을 사용할 때면 그 경고를 무시해도 안전한 이유를 항상 주석으로 남겨야 한다. 

 

 

정리

 

 비검사 경고는 중요하니 무시하지 말자. 모든 비검사 경고는 런타임에 ClassCaseException을 일으킬 수 있는 잠재적 가능성을 뜻하니 최선을 다해 제거하라. 경고를 없앨 방법을 못찾겠다면, 그 코드가 타입 안전함을 증명하고 가능한 한 범위를 좁혀 @SuppressWarnings("unchecked") 애너테이션으로 경고를 숨겨라. 그런 다음 경고를 숨기기로 한 근거를 주석으로 남겨라

 

 

 

step28: 배열보다는 리스트를 사용하라

 

 배열과 제네릭 타입에는 중요한 2가지 차이가 있다. 첫 번째, 배열은 공변이다. Sub가 Super의 하위 타입이라면 배열 Sub[]는 배열 Super[]의 하위 타입이 된다. 즉 함께 변한다는 뜻이다. 반면, 제네릭은 불공변이다. 서로 다른 Type1, Type1가 있을 때 List<Type1>는 List<Type2>의 하위 타입도 아니고 상위 타입도 아니다. 

 

 두 번째 주요 차이로, 배열은 실체화된다. 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다. 만약 Long 배열에 String을 넣으려 하면 ArrayStoreException이 발생한다. 반면 제너릭은 타입 정보가 런타임에는 소거된다. 원소 타입을 컴파일 타임에만 검사하면 런타임에는 알수조차 없다는 뜻이다. 소거는 제네릭이 지원되기 전의 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 매커니즘으로, 자바 5가 제네릭으로 순조롭게 전환될 수 있도록 해줬다. 이상의 주요 차이로 인해 배열과 제네릭은 잘 어울리지 못한다.

 

 E, List<E>, List<String> 같은 타입을 실체화 불가 타입이라 한다. 실체화되지 않아서 런타임에는 컴파일타임보다 타입 정보를 적게 가지는 타입이다. 소거 메커니즘 때문에 매개변수화 타입 가운테 실체화 될 수 있는 타입은 List<?> 와 Map<?,?> 같은 비한정적 와일드 카드 타입 뿐이다. 또한 가변인수 매개변수 메서드를 함께 쓰면 해석하기 어려운 경고 메시지를 받는데, 가변인수 메서드를 호출할 때마다 가변인수 매개변수를 담을 배열이 하나 만들어지는데, 이때 그 배열의 원소가 실체화 불가 타입이라면 경고가 발생한다. 이 문제는 @SafeVarargs 애너테이션으로 대처할 수 있다. 

 

 배열로 형변환할 때 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 대부분의 경우 배열인 E[] 대신 컬렉션인 List<E>를 사용하면 해결된다. 제네릭에서는 원소의 타입 정보가 소거되어 런타임에는 무슨 타입인지 할 수 없다. 

 

정리

 

 배열과 제네릭에는 매우 다른 규칙이 적용된다. 배열은 공변이고 실체화되는 반면, 제네릭은 불공변이고 타입 정보가 소거된다. 그 결과 배열은 런타임에는 타입 안전하지만 컴파일 타임에는 그렇지 않다. 제네릭은 반대다. 그래서 둘을 섞어 쓰기란 쉽지 않다. 둘을 섞어 쓰다가 컴파일 오류나 경고를 만나면, 가장 먼저 배열을 리스트로 데체하는 방법을 적용해보자.

 

 

 

step29: 이왕이면 제네릭 타입으로 만들라

 

제네릭 타입(class<T>, interface<T>)

제네릭 타입은 타입을 파라미터로 가지는 클래스와 인터페이스를 말한다. 제네릭 타입은 클래스 또는 인터페이스 이름 뒤에 "< >" 부호가 붙고, 사이에 타입 파라미터가 위치한다.

 

위는 스택 클래스를 제네릭으로 바꾼것인데

 

!! Error java: generic array creation

  Item 28을 돌아보면, E와 같은 실체화 불가 타입으로는 배열을 만들 수 없다. 따라서 배열을 사용하는 코드를 제네릭으로 만들려 할땐 항상 이 문제가 발생한다.

 

해결책은 2가지가 있는데 첫번째는 제네릭 배열 생성을 금지하는 제약을 대놓고 우회하는 방법이다.

 

 

 위를 아래와 같이 Object 배열을 생성한 다음, 제네릭으로 형변환한다. 이때 컴파일러는 오류 대신 타입이 안전하지 않다는 경고를 보낸다. 왜냐하면 컴파일러는 해당 프로그램의 타입이 안전한지 증명할 방법이 없기 때문이다. 그렇기에 이러한 비검사 형변환 프로그램의 타입 안전성을 확인해야 하는데 위 코드를 확인해보면

 

- 문제의 배열 elements는 private 필드에 저장했고

- 클라이언트로 반환되거나, 다른 메서드에 전달되지 않는다.

- push 메서드를 통해 배열에 저장되는 원소의 타입은 항상 E

 

So 이 비검사 형변환은 안전하다고 할 수 있다. 이후 안전함을 확인한 후 @SuppressWarnings 애너테이션으로 해당 경고를 숨긴다.

 

 

 두 번째 방법은 elements 의 필드 타입을 E[]에서 Object[]로 바꾼다. 이때 배열이 반환한 원소를 E로 형변환하면 오류 대신 경고가 뜬다.  E는 실체화 불가 타입임으로 컴파일러는 런타임에 이뤄지는 형변환이 안전한지 증명할 방법이 없기에, 전체 메서드에 경고를 숨기는 것이 아니라 비검사 형변환을 수행하는 할당물에서만 숨기도록 하자.

 

 

정리

 

 클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하다. 그러니 새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 하라. 그렇게 하려면 제네릭 타입으로 만들어야 하는 경우가 많다. 기존 타입 중 제네릭이었어야 하는 게 있다면 제네릭 타입으로 변경하자. 기존 클라이언트는 아무 영향을 주지 않으면서, 새로운 사용자를 훨씬 편하게 해주는 길이다.

 

 

 

step30: 이왕이면 제네릭 메서드로 만들라

 

 클래스와 마찬가지로 메서드도 제네릭으로 만들 수 있다. 매개변수화 타입을 받는 정적 유틸리티 메서드는 보통 제네릭이다. 

 위 코드는 경고가 2개 발생한다. 경고를 없애려면 메서드를 타입 안전하게 만들어야 한다. 메서드 선언에서의 세 집합(입력 2개, 반환 1개)의 원소 타입을 타입 매개변수로 명시하고, 메서드 안에서도 이 타입 매개변수만 사용하게 수정한다. 타입 매개변수 목록은 메서드의 제한자와 반환 타입 사이에 온다. 아래 코드에서 매개변수 목록은 <E> 이고, 반환타입은 Set<e>이다.



 때때로 불변 객체를 여러 타입으로 활용할 수 있게 만들어야 할 때가 있다. 제네릭은 런타임에 타입 정보가 소거되므로 하나의 객체를 어던 타입으로든 매개변수화 할 수 있다. 하지만 이렇게 요청한 타입 매개변수에 맞게 매번 그 객체의 타입을 바꿔주는 정적 팩터리를 만들어야 하는데, 이 패턴을 제네릭 싱글턴 팩터리라 하고 Collections.reverseOrder 같은 함수 객체나 Collections.emptySet 같은 컬렉션용으로 사용한다.

 

 

정리

 

 제네릭 타입과 마찬가지로 클라이언트에서 입력 매개변수와 반환값을 명시적으로 형변환해야 하는 메서드보다 제네릭 메서드가 더 안전하며 사용하기도 쉽다. 타입과 마찬가지로, 메서드도 형변환 없이 사용할 수 있는 편이 좋으며, 많은 경우 그렇게 하려면 제네릭 메서드가 되어야 한다. 역시 타입과 마찬가지로, 형변환을 해줘여 하는 기존 메서드는 제네릭하게 만들자. 기존 클라이언트는 그대로 둔 채 새로운 사용자의 삶을 훨씬 편하게 만들어줄 것이다.

 

 

 

step31: 한정적 와일드 카드를 사용해 API 유연성을 높이라

 

 때론 불공변 방식보다 유연한 무언가가 필요하다. Stack의 클래스를 떠올려보자. 여기에 pushAll 메서드를 추가한다고 할 때, 아래 메서드는 컴파일되지만 완벽하지 않다. Iterable src의 원소 타입이 스택의 원소 타입과 일치하면 잘 작동한다. 하지만 Stack<Number>로 선언 후 pushAll(intVal)을 호출하면 어떨까? 여기서 intVal은 Integer이다.

 

 

 Integer는 Number의 하위 타입이니 잘 동작한다. 아니 논리적으로는 잘 동작해야 할 것 같다. 하지만 실제로는 매개변수화 타입이 불공변이기 때문에 오류메세지가 뜬다. 이때 해결방안으로 한정적 와일드카드 타입이라는 특별한 매개변수화 타입을 지원한다. pushAll의 입력 매개변수 타입은 'E의 Iterable'이 아니라 'E의 하위 타입의 Iterable'이어야 하며 와일드 카드 타입 Iterable<? extends E>가 정확히 이런 듯이다. 하위 타입이란 자기 자신도 포함하지만, 그렇다고 자신을 확장한것은 아니기 때문이다.

 

 그러므로 위를 public void pushAll (Iterable<? extends E> src) { 로 바꾸면 안전하게 바꿔진다. 메세지는 분명하다. 유연성을 극대화하려면 원소의 생산자나 소비자용 입력매개변수에 와일드카드 타입을 사용하라. 한편 입력 매개변수가 생산자와 소비자 역할을 동시에 한다면 와일드카드 타입을 써도 좋을게 없다.

 

다음 공식을 외워두면 어떤 와일드카드 타입을 써야 하는지 기억하는데 도움이 될 것이다.

팩스(PECS) : producer-extends, consumer-super

 

즉, 매개변수화 타입 T가 생산자라면 <? extends T>를 사용하고, 소비자라면 <super T>를 사용하라.

 

 

정리

 

 조금 복잡하더라도 와일드카드 타입을 적용하면 API가 훨씬 유연해진다. 그러니 널리 쓰일 라이브러리를 작성한다면 반드시 와일드카드 타입을 적절히 사용하자. PECS 공식을 기억하고, 생산자는 extends를 소비자는 super를 사용한다. Comparable 과 Comparator는 모두 소비자라는 사실도 잊지말자.

 

 

 

step32: 제네릭과 가변인수를 함께 쓸 때는 신중하라

 

 가변인수 메서드와 제네릭은 자바 5때 함께 추가되었으니 서로 잘 어우러지리라 기대하겠지만, 그렇지 않다. 가변인수는 메서드에 넘기는 인수의 개수를 클라이언트가 조절할 수 있게 해주는데, 구현 방식에 허점이 있다. 가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 자동으로 하나 만들어진다. 그런데 내부로 감춰야 했을 때 이 배열을 그만 클라이언트에 노출하는 문제가 생겼다. 그 결과 varargs 매개변수에 제네릭이나 매개변수화 타입이 포함되면 알기 어려운 컴파일 경고가 발생한다.

 

 매개변수화 타입의 변수가 타입이 다른 객체를 참조하면 힙 오염이 발생한다. 이렇게 다른 타입 객체를 참조하는 상황에서는 컴파일러가 자동 생성한 형변환이 실패할 수 있으니, 제네릭 타입 시스템이 약속한 타입 안전성의 근간이 흔들려버린다. 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다.

 

 @SafeVarargs 애너테이션은 메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치다. 어떻게 보장할까? 가변인수 메서드를 호출할 때 varargs 매개변수를 담는 제네릭 배열이 만들어진다는 사실을 기억하자. 메서드가 이 배열에 아무것도 저장하지 않고 그 배열의 참조가 밖으로 노출되지 않는다면 타입안전하다. 달리 말하면, 이 varargs 매개변수 배열이 호출자로부터 그 메서드로 순수하게 인수들을 전달하는 일만 한다면(varargs의 목적대로만 쓰인다면) 그 메서드는 안전하다.

 

 @SafeVarargs 애너테이션을 사용해야 할 때를 정하는 규칙은 간단하다. 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 모든 메서드에 @SafeVarargs를 달라. 그래야 사용자를 헷갈리게 하는 컴파일러 경고를 없앨 수 있다. 즉 안전하지 않은 varargs 메서드는 절대 작성하지 말고 동제할 수 있는 메서드 중 제네릭 varargs 매개변수를 사용하며 힙 오염 경고가 뜨는 메서드가 있다면, 그 메서드가 안전한지 점검하라. 아래 2조건을 만족하는 제네릭 varargs 메서드는 안전하다

 

1st varargs 매개변수 배열에 아무것도 저장하지 않는다.

2nd 그 배열을 신뢰할 수 없는 코드에 노출하지 않는다.

 

참고

 @SafeVarargs 애너테이션은 재정의할 수 없는 메서드에만 달아야 한다. 재정의한 메서드도 안전할지는 보장할 수 없기 때문이다. 재정의한 메서드도 안전할지는 보장할 수 없기 때문이다. 자바 8에서 이 애너테이션은 오직 정적 메서드와 final 인스턴스 메서드에만 붙일 수 있고, 자바 9부터는 private 인스턴스 메서드에도 허용된다. 

 

@SafeVarargs 애너테이션 말고도, varargs 매개변수를 List 매개변수로 바꿀 수도 있다. 

 

 

정리

 

 가변인수와 제네릭은 궁합이 좋지 않다. 가변인수 기능은 배열을 노출하여 추상화가 완벽하지 못하고, 배열과 제네릭의 타입 규칙이 다르기 때문이다. 제네릭 varargs 매개변수는 타입 안전하지는 않지만, 허용된다. 메서드에 제네릭 varargs 매개변수를 사용하고자 한다면, 먼저 그 메서드가 타입 안전한지 확인한 다음 @SafeVarargs 애너테이션을 달아 사용하는 데 불편함이 없게끔 하자.

 

 

step33: 타입 안전 이종 컨테이너를 고려하라

 

 예로 데이터베이스의 행은 임의 개수의 열을 가질 수 있는데, 모두 열을 타입 안전하게 이용할 수 있다면 멋질 것이다. 다행히 쉬운 방법이 하나 있는데, 컨테이너 대신 키를 매개변수화 한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하면 된다. 이렇게 하면 제네릭 타입 시스템이 값의 타입이 키와 같음을 보장해줄 것이다. 이러한 설계 방식을 타입 안전 이종 컨테이너 패턴이라 한다.

 

 간단한 예로 타입별로 즐겨찾는 인스턴스를 저장하고 검색 할 수 있는 Favorites 클래스를 생각해보자. 각 타입의 Class 객체를 매개변수화한 키 역할로 사용하면 되는데 이 방식이 동작하는 이유는 class의 각 클래스가 제네릭이기 때문이다. class 리터럴의 타입은 Class가 아닌 Class<T>다. 예컨대 String.class의 타입은 Class<String>이고 Integer.class 타입은 Class<Integer>인식이다. 한편 컴파일타임 타입 정보와 런타임 타입 정보를 알아내기 위해 메서드들이 주고 받는 class 리터럴을 타입 토큰이라 한다. 

 

 

정리

 

 컬렉션 API로 대표되는 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되어 있다. 하지만 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 타입 안전 이종 컨테이너를 만들 수 있다. 타입 안전 이종 컨테이너는 Class를 키로 쓰며, 이런 식으로 쓰이는 Class 객체를 타입 토큰이라 한다. 또한, 직접 구현한 키 타입도 쓸수 있으며 데이터베이스의 행을 표현한 DatabaseRow 타입에는 제네릭 타입인 Column<T>를 키로 사용할 수 있다.

 

 

 

 

 

 

 

 

 

 

 

'독서 > 이펙티브 자바 3판' 카테고리의 다른 글

CH6 람다와 스트림  (0) 2023.05.15
CH05 열거 타입과 애너테이션  (0) 2023.05.05
Ch02 모든 객체의 공통 메서드  (0) 2023.04.25
Ch03 클래스와 인터페이스  (0) 2023.04.25
Ch01 객체 생성과 파괴  (0) 2023.04.05
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.