Home 코틀린 - 변성
포스트
취소

코틀린 - 변성

변성


  • 변성이란?
    • List<String>, List<Any>와 같이 기저 타입이 같고 타입 인자가 다른 여러 타입이 서로 어떤 관계가 있는지 설명하는 개념
  • 변성? 이런 관계가 왜 중요하지?
    • 코틀린에서 제네릭 클래스나 함수를 정의하는 경우
    • 타입 안정성을 보장하는 API를 만드는 경우
    • 꼭 잘 알고 사용해야 한다.
  • 변성이 있는 이유
    • List<Any> 타입의 파라미터를 받는 함수에 List<String>을 넘기면 안전할까?

      1
      2
      3
      4
      5
      6
      7
      
        fun printContentList(list: List<Any>) {
            println(list.joinToString())
        }
                  
        printContentList(listOf("abc", "bac"))
      
        // abc, bac
      
      • 음 컴파일, 동작에도 문제없고 딱 봐도 문제가 전혀 없어보인다.
      • 이제 리스트를 변경하는 다른 함수를 보자

        1
        2
        3
        4
        5
        6
        7
        
          fun addAnswer(list: MutableList<Any>) {
              list.add(42)
          }
        
          val stringList = mutableListOf("abc", "bac")
          addAnswer(stringList) // 이미 여기서 부터 인텔리제이가 오류를 내뱉어 준다.
          println(stringList)
        
        • addAnswer(stringList) 부분 에서 부터 애초에 컴파일이 안된다.
        • 위 예제는 MutableList<Any>가 필요한 곳에 MutableList<String>을 넘기면 안된다는 사실을 보여준다
    • 이제 List<Any> 타입의 파라미터를 받는 함수에 List<String>을 넘기면 안전할까? 라는 질문에 답할 수 있다.
      1. 어떤 함수가 리스트의 원소를 추가하거나 변경한다면 타입 불일치가 생길 수 있어서 → 불가
      2. 원소 추가나 변경이 없는 경우에는 List<Any> 타입 대신 List<String>을 대신 넘겨도 안전하다.
    • List 클래스와 MutableList의 변성이 왜 다를까?
    • List 뿐 아니라 모든 제네릭 클래스에 대해 같은 질문을 해보고 일반화 해보자!

    ## 클래스, 타입, 하위 타입


    • List 뿐 아니라 모든 제네릭 클래스에 대해 같은 질문을 해보고 일반화 해보자! 라는 질문에 답하기 전에 먼저 타입, 하위타입 이라는 개념에 대해 먼저 알아보자
    • 간단히 설명하면
      • String 은 클래스지만 String? 은 타입이다.
      • List는 클래스지만 List, List, List<List>은 타입이다.
    • 타입 사이의 관계를 논하기 위해 하위 타입 이라는 개념을 알아야 한다.
    • 하위타입
      • A의 값이 필요한 모든 곳에 어떤 타입 B의 값을 넣어도 아무 문제가 없다면 → 타입B는 타입A의 하위 타입이다.
      • ex) Int는 Number의 하위타입 / 모든 타입은 자신의 하위타입
    • 상위타입
      • 하위 타입의 반대
    • 그렇다면 하위 클래스 란?
      • 간단한 경우 보통은 하위 타입은 하위 클래스와 동일하다.
        • ex) Int클래스는 Number클래스의 하위 클래스, String은 CharSequence의 하위 클래스이자 하위 타입
      • 하지만! 널이 될 수 있는 타입은 ⇒ 하위 타입과 하위 클래스가 같지 않다는걸 보여주는 예이다.
        • ex) A는 A?의 하위타입이지만 A?는 A의 하위타입이 아니다. 하지만 두타입 모두 같은 클래스에 해당한다.
    • 다시 처음으로 돌아와서 List<Any> 타입의 파라미터를 받는 함수에 List<String>을 넘기면 안전할까? 라는 질문을 하위 타입 관계를 써서 다시 쓰면
      • List<String>List<Any>의 하위 타입인가? ⇒ 맞다
      • MutableList<String>MutableList<Any> 의 하위타입인가? ⇒ 아니다
    • 무공변
      • 제네릭 타입을 인스턴화할 때 타입 인자로 서로 다른 타입이 들어가면 인스턴스 타입 사이의 하위 타입 관계가 성립하지 않으면 그 제네릭 타입을 무공변 이라고 한다.
        • MutableList 로 예를 들면 A와 B가 서로 다르기만 하면 MutableList<A>는 항상 MutableList<B>의 하위 타입이 아니다. (무공변)
    • 공변 : 하위 타입 관계가 유지된다.
      • List로 예를 들면 A가 B의 하위타입이면 List<A>List<B>의 하위타입이다.
      • → List 클래스는 공변적이다.
      • → 하위 타입 관계가 유지된다.
      • 특정 클래스나 인터페이스가 공변적임을 선언하는 방법은?
        • 타입 파라밑터 이름 앞에 out을 넣으면 된다.

          1
          2
          3
          
            interface List<out T> {
            	fun test(): T
            }
          
          • 이렇게 타입 파라미터를 공변적으로 만들면 함수 정의에 사용한 파라미터 타입과 타입인자의 타입이 정확히 일치하지 않더라도 그 클래스의 인스턴스를 함수 인자나 반환값으로 사용할 수 있다.

공변


  • 고양이 예시

    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
    31
    32
    33
    34
    35
    
      open class Animal {
          fun feed() {
              println("feed....")
          }
      }
    
      class AnimalFamily<out T : Animal> {
          val size: Int
              get() {
                  TODO()
              }
    
          operator fun get(i: Int): T {
              TODO()
          }
      }
    
      fun feedAll(animalFamily: AnimalFamily<Animal>) {
          for (i in 0 until animalFamily.size) {
              animalFamily[i].feed()
          }
      }
    
      class Cat : Animal() {
          fun cleanTrash() {
              TODO()
          }
      }
    
      fun takeCareOfCats(catList: AnimalFamily<Cat>) {
          for (i in 0 until catList.size) {
              catList[i].cleanTrash()
          }
      		feedAll(catList)
      }
    
    • 문제점 : 고양이 가족은 ⇒ 동물 가족의 하위 클래스가 아니다.
    • 해결방법?
      • AnimalFamily 클래스는 동물을 추가하거나 가족안의 동물을 다른 종류의 동물로 바꿀 수 없다.
      • 따라서 AnimalFamily 클래스를 공변적 클래스로 만들면 된다.

        1
        
          class AnimalFamily<out T : Animal> {
        
  • 타입 파라미터를 공변적으로 만들면 → 클래스 내부에서 그 파라미터를 사용하는 방법이 제한된다.
  • 이는 클래스가 T 타입의 값을 생산할 수는 있지만 T 타입의 값을 소비할 수는 없다는 뜻
    • T가 함수의 반환 타입에 쓰인다면 T는 out 위치에 있어야 함 - 생산 (out)
    • T가 함수의 파라미터 타입에 쓰인다면 T는 in 위치에 있어야 함 - 소비 (in)

      1
      2
      
        fun(t: T): T
        		//in // out
      
  • 즉, out 키워드는 T로 인해 생기는 하위 타입 관계의 타입 안정성을 보장한다.
    • ex)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      
        class AnimalFamily<out T : Animal> {
            val size: Int
                get() {
                    TODO()
                }
      
            operator fun get(i: Int): T {
                TODO()
            }
        }
      
      • AnimalFamily에서 타입 파라미터 T를 사용하는 장소는 오직 get 메소드의 반환 타입뿐!
      • 따라서 이 클래스를 공변적(하위 타입 관계가 유지되는 상태로)으로 선언해도 안전하다.
  • 마지막 정리 T에 붙은 out 키워드는 2가지 역할을 한다.
    • 공변성
      • 하위 타입 관계가 유지 된다. (A가 B의 하위타입이면 List<A>List<B>의 하위타입이다.)
    • 사용 제한
      • T를 아웃 위치에서만 사용할 수 있다.
      • (생성자 파라미터는 in, out 어느쪽도 아님에 유의)
  • 이렇게 변성은 코드에서 위함할 여지가 있는 메소드를 호출할 수 없게 만드는 역할을 한다.

반공변


  • 공변의 반대
  • in 키워드
  • T값을 소비하기만 한다. ⇒ 함수의 파라미터로만 받을 수 있다.
  • 타입 B가 타입 A의 하위 타입인 경우 Class<A>Class<B>의 하위 타입인 관계가 성립하면 제네릭 클래스 Class<T>는 타입인자 T에 대해서 반공변이다.
    • A와 B의 위치가 뒤바뀐다 → 하위 타입 관계가 뒤집힌다고 표현함
  • ex)

    1
    2
    3
    4
    5
    
      listOf("AAA", "BBB").sortedWith(
          Comparator<Any> {
              e1, e2 -> e1.hashCode() - e2.hashCode()            
          }
      )
    
    1
    2
    3
    
      listOf("AAA", "BBB").sortedWith { e1, e2 ->
          e1.hashCode() - e2.hashCode()
      }
    
    • 위 두개는 같은 코드
    • Comparator는

      1
      2
      3
      
        interface Comparator<in T> {
        	fun compare(e1: T, e2: T): Int { ... }
        }
      
      • 대충 이렇게 구현 되어있는데
      • 어차피 T라는 타입을 함수의 인자로만 사용하기 때문에
      • 위 예제에서 sortedWith가 Comparator<String>을 받도록 되어있지만 더 일반적인 타입을 비교할 수 있는 Comparator<Any>를 넘겨도 안전하다.
      • 즉, String은 Any의 하위 타입 이지만 논리적으로 Comparator<Any>Comparator<String>의 하위 타입이 되었다.

정리


  • Cat은 Animal의 하위 타입
  • Class<Cat>Class<Animal>의 하위 타입 : 공변성
  • Class<Animal>Class<Cat>의 하위 타입 : 반공변성

무공변성


  • Class<T>
  • T를 어느 위치에서나 사용가능

공변과 반공변의 공존


  • 클래스나 인터페이스가 어떤 타입 파라미터에 대해서는 공변적이면서 다른 타입 파라미터에 대해서는 반공변적일 수 있다.
  • Function 인터페이스가 고전적인 예
  • ex) 파라미터가 하나뿐인 Function 인터페이스인 Function1

    1
    2
    3
    
      interface Function1<in P, out R> {
      	operator fun invoke(p: P): R
      }
    
    • 위 코드는 사실상 코틀린에서는 아래와 같다

      1
      2
      
        (P)-> R
        // Funcation1<P, R>
      

선언 지점 변성과 사용자 지점 변성


  • 선언 지점 변성
    • 지금까지 봤던 클래스를 선언하면서 변성을 지정하는 방식
    • 해당 클래스를 사용하는 모든 장소에 변성 지정자가 영향을 끼치므로 편리하다
  • 사용자 지점 변성
    • 타입 파라미터가 있는 타입을 사용할 때마다. 해당 타입 파라미터를 하위 타입이나 상위 타입 중 어떤 타입으로 대치할지 명시하는 방법
    • 코틀린도 지원한다.
      • 코틀린 → 자바
        • MutableList<out T>MutableList<? extends T>
        • MutableList<in T>MutableList<? super T>

참고 : 코틀린 인 액션 이미지 참고 : https://www.slipp.net/wiki/pages/viewpage.action?pageId=30769686

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

coroutine 에서 원자성 위반 문제를 해결하는 방법

Local에 docker로 카프카 띄우기 - 카프카 연습용

Comments powered by Disqus.