낙서장
Kotlin의 Generic에 대하여.. (1) 본문
나는 사실 진성 자바 유저가 아니기 때문에 자바의 제네릭에 대해서도 정확히 모르고, 그저 깜냥으로 손에 잡히는대로, 필요한대로 제네릭을 사용하곤 했다. 그러다가 코틀린 마이그레이션을 하면서 제네릭에 대해 조금 더 자세히 알아야 할 필요성을 느끼게 되었는데, 이 포스팅에서 코틀린 공식 문서를 기본으로 코틀린의 제네릭에 대해 알아보려고 한다. (영어 공부도 할 겸 ㅎㅎ)
본문은 아래의 글을 번역 및 사족을 덧붙인 것으로, 오탈 및 오역이 있을 수도 있다~
https://kotlinlang.org/docs/reference/generics.html#declaration-site-variance
--------
자바 제네릭의 가장 모호한, 혹은 애매한? 부분 중 하나는 바로 와일드카드라고 할 수 있다. 코틀린에는 자바 와일드카드가 존재하지 않는 대신 두 가지 특성이 존재하는데, 바로 declaration-site variable 과 type projection 이다.
코틀린의 제네릭에 대해 알아보기에 앞서 자바에 왜 이런 모호한 와일드카드라는 것이 필요한지 한 번 생각해보자.
우선, 자바의 제네릭 타입은 invariant(무공변)이다. 이 말인 즉슨, List<T>를 기준으로 보면.. List<String>이 List<Object>의 하위 타입이 아니라는 것이다. (String이 Object의 하위 타입임에도 불구하고 말이다..)
아래의 코드는 만약 기본적으로 제네릭이 무공변이 아닐 때(not invariant) 일어날 수 있는 일이다.
// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // 자바에서는 허락하지 않는다.
objs.add(1); // 여기에 Integer 타입의 1을 넣고.
String s = strs.get(0); // 런타임에 가져올 때 ClassCastException: Cannot cast Integer to String 발생!
자바는 이러한 런타임 에러를 방지하기 위해 무공변성을 갖게 되는 것이다. 이런 특성은 어떤 의미를 내포하는 것일까?
Collection interface의 addAll() 이라는 메소드를 한 번 생각해보자. 어떻게 생겼을 거라고 예상하나? 아마도 요렇게 생기지 않았을까..?
// Java
interface Collection<E> ... {
void addAll(Collection<E> items);
}
음.. 합리적인 것으로 보인다. 제네릭으로 어떤 타입의 Collection 변수를 만들 수 있고, 추가할 수도 있을테니까.. 그런데 그러면 아래의 코드는 동작하지 않아야할텐데 이상하게도 동작이 잘 된다.
void copyAll(Collection<Object> to, Collection<String> from) {
to.addAll(from);
// !!! 위에서 상정한 addAll() 메소드라면 동작하지 않아야하는데? 잘 컴파일된다!
// Collection<String>은 Collection<Object>의 하위타입이 아닌데도말이지?
}
엥? 도대체 무슨 일일까? 우리가 위에서 상정했던 Collection.addAll()이 잘못된 걸까?
사실 위에서 가정했던 addAll()은 반쪽짜리 정답이라고 할 수 있다. 실제 addAll() 메소드의 구현부를 살펴보면 다음과 같다.
// Java
interface Collection<E> ... {
void addAll(Collection<? extends E> items);
}
앗! Collection<E> 였던 것이 Collection<? extends E> 로 바뀌어있다. 저 ?(question mark)의 정체가 바로 와일드카드인 것이다.
? extends E 의 의미는, 이 addAll() 메소드가 E에 대한 Collection 뿐만아니라 E의 어떤 하위 타입의 Collection도 받을 수 있다고 선언하는 것이다!
한번 더 생각해보면, 이 메소드는 안전하게 E의 프로퍼티들을 읽어올 수 있지만 정확히 E의 어떤 하위 타입인지 알 수 없기 때문에 프로퍼티에 값을 쓸 수는 없다.
여기서 정리하자면, Collection<String>은 Collection<Object>의 하위 타입은 아니지만, Collection<? extends Object>의 하위 타입은 맞다는 것이다. 유식한 말로 하자면, extends-bound wildcard(? extends)가 공변성을 만들어줬다! 라고 말할 수 있다.
이런 개념에 대한 이해는 다음과 같이 심플하게 정리할 수 있다. 만약 Collection의 아이템들을 가져오게만 가능하다고 하면 String 콜렉션을 이용해서 Object를 읽어오는 것이 가능하다. 반대로 우리가 Collection에 값을 넣기만 가능하게 한다면, Object 콜렉션에 String 아이템을 넣는 것이 가능하다는 것이다. 자바에서는 List<? super String>이 List<Object>의 상위 타입이기 때문이다.
참고로 후자의 경우를 반공변성(contravariance)라고 하는데, List<? super String> 객체에 대해서는 String을 인자로 받는 메소드만 호출할 수 있다는 것이다(예를 들면 add(String) 혹은 set(int, String) 등..). 다만 List<T> 객체에서 T를 반환하는 메소드를 호출하면 String이 아닌 Object 객체를 반환받게 된다.
Effective Java의 저자인 Joshua Bloch는 Producer로부터는 오직 읽기(read)만 가능하고, Consumer에는 오직 쓰기(wirte)만 가능하다고 표현했다.
참고: 만약 List<? extends Foo>와 같은 producer-object를 사용할 때, add()나 set()과 같은 메소드를 사용할 수 없다(read만 가능하기 때문에). 하지만 이것이 객체가 immutable 하다는 것과는 다른 의미인데, clear()와 같은 메소드는 호출할 수 있기 때문이다. wildcard의 유일한 관심사는 type safety이고, immutable은 전혀 다른 범주의 이야기이다.
-------------------
공변성, 반공변성, 무공변성에 대해서는 검색하면 아주 자세히 설명된 문서들이 많다. generic에 익숙하지 않은 사람들이 보기에는 kotlin 문서가 적합하지 않은것 같기도 하다(Java의 generic을 아주 자세히 다루진 않기 때문이다.)
자바의 Generic에 대해 궁금하다면 아래의 FAQ 문서를 보거나 구글 검색을 통해 더 깊은 지식을 쌓는 것이 좋겠다~
http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html
'개발' 카테고리의 다른 글
[취미] 정부 사이트 캡챠(Captcha) 뚫기 (0) | 2020.09.29 |
---|---|
Object creation with Groovy! (0) | 2019.07.27 |
What is Groovy? (0) | 2019.07.27 |