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가 없다면 상하관계가 보장되지 않는다” 이다.
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는 상하관계를 역전시킨다. 이 관계를 역전시키는 경우는 잘 없기 때문에 처음 보면 머리속에 구조가 잘 그려지지 않는다. 그림으로 그려 보자면 다음과 같을 것이다:
[아반떼]의 서브타입이 [자동차]이다. 그러므로, [아반떼]를 받을 수 있는 곳은 [자동차]를 받을 수 있다. 하지만, 이 관계 안에서 [자동차]를 받는 곳에서는 [아반떼]를 받을 수는 없다. 다음의 파란색 빗금친 부분이 들어올 수 있고, 그 경우 타입 오류가 발생할 수 있기 때문이다.
편하게 생각하기 위해서, 잠시 관계를 원상 복구 해 보자면 ([자동차]가 슈퍼타입인 경우로 돌아가자면), [아반떼]를 받는 곳에서 [자동차]를 받을 수는 없다. [자동차]안에는 [소나타], [코란도]등 여러 다른 집합들도 포함되기 때문에, [아반때] 대신에 [자동차]를 집어넣는 것은(치환하는 것은) 안전하지 못하다.
Covariance는 그래도 어느정도 쓰인다. 하지만 ContraVariance는 머리가 아픈것에 대비하여 실질적으로 사용하는곳도 찾기 힘들다. 그나마 찾을 수 있는곳은 콜백 처리를 위해 함수를 파라메터로 받을 때 이다. 예시를 위해서 좀 더 많은 계층의 타입을 생각해 보자: [운송수단] → [자동차] → [현대차] → [승용] → [아반떼]. 이 중 가장 중간에 있는 [현대차]를 기준으로 생각해 보겠다.
다음 함수가 있다고 생각 해 보자. 이 함수는 Callback
으로서 함수 하나를 받는다. Callback
함수는 파라메터로서 [현대차]를 받는다.