List<Generic> 과 같은것을 다루다 보면 Covariance, Contra-Variance와 같은것을 접하게 된다. Variance 상황을 객체지향을 통해서 알아보자. 우리에게는 3개의 클래스가 있다. 그리고 이 클래스들은 상속 관계를 가진다: [자동차] → { [아반떼], [소나타] }

Untitled

이때, 타입을 집합으로 본다면 다음과 같은 구조로 볼 수 있다: [자동차]라는 집합안에 [아반떼]와 [소나타]라는 집합이 들어간다.

Untitled

[자동차]를 저장할 수 있는 변수라면 [아반떼]와 [소나타]를 저장할 수 있다. 어쨋든 [자동차]의 특정한 부분이 [아반떼]와 [소나타]이기 때문이다. 다음과 같은 코드는 정상 작동한다. 타입을 집합으로 생각하면 문제 없는 코드이다. 자동차 집합을 담을 수 있는 변수라면 그 안의 항목은 당연히 담을 수 있을 것이다.

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> 가 있다고 생각해 보자. CarAvante 자체는 상하 관계가 있지만, 각각의 리스트는 상하 관계를 가지지 않는다. 이렇듯 타입을 감싸는 무언가가 (여기서는 List) 해당 타입의 상하관계를 유지하면 CoVaraint, 상하관계를 유지하지 못하면 InVariant, 상하관계가 역전되면 ContraVariant이다. 즉, 다음의 그림과 같은 구조를 유지하면 CoVariant이다:

Untitled

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는 상하관계를 역전시킨다. 이 관계를 역전시키는 경우는 잘 없기 때문에 처음 보면 머리속에 구조가 잘 그려지지 않는다. 그림으로 그려 보자면 다음과 같을 것이다:

Untitled

[아반떼]의 서브타입이 [자동차]이다. 그러므로, [아반떼]를 받을 수 있는 곳은 [자동차]를 받을 수 있다. 하지만, 이 관계 안에서 [자동차]를 받는 곳에서는 [아반떼]를 받을 수는 없다. 다음의 파란색 빗금친 부분이 들어올 수 있고, 그 경우 타입 오류가 발생할 수 있기 때문이다.

Untitled

편하게 생각하기 위해서, 잠시 관계를 원상 복구 해 보자면 ([자동차]가 슈퍼타입인 경우로 돌아가자면), [아반떼]를 받는 곳에서 [자동차]를 받을 수는 없다. [자동차]안에는 [소나타], [코란도]등 여러 다른 집합들도 포함되기 때문에, [아반때] 대신에 [자동차]를 집어넣는 것은(치환하는 것은) 안전하지 못하다.

Covariance는 그래도 어느정도 쓰인다. 하지만 ContraVariance는 머리가 아픈것에 대비하여 실질적으로 사용하는곳도 찾기 힘들다. 그나마 찾을 수 있는곳은 콜백 처리를 위해 함수를 파라메터로 받을 때 이다. 예시를 위해서 좀 더 많은 계층의 타입을 생각해 보자: [운송수단] → [자동차] → [현대차] → [승용] → [아반떼]. 이 중 가장 중간에 있는 [현대차]를 기준으로 생각해 보겠다.

다음 함수가 있다고 생각 해 보자. 이 함수는 Callback으로서 함수 하나를 받는다. Callback 함수는 파라메터로서 [현대차]를 받는다.