코틀린 언어 정리 7-2
Variance(불변 공변 반공변 개념과 코틀린)
불변 공변 반공변에 대한 개념
SubA와 SuperA라는 class가 있고 SubA가 SuperA를 상속 받는다고 가정하면, 객체지향 개념에 따라 SubA is a SuperA라는 관계가 성립합니다. 그런데 객체지향과 제네릭을 같이 사용하게 되면 보통 GenClassA<SubA>, GenClassA<SuperA>등과 같이 상속 관계에 있는 타입을 제네릭 클래스의 타입 매개변수로 입력하여 생성하는 경우가 생길 수 있고, 이때 GenClassA<SubA>와 GenClassA<SuperA>는 어떤 관련이 있을 것 같지만, 제네릭의 불변 원칙에 따라 아무런 관계가 없게 됩니다. 이렇게 불변으로 처리된 코드만 놓고 보면, 개발자의 입장에서는 타입간의 관계를 고려할 필요가 없기 때문에 코드가 단순해집니다. 하지만 상속 관계에 있는 클래스를 타입매개변수로 입력하여 생성한 제네릭 클래스끼리는 "is a" 관계를 통한 객체지향 연산(SuperA를 통한 인수 전달)을 수행할 수 없으며 GenClassA만의 일반화 코드(SubA, SuperA와 무관한 GenClassA의 일반화된 코드)도 재활용 할 수 없습니다. 즉 제네릭의 불변 개념은 전체적인 입장에서는 코드 재활용성이 떨어지게 되는 것으로 볼 수도 있습니다. 따라서 언어 차원에서 제네릭의 불변을 기반으로 제네릭에 공변, 반공변이라는 개념을 추가적으로 지원하여 활용하면 코드의 재활용성을 높일 수 있습니다.
불변(invariance)
제네릭은 기본적으로 불변입니다. SubA가 SuperA를 상속받은 관계라고 하더라도 GenClassA<SubA>와 GenClassA<SuperA>는 서로 관련이 없는 완전히 독립된 타입입니다. 개발자가 각각의 타입을 다룰 때 혼동의 여지가 없으며 컴파일러 입장에서도 서로 연관 관계가 없으므로 매우 안정적입니다.
공변(covariance)
직관적이고 효과적인 개념입니다. SubA가 SuperA를 상속 받는다면 GenClassA<SubA>와 GenClassA<SuperA>에도 동일한 관계가 적용됩니다. 예를 들면, GenClassA<SuperA>로 선언한 변수에 GenClassA<SubA>를 전달(대입)하는 것이 가능합니다. SubA is a SuperA 라면 GenClassA<SubA> is a GenClassA<SuperA> 입니다.
즉 제네릭으로 선언된 클래스에 입력된 타입 매개변수가 원래 가지고 있는 관계(상속 계층 관계)에 의한 객체지향적(다형적) 연산 규칙을 제네릭 클래스에도 직접적으로 적용할 수 있습니다.
반공변(contra variance)
공변 개념을 적용하면 GenClassA<Sub> is a GenClassA<Super>의 관계가 적용됩니다. 그리고 GenClassA<Super> is a GenClassA<Sub>는 지원하지 않습니다. 그런데 공변과 반대로, 반공변이라는 것이 있습니다. 반공변을 적용하면 GenClassA<Super> is a GenClassA<Sub>의 관계가 적용됩니다.
개념에 대해 자세히 설명하기 전에, 좀 더 쉬운 이해를 위해 우선 GenClassA를 List, Array등의 일반적인 Collection이라고 가정합니다.
이와 같은 경우 GenClassA<T> 내부에는 T 타입의 연산을 직접 호출하는 코드가 없습니다. 즉 T 타입의 객체를 입력 받아 저장하거나 외부로 T타입의 객체 참조를 제공하는 것을 제외하면 GenClassA<T>는 T 타입의 객체와 관련이 없는, T 타입에 종속적이지 않은 코드입니다. 이런 경우 GenClassA<T>의 객체에서 T 타입의 객체를 입력받고 출력하지 않는다고 명시적으로 강제할 수 있다면, GenClassA<Sub>에서 Sub가 상위타입(supertype)으로 대체된 GenClassA<Super>타입으로 치환할 때에도 타입 매개변수로 입력된 Super 객체를 클래스 외부로 리턴하면서 하위 타입인 Sub로 참조할 일을 만들지 않기 때문에 GenClassA<Sub>를 사용하는 외부 코드 관점에서 타입 안정성이 보장될 수 있습니다. 하지만 범용적인 Collection이라는 관점에서는 GenClassA<Sub>에서 Sub타입의 객체를 입력만 받을 수 있고 출력은 불가능 하다면 이런 클래스는 활용도가 거의 없을지도 모른다고 생각할 수 있습니다. 그래서 직전의 가정을 조금 확장해 볼 필요가 있습니다. GenClassA<Sub>가 만약 Collection을 상속 받아 내부적으로 Sub타입의 객체를 저장, 관리하고, Super 객체의 연산만 호출하는 클래스 이거나, 아니면 Super 타입만 활용하도록 구현된 함수(ex. 매개변수로 Super타입 사용)를 입력 받아 메서드 내에서 호출하고 있다면 GenClassA<Super>가 GenClassA<Sub>로 참조되어도 유용하다고 생각할 것입니다. 좀 더 구체적으로, GenClassA에서 내부적으로 Sub타입의 객체를 소모하는 간단한 상황을 가정해 보겠습니다.. GenClassA<Sub>로 선언된 변수에 GenClassA<Super>객체가 대입된 후 GenClassA 내부에서 Sub객체를 다룰 때 안전하게 동적으로 타입을 확인 하고 각 타입의 연산을 호출 하던지 아니면 Sub의 최상위 타입에 정의된 연산만 호출하여 처리하는 상황을 생각해 볼 수 있습니다. 그리고 Sub의 최상위 타입만 활용하는 함수의 상황을 더 생각해 보면, GenClass<SubA>에서 SubA 타입을 타입 매개변수로 입력하여 만든 제네릭 함수( ex. fun <SubA> func( a: SubA ) )를 입력 받는데 이 함수에서 SubA의 연산은 사용하지 않고 SuperA의 연산만 수행 한다고 가정하면, 매개변수 타입만 다르고 구현이 완전히 동일한 제네릭 함수를 SuperA의 하위 Type(SuperA를 상속 받는 SubA, SubA1, SubA2 등)별로 각각 생성하지 않고 SuperA 타입( ex. fun <SuperA> func( a: SuperA ) )의 제네릭 함수 하나만 생성하여 SuperA의 모든 하위 타입에 대해 재활용 하는 것이 가능합니다.
정리
반공변을 사용하면 내부적으로 T의 연산을 직접 호출하지 않거나 T의 상속 계층에서 최상위 타입의 연산만 호출하는 GenClassA<T> 에서 T를 포함한 모든 상위 타입의 객체에 대해 동일한 제네릭 클래스의 인스턴스로 처리할 수 있습니다. (제네릭 클래스의 가짓수를 줄일 수 있습니다.)
반공변 개념은 제네릭 코드에 객체지향 연산 규칙을 적용하기 위한 것이 아니라 제네릭 코드의 재활용성을 높이기 위한 개념으로 볼 수 있습니다. 즉 기존에 제네릭 코드의 재활용이 안되고 있던 불합리한 틈새 상황을 제거해 주는 것으로 볼 수 있을 것 같습니다.
예제
불변, 공변, 반공변 전체 예제
예제
'코틀린( Kotlin )' 카테고리의 다른 글
코틀린 8-1 함수의 다형성, 하위 호환성 (0) | 2020.04.26 |
---|---|
코틀린 7-3 Variance - 코틀린에서의 불변, 공변, 반공변 개념 및 문법 (0) | 2020.04.25 |
코틀린 7-1 Generics (0) | 2020.04.23 |
코틀린 6-10 Annotations - 표준 애노테이션들 (0) | 2020.04.22 |
코틀린 6-9 Annotations - Annotation instance 접근 (0) | 2020.04.21 |