TL;DR
어떤 제너릭 Type<T>
을 사용할 때, 타입 파라메터 T
에서 최초에 지정한 타입 T
만 받을 수 있으면 invariant
Type<T>
에서 타입 T
와 T의 서브클래스
까지 받을 수 있으면 covariant 이다. (List<T>
를 파라메터로 함수에 List<TSubClass>
를 대신 집어 넣을 수 있다.)
Type<T>
에서 타입 T
와 T의 슈퍼클래스
까지 받을 수 있으면 contravariant 이다. (Func<T>
를 파라메터로 함수에 Func<TSuperClass>
를 대신 집어 넣을 수 있다.):
아무런 조건이 없으면(invariant),List<Car>
에는 List<승용차>
처럼 서브 클래스를 넣을 수 없다.
contravaraint는 파라메터로 함수를 받을때 (콜백을 받을 때) 많이 쓰인다. fun ClickHandler(callback: Func<T: ClickEvent>)
라는 함수가 있다면 callback
파라메터 자리에 Func<Event>
를 받는것은 괜찮지만 (치환될 수 있지만), 서브클래스인 Func<ClickEventSub>
를 받는것은 문제가 생긴다 (치횔 될 수 없다)
ClickEvent
는 하위에 WindowsClickEvent
와 AndroidClickEvent
가 있다고 하자. 외부에서 ClickHandler(HandlerCallbackFunc)
형식으로 함수가 호출될 건데, 이때 기본적으로 Func<ClickEvent>
를 기준으로 파라메터가 들어올 것이다.
해당 함수를 ClickEvent
의 SubClass인 fun ClickHandler(callback: Func<WindowsClickEvent>)
형태로 작성한다면 AndroidClickEvent
를 읽지 못한다. 그러나, SuperClass인 fun ClickHandler(callback: Func<ClickEvent>)
로 작성한다면 접근할 수 있는 필드는 줄어들지만, 아무 문제가 발생하질 않는다.
Variance
List<Generic>
과 같은것을 다루다 보면 Covariance, Contra-Variance와 같은것을 접하게 된다. Variance 상황을 객체지향을 통해서 알아보자. 우리에게는 3개의 클래스가 있다. 그리고 이 클래스들은 상속 관계를 가진다: [자동차] → { [아반떼], [소나타] }
이때, 타입을 집합끼리의 포함 관계로 본다면 다음과 같은 구조로 볼 수 있다: [자동차]라는 집합안에 [아반떼]와 [소나타]라는 집합이 들어간다.
[자동차]를 저장할 수 있는 변수라면 [아반떼]와 [소나타]를 저장할 수 있다. 어쨋든 [자동차]의 특정한 부분이 [아반떼]와 [소나타]이기 때문이다. 다음과 같은 코드는 정상 작동한다. 타입을 집합으로 생각하면 문제 없는 코드이다. 자동차 집합을 담을 수 있는 변수라면 그 안의 항목은 당연히 담을 수 있을 것이다.
public class MyClass {
public static void main(String args[]) {
Car car = new Car();
car = new Avante();
System.out.println(car);
}
}
class Car {
public void startup() { System.out.println("시동"); }
}
class Avante extends Car { public void avt() { System.out.println("아반떼 특수 기능"); }}
class Sonata extends Car { public void snt() { System.out.println("소나타 특수 기능"); }}
그렇다면, 이것을 어딘가에 감싼다면 상하 관계가 유지될까? List안에 [자동차]와 [아반떼]를 각각 담았을때 상하 관계를 유지할 수 있을까 라는 이야기이다. 이때 사용되는 용어가 Variance이다. 참고로, 앞의 답은 “Variance가 존재한다면 상하관계가 유지되고, Variance가 없다면 상하관계가 보장되지 않는다” 이다.
Covariance
List<Car>
하고 List<Avante>
가 있다고 생각해 보자. Car
와 Avante
자체는 상하 관계가 있지만, 각각의 리스트는 상하 관계를 가지지 않는다. 이렇듯 타입을 감싸는 무언가가 (여기서는 List
) 해당 타입의 상하관계를 유지하면 CoVaraint, 상하관계를 유지하지 못하면 InVariant, 상하관계가 역전되면 ContraVariant이다. 즉, 다음의 그림과 같은 구조를 유지하면 CoVariant이다:
Java에서는 ? extends
라는것을 이용해서 Covariance를 사용할 수 있기 때문에, 다음과 같은 코드를 사용할 수 있다.
import java.util.ArrayList;
import java.util.List;
public class MyClass {
public static void main(String args[]) {
List<Avante> avantes = new ArrayList();
avantes.add(new Avante());
printCars(avantes);
}
public static void printCars(List<? extends Car> cars) {
System.out.println(cars);
}
}
class Car {
public void startup() { System.out.println("시동"); }
}
class Avante extends Car { public void avt() { System.out.println("아반떼 특수 기능"); }}
class Sonata extends Car { public void snt() { System.out.println("소나타 특수 기능"); }}
만약 ? extends
를 사용하지 않는다면 List<Car>
와 List<Avante>
끼리는 관계를 가지지 않는다. 즉, 두개의 List는 invarint 이다. 그렇기 때문에, 아래와 같은 코드는 오류가 발생한다. Invariant 하기 때문에 각 리스트를 완전 별개의 타입으로 생각한다. 그래서 avantes를 인자로서 받을 수 없다.
import java.util.ArrayList;
import java.util.List;
public class MyClass {
public static void main(String args[]) {
List<Avante> avantes = new ArrayList();
avantes.add(new Avante());
printCars(avantes);
}
public static void printCars(List<Car> cars) {
System.out.println(cars);
}
}
class Car {
public void startup() { System.out.println("시동"); }
}
class Avante extends Car { public void avt() { System.out.println("아반떼 특수 기능"); }}
class Sonata extends Car { public void snt() { System.out.println("소나타 특수 기능"); }}
/// /MyClass.java:9: error: incompatible types: List<Avante> cannot be converted to List<Car>
/// printCars(avantes);
/// ^
/// Note: /MyClass.java uses unchecked or unsafe operations.
/// Note: Recompile with -Xlint:unchecked for details.
/// Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
/// 1 error
Contravariance
ContraVariance는 상하관계를 역전시킨다. 이 관계를 역전시키는 경우는 잘 없기 때문에 처음 보면 머리속에 구조가 잘 그려지지 않는다. 역전된 관계를 그림으로 그려 보자면 다음과 같을 것이다:
[아반떼]의 서브타입이 [자동차]이다. 그러므로, [아반떼]를 받을 수 있는 곳은 [자동차]를 받을 수 있다. 하지만, 이 관계 안에서 [자동차]를 받는 곳에서는 [아반떼]를 받을 수는 없다. 다음의 파란색 빗금친 부분이 들어올 수 있고, 그 경우 타입 오류가 발생할 수 있기 때문이다.
편하게 생각하기 위해서, 잠시 관계를 원상 복구 해 보자면 ([자동차]가 슈퍼타입인 경우로 돌아가자면), [아반떼]를 받는 곳에서 [자동차]를 받을 수는 없다. [자동차]안에는 [소나타], [코란도]등 여러 다른 집합들도 포함되기 때문에, [아반때] 대신에 [자동차]를 집어넣는 것은(치환하는 것은) 안전하지 못하다.
Covariance는 그래도 어느정도 쓰인다. 하지만 ContraVariance는 실질적으로 사용하는곳이 찾기 힘들다. 그나마 찾을 수 있는곳은 콜백 처리를 위해 함수를 파라메터로 받을 때 이다. 예시를 위해서 좀 더 많은 계층의 타입을 생각해 보자: [운송수단] → [자동차] → [현대차] → [승용] → [아반떼]. 이 중 가장 중간에 있는 [현대차]를 기준으로 생각해 보겠다.
다음 함수가 있다고 생각 해 보자. 이 함수는 Callback
으로서 함수 하나를 받는다. Callback
함수는 파라메터로서 [현대차]를 받는다.
public void HandleHyundai(int arg, Callback<현대차> fn)
이 Callback은 Super Class인 [운송수단], [자동차]를 받는것은 문제가 없다. 그러나 Sub Class인 [승용] 또는 [아반떼]를 파라메터로 받는다면 타입 범위에 문제가 생긴다. 현대차 아래의 일부분이 [승용]이고, [아반떼] 이다. 만약 [승용]을 Callback의 제너릭으로 잡는다면, 현대 자동차의 다른 SUV, 상용차(트럭) 같은것을 처리할 수 없다. 우리는 현대차를 처리해야 하는데, 그 세부 클래스인 승용만 처리할 수 있게 된다.
그래서 콜백을 받을 때는 오히려 반대로 Super Class만 받을 수 있다. Sub Class를 Super Class로 생각해야 하는 것이다. [현대차]의 Super Class인 [자동차]는 파라메터로 받을 수 있다. [자동차]의 일부분이 [현대차]이기 때문이다. 그래서 [현대차]를 받는 곳이라면 [자동차]를 받을 수 있다. 물론, 그러면 좀 더 범용적인 접근만 가능하다. [현대차] 특유의 메서드나 필드는 접근할 수 없다. 같은 이유로, [운송수단]을 받는 콜백도 여기서 사용할 수 있다.
사실 꼭 객체지향일 필요는 없다. 그러나 프로그래밍을 하면서 각 타입(데이터)의 관계를 설정하고 포함 관계를 설정하는데는 객체지향이 가장 무난한 예이기에 들었다. Rust의 경우에는 Lifetime 또한 서로간의 관계를 가지기 때문에 Variance 관계를 볼 수 있다.
Covariance는 Immutable 해야 한다
이제는 CoVariance일 때 데이터가 Mutable하면 안된다는것을 살펴보자. 맨 위의 예제 처럼 [자동차] → {[아반떼], [소나타]}인 상황을 생각해 보자. 이 코드에서 printCars는 Car에 Covariant인 List를 받는다.
import java.util.ArrayList;
import java.util.List;
public class MyClass {
public static void main(String args[]) {
List<Avante> avantes = new ArrayList();
avantes.add(new Avante());
printCars(avantes);
}
public static void printCars(List<? extends Car> cars) {
System.out.println(cars);
}
}
class Car {
public void startup() { System.out.println("시동"); }
}
class Avante extends Car { public void avt() { System.out.println("아반떼 특수 기능"); }}
class Sonata extends Car { public void snt() { System.out.println("소나타 특수 기능"); }}
이때, Cars
에 Sonata
를 넣을 수 있을까? List<Car> cars
로만 생각한다면 문제가 없다. Car의 Subclass인 Sonata나 Avante 모두 넣을 수 있다. Car은 Avanate와 Sonata를 모두 포함하는 영역을 담을 수 있기 때문이다.
그러나, 실제 타입을 생각한다면 Sonata를 cars에 넣을 수 없다. cars
는 List<Avante>
를 Covariance를 이용해서 List<Car>
로 치환한 객체이다. 그렇기 때문에, cars에 Sonata를 넣는것은 List<Avante>에 Sonata를 집어넣는 것이다.
Java 컴파일러도 이것을 알고 오류를 내뱉는다:
/MyClass.java:13: error: incompatible types: Sonata cannot be converted to CAP#1
cars.add(new Sonata());
^
where CAP#1 is a fresh type-variable:
CAP#1 extends Car from capture of ? extends Car
Note: /MyClass.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error
List
를 잘 살펴보면 위에서 말한 CoVariance와 ContraVariance를 찾아볼 수 있다. 다음 코드는 내부 항목을 정렬하는 함수이다. 이 함수는 Comparator c를 받는다. 이때 E는 ContraVaraince이기 때문에 좀 더 상위의 타입을 받을 수 있다.
다른 언어에서는 어떻게 되어있을까? 다음은 Rust 언어의 Varaince 목록이다
여기서 봐야 할 부분은 &mut T
은 Invariant이고, fn(T) → U
에서 T
는 contraVaraint라는 점이다. &mut T
에서 T가 Mutable하게 적용되면 Invariant (서로간에 관계가 없음)이지만, Immutable하다는 조건에서는 Convariant이기 때문에 T의 서브 타입이 올 수 있다.
답글 남기기