코틀린에서 제네릭 클래스의 타입 파라미터 앞에 in
키워드를 붙이면 해당 타입은 contravariance(반공변)임을 의미하고 out
키워드를 붙이면 covariance(공변), 아무런 키워드도 붙이지 않으면 invariance(무공변)임을 의미합니다.
<in T>
: contravariance, 반공변<out T>
: covariance, 공변<T>
: invariance, 무공변
반공변, 공변이라는 무시무시해보이는 단어들이 의미하는 바가 무엇인지 몰라도 괜찮습니다. 이번 글에서 모두 다룰 거니까요.
Variance 변성
변성(variance)은 기저 타입(base type)이 같고 타입 인자(type argument)가 다른 제네릭 타입간의 계층 관계(type hierarchy)를 설명하는 개념입니다.
List<String> 은 List<Any> 의 하위 타입일까요?
변성은 이 질문에 답을 하기위해 고안해낸 개념입니다.
변성의 종류에 대해 각각 알아보기 전에 먼저 타입의 개념부터 명확하게 잡고 넘어갈게요.
타입 Type
타입이란 “해당 변수에 담을 수 있는 값의 집합”을 의미합니다. 보통 클래스라는 용어와 타입을 혼용하기도 합니다. 왜냐면 보통은 이 둘은 같거든요. 가령 String
클래스의 경우 var x: String
처럼 변수 타입으로 바로 사용하는 경우 클래스와 타입은 같습니다. 하지만 클래스와 타입이 다른 경우도 있습니다. (더 많습니다.)
타입 — 해당 변수에 담을 수 있는 값의 집합
먼저 단순한 경우로, 코틀린에는 nullable 타입이 존재합니다. 그래서 코틀린의 모든 클래스는 적어도 둘 이상의 타입을 구성할 수 있습니다. String
클래스가 String
, String?
두 개의 타입으로 나뉘는 것 처럼요.
더 복잡한 경우는 제네릭 클래스입니다. 제네릭 클래스에서 올바른 타입을 얻어내려면 타입 파라미터를 인자로 받아야 하죠. 예를 들어 List
는 타입이 아니라 클래스입니다. 하지만 타입 파라미터를 넣은 List<Int>
, List<String?>
, List<List<String>>
등은 모두 제대로 된 타입입니다. 이렇듯 각각의 제네릭 클래스는 무수히 많은 타입을 만들어 낼 수 있습니다.
하위 타입 Sub-Type
‘어떤 타입 A의 값이 필요한 모든 위치에 어떤 다른 타입 B의 값을 넣어도 아무런 문제가 없는 경우’ 타입 B는 타입 A의 하위 타입입니다.
한 타입이 다른 타입의 하위 타입인지가 왜 중요한가 싶겠지만 컴파일러는 변수 대입이나 함수 인자 전달 시 하위 타입 검사를 매번 수행합니다. 인자가 파라미터 타입의 하위 타입이 아니라면 컴파일조차 안되겠죠.
보통 하위 타입은 하위 클래스와 같습니다. 예를 들어 Int
클래스는 Number 클래스의 하위 클래스이므로 Int 는 Number 의 하위 타입입니다.
하지만 하위 타입이 하위 클래스와 같지 않은 경우도 물론 있죠. 아까 봤던 제네릭 클래스 때문입니다.
글 맨 처음에 봤던 질문을 다시 떠올려봅시다. List<String>
타입은 List<Any>
타입의 하위 타입일까요?
앞서 봤던 하위 타입의 정의로 질문을 바꿔보죠. List<Any>
타입을 인자로 받는 함수에 List<String>
타입을 전달해도 괜찮을까요? 네! List<Any>
의 자리에 List<String>
을 넣어도 아무런 문제가 없습니다. 리스트에 추가나 변경이 일어나지 않기 때문에 타입 불일치가 생길 여지가 없기 때문이죠. 그럼 List<String>
타입은 List<Any>
타입의 하위 타입이라고 할 수 있겠네요.
그럼 MutableList<String>
은 MutableList<Any>
의 하위 타입일까요? 아쉽게도 아닙니다. 리스트에 변경의 여지가 있기 때문입니다. 그래서 컴파일러는 아래와 같은 호출을 금지합니다.
그럼 MutableList<String>
은 MutableList<Any>
의 하위 타입이라고 할 수 없겠네요!
이처럼 타입 인자로 서로 다른 클래스를 넣었을 때 본래 클래스의 상관관계가 무의미해지는 제네릭 타입을 invariant(무공변) 타입이라고 부릅니다.
MutableList
와는 다르게 List
는 타입 인자의 상관 관계가 유지되었죠. A가 B의 하위 타입일 때 List<A>
가 List<B>
의 하위 타입이 되었습니다. 이같은 제네릭 클래스 타입을 공변(covariance) 타입이라고 부릅니다.
Covariance 공변
공변(covariance)은 A가 B의 하위 타입일 때 Base<A>
가 Base<B>
의 하위 타입이 되는 경우를 말합니다. Base 라는 제네릭 클래스가 타입 인자의 기존 상속 관계를 유지하는 경우 Base 클래스는 해당 타입 파라미터에 대해 공변이라고 말할 수 있는거죠.
코틀린에서는 특정 타입 파라미터 앞에 out
키워드를 붙임으로서 해당 클래스를 특정 타입 파라미터에 대해 공변적인 클래스로 선언할 수 있습니다.
Contravariance 반공변
반공변 클래스의 타입 관계는 공변 클래스의 경우와 반대입니다. 타입 A가 타입 B의 하위 타입일 때 Base<A>
가 Base<B>
의 상위 타입이 되는 경우를 말하죠.
코틀린에서 반공변은 타입 파라미터 앞에 in
키워드를 붙여서 해당 클래스를 해당 타입 파라미터에 대해 반공변인 클래스로 선언할 수 있습니다.
Invariance 무공변
변성을 지정하지 않은 타입인자를 말합니다. 타입 파라미터 선언시에 아무런 키워드를 붙이지 않으면 기본적으로 무공변입니다.
위에서 잠시 언급됐던 MutableList
인터페이스가 이에 해당합니다. 무공변이기때문에 MutableList<String>
타입 은 MutableList<Any>
타입과 아무런 계층관계를 갖지 않습니다.
In & Out
이제 변성이 무엇인지, 공변, 반공변이 어떤 뜻인지는 알겠습니다. 그런데 왜 공변과 무공변은 각각 out
과 in
키워드를 붙이게 된걸까요? out
과 in
키워드가 의미하고자 하는 바가 무엇일까요?
T 라는 타입 파라미터를 선언하고 T 를 사용하는 메소드를 가진 클래스를 생각해봅시다. T 가 메소드의 반환 타입으로 쓰인다면 T 는 나가는(out) 형태이므로 out 위치에 있다고 볼 수 있고(생산자), T 가 메소드의 파라미터 타입에 쓰인다면 T 는 들어오는(in) 형태이므로 in 위치에 있다고 볼 수 있습니다.(소비자)
클래스의 타입 파라미터 T 앞에 out 키워드를 붙였다는 건 클래스 내부에서 T 를 사용하는 메소드가 out 위치에서만 T 를 사용하도록 제한하겠다는 뜻입니다. 함수의 반환 타입으로만 T 를 사용하라는 뜻이죠.
반대로 T 앞에 in 키워드를 붙이면 T 를 사용하는 메소드가 in 위치, 즉 함수의 파라미터 타입으로만 사용하도록 제한하는 것입니다.
타입 인자가 반공변성을 유지하려면 해당 타입을 소비하는 위치, 즉 in 위치에만 쓰여야하기에 in 키워드는 곧 반공변성을 뜻하게 되었고, 공변성을 유지하려면 해당 타입을 생산하는 위치인 out 위치에만 쓰여야하기에 out 키워드는 공변성을 의미하게 되었습니다.
변성을 굳이 알아야할까?
저는 좋은 기술이라 함은 이를 사용하는 개발자로 하여금 실수를 덜 하도록 도와주는 기술이라고 생각합니다. 개발자가 실수를 덜 하려면 기술은 개발자에게 조금씩 제약을 가하기도 합니다.
변성은 코드에서 위험할 여지가 있는 메소드를 호출할 수 없게 만듦으로써 제네릭 타입의 인스턴스 역할을 하는 클래스 인스턴스를 잘못 사용하는 일이 없게 방지하는 역할을 합니다. 클래스 외부의 사용자가 클래스를 잘못 사용하는 일을 막기 위한 장치라는거죠. (그래서 private 메소드의 파라미터는 인도 아웃도 아닙니다. 클래스 내부에서만 사용하니까요.)
그래서 주로 라이브러리 형태의 API 를 개발할 때 적절히 활용하여 클라이언트가 API 를 잘못 사용하는 일이 없도록 합니다. 이 때문인지 얼핏 보면 변성은 일반적인 서비스 개발을 하는 경우에는 직접 활용할 일이 없어보이기도 합니다.
하지만 우리는 간혹, 아니 자주 라이브러리나 프레임워크의 코드를 들여다보고 이해해야하는 경우가 생기곤 합니다. 이때 변성이 첨가된 코드를 보고 이해할 수 없다면 조금 당혹스러울 수도 있겠습니다.
그리고 나의 편의를 위해, 혹은 우리 팀의 편의를 위해 공통 라이브러리 형태의 코드를 작성하는 경우는 빈번합니다. 모두가 사용하는 도구를 만든다면, 기왕 만드는거 더 좋은 도구를 만드는게 낫겠죠.
도구를 쓰는 사람으로 하여금 도구의 설계 의도를 명확히 하고 실수를 방지하는 장치를 마련해두는건 어떨까요?
변성이 이를 조금은 도와줄 수 있을 것 같습니다.