Post

코틀린에서 변성 알아보기

들어가며

최근에 코틀린에서 간단한 배치 작업을 하다가 ItemProcessor <I,O> 에서 I 에 in 키워드를 O 에 out 키워드를 사용한 사례를 접했는데 이러한 in, out 키워드는 무엇이고 왜 사용할까?

타입 변성

in , out 키워드를 알기 위해서는 재네릭에서 타입이 다른 타입과 어떻게 관계를 맺는지 , 특히 타입 매개변수를 사용할 때 타입 사이의 상속 관계를 어떻게 유지할지를 설명하는 개념인 타입 변성에 대해서 알 필요가 있다.

타입 변성에는 공변성 , 반공변성 , 무변성이 있는데 간단히 이 세가지에 대해서 간단히 알아보면 A 타입이 B 타입의 하위타입 일 때

  • Clazz<A> 타입이 Clazz<B> 타입의 하위 타입으로 상속 관계가 유지되는 경우를 공변성,

  • Clazz<A> 타입이 Clazz<B> 타입의 상위 타입으로 상속 관계가 반대 방향으로 유지되는 것을 반공변성,

  • Clazz<A> 타입이 Clazz<B> 타입이 아무런 관계를 가지지 않는 것을 무변성이라고한다.

A 타입이 B 타입의 하위타입이라면 Clazz<A> 타입도 Clazz<B> 타입의 하위타입이 되는 공변성이 직관적으로 이해도 되고 문제가 없을 것 같지만 타입 안정성 측면에서 보았을 때에 큰 문제가 있을 수 있다.

예를 들어 Galaxy 가 Phone 의 하위타입이라고해서 MutableList<Galaxy> 타입도 MutableList<Phone> 타입이 되어버리면 아래와 같이 val phones : MutableList<Phone> 과 같은 참조가 galaxies 를 가르킬 수 있게되고 이럴 경우 기존의 galaxies 가 참조하는 영역에 Phone 의 다른 타입인 IPhone 을 추가할 수 있게 되므로 더 이상 galaxies 에서 Galaxy 타입의 객체만 존재한다는 타입 안정성 전제가 깨지게 되므로 오류가 날 수 있다.

다행이도 코틀린에서 이 경우 MutableList<A> 타입과 MutableList<B> 타입은 무공변이기 때문에 컴파일 오류가 나서 실행조차할 수 없다.

한편 MutableList<> 에서 List<> 로 변경하면 컴파일 오류가 나지 않고 정상적으로 galaxies 를 phones 에 대입할 수 있게 되는데 이는 List<Galaxy> 타입과 List<Phone> 는 공변성이 있다고 볼 수 있다.

이게 가능한 이유는 코틀린에서 List<> 의 경우에는 read-only 이기 때문에 쓰기를 하지 않아서 위에서 언급한 타입 안정성이 깨질 이유가 없기 때문이다.

타입 변성은 제네릭 타입을 사용할 때 타입 안정성을 유지하고, 불필요한 타입 변환을 피하기 위해 사용된다. 이를 통해 코드의 재사용성을 높이고, 타입 안전성을 보장할 수 있다.

변성 어노테이션

MutableList 에서는 컴파일 오류가 나고 List 에서는 컴파일 오류가 나지 않았는데 코틀린에서는 이를 어떻게 해결했을까?

List 인터페이스를 살펴보면 <out E> 라고 out 키워드가 있는 것을 볼 수 있다. 이를 변성 어노테이션이라고하며 코틀린에서는 in , out 키워드로 타입 변성을 정의한다.

out

먼저 out 키워드는 공변성임을 지정하는 키워드이다. A 타입이 B 타입의 하위 타입인 경우 Clazz< out T> 라면

Clazz<A> 타입은 Class<B> 타입의 하위 타입으로 인식한다. 주로 읽기 전용 컨텍스트에서 활용되며 T 타입의 값을 반환하기만 하고 T 타입의 값을 인자로 받지 못한다.

List 의 contains 의 경우 contains 메서드에서 E 타입을 인자로 받고 있는데 이는 @UnsafeVariance 어노테이션을 사용해서 변성 규칙을 무시하여 E 타입을 인자로 받을 수 있도록 허용한 것이다.

in

in 키워드의 경우에는 반공변성임을 지정하는 키워드이며 A 타입이 B 타입의 하위 타입인 경우 Clazz<in T> 라면 Clazz<A>타입은 Class<B> 타입의 상위 타입으로 인식하여 다음과 같이 glaxies 에 phones 를 대입 가능하다. 주로 쓰기 전용에서 활용되며 인자로 받기만하고 반환할 수 없다.

마무리하며

결론은 배치에서 PlayerCreatorProcessor의 ItemProcessor<I, O> 제네릭 타입 매개변수에서 in 키워드를 사용해서 I에는 InputPlayer 상위 타입만을 오도록 제한한 것이며 O 에는 out 키워드를 사용해서 Player 의 하위타입만 받고록 제한한 것으로 볼 수 있다

This post is licensed under CC BY 4.0 by the author.