티스토리 뷰

Generic하면 빠질 수 없는게 Covariance와 Contravariance의 개념이다. 처음 이 개념을 Java의 Generic을 공부하면서 접했었는데, 그 때는 분명 다 이해했다고 생각하고 넘어갔지만 Kotlin을 공부하면서 다시 보니 헷갈리기 시작했다.

 

그래서 앞으로 몇 개의 포스팅에 걸쳐서 Covariance와 Contravariance의 개념에 대해 다시 공부해서 이해한 내용을 정리해 보려고 한다.

 


 

Covariance는 간단하게 “a more derived type”이라고 표현하고, Contravariance는 “a less derived type”이라고 표현할 수 있다.

 

more derived type이란, Parent Class와 Child Class가 있을 때 Child Class를 Parent Class의 more derived type이라고 할 수 있다.

반대로 less derived type일 때는 Parent Class가 Child Class의 less derived type이 되는 것이다.

 

 

하지만 Covariance와 Contravariance는 Generic에서 훨씬 더 많은 의미를 가진다.

 

일단, 간단히 용어부터 정리하고 넘어가보자.

 

Covariance(공변성)

- 공변성은 타입생성자에게 자식 객체를 부모 객체로 대입을 허용한다. 즉, 우리가 흔히 알고 있는 업캐스팅이 이에 포함된다.

ex) Parent p = new Child();

앞서 말한 a more derived type 객체를 대입할 수 있다.

 

Contravariance(반공변성)

- 공변성의 반대 개념으로, 특정 객체는 자기 자신 타입 또는 부모 타입의 객체만 대입이 허용된다. 

이는 a less derived type 객체를 대입할 수 있다.

 

Invariance(무공변성)

- 상속 관계에 상관없이, 자기 타입만 허용하는 것을 의미한다.

상속 관계와 상관 없이 exact type 객체만을 대입할 수 있다.

 

 

앞서 말한것 처럼 가장 간단한 covariance는 아래 예제와 같다.

open class Being
open class Person : Being()
class Student: Person()

fun main() {
    val p: Person = Student()       // covariance
}

 

Student 타입의 객체는 부모 타입인 Person 타입에 대입할 수 있고, 이를 covariance라고 한다.

이는 기본적으로 객체지향 프로그램에서 업캐스팅을 지원해주기 때문에 가능하다.

 

 

그럼 이제 본격적으로 Generic에서의 쓰임을 알아보기 위해 Group이라는 Generic class와 groupOf()라는 Generic Method를 아래와 같이 만들어보자.

 

1. Invariance

class Group<E>(private val elements: MutableList<E> = mutableListOf()) {
    fun add(e: E) = elements.add(e)
    fun get(i: Int): E = elements[i]
}

fun <E> groupOf(vararg elements: E): Group<E> {
    val group: Group<E> = Group()
    if (elements.isNotEmpty())
        elements.forEach { group.add(it) }

    return group
}

 

여기서 Group Class는 기본적으로 Invariance 성질을 갖는다.

즉, Group<부모타입>와 Group<자식타입>이 서로 다른 객체이기 때문에 호환되지 않는다는 의미이다.

fun main() {
    val people: Group<Person> = Group<Person>()           // OK
    val people2: Group<Person> = groupOf<Person>()        // OK

    val students : Group<Student> = Group<Student>()      // OK
    val students2: Group<Student> = groupOf<Student>()    // OK

    val people3  : Group<Person> = Group<Student>()       // compile error
    val people4  : Group<Person> = groupOf<Student>()     // compile error

    val students3: Group<Student> = Group<Person>()       // compile error
    val students4: Group<Student> = groupOf<Person>()     // compile error
}

 

여기서 한 가지 헷갈릴 수도 있는데, 가장 먼저 본 예제처럼 val p: Person = Student() 는 가능하기 때문에 아래와 같은 covariance는 가능하다.

fun main() {
    val p: Group<Person> = Group<Person>()
    p.add(Person())
    p.add(Student())        // covariance

    val p2: Group<Person> = groupOf<Person>(Person(), Student())        // covariance
    val p3: Group<Person> = groupOf<Person>(Student(), Student())       // covariance
}

 

2. Covariance

위의 Group Class가 Covariance 성질을 갖도록 바꾸어보자.

class Group<out E>(private val elements: MutableList<E> = mutableListOf()) {
    //fun add(e: E) = elements.add(e)
    fun get(i: Int): E = elements[i]
}

fun <E> groupOf(vararg elements: E): Group<E> = Group(elements.toMutableList())

 

이 코드에서 핵심은 Group<out E> 부분이다. 

Group<E>일 때는 Invariace이기 때문에 Group<Student> 타입과 Group<Person> 타입을 서로 대입(변환)할 수 없었지만,  Group<out E>으로 바꾸면서 해당 클래스는 이제 Covariance 성질을 가지게 된다. 즉, Group<Student> 타입을 Group<Person> 타입으로 대입(변환)할 수 있다.

 

Kotlin의 out E 키워드는 Java에서 "? extends E"와 같은 역할을 한다. 즉, out E는 인자의 타입을 E 또는 E의 자식 타입으로 제한한다는 의미가 된다.

 

 

여기서 PECS나 read-only & write-only와 같은 용어가 등장한다. 이 부분은 다음 포스팅에서 다룰 예정이다.

간단히 정리하자면 PECS는 Producer-Extends-Consumer-Super의 약자로 Producer는 extends를 사용하고 Consumer는 super를 사용한다는 의미이다. 

 

Producer는 정보를 주고 제공하는 역할을 하기 때문에 read-only, 즉 데이터를 변경할 수는 없고 접근해서 가져올 수만 있고, Consumer는 정보를 받아 사용하는 역할이므로 write-only, 즉 데이터를 변경할 수만 있고 접근해서 가져올 수는 없다. 

이런 의미에서 Producer의 키워드를 정보가 나가는 out, Consumer의 키워드를 정보가 들어오는 in으로 정한 것이라고 한다.

 

정리하자면, Covariance의 키워드인 out을 사용하면 해당 클래스는 Producer 역할을 하기 때문에 read-only가 된다. 그렇기 때문에 데이터를 가져올 수는 있지만 추가하거나 삭제하거나 변경할 수 없다.

뒤에 나올 Contravariance는 Covariance의 반대 개념으로, in 키워드를 사용하면 해당 클래스는 Consumer 역할을 하고 write-only가 되어 데이터를 변경할 수는 있지만 데이터에 접근할 수는 없다.

 

 

이렇게 되면 Group<out E>의 add() 메서드에 컴파일 에러가 생긴다. Group 클래스는 Producer이기 때문에 데이터를 변경할 수 없다는 것이다. 그렇기 때문에 정적 메서드인 groupOf()에서도 add() 메서드를 사용하지 못해 위와 같은 코드로 변경한 것이다.

 

아래 코드처럼 테스트 해 보았을 때 Invariance에서는 에러가 났던 Group<Person> = Group<Student> 코드에서 에러가 사라졌다.

fun main() {
    val people: Group<Person> = Group<Person>()           // OK
    val people2: Group<Person> = groupOf<Person>()        // OK

    val students : Group<Student> = Group<Student>()      // OK
    val students2: Group<Student> = groupOf<Student>()    // OK

    val people3  : Group<Person> = Group<Student>()       // OK, covariance
    val people4  : Group<Person> = groupOf<Student>()     // OK, covariance

    val students3: Group<Student> = Group<Person>()       // compile error
    val students4: Group<Student> = groupOf<Person>()     // compile error
}

 

여기서 한 가지 재미있는 사실은, groupOf()로 만들 때 파라미터에 따라 디폴트 타입이 달라진다는 것이다.

fun main() {
    val p: Group<Person> = groupOf<Person>(Person())
    val p2: Group<Person> = groupOf<Student>(Student())           // covariance
    val p3: Group<Person> = groupOf<Person>(Student())            // covariance
    val p4: Group<Person> = groupOf<Person>(Person(), Student())  // covariance
 }

이 코드에서 보면 groupOf() 파라미터가 Person 객체로만 이뤄져 있을 때는 디폴트 타입이 Person이 된다.

하지만 groupOf() 파라미터가 Student 객체로만 이뤄져 있을 때 디폴트 타입은 Student이다.

기본적으로 Person 타입에 Student 객체를 넣을 수 있기 때문에 groupOf() 파라미터에 Student 객체만 있을 때에 Person 타입으로 받아서 넣는 것이 허용된다.

하지만 Student 타입에 Person 객체를 넣는 것은 컴파일 에러가 나기 때문에 groupOf() 파라미터에 Person 객체가 하나라도 있다면 디폴트 타입은 Person이고, 이를 Student로 바꾸면 에러가 난다.

 

 

3. Contravariance

이제 Contravariance가 가능하도록 코드를 수정해 보자.

class Group<in E>(private val elements: MutableList<E> = mutableListOf()) {
    fun add(e: E) = elements.add(e)
    //fun get(i: Int): E = elements[i]
}

fun <E> groupOf(vararg elements: E): Group<E> {
    val group: Group<E> = Group()
    if (elements.isNotEmpty())
        elements.forEach { group.add(it) }

    return group
}

 

위에서 언급했듯, in 키워드로 인해 Group 클래스는 Consumer가 되어 get() 메서드를 통해 데이터를 꺼내서 제공해 주려고 하면 컴파일 에러가 발생한다. 

 

그리고 아래와 같이 테스트 하면 Covariance에서는 가능했던 Group<Person> = Group<Student> 코드에서 컴파일 에러가 생기고, Group<Student> = Group<Person>() 코드에서 에러가 사라졌음을 알 수 있다.

fun main() {
    val people: Group<Person> = Group<Person>()           // OK
    val people2: Group<Person> = groupOf<Person>()        // OK

    val students : Group<Student> = Group<Student>()      // OK
    val students2: Group<Student> = groupOf<Student>()    // OK

    val people3  : Group<Person> = Group<Student>()       // compile error
    val people4  : Group<Person> = groupOf<Student>()     // compile error

    val students3: Group<Student> = Group<Person>()       // OK, contravariance
    val students4: Group<Student> = groupOf<Person>()     // OK, contravariance
}

 

여기서도 아래와 같은 디폴트 타입이 groupOf() 인자에 따라 바뀌는 것을 확인할 수 있다.

fun main() {
    // contravariance
    val s: Group<Student> = groupOf<Person>(Person())        
    
    val s2: Group<Student> = groupOf<Student>(Student())
    
    // contravariance & covariance
    val s3: Group<Student> = groupOf<Person>(Student()) 
    
    // contravariance & covariance
    val s4: Group<Student> = groupOf<Person>(Person(), Student())  
 }

이 코드에서는 Person 타입에 Student 객체가 들어간 것도 covariance이기 때문에 s3와 s4 코드에서는 contravariance 뿐만 아니라 covariance도 확인할 수 있다.

 

 

다음 포스팅에서는 Covariance & Contravariance 개념을 적용한 PECS 원칙을 다룰 것이다.

 

[Kotlin] PECS 원칙

이전 포스팅에서 Covariance와 Contravariance에 대해 알아보았다. 이 포스팅을 읽기 전에 [Kotlin] Covariance & Contravariance을 먼저 보고 오면 이 글을 이해하는 데 도움이 될 것이다. [Kotlin] Covariance &..

s2choco.tistory.com

 

'Mobile > Kotlin' 카테고리의 다른 글

[Kotlin] Java와 Kotlin의 Covariance 차이  (0) 2020.03.26
[Kotlin] PECS 원칙  (0) 2020.03.26
[Kotlin]의 apply, with, also, let, run 구분  (0) 2020.03.24
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함